home · contact · privacy
Refactor request handler delete or retrieving items on POST.
[plomtask] / plomtask / http.py
index 55473ea85410f840a6bbfe32e41ba245b1a85202..18b77a6f0a97ccea4718261f69e23be7ab9d3866 100644 (file)
@@ -11,13 +11,12 @@ from os.path import split as path_split
 from jinja2 import Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader
 from plomtask.dating import date_in_n_days
 from plomtask.days import Day
 from jinja2 import Environment as JinjaEnv, FileSystemLoader as JinjaFSLoader
 from plomtask.dating import date_in_n_days
 from plomtask.days import Day
-from plomtask.exceptions import HandledException, BadFormatException, \
-        NotFoundException
+from plomtask.exceptions import (HandledException, BadFormatException,
+                                 NotFoundException)
 from plomtask.db import DatabaseConnection, DatabaseFile
 from plomtask.processes import Process, ProcessStep, ProcessStepsNode
 from plomtask.conditions import Condition
 from plomtask.todos import Todo
 from plomtask.db import DatabaseConnection, DatabaseFile
 from plomtask.processes import Process, ProcessStep, ProcessStepsNode
 from plomtask.conditions import Condition
 from plomtask.todos import Todo
-from plomtask.db import BaseModel
 
 TEMPLATES_DIR = 'templates'
 
 
 TEMPLATES_DIR = 'templates'
 
@@ -42,15 +41,25 @@ class TaskServer(HTTPServer):
     def ctx_to_json(ctx: dict[str, object]) -> str:
         """Render ctx into JSON string."""
         def walk_ctx(node: object) -> Any:
     def ctx_to_json(ctx: dict[str, object]) -> str:
         """Render ctx into JSON string."""
         def walk_ctx(node: object) -> Any:
-            if isinstance(node, BaseModel):
+            if hasattr(node, 'as_dict_into_reference'):
+                if hasattr(node, 'id_') and node.id_ is not None:
+                    return node.as_dict_into_reference(library)
+            if hasattr(node, 'as_dict'):
                 return node.as_dict
             if isinstance(node, (list, tuple)):
                 return [walk_ctx(x) for x in node]
                 return node.as_dict
             if isinstance(node, (list, tuple)):
                 return [walk_ctx(x) for x in node]
+            if isinstance(node, dict):
+                d = {}
+                for k, v in node.items():
+                    d[k] = walk_ctx(v)
+                return d
             if isinstance(node, HandledException):
                 return str(node)
             return node
             if isinstance(node, HandledException):
                 return str(node)
             return node
+        library: dict[str, dict[str | int, object]] = {}
         for k, v in ctx.items():
             ctx[k] = walk_ctx(v)
         for k, v in ctx.items():
             ctx[k] = walk_ctx(v)
+        ctx['_library'] = library
         return json_dumps(ctx)
 
     def render(self, ctx: dict[str, object], tmpl_name: str = '') -> str:
         return json_dumps(ctx)
 
     def render(self, ctx: dict[str, object], tmpl_name: str = '') -> str:
@@ -143,7 +152,7 @@ class TaskHandler(BaseHTTPRequestHandler):
                    tmpl_name: str,
                    code: int = 200
                    ) -> None:
                    tmpl_name: str,
                    code: int = 200
                    ) -> None:
-        """Send HTML as proper HTTP response."""
+        """Send ctx as proper HTTP response."""
         body = self.server.render(ctx, tmpl_name)
         self.send_response(code)
         for header_tuple in self.server.headers:
         body = self.server.render(ctx, tmpl_name)
         self.send_response(code)
         for header_tuple in self.server.headers:
@@ -244,7 +253,7 @@ class TaskHandler(BaseHTTPRequestHandler):
     def do_GET_day(self) -> dict[str, object]:
         """Show single Day of ?date=."""
         date = self._params.get_str('date', date_in_n_days(0))
     def do_GET_day(self) -> dict[str, object]:
         """Show single Day of ?date=."""
         date = self._params.get_str('date', date_in_n_days(0))
-        day = Day.by_id(self.conn, date, create=True)
+        day = Day.by_id_or_create(self.conn, date)
         make_type = self._params.get_str('make_type')
         conditions_present = []
         enablers_for = {}
         make_type = self._params.get_str('make_type')
         conditions_present = []
         enablers_for = {}
@@ -335,7 +344,8 @@ class TaskHandler(BaseHTTPRequestHandler):
         adoptables: dict[int, list[Todo]] = {}
         any_adoptables = [Todo.by_id(self.conn, t.id_)
                           for t in Todo.by_date(self.conn, todo.date)
         adoptables: dict[int, list[Todo]] = {}
         any_adoptables = [Todo.by_id(self.conn, t.id_)
                           for t in Todo.by_date(self.conn, todo.date)
-                          if t != todo]
+                          if t.id_ is not None
+                          and t != todo]
         for id_ in collect_adoptables_keys(steps_todo_to_process):
             adoptables[id_] = [t for t in any_adoptables
                                if t.process.id_ == id_]
         for id_ in collect_adoptables_keys(steps_todo_to_process):
             adoptables[id_] = [t for t in any_adoptables
                                if t.process.id_ == id_]
@@ -400,7 +410,7 @@ class TaskHandler(BaseHTTPRequestHandler):
     def do_GET_condition(self) -> dict[str, object]:
         """Show Condition of ?id=."""
         id_ = self._params.get_int_or_none('id')
     def do_GET_condition(self) -> dict[str, object]:
         """Show Condition of ?id=."""
         id_ = self._params.get_int_or_none('id')
-        c = Condition.by_id(self.conn, id_, create=True)
+        c = Condition.by_id_or_create(self.conn, id_)
         ps = Process.all(self.conn)
         return {'condition': c, 'is_new': c.id_ is None,
                 'enabled_processes': [p for p in ps if c in p.conditions],
         ps = Process.all(self.conn)
         return {'condition': c, 'is_new': c.id_ is None,
                 'enabled_processes': [p for p in ps if c in p.conditions],
@@ -410,20 +420,20 @@ class TaskHandler(BaseHTTPRequestHandler):
 
     def do_GET_condition_titles(self) -> dict[str, object]:
         """Show title history of Condition of ?id=."""
 
     def do_GET_condition_titles(self) -> dict[str, object]:
         """Show title history of Condition of ?id=."""
-        id_ = self._params.get_int_or_none('id')
+        id_ = self._params.get_int('id')
         condition = Condition.by_id(self.conn, id_)
         return {'condition': condition}
 
     def do_GET_condition_descriptions(self) -> dict[str, object]:
         """Show description historys of Condition of ?id=."""
         condition = Condition.by_id(self.conn, id_)
         return {'condition': condition}
 
     def do_GET_condition_descriptions(self) -> dict[str, object]:
         """Show description historys of Condition of ?id=."""
-        id_ = self._params.get_int_or_none('id')
+        id_ = self._params.get_int('id')
         condition = Condition.by_id(self.conn, id_)
         return {'condition': condition}
 
     def do_GET_process(self) -> dict[str, object]:
         """Show Process of ?id=."""
         id_ = self._params.get_int_or_none('id')
         condition = Condition.by_id(self.conn, id_)
         return {'condition': condition}
 
     def do_GET_process(self) -> dict[str, object]:
         """Show Process of ?id=."""
         id_ = self._params.get_int_or_none('id')
-        process = Process.by_id(self.conn, id_, create=True)
+        process = Process.by_id_or_create(self.conn, id_)
         title_64 = self._params.get_str('title_b64')
         if title_64:
             title = b64decode(title_64.encode()).decode()
         title_64 = self._params.get_str('title_b64')
         if title_64:
             title = b64decode(title_64.encode()).decode()
@@ -443,19 +453,19 @@ class TaskHandler(BaseHTTPRequestHandler):
 
     def do_GET_process_titles(self) -> dict[str, object]:
         """Show title history of Process of ?id=."""
 
     def do_GET_process_titles(self) -> dict[str, object]:
         """Show title history of Process of ?id=."""
-        id_ = self._params.get_int_or_none('id')
+        id_ = self._params.get_int('id')
         process = Process.by_id(self.conn, id_)
         return {'process': process}
 
     def do_GET_process_descriptions(self) -> dict[str, object]:
         """Show description historys of Process of ?id=."""
         process = Process.by_id(self.conn, id_)
         return {'process': process}
 
     def do_GET_process_descriptions(self) -> dict[str, object]:
         """Show description historys of Process of ?id=."""
-        id_ = self._params.get_int_or_none('id')
+        id_ = self._params.get_int('id')
         process = Process.by_id(self.conn, id_)
         return {'process': process}
 
     def do_GET_process_efforts(self) -> dict[str, object]:
         """Show default effort history of Process of ?id=."""
         process = Process.by_id(self.conn, id_)
         return {'process': process}
 
     def do_GET_process_efforts(self) -> dict[str, object]:
         """Show default effort history of Process of ?id=."""
-        id_ = self._params.get_int_or_none('id')
+        id_ = self._params.get_int('id')
         process = Process.by_id(self.conn, id_)
         return {'process': process}
 
         process = Process.by_id(self.conn, id_)
         return {'process': process}
 
@@ -485,6 +495,32 @@ class TaskHandler(BaseHTTPRequestHandler):
 
     # POST handlers
 
 
     # POST handlers
 
+    @staticmethod
+    def _delete_or_post(target_class: Any, redir_target: str = '/'
+                        ) -> Callable[..., Callable[[TaskHandler], str]]:
+        def decorator(f: Callable[..., str]
+                      ) -> Callable[[TaskHandler], str]:
+            def wrapper(self: TaskHandler) -> str:
+                # pylint: disable=protected-access
+                # (because pylint here fails to detect the use of wrapper as a
+                # method to self with respective access privileges)
+                id_ = self._params.get_int_or_none('id')
+                for _ in self._form_data.get_all_str('delete'):
+                    if id_ is None:
+                        msg = 'trying to delete non-saved ' +\
+                                f'{target_class.__name__}'
+                        raise NotFoundException(msg)
+                    item = target_class.by_id(self.conn, id_)
+                    item.remove(self.conn)
+                    return redir_target
+                if target_class.can_create_by_id:
+                    item = target_class.by_id_or_create(self.conn, id_)
+                else:
+                    item = target_class.by_id(self.conn, id_)
+                return f(self, item)
+            return wrapper
+        return decorator
+
     def _change_versioned_timestamps(self, cls: Any, attr_name: str) -> str:
         """Update history timestamps for VersionedAttribute."""
         id_ = self._params.get_int_or_none('id')
     def _change_versioned_timestamps(self, cls: Any, attr_name: str) -> str:
         """Update history timestamps for VersionedAttribute."""
         id_ = self._params.get_int_or_none('id')
@@ -495,13 +531,12 @@ class TaskHandler(BaseHTTPRequestHandler):
             if old[19:] != v:
                 attr.reset_timestamp(old, f'{v}.0')
         attr.save(self.conn)
             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_}'
+        return f'/{cls.name_lowercase()}_{attr_name}s?id={item.id_}'
 
     def do_POST_day(self) -> str:
         """Update or insert Day of date and Todos mapped to it."""
         date = self._params.get_str('date')
 
     def do_POST_day(self) -> str:
         """Update or insert Day of date and Todos mapped to it."""
         date = self._params.get_str('date')
-        day = Day.by_id(self.conn, date, create=True)
+        day = Day.by_id_or_create(self.conn, date)
         day.comment = self._form_data.get_str('day_comment')
         day.save(self.conn)
         make_type = self._form_data.get_str('make_type')
         day.comment = self._form_data.get_str('day_comment')
         day.save(self.conn)
         make_type = self._form_data.get_str('make_type')
@@ -525,16 +560,9 @@ class TaskHandler(BaseHTTPRequestHandler):
             todo.save(self.conn)
         return f'/day?date={date}&make_type={make_type}'
 
             todo.save(self.conn)
         return f'/day?date={date}&make_type={make_type}'
 
-    def do_POST_todo(self) -> str:
+    @_delete_or_post(Todo, '/')
+    def do_POST_todo(self, todo: Todo) -> str:
         """Update Todo and its children."""
         """Update Todo and its children."""
-        # pylint: disable=too-many-locals
-        # pylint: disable=too-many-branches
-        id_ = self._params.get_int('id')
-        for _ in self._form_data.get_all_str('delete'):
-            todo = Todo .by_id(self.conn, id_)
-            todo.remove(self.conn)
-            return '/'
-        todo = Todo.by_id(self.conn, id_)
         adopted_child_ids = self._form_data.get_all_int('adopt')
         processes_to_make_full = self._form_data.get_all_int('make_full')
         processes_to_make_empty = self._form_data.get_all_int('make_empty')
         adopted_child_ids = self._form_data.get_all_int('adopt')
         processes_to_make_full = self._form_data.get_all_int('make_full')
         processes_to_make_empty = self._form_data.get_all_int('make_empty')
@@ -570,8 +598,8 @@ class TaskHandler(BaseHTTPRequestHandler):
         effort = self._form_data.get_str('effort', ignore_strict=True)
         todo.effort = float(effort) if effort else None
         todo.set_conditions(self.conn,
         effort = self._form_data.get_str('effort', ignore_strict=True)
         todo.effort = float(effort) if effort else None
         todo.set_conditions(self.conn,
-                            self._form_data.get_all_int('condition'))
-        todo.set_blockers(self.conn, self._form_data.get_all_int('blocker'))
+                            self._form_data.get_all_int('conditions'))
+        todo.set_blockers(self.conn, self._form_data.get_all_int('blockers'))
         todo.set_enables(self.conn, self._form_data.get_all_int('enables'))
         todo.set_disables(self.conn, self._form_data.get_all_int('disables'))
         todo.is_done = len(self._form_data.get_all_str('done')) > 0
         todo.set_enables(self.conn, self._form_data.get_all_int('enables'))
         todo.set_disables(self.conn, self._form_data.get_all_int('disables'))
         todo.is_done = len(self._form_data.get_all_str('done')) > 0
@@ -592,21 +620,16 @@ class TaskHandler(BaseHTTPRequestHandler):
         """Update history timestamps for Process.title."""
         return self._change_versioned_timestamps(Process, 'title')
 
         """Update history timestamps for Process.title."""
         return self._change_versioned_timestamps(Process, 'title')
 
-    def do_POST_process(self) -> str:
+    @_delete_or_post(Process, '/processes')
+    def do_POST_process(self, process: Process) -> str:
         """Update or insert Process of ?id= and fields defined in postvars."""
         """Update or insert Process of ?id= and fields defined in postvars."""
-        # pylint: disable=too-many-branches
-        id_ = self._params.get_int_or_none('id')
-        for _ in self._form_data.get_all_str('delete'):
-            process = Process.by_id(self.conn, id_)
-            process.remove(self.conn)
-            return '/processes'
-        process = Process.by_id(self.conn, id_, create=True)
         process.title.set(self._form_data.get_str('title'))
         process.description.set(self._form_data.get_str('description'))
         process.effort.set(self._form_data.get_float('effort'))
         process.set_conditions(self.conn,
         process.title.set(self._form_data.get_str('title'))
         process.description.set(self._form_data.get_str('description'))
         process.effort.set(self._form_data.get_float('effort'))
         process.set_conditions(self.conn,
-                               self._form_data.get_all_int('condition'))
-        process.set_blockers(self.conn, self._form_data.get_all_int('blocker'))
+                               self._form_data.get_all_int('conditions'))
+        process.set_blockers(self.conn,
+                             self._form_data.get_all_int('blockers'))
         process.set_enables(self.conn, self._form_data.get_all_int('enables'))
         process.set_disables(self.conn,
                              self._form_data.get_all_int('disables'))
         process.set_enables(self.conn, self._form_data.get_all_int('enables'))
         process.set_disables(self.conn,
                              self._form_data.get_all_int('disables'))
@@ -669,15 +692,10 @@ class TaskHandler(BaseHTTPRequestHandler):
         """Update history timestamps for Condition.title."""
         return self._change_versioned_timestamps(Condition, 'title')
 
         """Update history timestamps for Condition.title."""
         return self._change_versioned_timestamps(Condition, 'title')
 
-    def do_POST_condition(self) -> str:
+    @_delete_or_post(Condition, '/conditions')
+    def do_POST_condition(self, condition: Condition) -> str:
         """Update/insert Condition of ?id= and fields defined in postvars."""
         """Update/insert Condition of ?id= and fields defined in postvars."""
-        id_ = self._params.get_int_or_none('id')
-        for _ in self._form_data.get_all_str('delete'):
-            condition = Condition.by_id(self.conn, id_)
-            condition.remove(self.conn)
-            return '/conditions'
-        condition = Condition.by_id(self.conn, id_, create=True)
-        condition.is_active = self._form_data.get_all_str('is_active') != []
+        condition.is_active = self._form_data.get_str('is_active') == 'True'
         condition.title.set(self._form_data.get_str('title'))
         condition.description.set(self._form_data.get_str('description'))
         condition.save(self.conn)
         condition.title.set(self._form_data.get_str('title'))
         condition.description.set(self._form_data.get_str('description'))
         condition.save(self.conn)