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],,,,,,[note] A user borrows an umbrella normally. GIVEBACK,[date],,,,,,[note] A user returns an umbrella normally. OVERDUE,[date],,,,,,[note] An umbrella is judged overdue by JImbrella's routine process. ADMIN_MODIFY_DB,[date],[admin_name],,[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()