From: Christian Heller Date: Sat, 10 Aug 2024 03:32:55 +0000 (+0200) Subject: Add TaskHandler code to actually make previous commit work. X-Git-Url: https://plomlompom.com/repos/%7B%7B%20web_path%20%7D%7D/decks/%7B%7Bdeck_id%7D%7D/cards/%7B%7Bcard_id%7D%7D/static/todo?a=commitdiff_plain;p=plomtask Add TaskHandler code to actually make previous commit work. --- diff --git a/plomtask/http.py b/plomtask/http.py index cba55d6..e307f14 100644 --- a/plomtask/http.py +++ b/plomtask/http.py @@ -88,11 +88,11 @@ class InputsParser: return None return val in {'True', 'true', '1', 'on'} - def get_firsts_of_key_prefixed(self, prefix: str) -> dict[str, str]: - """Retrieve dict of (first) strings at key starting with prefix.""" + def get_all_of_key_prefixed(self, key_prefix: str) -> dict[str, list[str]]: + """Retrieve dict of strings at keys starting with key_prefix.""" ret = {} - for key in [k for k in self.inputs.keys() if k.startswith(prefix)]: - ret[key] = self.inputs[key][0] + for key in [k for k in self.inputs.keys() if k.startswith(key_prefix)]: + ret[key[len(key_prefix):]] = self.inputs[key] return ret def get_float_or_fail(self, key: str) -> float: @@ -581,10 +581,9 @@ class TaskHandler(BaseHTTPRequestHandler): 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.get_firsts_of_key_prefixed('at:').items(): - old = k[3:] - if old[19:] != v: - attr.reset_timestamp(old, f'{v}.0') + for k, vals in self._form.get_all_of_key_prefixed('at:').items(): + if k[19:] != vals[0]: + attr.reset_timestamp(k, f'{vals[0]}.0') attr.save(self._conn) return f'/{cls.name_lowercase()}_{attr_name}s?id={item.id_}' @@ -633,10 +632,14 @@ class TaskHandler(BaseHTTPRequestHandler): """Update Todo and its children.""" # pylint: disable=too-many-locals # pylint: disable=too-many-branches - adoptees = self._form.get_all_int('adopt') - to_make = {'full': self._form.get_all_int('make_full'), - 'empty': self._form.get_all_int('make_empty')} - step_fillers = self._form.get_all_str('step_filler') + # pylint: disable=too-many-statements + assert todo.id_ is not None + adoptees = [(id_, todo.id_) for id_ in self._form.get_all_int('adopt')] + to_make = {'full': [(id_, todo.id_) + for id_ in self._form.get_all_int('make_full')], + 'empty': [(id_, todo.id_) + for id_ in self._form.get_all_int('make_empty')]} + step_fillers_to = self._form.get_all_of_key_prefixed('step_filler_to_') to_update: dict[str, Any] = { 'comment': self._form.get_str_or_fail('comment', '')} for k in ('is_done', 'calendarize'): @@ -655,39 +658,50 @@ class TaskHandler(BaseHTTPRequestHandler): except ValueError as e: msg = 'cannot float form field value for key: effort' raise BadFormatException(msg) from e - for filler in [f for f in step_fillers if f != 'ignore']: - target_id: int - to_int = filler - for prefix in [p for p in ['make_empty_', 'make_full_'] - if filler.startswith(p)]: - to_int = filler[len(prefix):] + for k, fillers in step_fillers_to.items(): try: - target_id = int(to_int) + parent_id = int(k) except ValueError as e: - msg = 'bad fill_for target: {filler}' + msg = f'bad step_filler_to_ key: {k}' raise BadFormatException(msg) from e - if filler.startswith('make_empty_'): - to_make['empty'] += [target_id] - elif filler.startswith('make_full_'): - to_make['full'] += [target_id] - else: - adoptees += [target_id] + for filler in [f for f in fillers if f != 'ignore']: + target_id: int + prefix = 'make_' + to_int = filler[5:] if filler.startswith(prefix) else filler + try: + target_id = int(to_int) + except ValueError as e: + msg = f'bad fill_for target: {filler}' + raise BadFormatException(msg) from e + if filler.startswith(prefix): + to_make['empty'] += [(target_id, parent_id)] + else: + adoptees += [(target_id, parent_id)] # todo.set_condition_relations(self._conn, *cond_rels) - for child in [c for c in todo.children if c.id_ not in adoptees]: - todo.remove_child(child) - for child_id in [id_ for id_ in adoptees - if id_ not in [c.id_ for c in todo.children]]: - todo.add_child(Todo.by_id(self._conn, child_id)) + for parent in [Todo.by_id(self._conn, a[1]) + for a in adoptees] + [todo]: + for child in parent.children: + if child not in [t[0] for t in adoptees + if t[0] == child.id_ and t[1] == parent.id_]: + parent.remove_child(child) + parent.save(self._conn) + for child_id, parent_id in adoptees: + parent = Todo.by_id(self._conn, parent_id) + if child_id not in [c.id_ for c in parent.children]: + parent.add_child(Todo.by_id(self._conn, child_id)) + parent.save(self._conn) todo.update_attrs(**to_update) - for approach, proc_ids in to_make.items(): - for process_id in proc_ids: + for approach, make_data in to_make.items(): + for process_id, parent_id in make_data: + parent = Todo.by_id(self._conn, parent_id) process = Process.by_id(self._conn, process_id) made = Todo(None, process, False, todo.date) made.save(self._conn) if 'full' == approach: made.ensure_children(self._conn) - todo.add_child(made) + parent.add_child(made) + parent.save(self._conn) # todo.save() may destroy Todo if .effort < 0, so retrieve .id_ early url = f'/todo?id={todo.id_}' todo.save(self._conn) diff --git a/templates/todo.html b/templates/todo.html index 61d4675..de5dbd2 100644 --- a/templates/todo.html +++ b/templates/todo.html @@ -22,19 +22,21 @@ select{ font-size: 0.5em; margin: 0; padding: 0; } {{item.todo.title_then|e}} {% else %} {{item.process.title.newest|e}} -· fill: - - + {% for adoptable in adoption_candidates_for[item.process.id_] %} {% endfor %} +{% endif %} + {% endif %} {% for child in item.children %} -{{ draw_tree_row(child, item, indent+1) }} +{{ draw_tree_row(child, item.todo, indent+1) }} {% endfor %} {% endmacro %} diff --git a/tests/misc.py b/tests/misc.py index 1efa335..86474c7 100644 --- a/tests/misc.py +++ b/tests/misc.py @@ -36,31 +36,31 @@ class TestsSansServer(TestCase): parser = InputsParser({'foo': ['baz', 'quux']}) self.assertEqual('baz', parser.get_str('foo', 'bar')) - def test_InputsParser_get_firsts_of_key_prefixed(self) -> None: - """Test InputsParser.get_firsts_of_key_prefixed.""" + def test_InputsParser_get_all_of_key_prefixed(self) -> None: + """Test InputsParser.get_all_of_key_prefixed.""" parser = InputsParser({}) self.assertEqual({}, - parser.get_firsts_of_key_prefixed('')) + parser.get_all_of_key_prefixed('')) self.assertEqual({}, - parser.get_firsts_of_key_prefixed('foo')) + parser.get_all_of_key_prefixed('foo')) parser = InputsParser({'foo': ['bar']}) - self.assertEqual({'foo': 'bar'}, - parser.get_firsts_of_key_prefixed('')) - parser = InputsParser({'x': ['y']}) - self.assertEqual({'x': 'y'}, - parser.get_firsts_of_key_prefixed('x')) - parser = InputsParser({'xx': ['y']}) - self.assertEqual({'xx': 'y'}, - parser.get_firsts_of_key_prefixed('x')) + self.assertEqual({'foo': ['bar']}, + parser.get_all_of_key_prefixed('')) + parser = InputsParser({'x': ['y', 'z']}) + self.assertEqual({'': ['y', 'z']}, + parser.get_all_of_key_prefixed('x')) + parser = InputsParser({'xx': ['y', 'Z']}) + self.assertEqual({'x': ['y', 'Z']}, + parser.get_all_of_key_prefixed('x')) parser = InputsParser({'xx': ['y']}) self.assertEqual({}, - parser.get_firsts_of_key_prefixed('xxx')) + parser.get_all_of_key_prefixed('xxx')) parser = InputsParser({'xxx': ['x'], 'xxy': ['y'], 'xyy': ['z']}) - self.assertEqual({'xxx': 'x', 'xxy': 'y'}, - parser.get_firsts_of_key_prefixed('xx')) - parser = InputsParser({'xxx': ['x', 'y', 'z'], 'xxy': ['y', 'z']}) - self.assertEqual({'xxx': 'x', 'xxy': 'y'}, - parser.get_firsts_of_key_prefixed('xx')) + self.assertEqual({'x': ['x'], 'y': ['y']}, + parser.get_all_of_key_prefixed('xx')) + parser = InputsParser({'xxx': ['x', 'y'], 'xxy': ['y', 'z']}) + self.assertEqual({'x': ['x', 'y'], 'y': ['y', 'z']}, + parser.get_all_of_key_prefixed('xx')) def test_InputsParser_get_int_or_none(self) -> None: """Test InputsParser.get_int_or_none.""" diff --git a/tests/todos.py b/tests/todos.py index d84bb70..2ecf3b8 100644 --- a/tests/todos.py +++ b/tests/todos.py @@ -282,14 +282,16 @@ class TestsWithServer(TestCaseWithServer): self.check_post({}, '/todo?id=1', 404) # test malformed values on existing Todo self.post_exp_day([], {'new_todo': [1]}) - for name in [ - 'adopt', 'effort', 'make_full', 'make_empty', 'step_filler', - 'conditions', 'disables', 'blockers', 'enables']: + for name in ['adopt', 'effort', 'make_full', 'make_empty', + 'conditions', 'disables', 'blockers', 'enables']: self.check_post({name: 'x'}, '/todo?id=1', 400, '/todo') - for prefix in ['make_empty_', 'make_full_']: + for prefix in ['make_', '']: for suffix in ['', 'x', '1.1']: - self.check_post({'step_filler': f'{prefix}{suffix}'}, - '/todo?id=1', 400, '/todo') + self.check_post({'step_filler_to_1': [f'{prefix}{suffix}']}, + '/todo?id=1', 400, '/todo') + for suffix in ['', 'x', '1.1']: + self.check_post({'step_filler_to_{suffix}': ['1']}, + '/todo?id=1', 400, '/todo') def test_basic_POST_todo(self) -> None: """Test basic POST /todo manipulations.""" @@ -398,14 +400,15 @@ class TestsWithServer(TestCaseWithServer): self.post_exp_day([exp], {'new_todo': [2]}) self.post_exp_day([exp], {'new_todo': [3]}) self.check_json_get('/todo?id=1', exp) - self._post_exp_todo(1, {'step_filler': 5, 'adopt': [4]}, exp) + self._post_exp_todo(1, {'step_filler_to_1': 5, 'adopt': [4]}, exp) + exp.lib_get('Todo', 1)['children'] += [5] step1_proc2 = exp.step_as_dict(1, [], 2, 4, True) step2_proc3 = exp.step_as_dict(2, [], 3, 5, True) exp.set('steps_todo_to_process', [step1_proc2, step2_proc3]) self.check_json_get('/todo?id=1', exp) # test 'ignore' values for 'step_filler' are ignored, and intable # 'step_filler' values are interchangeable with those of 'adopt' - todo_post = {'adopt': 5, 'step_filler': ['ignore', 4]} + todo_post = {'adopt': 5, 'step_filler_to_1': ['ignore', 4]} self.check_post(todo_post, '/todo?id=1') self.check_json_get('/todo?id=1', exp) # test cannot adopt into non-top-level elements of chain, instead @@ -423,46 +426,8 @@ class TestsWithServer(TestCaseWithServer): step4_todo6]) self.check_json_get('/todo?id=1', exp) - def test_POST_todo_make_full(self) -> None: - """Test creation and adoption via POST /todo with "make_full".""" - # create chain of Processes - exp = ExpectedGetTodo(1) - self.post_exp_process([exp], {}, 1) - for i in range(1, 4): - self.post_exp_process([exp], {'new_top_step': i}, i+1) - exp.lib_set('ProcessStep', [exp.procstep_as_dict(1, 2, 1), - exp.procstep_as_dict(2, 3, 2), - exp.procstep_as_dict(3, 4, 3)]) - step3_proc1 = exp.step_as_dict(3, [], 1, None, False) - step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2, None, False) - step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, None, True) - exp.set('steps_todo_to_process', [step1_proc3]) - # post (childless) Todo of chain end, then make_full on next in line - self.post_exp_day([exp], {'new_todo': [4]}) - self.check_post({'step_filler': 'make_full_3'}, '/todo?id=1') - exp.set_todo_from_post(4, {'process_id': 1}) - exp.set_todo_from_post(3, {'process_id': 2, 'children': [4]}) - exp.set_todo_from_post(2, {'process_id': 3, 'children': [3]}) - exp.set_todo_from_post(1, {'process_id': 4, 'children': [2]}) - step3_proc1 = exp.step_as_dict(3, [], 1, 4, True) - step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2, 3, True) - step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, 2, True) - exp.set('steps_todo_to_process', [step1_proc3]) - self.check_json_get('/todo?id=1', exp) - # make new chain next to expected, find steps_todo_to_process extended, - # expect existing Todo demanded by new chain be adopted into new chain - self.check_post({'make_full': 2, 'adopt': [2]}, '/todo?id=1') - exp.set_todo_from_post(5, {'process_id': 2, 'children': [4]}) - exp.set_todo_from_post(1, {'process_id': 4, 'children': [2, 5]}) - step5_todo4 = exp.step_as_dict(5, [], None, 4) - step4_todo5 = exp.step_as_dict(4, [step5_todo4], None, 5) - exp.set('steps_todo_to_process', [step1_proc3, step4_todo5]) - self.check_json_get('/todo?id=1', exp) - # fail on trying to call make_full on non-existing Process - self.check_post({'make_full': 5}, '/todo?id=1', 404) - def test_POST_todo_make_empty(self) -> None: - """Test creation and adoption via POST /todo with "make_empty".""" + """Test creation via POST /todo "step_filler_to"/"make".""" # create chain of Processes exp = ExpectedGetTodo(1) self.post_exp_process([exp], {}, 1) @@ -478,7 +443,7 @@ class TestsWithServer(TestCaseWithServer): step1_proc3 = exp.step_as_dict(1, [step2_proc2], 3, None, True) exp.set('steps_todo_to_process', [step1_proc3]) self.check_json_get('/todo?id=1', exp) - self.check_post({'step_filler': 'make_empty_3'}, '/todo?id=1') + self.check_post({'step_filler_to_1': 'make_3'}, '/todo?id=1') exp.set_todo_from_post(2, {'process_id': 3}) exp.set_todo_from_post(1, {'process_id': 4, 'children': [2]}) step2_proc2 = exp.step_as_dict(2, [step3_proc1], 2, None, True) diff --git a/tests/utils.py b/tests/utils.py index 71da9fb..d1b6eac 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -753,11 +753,13 @@ class Expected: """Set Todo of id_ in library based on POST dict d.""" corrected_kwargs: dict[str, Any] = {'children': []} for k, v in d.items(): - if k in {'adopt', 'step_filler'}: + if k.startswith('step_filler_to_'): + continue + elif 'adopt' == k: new_children = v if isinstance(v, list) else [v] corrected_kwargs['children'] += new_children continue - if k in {'is_done', 'calendarize'}: + elif k in {'is_done', 'calendarize'}: v = v in VALID_TRUES corrected_kwargs[k] = v todo = self.lib_get('Todo', id_) @@ -1015,9 +1017,9 @@ class TestCaseWithServer(TestCaseWithDB): try: self.assertEqual(cmp, retrieved) except AssertionError as e: - # print('EXPECTED:') - # pprint(cmp) - # print('RETRIEVED:') - # pprint(retrieved) - # walk_diffs('', cmp, retrieved) + print('EXPECTED:') + pprint(cmp) + print('RETRIEVED:') + pprint(retrieved) + walk_diffs('', cmp, retrieved) raise e