import csv import os from datetime import datetime from .lockfile import Lockfile 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. This is why it cannot be a subclass of CsvTable, which expects the columns to be uniform. 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 self.lockfile = Lockfile(self.path) 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 log(self, event: str, entry: dict, date=None, note="") -> None: """Serialize a log, and append it to the end of the log file. Arguments: event | one of the events defined in the class docstring. entry | information pertaining to the log entry. date | if is None, defaults to `datetime.now()`. note | optional note. """ self.lockfile.lock() with open(self.path, "a") as f: # append only writer = csv.writer(f) date = date or datetime.now() line = [event, date.isoformat()] info = [] if event in ("TAKEAWAY", "GIVEBACK", "OVERDUE"): info = [ entry[col] for col in [ "serial", "tenant_name", "tenant_id", "tenant_phone", "tenant_email", ] ] elif event == "ADMIN_MODIFY_DB": info = [ entry[col] for col in [ "admin_name", "serial", "column", "past_value", "new_value", ] ] line.extend(info) line.append(note) writer.writerow(line) f.close() self.lockfile.unlock()