X-Git-Url: https://plomlompom.com/repos/berlin_corona.txt?a=blobdiff_plain;f=plomtask%2Fhttp.py;h=f5160f64d2504a3cf40733668623e1475eaa0cd8;hb=HEAD;hp=d602f07b9baf3c18df19fa88435928d2a33845d6;hpb=db62e6559fdd577dae38d4b6f5cbd5ef6a14cc57;p=plomtask diff --git a/plomtask/http.py b/plomtask/http.py index d602f07..b7040f7 100644 --- a/plomtask/http.py +++ b/plomtask/http.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any, Callable from base64 import b64encode, b64decode +from binascii import Error as binascii_Exception from http.server import BaseHTTPRequestHandler from http.server import HTTPServer from urllib.parse import urlparse, parse_qs @@ -11,13 +12,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 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 BaseModel TEMPLATES_DIR = 'templates' @@ -42,15 +42,25 @@ class TaskServer(HTTPServer): 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] + 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 + library: dict[str, dict[str | int, object]] = {} 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: @@ -128,6 +138,20 @@ class InputsParser: 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.""" @@ -143,7 +167,7 @@ class TaskHandler(BaseHTTPRequestHandler): 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: @@ -154,6 +178,38 @@ class TaskHandler(BaseHTTPRequestHandler): @staticmethod def _request_wrapper(http_method: str, not_found_msg: str ) -> Callable[..., Callable[[TaskHandler], None]]: + """Wrapper for do_GET… and do_POST… handlers, to init and clean up. + + Among other things, conditionally cleans all caches, but only on POST + requests, as only those are expected to change the states of objects + that may be cached, and certainly only those are expected to write any + changes to the database. We want to call them as early though as + possible here, either exactly after the specific request handler + returns successfully, or right after any exception is triggered – + otherwise, race conditions become plausible. + + Note that otherwise any POST attempt, even a failed one, may end in + problematic inconsistencies: + + - if the POST handler experiences an Exception, changes to objects + won't get written to the DB, but the changed objects may remain in + the cache and affect other objects despite their possibly illegal + state + + - even if an object was just saved to the DB, we cannot be sure its + current state is completely identical to what we'd get if loading it + fresh from the DB (e.g. currently Process.n_owners is only updated + when loaded anew via .from_table_row, nor is its state written to + the DB by .save; a questionable design choice, but proof that we + have no guarantee that objects' .save stores all their states we'd + prefer at their most up-to-date. + """ + + def clear_caches() -> None: + for cls in (Day, Todo, Condition, Process, ProcessStep): + assert hasattr(cls, 'empty_cache') + cls.empty_cache() + def decorator(f: Callable[..., str | None] ) -> Callable[[TaskHandler], None]: def wrapper(self: TaskHandler) -> None: @@ -170,6 +226,8 @@ class TaskHandler(BaseHTTPRequestHandler): if hasattr(self, handler_name): handler = getattr(self, handler_name) redir_target = f(self, handler) + if 'POST' == http_method: + clear_caches() if redir_target: self.send_response(302) self.send_header('Location', redir_target) @@ -178,9 +236,8 @@ class TaskHandler(BaseHTTPRequestHandler): msg = f'{not_found_msg}: {self._site}' raise NotFoundException(msg) except HandledException as error: - for cls in (Day, Todo, Condition, Process, ProcessStep): - assert hasattr(cls, 'empty_cache') - cls.empty_cache() + if 'POST' == http_method: + clear_caches() ctx = {'msg': error} self._send_page(ctx, 'msg', error.http_code) finally: @@ -212,6 +269,25 @@ class TaskHandler(BaseHTTPRequestHandler): # 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' @@ -244,7 +320,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)) - 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 = {} @@ -270,7 +346,8 @@ class TaskHandler(BaseHTTPRequestHandler): '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 @@ -321,8 +398,6 @@ class TaskHandler(BaseHTTPRequestHandler): 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] = [] @@ -335,7 +410,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) - 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_] @@ -358,22 +434,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)] - 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 = 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} @@ -381,24 +442,16 @@ class TaskHandler(BaseHTTPRequestHandler): 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') - 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) + conditions = Condition.matching(self.conn, pattern) + sort_by = Condition.sort_by(conditions, sort_by) 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=.""" - id_ = self._params.get_int_or_none('id') - c = Condition.by_id(self.conn, id_, create=True) 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], @@ -406,31 +459,35 @@ 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]} - 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=.""" - id_ = self._params.get_int_or_none('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=.""" - id_ = self._params.get_int_or_none('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=.""" - id_ = self._params.get_int_or_none('id') - process = Process.by_id(self.conn, id_, create=True) + 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() + try: + title = b64decode(title_64.encode()).decode() + except binascii_Exception as exc: + msg = 'invalid base64 for ?title_b64=' + raise BadFormatException(msg) from exc process.title.set(title) + preset_top_step = None 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 - for process_id in self._params.get_all_int('has_step'): + for process_id in owned_ids: + Process.by_id(self.conn, process_id) # to ensure ID exists preset_top_step = process_id return {'process': process, 'is_new': process.id_ is None, 'preset_top_step': preset_top_step, @@ -439,49 +496,57 @@ class TaskHandler(BaseHTTPRequestHandler): '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=.""" - id_ = self._params.get_int_or_none('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=.""" - id_ = self._params.get_int_or_none('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=.""" - id_ = self._params.get_int_or_none('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') - processes = Process.matching(self.conn, pattern) 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) + 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 + @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') @@ -492,50 +557,61 @@ class TaskHandler(BaseHTTPRequestHandler): 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.""" + # pylint: disable=too-many-locals date = self._params.get_str('date') - day = Day.by_id(self.conn, date, create=True) - day.comment = self._form_data.get_str('day_comment') - day.save(self.conn) + day_comment = self._form_data.get_str('day_comment') make_type = self._form_data.get_str('make_type') - for process_id in sorted(self._form_data.get_all_int('new_todo')): + 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.comment = day_comment + day.save(self.conn) + 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) - 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.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}' - 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 - # 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_') + 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:])] @@ -564,16 +640,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) - 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')) - 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_}' @@ -589,60 +663,70 @@ class TaskHandler(BaseHTTPRequestHandler): """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.""" - # 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, - 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) + new_step_title = None 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') - 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) - 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) - 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 - 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: @@ -666,16 +750,14 @@ class TaskHandler(BaseHTTPRequestHandler): """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.""" - 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.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_}'