1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
import logging
from .database import Database
from .jform import JForm
from .admin_log import AdminLog
from .config import *
from .utils import local_now
from .exceptions import *
"""A set of routine methods, run at an interval (somewhere from ten minutes to one hour), to:
- sync JImbrella's databse against data pulled from jForm
- check if any umbrella is now overdue
- send an SMS where applicable, to a user or an admin
"""
def chronological_merge(*sheet_lists) -> list:
"""Merges all lists answer sheets passed in, in chronological order, into a single list.
All lists of sheets in sheet_lists MUST be already in chronological order.
By "chronological", we mean sorting by the value under key "date".
"""
chronicle = []
while any(sheet_lists): # at least one list in `sheet_lists` is non-empty
# for each list of sheets, read date from its first element, then find the earliest
# is an instance of datetime.datetime
# only pass non-empty lists to min():
earliest_date = min(
[sheet_list[0]["date"] for sheet_list in filter(None, sheet_lists)]
)
for idx, sheet_list in enumerate(sheet_lists):
if not sheet_list: # is empty
continue
if sheet_list[0]["date"] == earliest_date:
# remove the first element and append it to `chronicle`, if it is the earliest
chronicle.append(sheet_lists[idx].pop(0))
# we do not directly break here because there exists a tiny chance
# two answer sheets were submitted at the exact same millisecond
return chronicle
def sync_database(takeaway: JForm, giveback: JForm, db: Database):
takeaway_unread = takeaway.get_unread()
giveback_unread = giveback.get_unread()
logging.info(
"Sync database: found %d unread takeaway sheet(s) and %d unread giveback sheet(s)",
len(takeaway_unread),
len(giveback_unread),
)
unread = chronological_merge(takeaway_unread, giveback_unread)
# NOTE: beyond this point `takeaway_unread` and `giveback_unread` are empty
# because chronological_merge popped all their elements
for sheet in unread:
sheet["date"] = sheet["date"].replace(tzinfo=None) # it's UTC+8 anyway
log_args = (
sheet["name"],
sheet["id"],
sheet["phone"],
sheet["key"],
sheet["date"].isoformat(timespec="seconds"),
)
if sheet["jform_name"] == "takeaway":
try:
db.take_away(
sheet["key"],
sheet["date"],
sheet["name"],
sheet["id"],
sheet["phone"],
)
logging.info(
"%s (ID: %s, phone: %s) borrowed umbrella #%d at %s", *log_args
)
except UmbrellaStatusError:
logging.warning(
"%s (ID: %s, phone: %s) attempted to borrow inavailable umbrella #%d at %s",
*log_args
)
except UmbrellaNotFoundError:
logging.warning(
"%s (ID: %s, phone: %s) attempted to borrow non-existent umbrella #%d at %s",
*log_args
)
elif sheet["jform_name"] == "giveback":
try:
db.give_back(sheet["key"])
logging.info(
"%s (ID: %s, phone: %s) returned umbrella #%d at %s", *log_args
)
except UmbrellaStatusError:
logging.warning(
"%s (ID: %s, phone: %s) attempted to return available umbrella #%d at %s",
*log_args
)
except UmbrellaNotFoundError:
logging.warning(
"%s (ID: %s, phone: %s) attempted to return non-existent umbrella #%d at %s",
*log_args
)
def process_overdue(db: Database):
overdue = Database.find_overdue(db.read())
# mark and log umbrellas that were not, but just became overdue
for umb in filter(lambda u: u["status"] == "lent", overdue):
db.mark_overdue(umb["serial"])
logging.warning(
"Umbrella #%d is now overdue, tenant: %s (ID: %s, phone: %s)",
umb["serial"],
umb["tenant_name"],
umb["tenant_id"],
umb["tenant_phone"],
)
if __name__ == "__main__":
takeaway = JForm("takeaway", JFORM_TAKEAWAY_URL, JFORM_BOOKMARK_DIR)
# giveback = JForm(
# "giveback", JFORM_GIVEBACK_URL, JFORM_BOOKMARK_DIR
# )
giveback = None
db = Database(DATABASE_PATH)
sync_database(takeaway, giveback, db)
|