home · contact · privacy
Add TaskHandler code to actually make previous commit work. master
authorChristian Heller <c.heller@plomlompom.de>
Sat, 10 Aug 2024 03:32:55 +0000 (05:32 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Sat, 10 Aug 2024 03:32:55 +0000 (05:32 +0200)
plomtask/http.py
templates/todo.html
tests/misc.py
tests/todos.py
tests/utils.py

index cba55d617d32b339bcdd29c9b8f6ef4403e40dc2..e307f1429f93227f7b960d2cad3d55b95e3d737c 100644 (file)
@@ -88,11 +88,11 @@ class InputsParser:
             return None
         return val in {'True', 'true', '1', 'on'}
 
             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 = {}
         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:
         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)
         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_}'
 
         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
         """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'):
         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
                 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:
             try:
-                target_id = int(to_int)
+                parent_id = int(k)
             except ValueError as e:
             except ValueError as e:
-                msg = 'bad fill_for target: {filler}'
+                msg = f'bad step_filler_to_ key: {k}'
                 raise BadFormatException(msg) from e
                 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)
         #
         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)
         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)
                 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)
         # todo.save() may destroy Todo if .effort < 0, so retrieve .id_ early
         url = f'/todo?id={todo.id_}'
         todo.save(self._conn)
index 61d46759d0f0ded201465e9075a0c3e8e3c52b98..de5dbd27c9a0b9ca6299cdb48ea283464129c077 100644 (file)
@@ -22,19 +22,21 @@ select{ font-size: 0.5em; margin: 0; padding: 0; }
 <a href="todo?id={{item.todo.id_}}">{{item.todo.title_then|e}}</a>
 {% else %}
 {{item.process.title.newest|e}}
 <a href="todo?id={{item.todo.id_}}">{{item.todo.title_then|e}}</a>
 {% else %}
 {{item.process.title.newest|e}}
-· fill: <select name="step_filler">
+{% if parent_todo %}
+· fill: <select name="step_filler_to_{{parent_todo.id_}}">
 <option value="ignore">--</option>
 <option value="ignore">--</option>
-<option value="make_empty_{{item.process.id_}}">make empty</option>
-<option value="make_full_{{item.process.id_}}">make full</option>
+<option value="make_{{item.process.id_}}">make empty</option>
 {% for adoptable in adoption_candidates_for[item.process.id_] %}
 <option value="{{adoptable.id_}}">adopt #{{adoptable.id_}}{% if adoptable.comment %} / {{adoptable.comment}}{% endif %}</option>
 {% endfor %}
 </select>
 {% for adoptable in adoption_candidates_for[item.process.id_] %}
 <option value="{{adoptable.id_}}">adopt #{{adoptable.id_}}{% if adoptable.comment %} / {{adoptable.comment}}{% endif %}</option>
 {% endfor %}
 </select>
+{% endif %}
+
 {% endif %}
 </td>
 </tr>
 {% for child in item.children %}
 {% endif %}
 </td>
 </tr>
 {% for child in item.children %}
-{{ draw_tree_row(child, item, indent+1) }}
+{{ draw_tree_row(child, item.todo, indent+1) }}
 {% endfor %}
 {% endmacro %}
 
 {% endfor %}
 {% endmacro %}
 
index 1efa3356b967ca061924579493bfa57fdb9e6e9c..86474c7204cab11d2624a73a88a82554362b7df5 100644 (file)
@@ -36,31 +36,31 @@ class TestsSansServer(TestCase):
         parser = InputsParser({'foo': ['baz', 'quux']})
         self.assertEqual('baz', parser.get_str('foo', 'bar'))
 
         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 = InputsParser({})
         self.assertEqual({},
-                         parser.get_firsts_of_key_prefixed(''))
+                         parser.get_all_of_key_prefixed(''))
         self.assertEqual({},
         self.assertEqual({},
-                         parser.get_firsts_of_key_prefixed('foo'))
+                         parser.get_all_of_key_prefixed('foo'))
         parser = InputsParser({'foo': ['bar']})
         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 = 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']})
         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."""
 
     def test_InputsParser_get_int_or_none(self) -> None:
         """Test InputsParser.get_int_or_none."""
index d84bb7020062a01fadbf9d00116aa38e58379c32..2ecf3b845d4f0128712dfdd63510a683c2d9aeaf 100644 (file)
@@ -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]})
         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')
             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']:
             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."""
 
     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_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'
         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
         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)
 
                                           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:
     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)
         # 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)
         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)
         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)
index 71da9fbedd8c33faff74c081803597d0260abba2..d1b6eac52ea08c196afc7501d10224de138879ec 100644 (file)
@@ -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():
         """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
                 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_)
                 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:
         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
             raise e