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:
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_}'
"""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'):
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)
<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="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>
+{% endif %}
+
{% 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 %}
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."""
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."""
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
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)
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)
"""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_)
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