home · contact · privacy
Refactor BaseModel sorting from GET handlers into class definitions.
[plomtask] / plomtask / http.py
index 7c7fbd408edeb47dd48b9a4a04f93beca085f70a..417c5a609c49bff6a7b0f6c85cda1be3d9efb0da 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:
@@ -128,6 +137,20 @@ class InputsParser:
             msg = f'cannot int a form field value for key {key} in: {all_str}'
             raise BadFormatException(msg) from e
 
             msg = f'cannot int a form field value for key {key} in: {all_str}'
             raise BadFormatException(msg) from e
 
+    def get_all_floats_or_nones(self, key: str) -> list[float | None]:
+        """Retrieve list of float value at key, None if empty strings."""
+        ret: list[float | None] = []
+        for val in self.get_all_str(key):
+            if '' == val:
+                ret += [None]
+            else:
+                try:
+                    ret += [float(val)]
+                except ValueError as e:
+                    msg = f'cannot float form field value for key {key}: {val}'
+                    raise BadFormatException(msg) from e
+        return ret
+
 
 class TaskHandler(BaseHTTPRequestHandler):
     """Handles single HTTP request."""
 
 class TaskHandler(BaseHTTPRequestHandler):
     """Handles single HTTP request."""
@@ -143,7 +166,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:
@@ -212,6 +235,25 @@ class TaskHandler(BaseHTTPRequestHandler):
 
     # GET handlers
 
 
     # GET handlers
 
+    @staticmethod
+    def _get_item(target_class: Any
+                  ) -> Callable[..., Callable[[TaskHandler],
+                                              dict[str, object]]]:
+        def decorator(f: Callable[..., dict[str, object]]
+                      ) -> Callable[[TaskHandler], dict[str, object]]:
+            def wrapper(self: TaskHandler) -> dict[str, object]:
+                # 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')
+                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 do_GET_(self) -> str:
         """Return redirect target on GET /."""
         return '/day'
     def do_GET_(self) -> str:
         """Return redirect target on GET /."""
         return '/day'
@@ -270,7 +312,8 @@ class TaskHandler(BaseHTTPRequestHandler):
                 'conditions_present': conditions_present,
                 'processes': Process.all(self.conn)}
 
                 'conditions_present': conditions_present,
                 'processes': Process.all(self.conn)}
 
-    def do_GET_todo(self) -> dict[str, object]:
+    @_get_item(Todo)
+    def do_GET_todo(self, todo: Todo) -> dict[str, object]:
         """Show single Todo of ?id=."""
 
         @dataclass
         """Show single Todo of ?id=."""
 
         @dataclass
@@ -321,8 +364,6 @@ class TaskHandler(BaseHTTPRequestHandler):
                 ids = ids | collect_adoptables_keys(node.children)
             return ids
 
                 ids = ids | collect_adoptables_keys(node.children)
             return ids
 
-        id_ = self._params.get_int('id')
-        todo = Todo.by_id(self.conn, id_)
         todo_steps = [step.todo for step in todo.get_step_tree(set()).children]
         process_tree = todo.process.get_steps(self.conn, None)
         steps_todo_to_process: list[TodoStepsNode] = []
         todo_steps = [step.todo for step in todo.get_step_tree(set()).children]
         process_tree = todo.process.get_steps(self.conn, None)
         steps_todo_to_process: list[TodoStepsNode] = []
@@ -359,23 +400,7 @@ class TaskHandler(BaseHTTPRequestHandler):
         todos = [t for t in todos_by_date_range
                  if comment_pattern in t.comment
                  and ((not process_id) or t.process.id_ == process_id)]
         todos = [t for t in todos_by_date_range
                  if comment_pattern in t.comment
                  and ((not process_id) or t.process.id_ == process_id)]
-        if sort_by == 'doneness':
-            todos.sort(key=lambda t: t.is_done)
-        elif sort_by == '-doneness':
-            todos.sort(key=lambda t: t.is_done, reverse=True)
-        elif sort_by == 'title':
-            todos.sort(key=lambda t: t.title_then)
-        elif sort_by == '-title':
-            todos.sort(key=lambda t: t.title_then, reverse=True)
-        elif sort_by == 'comment':
-            todos.sort(key=lambda t: t.comment)
-        elif sort_by == '-comment':
-            todos.sort(key=lambda t: t.comment, reverse=True)
-        elif sort_by == '-date':
-            todos.sort(key=lambda t: t.date, reverse=True)
-        else:
-            todos.sort(key=lambda t: t.date)
-            sort_by = 'title'
+        sort_by = Todo.sort_by(todos, sort_by)
         return {'start': start, 'end': end, 'process_id': process_id,
                 'comment_pattern': comment_pattern, 'todos': todos,
                 'all_processes': Process.all(self.conn), 'sort_by': sort_by}
         return {'start': start, 'end': end, 'process_id': process_id,
                 'comment_pattern': comment_pattern, 'todos': todos,
                 'all_processes': Process.all(self.conn), 'sort_by': sort_by}
@@ -383,25 +408,16 @@ class TaskHandler(BaseHTTPRequestHandler):
     def do_GET_conditions(self) -> dict[str, object]:
         """Show all Conditions."""
         pattern = self._params.get_str('pattern')
     def do_GET_conditions(self) -> dict[str, object]:
         """Show all Conditions."""
         pattern = self._params.get_str('pattern')
-        conditions = Condition.matching(self.conn, pattern)
         sort_by = self._params.get_str('sort_by')
         sort_by = self._params.get_str('sort_by')
-        if sort_by == 'is_active':
-            conditions.sort(key=lambda c: c.is_active)
-        elif sort_by == '-is_active':
-            conditions.sort(key=lambda c: c.is_active, reverse=True)
-        elif sort_by == '-title':
-            conditions.sort(key=lambda c: c.title.newest, reverse=True)
-        else:
-            conditions.sort(key=lambda c: c.title.newest)
-            sort_by = 'title'
+        conditions = Condition.matching(self.conn, pattern)
+        sort_by = Condition.sort_by(conditions, sort_by)
         return {'conditions': conditions,
                 'sort_by': sort_by,
                 'pattern': pattern}
 
         return {'conditions': conditions,
                 'sort_by': sort_by,
                 'pattern': pattern}
 
-    def do_GET_condition(self) -> dict[str, object]:
+    @_get_item(Condition)
+    def do_GET_condition(self, c: Condition) -> dict[str, object]:
         """Show Condition of ?id=."""
         """Show Condition of ?id=."""
-        id_ = self._params.get_int_or_none('id')
-        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],
@@ -409,31 +425,30 @@ class TaskHandler(BaseHTTPRequestHandler):
                 'enabling_processes': [p for p in ps if c in p.enables],
                 'disabling_processes': [p for p in ps if c in p.disables]}
 
                 'enabling_processes': [p for p in ps if c in p.enables],
                 'disabling_processes': [p for p in ps if c in p.disables]}
 
-    def do_GET_condition_titles(self) -> dict[str, object]:
+    @_get_item(Condition)
+    def do_GET_condition_titles(self, c: Condition) -> dict[str, object]:
         """Show title history of Condition of ?id=."""
         """Show title history of Condition of ?id=."""
-        id_ = self._params.get_int('id')
-        condition = Condition.by_id(self.conn, id_)
-        return {'condition': condition}
+        return {'condition': c}
 
 
-    def do_GET_condition_descriptions(self) -> dict[str, object]:
+    @_get_item(Condition)
+    def do_GET_condition_descriptions(self, c: Condition) -> dict[str, object]:
         """Show description historys of Condition of ?id=."""
         """Show description historys of Condition of ?id=."""
-        id_ = self._params.get_int('id')
-        condition = Condition.by_id(self.conn, id_)
-        return {'condition': condition}
+        return {'condition': c}
 
 
-    def do_GET_process(self) -> dict[str, object]:
+    @_get_item(Process)
+    def do_GET_process(self, process: Process) -> dict[str, object]:
         """Show Process of ?id=."""
         """Show Process of ?id=."""
-        id_ = self._params.get_int_or_none('id')
-        process = Process.by_id_or_create(self.conn, id_)
+        owner_ids = self._params.get_all_int('step_to')
+        owned_ids = self._params.get_all_int('has_step')
         title_64 = self._params.get_str('title_b64')
         if title_64:
             title = b64decode(title_64.encode()).decode()
             process.title.set(title)
         owners = process.used_as_step_by(self.conn)
         title_64 = self._params.get_str('title_b64')
         if title_64:
             title = b64decode(title_64.encode()).decode()
             process.title.set(title)
         owners = process.used_as_step_by(self.conn)
-        for step_id in self._params.get_all_int('step_to'):
+        for step_id in owner_ids:
             owners += [Process.by_id(self.conn, step_id)]
         preset_top_step = None
             owners += [Process.by_id(self.conn, step_id)]
         preset_top_step = None
-        for process_id in self._params.get_all_int('has_step'):
+        for process_id in owned_ids:
             preset_top_step = process_id
         return {'process': process, 'is_new': process.id_ is None,
                 'preset_top_step': preset_top_step,
             preset_top_step = process_id
         return {'process': process, 'is_new': process.id_ is None,
                 'preset_top_step': preset_top_step,
@@ -442,50 +457,57 @@ class TaskHandler(BaseHTTPRequestHandler):
                 'process_candidates': Process.all(self.conn),
                 'condition_candidates': Condition.all(self.conn)}
 
                 'process_candidates': Process.all(self.conn),
                 'condition_candidates': Condition.all(self.conn)}
 
-    def do_GET_process_titles(self) -> dict[str, object]:
+    @_get_item(Process)
+    def do_GET_process_titles(self, p: Process) -> dict[str, object]:
         """Show title history of Process of ?id=."""
         """Show title history of Process of ?id=."""
-        id_ = self._params.get_int('id')
-        process = Process.by_id(self.conn, id_)
-        return {'process': process}
+        return {'process': p}
 
 
-    def do_GET_process_descriptions(self) -> dict[str, object]:
+    @_get_item(Process)
+    def do_GET_process_descriptions(self, p: Process) -> dict[str, object]:
         """Show description historys of Process of ?id=."""
         """Show description historys of Process of ?id=."""
-        id_ = self._params.get_int('id')
-        process = Process.by_id(self.conn, id_)
-        return {'process': process}
+        return {'process': p}
 
 
-    def do_GET_process_efforts(self) -> dict[str, object]:
+    @_get_item(Process)
+    def do_GET_process_efforts(self, p: Process) -> dict[str, object]:
         """Show default effort history of Process of ?id=."""
         """Show default effort history of Process of ?id=."""
-        id_ = self._params.get_int('id')
-        process = Process.by_id(self.conn, id_)
-        return {'process': process}
+        return {'process': p}
 
     def do_GET_processes(self) -> dict[str, object]:
         """Show all Processes."""
         pattern = self._params.get_str('pattern')
 
     def do_GET_processes(self) -> dict[str, object]:
         """Show all Processes."""
         pattern = self._params.get_str('pattern')
-        processes = Process.matching(self.conn, pattern)
         sort_by = self._params.get_str('sort_by')
         sort_by = self._params.get_str('sort_by')
-        if sort_by == 'steps':
-            processes.sort(key=lambda p: len(p.explicit_steps))
-        elif sort_by == '-steps':
-            processes.sort(key=lambda p: len(p.explicit_steps), reverse=True)
-        elif sort_by == 'owners':
-            processes.sort(key=lambda p: p.n_owners or 0)
-        elif sort_by == '-owners':
-            processes.sort(key=lambda p: p.n_owners or 0, reverse=True)
-        elif sort_by == 'effort':
-            processes.sort(key=lambda p: p.effort.newest)
-        elif sort_by == '-effort':
-            processes.sort(key=lambda p: p.effort.newest, reverse=True)
-        elif sort_by == '-title':
-            processes.sort(key=lambda p: p.title.newest, reverse=True)
-        else:
-            processes.sort(key=lambda p: p.title.newest)
-            sort_by = 'title'
+        processes = Process.matching(self.conn, pattern)
+        sort_by = Process.sort_by(processes, sort_by)
         return {'processes': processes, 'sort_by': sort_by, 'pattern': pattern}
 
     # POST handlers
 
         return {'processes': processes, 'sort_by': sort_by, 'pattern': pattern}
 
     # 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')
@@ -496,50 +518,61 @@ 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."""
 
     def do_POST_day(self) -> str:
         """Update or insert Day of date and Todos mapped to it."""
+        # pylint: disable=too-many-locals
         date = self._params.get_str('date')
         date = self._params.get_str('date')
+        day_comment = self._form_data.get_str('day_comment')
+        make_type = self._form_data.get_str('make_type')
+        old_todos = self._form_data.get_all_int('todo_id')
+        new_todos = self._form_data.get_all_int('new_todo')
+        comments = self._form_data.get_all_str('comment')
+        efforts = self._form_data.get_all_floats_or_nones('effort')
+        done_todos = self._form_data.get_all_int('done')
+        for _ in [id_ for id_ in done_todos if id_ not in old_todos]:
+            raise BadFormatException('"done" field refers to unknown Todo')
+        is_done = [t_id in done_todos for t_id in old_todos]
+        if not (len(old_todos) == len(is_done) == len(comments)
+                == len(efforts)):
+            msg = 'not equal number each of number of todo_id, comments, ' +\
+                    'and efforts inputs'
+            raise BadFormatException(msg)
         day = Day.by_id_or_create(self.conn, date)
         day = Day.by_id_or_create(self.conn, date)
-        day.comment = self._form_data.get_str('day_comment')
+        day.comment = day_comment
         day.save(self.conn)
         day.save(self.conn)
-        make_type = self._form_data.get_str('make_type')
-        for process_id in sorted(self._form_data.get_all_int('new_todo')):
+        for process_id in sorted(new_todos):
             if 'empty' == make_type:
                 process = Process.by_id(self.conn, process_id)
                 todo = Todo(None, process, False, date)
                 todo.save(self.conn)
             else:
                 Todo.create_with_children(self.conn, process_id, date)
             if 'empty' == make_type:
                 process = Process.by_id(self.conn, process_id)
                 todo = Todo(None, process, False, date)
                 todo.save(self.conn)
             else:
                 Todo.create_with_children(self.conn, process_id, date)
-        done_ids = self._form_data.get_all_int('done')
-        comments = self._form_data.get_all_str('comment')
-        efforts = self._form_data.get_all_str('effort')
-        for i, todo_id in enumerate(self._form_data.get_all_int('todo_id')):
+        for i, todo_id in enumerate(old_todos):
             todo = Todo.by_id(self.conn, todo_id)
             todo = Todo.by_id(self.conn, todo_id)
-            todo.is_done = todo_id in done_ids
-            if len(comments) > 0:
-                todo.comment = comments[i]
-            if len(efforts) > 0:
-                todo.effort = float(efforts[i]) if efforts[i] else None
+            todo.is_done = is_done[i]
+            todo.comment = comments[i]
+            todo.effort = efforts[i]
             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."""
         # pylint: disable=too-many-locals
         """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')
         fill_fors = self._form_data.get_first_strings_starting('fill_for_')
         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')
         fill_fors = self._form_data.get_first_strings_starting('fill_for_')
+        effort = self._form_data.get_str('effort', ignore_strict=True)
+        conditions = self._form_data.get_all_int('conditions')
+        disables = self._form_data.get_all_int('disables')
+        blockers = self._form_data.get_all_int('blockers')
+        enables = self._form_data.get_all_int('enables')
+        is_done = len(self._form_data.get_all_str('done')) > 0
+        calendarize = len(self._form_data.get_all_str('calendarize')) > 0
+        comment = self._form_data.get_str('comment', ignore_strict=True)
         for v in fill_fors.values():
             if v.startswith('make_empty_'):
                 processes_to_make_empty += [int(v[11:])]
         for v in fill_fors.values():
             if v.startswith('make_empty_'):
                 processes_to_make_empty += [int(v[11:])]
@@ -568,16 +601,14 @@ class TaskHandler(BaseHTTPRequestHandler):
         for process_id in processes_to_make_full:
             made = Todo.create_with_children(self.conn, process_id, todo.date)
             todo.add_child(made)
         for process_id in processes_to_make_full:
             made = Todo.create_with_children(self.conn, process_id, todo.date)
             todo.add_child(made)
-        effort = self._form_data.get_str('effort', ignore_strict=True)
         todo.effort = float(effort) if effort else None
         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'))
-        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.calendarize = len(self._form_data.get_all_str('calendarize')) > 0
-        todo.comment = self._form_data.get_str('comment', ignore_strict=True)
+        todo.set_conditions(self.conn, conditions)
+        todo.set_blockers(self.conn, blockers)
+        todo.set_enables(self.conn, enables)
+        todo.set_disables(self.conn, disables)
+        todo.is_done = is_done
+        todo.calendarize = calendarize
+        todo.comment = comment
         todo.save(self.conn)
         return f'/todo?id={todo.id_}'
 
         todo.save(self.conn)
         return f'/todo?id={todo.id_}'
 
@@ -593,62 +624,70 @@ 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'):
-            if id_ is None:
-                raise NotFoundException('trying to delete non-saved Process')
-            process = Process.by_id(self.conn, id_)
-            process.remove(self.conn)
-            return '/processes'
-        process = Process.by_id_or_create(self.conn, id_)
-        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'))
-        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.calendarize = self._form_data.get_all_str('calendarize') != []
+        # pylint: disable=too-many-locals
+        # pylint: disable=too-many-statements
+        title = self._form_data.get_str('title')
+        description = self._form_data.get_str('description')
+        effort = self._form_data.get_float('effort')
+        conditions = self._form_data.get_all_int('conditions')
+        blockers = self._form_data.get_all_int('blockers')
+        enables = self._form_data.get_all_int('enables')
+        disables = self._form_data.get_all_int('disables')
+        calendarize = self._form_data.get_all_str('calendarize') != []
+        suppresses = self._form_data.get_all_int('suppresses')
+        step_of = self._form_data.get_all_str('step_of')
+        keep_steps = self._form_data.get_all_int('keep_step')
+        step_ids = self._form_data.get_all_int('steps')
+        new_top_steps = self._form_data.get_all_str('new_top_step')
+        step_process_id_to = {}
+        step_parent_id_to = {}
+        new_steps_to = {}
+        for step_id in step_ids:
+            name = f'new_step_to_{step_id}'
+            new_steps_to[step_id] = self._form_data.get_all_int(name)
+        for step_id in keep_steps:
+            name = f'step_{step_id}_process_id'
+            step_process_id_to[step_id] = self._form_data.get_int(name)
+            name = f'step_{step_id}_parent_id'
+            step_parent_id_to[step_id] = self._form_data.get_int_or_none(name)
+        process.title.set(title)
+        process.description.set(description)
+        process.effort.set(effort)
+        process.set_conditions(self.conn, conditions)
+        process.set_blockers(self.conn, blockers)
+        process.set_enables(self.conn, enables)
+        process.set_disables(self.conn, disables)
+        process.calendarize = calendarize
         process.save(self.conn)
         assert isinstance(process.id_, int)
         process.save(self.conn)
         assert isinstance(process.id_, int)
+        new_step_title = None
         steps: list[ProcessStep] = []
         steps: list[ProcessStep] = []
-        for step_id in self._form_data.get_all_int('keep_step'):
-            if step_id not in self._form_data.get_all_int('steps'):
+        for step_id in keep_steps:
+            if step_id not in step_ids:
                 raise BadFormatException('trying to keep unknown step')
                 raise BadFormatException('trying to keep unknown step')
-        for step_id in self._form_data.get_all_int('steps'):
-            if step_id not in self._form_data.get_all_int('keep_step'):
-                continue
-            step_process_id = self._form_data.get_int(
-                    f'step_{step_id}_process_id')
-            parent_id = self._form_data.get_int_or_none(
-                    f'step_{step_id}_parent_id')
-            steps += [ProcessStep(step_id, process.id_, step_process_id,
-                                  parent_id)]
-        for step_id in self._form_data.get_all_int('steps'):
-            for step_process_id in self._form_data.get_all_int(
-                    f'new_step_to_{step_id}'):
-                steps += [ProcessStep(None, process.id_, step_process_id,
-                                      step_id)]
-        new_step_title = None
-        for step_identifier in self._form_data.get_all_str('new_top_step'):
+            step = ProcessStep(step_id, process.id_,
+                               step_process_id_to[step_id],
+                               step_parent_id_to[step_id])
+            steps += [step]
+        for step_id in step_ids:
+            new = [ProcessStep(None, process.id_, step_process_id, step_id)
+                   for step_process_id in new_steps_to[step_id]]
+            steps += new
+        for step_identifier in new_top_steps:
             try:
                 step_process_id = int(step_identifier)
             try:
                 step_process_id = int(step_identifier)
-                steps += [ProcessStep(None, process.id_, step_process_id,
-                                      None)]
+                step = ProcessStep(None, process.id_, step_process_id, None)
+                steps += [step]
             except ValueError:
                 new_step_title = step_identifier
         process.set_steps(self.conn, steps)
             except ValueError:
                 new_step_title = step_identifier
         process.set_steps(self.conn, steps)
-        process.set_step_suppressions(self.conn,
-                                      self._form_data.
-                                      get_all_int('suppresses'))
+        process.set_step_suppressions(self.conn, suppresses)
         owners_to_set = []
         new_owner_title = None
         owners_to_set = []
         new_owner_title = None
-        for owner_identifier in self._form_data.get_all_str('step_of'):
+        for owner_identifier in step_of:
             try:
                 owners_to_set += [int(owner_identifier)]
             except ValueError:
             try:
                 owners_to_set += [int(owner_identifier)]
             except ValueError:
@@ -672,18 +711,14 @@ 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'):
-            if id_ is None:
-                raise NotFoundException('trying to delete non-saved Condition')
-            condition = Condition.by_id_or_create(self.conn, id_)
-            condition.remove(self.conn)
-            return '/conditions'
-        condition = Condition.by_id_or_create(self.conn, id_)
-        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'))
+        is_active = self._form_data.get_str('is_active') == 'True'
+        title = self._form_data.get_str('title')
+        description = self._form_data.get_str('description')
+        condition.is_active = is_active
+        condition.title.set(title)
+        condition.description.set(description)
         condition.save(self.conn)
         return f'/condition?id={condition.id_}'
         condition.save(self.conn)
         return f'/condition?id={condition.id_}'