diff options
-rw-r--r-- | jimbrella/admin.py | 29 | ||||
-rw-r--r-- | jimbrella/umbrellas.py | 59 | ||||
-rw-r--r-- | jimbrella/utils.py | 17 |
3 files changed, 75 insertions, 30 deletions
diff --git a/jimbrella/admin.py b/jimbrella/admin.py index 8a52bb8..799ff66 100644 --- a/jimbrella/admin.py +++ b/jimbrella/admin.py @@ -1,10 +1,13 @@ from flask import Blueprint, request, session, render_template, redirect, url_for, abort from user_agents import parse as user_agent +from datetime import datetime +from dateutil.parser import isoparse from .umbrellas import Umbrellas from .admin_log import AdminLog from .users import Users from .exceptions import * from .config import * +from .utils import human_timedelta, CST bp = Blueprint("admin", __name__, url_prefix="/admin") db = Umbrellas(DATABASE_PATH) @@ -45,8 +48,30 @@ def index(): @bp.route("/umbrellas") def umbrellas(): - umbrellas = db.read() + # sqlite3.Row does not support item assignment, but dict does + umbrellas = [dict(umb) for umb in db.read()] edit = request.args.get("edit") + if edit is not None: + if not edit.isnumeric(): + abort(400) + edit = int(edit) + + for umb in umbrellas: + if umb["id"] == edit and not umb["lent_at"]: + # autofill current time when admin is editing and lent_at is empty + umb["lent_at"] = datetime.now().isoformat(timespec="seconds") + elif umb["lent_at"]: + try: + # web interface provides no timezone so UTC+8 is assumed + lent_at = isoparse(umb["lent_at"]).replace(tzinfo=CST) + umb["lent_at"] = lent_at.isoformat(timespec="seconds") + umb["lent_time_ago"] = human_timedelta( + datetime.now().astimezone(CST) - lent_at + ) + " ago" + except ValueError: + umb["lent_at"] = "Invalid date" + umb["lent_time_ago"] = "" + error = request.args.get("error") template = ( "admin/umbrellas_mobile.html" @@ -56,7 +81,7 @@ def umbrellas(): return render_template( template, umbrellas=umbrellas, - edit=int(edit) if edit is not None and edit.isnumeric() else None, + edit=edit, error=error, ) diff --git a/jimbrella/umbrellas.py b/jimbrella/umbrellas.py index ff88277..2525a31 100644 --- a/jimbrella/umbrellas.py +++ b/jimbrella/umbrellas.py @@ -1,7 +1,8 @@ import sqlite3 -from datetime import datetime, timedelta +from datetime import datetime, timezone, timedelta +from dateutil.parser import isoparse from typing import Union -from .utils import human_datetime, human_timedelta +from .utils import human_datetime, human_timedelta, CST from .config import DUE_HOURS, ADMIN_LOG_PATH from .exceptions import * @@ -27,8 +28,8 @@ class Umbrellas: - tenant_id | string. student or faculty ID. - tenant_phone | string. phone number via which to contact tenant when the lease is due. - tenant_email | string. for future compatibility. always None for the time being. - - lent_at | an ISO 8601 date string "YYYY-MM-DDThh:mm:ss.mmm+08:00" if status is - | "lent" or "overdue. is None otherwise. + - lent_at | an ISO 8601 datetime string if status is "lent" or "overdue", empty string + | otherwise. Schema: CREATE TABLE Umbrellas( @@ -54,7 +55,9 @@ class Umbrellas: if umbid is None: data = db.execute("SELECT * FROM Umbrellas").fetchall() else: - data = db.execute("SELECT * FROM Umbrellas WHERE id = ?", (umbid,)).fetchone() + data = db.execute( + "SELECT * FROM Umbrellas WHERE id = ?", (umbid,) + ).fetchone() db.close() return data @@ -64,13 +67,14 @@ class Umbrellas: Not all fields in an umbrella dict need to be present in `umb`. Only `id` is required. If an optional field is not found, its value is left untouched. If an optional field is - present but its value is an empty string or None, the old datum will become NULL. + present but its value is an empty string, it will be erased. Invalid values are rejected as an UmbrellaValueError. If `status` is not "lent" or "overdue", `tenant_*` and `lent_at` are automatically erased. - `lent_at` may be either an ISO 8601 string or a datetime.datetime object. Must be UTC+8. + `lent_at` may be either an ISO 8601 string or a datetime.datetime object. If no timezone is + supplied, UTC+8 is assumed. Returns a dict of <field>: (<prior_value>, <updated_value>) for each updated field unless its erasure can be inferred. For AdminLog. @@ -82,11 +86,13 @@ class Umbrellas: raise UmbrellaValueError("id") db = sqlite3.connect(self.path) - db.row_factory = sqlite.Row + db.row_factory = sqlite3.Row # check if umbrella #<id> exists in database umbid = umb["id"] - umb_in_db = db.execute("SELECT * FROM Umbrellas WHERE id = ?", (umbid,)).fetchone() + umb_in_db = db.execute( + "SELECT * FROM Umbrellas WHERE id = ?", (umbid,) + ).fetchone() if umb_in_db is None: raise UmbrellaNotFoundError(umbid) @@ -99,12 +105,14 @@ class Umbrellas: if umb_in_db["status"] != umb["status"]: diff["status"] = (umb_in_db["status"], umb["status"]) - db.execute("UPDATE Umbrellas SET status = ? WHERE id = ?", (status, umbid)) + db.execute( + "UPDATE Umbrellas SET status = ? WHERE id = ?", (status, umbid) + ) else: raise UmbrellaValueError("status") if status in ("lent", "overdue"): - for key in ( + for col in ( "tenant_name", "tenant_id", "tenant_phone", @@ -115,16 +123,17 @@ class Umbrellas: diff[col] = (umb_in_db[col], umb[col]) db.execute( - "UPDATE Umbrellas SET ? = ? WHERE id = ?", - (col, - umb[col], - umbid,) + f"UPDATE Umbrellas SET {col} = ? WHERE id = ?", + ( + umb[col], + umbid, + ), ) if "lent_at" in umb: try: # lent_at could be a string, in which case it is parsed - lent_at = datetime.fromisoformat(umb["lent_at"]) + lent_at = isoparse(umb["lent_at"]) except TypeError: # or it could be a datetime.datetime lent_at = umb["lent_at"] @@ -132,16 +141,20 @@ class Umbrellas: # anything else is invalid raise UmbrellaValueError("lent_at") - if lent_at.tzinfo.utcoffset(None).seconds != 28800: - # timezone must be +08:00 - raise UmbrellaValueError("lent_at") + if lent_at.tzinfo is None: + lent_at = lent_at.replace(tzinfo=CST) - diff["lent_at"] = (umb_in_db["lent_at"], lent_at.isoformat(timespec="milliseconds")) + diff["lent_at"] = ( + umb_in_db["lent_at"], + lent_at.isoformat(timespec="milliseconds"), + ) db.execute( "UPDATE Umbrellas SET lent_at = ? WHERE id = ?", - ( lent_at.isoformat(timespec="milliseconds"), - umbid,) + ( + lent_at.isoformat(timespec="milliseconds"), + umbid, + ), ) else: # discard unneeded fields @@ -152,7 +165,7 @@ class Umbrellas: "tenant_email", "lent_at", ): - db.execute("UPDATE Umbrellas SET ? = NULL WHERE id = ?", (col, umbid)) + db.execute(f"UPDATE Umbrellas SET {col} = '' WHERE id = ?", (umbid,)) # now that new data are validated, commit the SQL transaction db.commit() diff --git a/jimbrella/utils.py b/jimbrella/utils.py index fd2c86d..6e02d1a 100644 --- a/jimbrella/utils.py +++ b/jimbrella/utils.py @@ -1,7 +1,4 @@ -from datetime import datetime, timedelta, tzinfo - -# identity function -identity = lambda x: x +from datetime import datetime, timedelta, timezone def human_datetime(time: datetime) -> str: @@ -18,4 +15,14 @@ def human_timedelta(delta: timedelta) -> str: hours = delta.seconds // 3600 minutes = (delta.seconds - (hours * 3600)) // 60 - return days + f"{hours:0>2}:{minutes:0>2}" # zero-pad to two digits + + if hours == 0 and minutes == 0: + return "<1 minute" + + if hours == 0: + return days + f"{minutes}min" + + return days + f"{hours}h {minutes}min" + + +CST = timezone(timedelta(hours=8)) |