msg = f'cannot float form field value for key {key}: {val}'
             raise BadFormatException(msg) from e
 
+    def get_float_or_none(self, key: str) -> float | None:
+        """Retrieve float value of key from self.postvars, None if empty."""
+        val = self.get_str(key)
+        if '' == val:
+            return None
+        try:
+            return float(val)
+        except ValueError as e:
+            msg = f'cannot float form field value for key {key}: {val}'
+            raise BadFormatException(msg) from e
+
     def get_all_str(self, key: str) -> list[str]:
         """Retrieve list of string values at key."""
         if key not in self.inputs.keys():
             day_comment = self._form_data.get_str('day_comment')
             make_type = self._form_data.get_str('make_type')
         except NotFoundException as e:
-            raise BadFormatException(e) from e
+            raise BadFormatException(e)
         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')
     def do_POST_todo(self, todo: Todo) -> str:
         """Update Todo and its children."""
         # pylint: disable=too-many-locals
+        # pylint: disable=too-many-branches
+        # pylint: disable=too-many-branches
         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)
+        with_effort_post = True
+        try:
+            effort = self._form_data.get_float_or_none('effort')
+        except NotFoundException:
+            with_effort_post = False
         conditions = self._form_data.get_all_int('conditions')
         disables = self._form_data.get_all_int('disables')
         blockers = self._form_data.get_all_int('blockers')
         for process_id in processes_to_make_full:
             made = Todo.create_with_children(self.conn, process_id, todo.date)
             todo.add_child(made)
-        todo.effort = float(effort) if effort else None
+        if with_effort_post:
+            todo.effort = effort
         todo.set_conditions(self.conn, conditions)
         todo.set_blockers(self.conn, blockers)
         todo.set_enables(self.conn, enables)
 
             with self.assertRaises(BadFormatException):
                 InputsParser({'foo': []}, strictness).get_float('foo')
 
+    def test_InputsParser_get_float_or_none(self) -> None:
+        """Test InputsParser.get_float_or_none on strict and non-strict."""
+        for strictness in (False, True):
+            with self.assertRaises(BadFormatException):
+                InputsParser({'foo': ['bar']}, strictness).\
+                        get_float_or_none('foo')
+            parser = InputsParser({'foo': ['']}, strictness)
+            self.assertEqual(None, parser.get_float_or_none('foo'))
+            parser = InputsParser({'foo': ['0']}, strictness)
+            self.assertEqual(0, parser.get_float_or_none('foo'))
+            parser = InputsParser({'foo': ['0.1']}, strictness)
+            self.assertEqual(0.1, parser.get_float_or_none('foo'))
+            parser = InputsParser({'foo': ['1.23', '456']}, strictness)
+            self.assertEqual(1.23, parser.get_float_or_none('foo'))
+        if strictness:
+            with self.assertRaises(NotFoundException):
+                InputsParser({}, strictness).get_float_or_none('foo')
+            with self.assertRaises(NotFoundException):
+                InputsParser({'foo': []}, strictness).get_float_or_none('foo')
+        else:
+            parser = InputsParser({}, strictness)
+            self.assertEqual(None, parser.get_float_or_none('foo'))
+            parser = InputsParser({'foo': []}, strictness)
+            self.assertEqual(None, parser.get_float_or_none('foo'))
+
     def test_InputsParser_get_all_str(self) -> None:
         """Test InputsParser.get_all_str on strict and non-strict."""
         for strictness in (False, True):
 
 class TestsWithServer(TestCaseWithServer):
     """Tests against our HTTP server/handler (and database)."""
 
+    def setUp(self) -> None:
+        super().setUp()
+        self._proc1_form_data = self.post_process(1)
+
+    def test_basic_fail_POST_todo(self) -> None:
+        """Test basic malformed/illegal POST /todo requests."""
+        # test we cannot just POST into non-existing Todo
+        self.check_post({}, '/todo', 404)
+        self.check_post({}, '/todo?id=FOO', 400)
+        self.check_post({}, '/todo?id=0', 404)
+        self.check_post({}, '/todo?id=1', 404)
+        # test malformed values on existing Todo
+        day_post = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
+        self.check_post(day_post, '/day?date=2024-01-01&make_type=full')
+        for name in ['adopt', 'effort', 'make_full', 'make_empty',
+                     'conditions', 'disables', 'blockers', 'enables']:
+            self.check_post({name: 'x'}, '/todo?id=1', 400, '/todo')
+        # test we cannot POST adoption of self or non-existing Todo
+        self.check_post({'adopt': 1}, '/todo?id=1', 400)
+        self.check_post({'adopt': 2}, '/todo?id=1', 404)
+
     def test_do_POST_day(self) -> None:
         """Test Todo posting of POST /day."""
-        self.post_process()
         self.post_process(2)
         proc = Process.by_id(self.db_conn, 1)
         proc2 = Process.by_id(self.db_conn, 2)
                             redir_url: str = '/todo?id=1') -> Todo:
             self.check_post(form_data, '/todo?id=1', status, redir_url)
             return Todo.by_date(self.db_conn, '2024-01-01')[0]
-        # test minimum
-        self.post_process()
         self.check_post({'day_comment': '', 'new_todo': 1,
                          'make_type': 'full'},
                         '/day?date=2024-01-01&make_type=full', 302)
-        # test posting to bad URLs
-        self.check_post({}, '/todo=', 404)
-        self.check_post({}, '/todo?id=', 404)
-        self.check_post({}, '/todo?id=FOO', 400)
-        self.check_post({}, '/todo?id=0', 404)
         # test posting naked entity
         todo1 = post_and_reload({})
         self.assertEqual(todo1.children, [])
         # test implicitly posting non-doneness
         todo1 = post_and_reload({})
         self.assertEqual(todo1.is_done, False)
-        # test malformed adoptions
-        self.check_post({'adopt': 'foo'}, '/todo?id=1', 400)
-        self.check_post({'adopt': 1}, '/todo?id=1', 400)
-        self.check_post({'adopt': 2}, '/todo?id=1', 404)
         # test posting second todo of same process
         self.check_post({'day_comment': '', 'new_todo': 1,
                          'make_type': 'full'},
 
     def test_do_POST_day_todo_adoption(self) -> None:
         """Test Todos posted to Day view may adopt existing Todos."""
-        form_data = self.post_process()
-        form_data = self.post_process(2, form_data | {'new_top_step': 1})
+        form_data = self.post_process(
+                2, self._proc1_form_data | {'new_top_step': 1})
         form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
         form_data['new_todo'] = 2
 
     def test_do_POST_day_todo_multiple(self) -> None:
         """Test multiple Todos can be posted to Day view."""
-        form_data = self.post_process()
+        # form_data = self.post_process()
         form_data = self.post_process(2)
         form_data = {'day_comment': '', 'new_todo': [1, 2],
                      'make_type': 'full'}
             self.assertEqual(todo3.children, [])
             self.assertEqual(sorted(todo3.parents), sorted([todo2, todo1]))
 
-        form_data = self.post_process()
-        form_data = self.post_process(2, form_data | {'new_top_step': 1})
+        self.post_process(2, self._proc1_form_data | {'new_top_step': 1})
         check_adoption('2024-01-01', [1, 2])
         check_adoption('2024-01-02', [2, 1])
         check_nesting_adoption(3, '2024-01-03', [1, 2])
 
     def test_do_POST_day_todo_doneness(self) -> None:
         """Test Todo doneness can be posted to Day view."""
-        self.post_process()
         form_data = {'day_comment': '', 'new_todo': [1], 'make_type': 'full'}
         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
         todo = Todo.by_date(self.db_conn, '2024-01-01')[0]
 
     def test_do_GET_todo(self) -> None:
         """Test GET /todo response codes."""
-        self.post_process()
         form_data = {'day_comment': '', 'new_todo': 1, 'make_type': 'full'}
         self.check_post(form_data, '/day?date=2024-01-01&make_type=full', 302)
         self.check_get('/todo', 404)
         self.check_get('/todo?id=foo', 400)
         self.check_get('/todo?id=0', 404)
         self.check_get('/todo?id=1', 200)
+        self.check_get('/todo?id=2', 404)