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
123
124
125
|
import csv
import os
from datetime import datetime
class AdminLog:
"""Logs JImbrella-specific events into a file.
The file is intended to be read, deserialized, and represented in a user-friendly format to an
admin on the web console. The file format is csv, but the number and meanings of columns may
not be uniform for all rows.
For each row, there are a minimum of three columns. The first column is called the "event".
It describes the log entry by and large. What other columns in the row represent depends on
the event. The second column is always the date and time, in ISO8601 format. The last column
is called the "note", an optional textual note in natural language.
Here we list possible events, what information will follow them, and the scenario for each one.
Words surrounded by angle brackets (< >) are defined in class Database, and those in square
brackets ([ ]) are defined here in AdminLog.
TAKEAWAY,[date],<serial>,<tenant_name>,<tenant_id>,<tenant_phone>,<tenant_email>,[note]
A user borrows an umbrella normally.
GIVEBACK,[date],<serial>,<tenant_name>,<tenant_id>,<tenant_phone>,<tenant_email>,[note]
A user returns an umbrella normally.
OVERDUE,[date],<serial>,<tenant_name>,<tenant_id>,<tenant_phone>,<tenant_email>,[note]
An umbrella is judged overdue by JImbrella's routine process.
ADMIN_MODIFY_DB,[date],[admin_name],<serial>,[column],[past_value],[new_value],[note]
An admin makes modifications to one cell of the database via the web console or API.
If multiple cells are modified, the same number of log entries are written, although
they can be multiplexed in one HTTP request.
"""
def __init__(self, path: str):
self.path = path
def _lock(self) -> int:
"""Create a lockfile for admin log. Operates in the sameway as `Database._lock`."""
try:
f = open(self.path + ".lock", "x")
f.close()
return 1
except FileExistsError:
return 0
def _unlock(self) -> None:
"""Remove lockfile created by `_lock`.
If there is no lockfile, simply ignore.
"""
try:
os.remove(self.path + ".lock")
except FileNotFoundError:
pass
def _read(self) -> list:
"""Deserialize admin log."""
# Create log if it does not yet exist
try:
f = open(self.path, "x")
f.close()
except FileExistsError:
pass
with open(self.path) as f:
reader = csv.reader(f)
logs = []
for row in reader:
event = row[0]
entry = {
"event": event,
"date": datetime.fromisoformat(row[1]),
"note": row[-1],
}
info = {}
if event in ("TAKEAWAY", "GIVEBACK", "OVERDUE"):
info = {
"serial": int(row[2]),
"tenant_name": row[3],
"tenant_id": row[4],
"tenant_phone": row[5],
"tenant_email": row[6],
}
elif event == "ADMIN_MODIFY_DB":
info = {
"admin_name": row[2],
"serial": int(row[3]),
"column": row[4],
"past_value": row[5],
"new_value": row[6],
}
entry.update(info)
logs.append(entry)
f.close()
return logs
def _write(self, logs: list) -> None:
"""Serialize logs.
`logs` is a list of log entries, to be appended at the end of the log file.
"""
# wait until database is locked for this write
while not self._lock():
continue
with open(self.path, "a") as f: # append only
writer = csv.writer(f)
for entry in logs:
event = entry["event"]
line = [event, entry["date"].isoformat()]
info = []
if event in ("TAKEAWAY", "GIVEBACK", "OVERDUE"):
info = [entry["serial"], entry["tenant_name"], entry["tenant_id"], entry["tenant_phone"], entry["tenant_email"]]
elif event == "ADMIN_MODIFY_DB":
info = [entry["admin_name"], entry["serial"], entry["column"], entry["past_value"], entry["new_value"]]
line.extend(info)
line.append(entry["note"])
writer.writerow(line)
f.close()
self._unlock()
|