summaryrefslogtreecommitdiff
path: root/jimbrella
diff options
context:
space:
mode:
Diffstat (limited to 'jimbrella')
-rw-r--r--jimbrella/config.py1
-rw-r--r--jimbrella/exceptions.py8
-rw-r--r--jimbrella/lockfile.py33
-rw-r--r--jimbrella/umbrellas.py41
-rw-r--r--jimbrella/utils.py11
5 files changed, 65 insertions, 29 deletions
diff --git a/jimbrella/config.py b/jimbrella/config.py
index e0963f5..81e5d09 100644
--- a/jimbrella/config.py
+++ b/jimbrella/config.py
@@ -23,7 +23,6 @@ JFORM_GIVEBACK_URL = config["jform"]["giveback_url"]
JFORM_BOOKMARK_DIR = JIMBRELLA_DIR / Path(config["jform"]["bookmark_dir"])
DATABASE_PATH = JIMBRELLA_DIR / Path(config["db"]["db_path"])
-USERS_PATH = JIMBRELLA_DIR / Path(config["db"]["users_path"])
DUE_HOURS = config["rules"]["due_hours"]
diff --git a/jimbrella/exceptions.py b/jimbrella/exceptions.py
index 498c6c4..8e7734b 100644
--- a/jimbrella/exceptions.py
+++ b/jimbrella/exceptions.py
@@ -2,11 +2,11 @@
class UmbrellaNotFoundError(Exception):
- """For when an umbrella with required serial is not found in database."""
+ """For when an umbrella with required id is not found in database."""
- def __init__(self, serial):
- self.serial = serial
- self.message = f"No umbrella {serial} found."
+ def __init__(self, umbid):
+ self.id = umbid
+ self.message = f"Umbrella {id} not found."
super().__init__(self.message)
diff --git a/jimbrella/lockfile.py b/jimbrella/lockfile.py
new file mode 100644
index 0000000..987f0e3
--- /dev/null
+++ b/jimbrella/lockfile.py
@@ -0,0 +1,33 @@
+import os
+
+
+class Lockfile:
+ """Prevent unwanted concurrency for file I/O.
+
+ For a file named "<file>", create or remove a lockfile named "<file>.lock".
+
+ When a process is modifying the file, call `lock(). When the modification
+ is done, call `unlock()`.
+ """
+
+ def __init__(self, filepath):
+ self.filepath = str(filepath)
+ self.lockpath = self.filepath + ".lock"
+
+ def lock(self):
+ """Continue attempting to lock until locked."""
+ locked = False
+ while not locked:
+ try:
+ f = open(self.lockpath, "x")
+ f.close()
+ locked = True
+ except FileExistsError:
+ continue
+
+ def unlock(self):
+ """Remove lockfile."""
+ try:
+ os.remove(self.lockpath)
+ except FileNotFoundError:
+ return
diff --git a/jimbrella/umbrellas.py b/jimbrella/umbrellas.py
index e59bb54..6da7184 100644
--- a/jimbrella/umbrellas.py
+++ b/jimbrella/umbrellas.py
@@ -26,7 +26,7 @@ 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+08:00" if status is
+ - lent_at | an ISO 8601 date string "YYYY-MM-DDThh:mm:ss.mmm+08:00" if status is
| "lent" or "overdue. is None otherwise.
Schema:
@@ -43,13 +43,13 @@ class Umbrellas:
self.path = path
def read(self) -> list:
- db = sqlite3.connect(path)
+ db = sqlite3.connect(self.path)
db.row_factory = sqlite.Row
umbrellas = db.execute("SELECT * FROM Umbrellas").fetchall()
db.close()
return umbrellas
- def update(self, umb) -> None:
+ def update(self, umb) -> dict:
"""Update Umbrella table with new data given in `umb`.
Not all fields in an umbrella dict need to be present in `umb`. Only `id` is required.
@@ -61,6 +61,9 @@ class Umbrellas:
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.
+
+ Returns a dict of <field>: (<prior_value>, <updated_value>) for each updated field unless
+ its erasure can be inferred. For AdminLog.
"""
# `id` must be specified.
try:
@@ -68,7 +71,7 @@ class Umbrellas:
except (KeyError, ValueError):
raise UmbrellaValueError("id")
- db = sqlite3.connect(path)
+ db = sqlite3.connect(self.path)
db.row_factory = sqlite.Row
# check if umbrella #<id> exists in database
@@ -77,12 +80,18 @@ class Umbrellas:
if umb_in_db is None:
raise UmbrellaNotFoundError(umbid)
+ diff = {}
+
status = umb_in_db["status"]
- if "status" in umb and umb["status"] in STATUSES:
- status = umb["status"]
- db.execute("UPDATE Umbrellas SET status = ? WHERE id = ?", status, umbid)
- else:
- raise UmbrellaValueError("status")
+ if "status" in umb:
+ if umb["status"] in STATUSES:
+ status = umb["status"]
+ 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)
+ else:
+ raise UmbrellaValueError("status")
if status in ("lent", "overdue"):
for key in (
@@ -92,10 +101,13 @@ class Umbrellas:
"tenant_email",
):
if col in umb:
+ if umb_in_db[col] != umb[col]:
+ diff[col] = (umb_in_db[col], umb[col])
+
db.execute(
"UPDATE Umbrellas SET ? = ? WHERE id = ?",
col,
- umb[col] or None,
+ umb[col],
umbid,
)
@@ -114,6 +126,8 @@ class Umbrellas:
# timezone must be +08:00
raise UmbrellaValueError("lent_at")
+ 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"),
@@ -133,12 +147,13 @@ class Umbrellas:
# now that new data are validated, commit the SQL transaction
db.commit()
db.close()
+ return diff
def take_away(
self, umbid, date, tenant_name, tenant_id, tenant_phone="", tenant_email=""
) -> None:
"""When a user has borrowed an umbrella."""
- db = sqlite3.connect(path)
+ db = sqlite3.connect(self.path)
db.row_factory = sqlite3.Row
umb = db.execute("SELECT * FROM Umbrellas WHERE id = ?", umbid)
db.close()
@@ -166,7 +181,7 @@ class Umbrellas:
`tenant_name` and `tenant_id` are used to verify if the umbrella is returned by the same
person who borrowed it.
"""
- db = sqlite3.connect(path)
+ db = sqlite3.connect(self.path)
db.row_factory = sqlite3.Row
umb = db.execute("SELECT * FROM Umbrellas WHERE id = ?", umbid)
db.close()
@@ -187,7 +202,7 @@ class Umbrellas:
def mark_overdue(self, umbid) -> None:
"""When an umbrella is overdue, change its status to "overdue"."""
- db = sqlite3.connect(path)
+ db = sqlite3.connect(self.path)
db.row_factory = sqlite3.Row
umb = db.execute("SELECT * FROM Umbrellas WHERE id = ?", umbid)
diff --git a/jimbrella/utils.py b/jimbrella/utils.py
index 31fafbc..fd2c86d 100644
--- a/jimbrella/utils.py
+++ b/jimbrella/utils.py
@@ -19,14 +19,3 @@ 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
-
-
-def group_by_key(data: list, key: str) -> dict:
- """Groups a list of dicts by the value of their `key` into a dict of lists of dicts."""
- keys = set([item[key] for item in data])
- # initiate a dict with `keys` as keys and [] as values
- groups = dict.fromkeys(keys, [])
- for k in keys:
- groups[k] = [item for item in data if item[key] == k]
-
- return groups