home · contact · privacy
Refactor total-effort day summation.
[plomtask] / plomtask / http.py
index 583203e6eba7a69bbee893d502b13bd132eb69d9..5ad3dd43332e3106d66a0327158d4aebcf690bdb 100644 (file)
@@ -1,7 +1,7 @@
 """Web server stuff."""
 from __future__ import annotations
 from dataclasses import dataclass
-from typing import Any
+from typing import Any, Callable
 from base64 import b64encode, b64decode
 from http.server import BaseHTTPRequestHandler
 from http.server import HTTPServer
@@ -20,16 +20,6 @@ from plomtask.todos import Todo
 TEMPLATES_DIR = 'templates'
 
 
-@dataclass
-class TodoStepsNode:
-    """Collect what's useful for Todo steps tree display."""
-    id_: int
-    todo: Todo | None
-    process: Process | None
-    children: list[TodoStepsNode]
-    fillable: bool = False
-
-
 class TaskServer(HTTPServer):
     """Variant of HTTPServer that knows .jinja as Jinja Environment."""
 
@@ -116,26 +106,84 @@ class TaskHandler(BaseHTTPRequestHandler):
     _form_data: InputsParser
     _params: InputsParser
 
-    def do_GET(self) -> None:
-        """Handle any GET request."""
-        try:
-            self._init_handling()
-            if hasattr(self, f'do_GET_{self._site}'):
-                template = f'{self._site}.html'
-                ctx = getattr(self, f'do_GET_{self._site}')()
-                html = self.server.jinja.get_template(template).render(**ctx)
-                self._send_html(html)
-            elif '' == self._site:
-                self._redirect('/day')
-            else:
-                raise NotFoundException(f'Unknown page: /{self._site}')
-        except HandledException as error:
-            self._send_msg(error, code=error.http_code)
-        finally:
-            self.conn.close()
+    def _send_html(self, html: str, code: int = 200) -> None:
+        """Send HTML as proper HTTP response."""
+        self.send_response(code)
+        self.end_headers()
+        self.wfile.write(bytes(html, 'utf-8'))
+
+    @staticmethod
+    def _request_wrapper(http_method: str, not_found_msg: str
+                         ) -> Callable[..., Callable[[TaskHandler], None]]:
+        def decorator(f: Callable[..., str | None]
+                      ) -> Callable[[TaskHandler], None]:
+            def wrapper(self: TaskHandler) -> None:
+                # pylint: disable=protected-access
+                # (because pylint here fails to detect the use of wrapper as a
+                # method to self with respective access privileges)
+                try:
+                    self.conn = DatabaseConnection(self.server.db)
+                    parsed_url = urlparse(self.path)
+                    self._site = path_split(parsed_url.path)[1]
+                    params = parse_qs(parsed_url.query, strict_parsing=True)
+                    self._params = InputsParser(params, False)
+                    handler_name = f'do_{http_method}_{self._site}'
+                    if hasattr(self, handler_name):
+                        handler = getattr(self, handler_name)
+                        redir_target = f(self, handler)
+                        if redir_target:
+                            self.send_response(302)
+                            self.send_header('Location', redir_target)
+                            self.end_headers()
+                    else:
+                        msg = f'{not_found_msg}: {self._site}'
+                        raise NotFoundException(msg)
+                except HandledException as error:
+                    html = self.server.jinja.\
+                            get_template('msg.html').render(msg=error)
+                    self._send_html(html, error.http_code)
+                finally:
+                    self.conn.close()
+            return wrapper
+        return decorator
+
+    @_request_wrapper('GET', 'Unknown page')
+    def do_GET(self, handler: Callable[[], str | dict[str, object]]
+               ) -> str | None:
+        """Render page with result of handler, or redirect if result is str."""
+        template = f'{self._site}.html'
+        ctx_or_redir = handler()
+        if str == type(ctx_or_redir):
+            return ctx_or_redir
+        assert isinstance(ctx_or_redir, dict)
+        html = self.server.jinja.get_template(template).render(**ctx_or_redir)
+        self._send_html(html)
+        return None
+
+    @_request_wrapper('POST', 'Unknown POST target')
+    def do_POST(self, handler: Callable[[], str]) -> str:
+        """Handle POST with handler, prepare redirection to result."""
+        length = int(self.headers['content-length'])
+        postvars = parse_qs(self.rfile.read(length).decode(),
+                            keep_blank_values=True, strict_parsing=True)
+        self._form_data = InputsParser(postvars)
+        redir_target = handler()
+        self.conn.commit()
+        return redir_target
+
+    # GET handlers
+
+    def do_GET_(self) -> str:
+        """Return redirect target on GET /."""
+        return '/day'
 
     def _do_GET_calendar(self) -> dict[str, object]:
-        """Show Days from ?start= to ?end=."""
+        """Show Days from ?start= to ?end=.
+
+        Both .do_GET_calendar and .do_GET_calendar_txt refer to this to do the
+        same, the only difference being the HTML template they are rendered to,
+        which .do_GET selects from their method name.
+        """
         start = self._params.get_str('start')
         end = self._params.get_str('end')
         if not end:
@@ -161,9 +209,6 @@ class TaskHandler(BaseHTTPRequestHandler):
         date = self._params.get_str('date', date_in_n_days(0))
         make_type = self._params.get_str('make_type')
         todays_todos = Todo.by_date(self.conn, date)
-        total_effort = 0.0
-        for todo in todays_todos:
-            total_effort += todo.performed_effort
         conditions_present = []
         enablers_for = {}
         disablers_for = {}
@@ -181,7 +226,7 @@ class TaskHandler(BaseHTTPRequestHandler):
         top_nodes = [t.get_step_tree(seen_todos)
                      for t in todays_todos if not t.parents]
         return {'day': Day.by_id(self.conn, date, create=True),
-                'total_effort': total_effort,
+                'total_effort': Todo.total_effort_at_date(self.conn, date),
                 'top_nodes': top_nodes,
                 'make_type': make_type,
                 'enablers_for': enablers_for,
@@ -192,6 +237,15 @@ class TaskHandler(BaseHTTPRequestHandler):
     def do_GET_todo(self) -> dict[str, object]:
         """Show single Todo of ?id=."""
 
+        @dataclass
+        class TodoStepsNode:
+            """Collect what's useful for Todo steps tree display."""
+            id_: int
+            todo: Todo | None
+            process: Process | None
+            children: list[TodoStepsNode]  # pylint: disable=undefined-variable
+            fillable: bool = False
+
         def walk_process_steps(id_: int,
                                process_step_nodes: list[ProcessStepsNode],
                                steps_nodes: list[TodoStepsNode]) -> None:
@@ -390,25 +444,20 @@ class TaskHandler(BaseHTTPRequestHandler):
             processes.sort(key=lambda p: p.title.newest)
         return {'processes': processes, 'sort_by': sort_by, 'pattern': pattern}
 
-    def do_POST(self) -> None:
-        """Handle any POST request."""
-        try:
-            self._init_handling()
-            length = int(self.headers['content-length'])
-            postvars = parse_qs(self.rfile.read(length).decode(),
-                                keep_blank_values=True, strict_parsing=True)
-            self._form_data = InputsParser(postvars)
-            if hasattr(self, f'do_POST_{self._site}'):
-                redir_target = getattr(self, f'do_POST_{self._site}')()
-                self.conn.commit()
-            else:
-                msg = f'Page not known as POST target: /{self._site}'
-                raise NotFoundException(msg)
-            self._redirect(redir_target)
-        except HandledException as error:
-            self._send_msg(error, code=error.http_code)
-        finally:
-            self.conn.close()
+    # POST handlers
+
+    def _change_versioned_timestamps(self, cls: Any, attr_name: str) -> str:
+        """Update history timestamps for VersionedAttribute."""
+        id_ = self._params.get_int_or_none('id')
+        item = cls.by_id(self.conn, id_)
+        attr = getattr(item, attr_name)
+        for k, v in self._form_data.get_first_strings_starting('at:').items():
+            old = k[3:]
+            if old[19:] != v:
+                attr.reset_timestamp(old, f'{v}.0')
+        attr.save(self.conn)
+        cls_name = cls.__name__.lower()
+        return f'/{cls_name}_{attr_name}s?id={item.id_}'
 
     def do_POST_day(self) -> str:
         """Update or insert Day of date and Todos mapped to it."""
@@ -500,30 +549,17 @@ class TaskHandler(BaseHTTPRequestHandler):
             condition.save(self.conn)
         return f'/todo?id={todo.id_}'
 
-    def _do_POST_versioned_timestamps(self, cls: Any, attr_name: str) -> str:
-        """Update history timestamps for VersionedAttribute."""
-        id_ = self._params.get_int_or_none('id')
-        item = cls.by_id(self.conn, id_)
-        attr = getattr(item, attr_name)
-        for k, v in self._form_data.get_first_strings_starting('at:').items():
-            old = k[3:]
-            if old[19:] != v:
-                attr.reset_timestamp(old, f'{v}.0')
-        attr.save(self.conn)
-        cls_name = cls.__name__.lower()
-        return f'/{cls_name}_{attr_name}s?id={item.id_}'
-
     def do_POST_process_descriptions(self) -> str:
         """Update history timestamps for Process.description."""
-        return self._do_POST_versioned_timestamps(Process, 'description')
+        return self._change_versioned_timestamps(Process, 'description')
 
     def do_POST_process_efforts(self) -> str:
         """Update history timestamps for Process.effort."""
-        return self._do_POST_versioned_timestamps(Process, 'effort')
+        return self._change_versioned_timestamps(Process, 'effort')
 
     def do_POST_process_titles(self) -> str:
         """Update history timestamps for Process.title."""
-        return self._do_POST_versioned_timestamps(Process, 'title')
+        return self._change_versioned_timestamps(Process, 'title')
 
     def do_POST_process(self) -> str:
         """Update or insert Process of ?id= and fields defined in postvars."""
@@ -597,11 +633,11 @@ class TaskHandler(BaseHTTPRequestHandler):
 
     def do_POST_condition_descriptions(self) -> str:
         """Update history timestamps for Condition.description."""
-        return self._do_POST_versioned_timestamps(Condition, 'description')
+        return self._change_versioned_timestamps(Condition, 'description')
 
     def do_POST_condition_titles(self) -> str:
         """Update history timestamps for Condition.title."""
-        return self._do_POST_versioned_timestamps(Condition, 'title')
+        return self._change_versioned_timestamps(Condition, 'title')
 
     def do_POST_condition(self) -> str:
         """Update/insert Condition of ?id= and fields defined in postvars."""
@@ -616,28 +652,3 @@ class TaskHandler(BaseHTTPRequestHandler):
         condition.description.set(self._form_data.get_str('description'))
         condition.save(self.conn)
         return f'/condition?id={condition.id_}'
-
-    def _init_handling(self) -> None:
-        """Our own __init__, as we're not supposed to use the original."""
-        self.conn = DatabaseConnection(self.server.db)
-        parsed_url = urlparse(self.path)
-        self._site = path_split(parsed_url.path)[1]
-        params = parse_qs(parsed_url.query, strict_parsing=True)
-        self._params = InputsParser(params, False)
-
-    def _redirect(self, target: str) -> None:
-        """Redirect to target."""
-        self.send_response(302)
-        self.send_header('Location', target)
-        self.end_headers()
-
-    def _send_html(self, html: str, code: int = 200) -> None:
-        """Send HTML as proper HTTP response."""
-        self.send_response(code)
-        self.end_headers()
-        self.wfile.write(bytes(html, 'utf-8'))
-
-    def _send_msg(self, msg: Exception, code: int = 400) -> None:
-        """Send message in HTML formatting as HTTP response."""
-        html = self.server.jinja.get_template('msg.html').render(msg=msg)
-        self._send_html(html, code)