home · contact · privacy
Add rump Processes, and to those VersionedAttributes.
authorChristian Heller <c.heller@plomlompom.de>
Tue, 26 Mar 2024 00:09:01 +0000 (01:09 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Tue, 26 Mar 2024 00:09:01 +0000 (01:09 +0100)
plomtask/days.py
plomtask/http.py
plomtask/processes.py [new file with mode: 0644]
scripts/init.sql
templates/base.html
templates/process.html [new file with mode: 0644]
templates/processes.html [new file with mode: 0644]

index 04592a311e3a1abb3fc067699c72237059ee5b5a..0622f1de7e1871138cd5f77e51693f0ca224d800 100644 (file)
@@ -39,7 +39,7 @@ class Day:
 
     @classmethod
     def from_table_row(cls, row: Row):
-        """Make new Day from database row."""
+        """Make Day from database row."""
         return cls(row[0], row[1])
 
     @classmethod
index 3ef1721e5aa1b4c9a1ff19856fb99d76c669e4bb..9a68221998ea81deea78a2770ac9a2f0e3bef470 100644 (file)
@@ -7,6 +7,7 @@ from jinja2 import Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader
 from plomtask.days import Day, todays_date
 from plomtask.misc import HandledException
 from plomtask.db import DatabaseConnection
+from plomtask.processes import Process
 
 TEMPLATES_DIR = 'templates'
 
@@ -35,6 +36,15 @@ class TaskHandler(BaseHTTPRequestHandler):
             elif 'day' == site:
                 date = params.get('date', [todays_date()])[0]
                 html = self.do_GET_day(conn, date)
+            elif 'process' == site:
+                id_ = params.get('id', [None])[0]
+                try:
+                    id_ = int(id_) if id_ else None
+                except ValueError as e:
+                    raise HandledException(f'Bad ?id= value: {id_}') from e
+                html = self.do_GET_process(conn, id_)
+            elif 'processes' == site:
+                html = self.do_GET_processes(conn)
             else:
                 raise HandledException('Test!')
             conn.commit()
@@ -54,6 +64,16 @@ class TaskHandler(BaseHTTPRequestHandler):
         day = Day.by_date(conn, date, create=True)
         return self.server.jinja.get_template('day.html').render(day=day)
 
+    def do_GET_process(self, conn: DatabaseConnection, id_: int | None):
+        """Show process of id_."""
+        return self.server.jinja.get_template('process.html').render(
+                process=Process.by_id(conn, id_, create=True))
+
+    def do_GET_processes(self, conn: DatabaseConnection):
+        """Show all Processes."""
+        return self.server.jinja.get_template('processes.html').render(
+                processes=Process.all(conn))
+
     def do_POST(self):
         """Handle any POST request."""
         try:
@@ -64,6 +84,13 @@ class TaskHandler(BaseHTTPRequestHandler):
             if 'day' == site:
                 date = params.get('date', [None])[0]
                 self.do_POST_day(conn, date, postvars)
+            elif 'process' == site:
+                id_ = params.get('id', [None])[0]
+                try:
+                    id_ = int(id_) if id_ else None
+                except ValueError as e:
+                    raise HandledException(f'Bad ?id= value: {id_}') from e
+                self.do_POST_process(conn, id_, postvars)
             conn.commit()
             conn.close()
             self._redirect('/')
@@ -76,6 +103,20 @@ class TaskHandler(BaseHTTPRequestHandler):
         day.comment = postvars['comment'][0]
         day.save(conn)
 
+    def do_POST_process(self, conn: DatabaseConnection, id_: int | None,
+                        postvars: dict):
+        """Update or insert Process of id_ and fields defined in postvars."""
+        process = Process.by_id(conn, id_, create=True)
+        if process:
+            process.title.set(postvars['title'][0])
+            process.description.set(postvars['description'][0])
+            effort = postvars['effort'][0]
+            try:
+                process.effort.set(float(effort))
+            except ValueError as e:
+                raise HandledException(f'Bad effort value: {effort}') from e
+            process.save(conn)
+
     def _init_handling(self):
         conn = DatabaseConnection(self.server.db)
         parsed_url = urlparse(self.path)
diff --git a/plomtask/processes.py b/plomtask/processes.py
new file mode 100644 (file)
index 0000000..8a5bf64
--- /dev/null
@@ -0,0 +1,115 @@
+"""Collecting Processes and Process-related items."""
+from __future__ import annotations
+from sqlite3 import Row
+from datetime import datetime
+from plomtask.db import DatabaseConnection
+
+
+class Process:
+    """Template for, and metadata for, Todos, and their arrangements."""
+
+    def __init__(self, id_: int | None) -> None:
+        self.id_ = id_ if id_ != 0 else None  # to avoid DB-confusing rowid=0
+        self.title = VersionedAttribute(self, 'title', 'UNNAMED')
+        self.description = VersionedAttribute(self, 'description', '')
+        self.effort = VersionedAttribute(self, 'effort', 1.0)
+
+    @classmethod
+    def from_table_row(cls, row: Row) -> Process:
+        """Make Process from database row, with empty VersionedAttributes."""
+        return cls(row[0])
+
+    @classmethod
+    def all(cls, db_conn: DatabaseConnection) -> list[Process]:
+        """Collect all Processes and their connected VersionedAttributes."""
+        processes = {}
+        for row in db_conn.exec('SELECT * FROM processes'):
+            process = cls.from_table_row(row)
+            processes[process.id_] = process
+        for row in db_conn.exec('SELECT * FROM process_titles'):
+            processes[row[0]].title.history[row[1]] = row[2]
+        for row in db_conn.exec('SELECT * FROM process_descriptions'):
+            processes[row[0]].description.history[row[1]] = row[2]
+        for row in db_conn.exec('SELECT * FROM process_efforts'):
+            processes[row[0]].effort.history[row[1]] = row[2]
+        return list(processes.values())
+
+    @classmethod
+    def by_id(cls, db_conn: DatabaseConnection,
+              id_: int | None, create: bool = False) -> Process | None:
+        """Collect all Processes and their connected VersionedAttributes."""
+        process = None
+        for row in db_conn.exec('SELECT * FROM processes '
+                                'WHERE id = ?', (id_,)):
+            process = cls(row[0])
+            break
+        if create and not process:
+            process = Process(id_)
+        if process:
+            for row in db_conn.exec('SELECT * FROM process_titles '
+                                    'WHERE process_id = ?', (process.id_,)):
+                process.title.history[row[1]] = row[2]
+            for row in db_conn.exec('SELECT * FROM process_descriptions '
+                                    'WHERE process_id = ?', (process.id_,)):
+                process.description.history[row[1]] = row[2]
+            for row in db_conn.exec('SELECT * FROM process_efforts '
+                                    'WHERE process_id = ?', (process.id_,)):
+                process.effort.history[row[1]] = row[2]
+        return process
+
+    def save(self, db_conn: DatabaseConnection) -> None:
+        """Add (or re-write) self and connected VersionedAttributes to DB."""
+        cursor = db_conn.exec('REPLACE INTO processes VALUES (?)', (self.id_,))
+        self.id_ = cursor.lastrowid
+        self.title.save(db_conn)
+        self.description.save(db_conn)
+        self.effort.save(db_conn)
+
+
+class VersionedAttribute:
+    """Attributes whose values are recorded as a timestamped history."""
+
+    def __init__(self,
+                 parent: Process, name: str, default: str | float) -> None:
+        self.parent = parent
+        self.name = name
+        self.default = default
+        self.history: dict[str, str | float] = {}
+
+    @property
+    def _newest_timestamp(self) -> str:
+        """Return most recent timestamp."""
+        return sorted(self.history.keys())[-1]
+
+    @property
+    def newest(self) -> str | float:
+        """Return most recent value, or self.default if self.history empty."""
+        if 0 == len(self.history):
+            return self.default
+        return self.history[self._newest_timestamp]
+
+    def set(self, value: str | float) -> None:
+        """Add to self.history if and only if not same value as newest one."""
+        if 0 == len(self.history) \
+                or value != self.history[self._newest_timestamp]:
+            self.history[datetime.now().strftime('%Y-%m-%d %H:%M:%S')] = value
+
+    def at(self, queried_time: str) -> str | float:
+        """Retrieve value of timestamp nearest queried_time from the past."""
+        sorted_timestamps = sorted(self.history.keys())
+        if 0 == len(sorted_timestamps):
+            return self.default
+        selected_timestamp = sorted_timestamps[0]
+        for timestamp in sorted_timestamps[1:]:
+            if timestamp > queried_time:
+                break
+            selected_timestamp = timestamp
+        return self.history[selected_timestamp]
+
+    def save(self, db_conn: DatabaseConnection) -> None:
+        """Save as self.history entries, but first wipe old ones."""
+        db_conn.exec(f'DELETE FROM process_{self.name}s WHERE process_id = ?',
+                     (self.parent.id_,))
+        for timestamp, value in self.history.items():
+            db_conn.exec(f'INSERT INTO process_{self.name}s VALUES (?, ?, ?)',
+                         (self.parent.id_, timestamp, value))
index cc68fb6302ef0b11e690e7596d66282603f336d5..a98e8286251c17422108a556f22054fd4a26c9f4 100644 (file)
@@ -2,3 +2,27 @@ CREATE TABLE days (
     date TEXT PRIMARY KEY,
     comment TEXT NOT NULL
 );
+CREATE TABLE process_descriptions (
+    process_id INTEGER NOT NULL,
+    timestamp TEXT NOT NULL,
+    description TEXT NOT NULL,
+    PRIMARY KEY (process_id, timestamp),
+    FOREIGN KEY (process_id) REFERENCES processes(id)
+);
+CREATE TABLE process_efforts (
+    process_id INTEGER NOT NULL,
+    timestamp TEXT NOT NULL,
+    effort REAL NOT NULL,
+    PRIMARY KEY (process_id, timestamp),
+    FOREIGN KEY (process_id) REFERENCES processes(id)
+);
+CREATE TABLE process_titles (
+    process_id INTEGER NOT NULL,
+    timestamp TEXT NOT NULL,
+    title TEXT NOT NULL,
+    PRIMARY KEY (process_id, timestamp),
+    FOREIGN KEY (process_id) REFERENCES processes(id)
+);
+CREATE TABLE processes (
+    id INTEGER PRIMARY KEY
+);
index 0068d1160a1c57b36a788763444e08a597b560e3..e030a021f4a61925f9eb7ec25c549aec0c20fd09 100644 (file)
@@ -2,6 +2,7 @@
 <html>
 <meta charset="UTF-8">
 <body>
+<a href="processes">processes</a>
 <a href="calendar">calendar</a>
 <hr>
 {% block content %}
diff --git a/templates/process.html b/templates/process.html
new file mode 100644 (file)
index 0000000..1743936
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<h3>Process</h3>
+<form action="process?id={{process.id_ or ''}}" method="POST">
+title: <input name="title" value="{{process.title.newest|e}}" />
+description: <input name="description" value="{{process.description.newest|e}}" />
+default effort: <input name="effort" type="number" step=0.1 value={{process.effort.newest}} />
+<input type="submit" value="OK" />
+</form>
+{% endblock %}
+
+
diff --git a/templates/processes.html b/templates/processes.html
new file mode 100644 (file)
index 0000000..37de8f8
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<ul>
+{% for process in processes %}
+<li><a href="process?id={{process.id_}}">{{process.title.newest}}</a>
+{% endfor %}
+</ul>
+{% endblock %}
+