def _check_initial_state(case): self.assertTrue(case.closed) self.assertEqual(closed_by, case.closed_by) self.assertEqual(closed_on, case.closed_on) self.assertEqual(case.get_case_property('p1'), 'p1-1') # original self.assertEqual(case.get_case_property('p2'), 'p2-2') # updated in second post self.assertEqual(case.get_case_property('p3'), 'p3-2') # new in second post self.assertEqual(case.get_case_property('p4'), 'p4-3') # updated in third post self.assertEqual(case.get_case_property('p5'), 'p5-3') # new in third post if should_use_sql_backend(REBUILD_TEST_DOMAIN): # SQL stores one transaction per form self.assertEqual(3, len(primary_actions(case))) # create + update + close else: self.assertEqual(5, len(primary_actions(case))) # create + 3 updates + close
def rebuild_case(case_id): """ Given a case ID, rebuild the entire case state based on all existing forms referencing it. Useful when things go wrong or when you need to manually rebuild a case after archiving / deleting it """ try: case = CommCareCase.get(case_id) found = True except ResourceNotFound: case = CommCareCase() case._id = case_id found = False # clear actions, xform_ids, close state, and all dynamic properties dynamic_properties = set([k for action in case.actions for k in action.updated_unknown_properties.keys()]) for k in dynamic_properties: try: delattr(case, k) except KeyError: pass # already deleted means it was explicitly set to "deleted", # as opposed to getting set to that because it has no actions already_deleted = case.doc_type == 'CommCareCase-Deleted' and primary_actions(case) if not already_deleted: case.doc_type = 'CommCareCase' case.xform_ids = [] case.actions = [] case.closed = False case.closed_on = None case.closed_by = '' form_ids = get_case_xform_ids(case_id) forms = [fetch_and_wrap_form(id) for id in form_ids] filtered_forms = [f for f in forms if f.doc_type == "XFormInstance"] sorted_forms = sorted(filtered_forms, key=lambda f: f.received_on) for form in sorted_forms: if not found and case.domain is None: case.domain = form.domain assert form.domain == case.domain case_updates = get_case_updates(form) filtered_updates = [u for u in case_updates if u.id == case_id] for u in filtered_updates: case.update_from_case_update(u, form) case.xform_ids = [f._id for f in sorted_forms] if not case.xform_ids: if not found: return None # there were no more forms. 'delete' the case case.doc_type = 'CommCareCase-Deleted' # add a "rebuild" action case.actions.append(_rebuild_action()) case.save() return case
def _check_initial_state(case): self.assertTrue(case.closed) self.assertEqual(closed_by, case.closed_by) self.assertEqual(closed_on, case.closed_on) self.assertEqual(case.p1, 'p1-1') # original self.assertEqual(case.p2, 'p2-2') # updated in second post self.assertEqual(case.p3, 'p3-2') # new in second post self.assertEqual(case.p4, 'p4-3') # updated in third post self.assertEqual(case.p5, 'p5-3') # new in third post self.assertEqual(5, len(primary_actions(case))) # create + 3 updates + close
def testReconcileActions(self): now = datetime.utcnow() # make sure we timestamp everything so they have the right order case_id = _post_util(create=True, form_extras={'received_on': now}) _post_util(case_id=case_id, p1='p1-1', p2='p2-1', form_extras={'received_on': now + timedelta(seconds=1)}) _post_util(case_id=case_id, p2='p2-2', p3='p3-2', form_extras={'received_on': now + timedelta(seconds=2)}) case = CommCareCase.get(case_id) update_strategy = ActionsUpdateStrategy(case) original_actions = [deepcopy(a) for a in case.actions] original_form_ids = [id for id in case.xform_ids] self.assertEqual(3, len(original_actions)) self.assertEqual(3, len(original_form_ids)) self._assertListEqual(original_actions, case.actions) # test reordering case.actions = [case.actions[2], case.actions[1], case.actions[0]] self._assertListNotEqual(original_actions, case.actions) update_strategy.reconcile_actions() self._assertListEqual(original_actions, case.actions) # test duplication case.actions = case.actions * 3 self.assertEqual(9, len(case.actions)) self._assertListNotEqual(original_actions, case.actions) update_strategy.reconcile_actions() self._assertListEqual(original_actions, case.actions) # test duplication, even when dates are off case.actions = original_actions + [deepcopy(case.actions[2])] case.actions[-1].server_date = case.actions[-1].server_date + timedelta(seconds=1) self._assertListNotEqual(original_actions, case.actions) update_strategy.reconcile_actions() self._assertListEqual(original_actions, case.actions) # test duplication with different properties is actually # treated differently case.actions = original_actions + [deepcopy(case.actions[2])] case.actions[-1].updated_unknown_properties['new'] = 'mismatch' self.assertEqual(4, len(case.actions)) self._assertListNotEqual(original_actions, case.actions) update_strategy.reconcile_actions() self._assertListNotEqual(original_actions, case.actions) # test clean slate rebuild case = rebuild_case_from_forms(case_id) self._assertListEqual(original_actions, primary_actions(case)) self._assertListEqual(original_form_ids, case.xform_ids)
def testRebuildCreateCase(self): case_id = _post_util(create=True) _post_util(case_id=case_id, p1='p1', p2='p2') # delete initial case case = CommCareCase.get(case_id) case.delete() try: CommCareCase.get(case_id) self.fail('get should fail on deleted cases') except ResourceNotFound: pass case = rebuild_case_from_forms(REBUILD_TEST_DOMAIN, case_id, RebuildWithReason(reason='test')) self.assertEqual(case.p1, 'p1') self.assertEqual(case.p2, 'p2') self.assertEqual(2, len(primary_actions(case))) # create + update
def test_couch_rebuild_deleted_case(self): # Note: Can't run this on SQL because if a case gets hard deleted then # there is no way to find out which forms created / updated it without # going through ALL the forms in the domain. ie. there is no SQL # equivalent to the "form_case_index/form_case_index" couch view case_id = _post_util(create=True) _post_util(case_id=case_id, p1='p1', p2='p2') # delete initial case delete_all_cases() with self.assertRaises(CaseNotFound): CaseAccessors(REBUILD_TEST_DOMAIN).get_case(case_id) case = rebuild_case_from_forms(REBUILD_TEST_DOMAIN, case_id, RebuildWithReason(reason='test')) self.assertEqual(case.p1, 'p1') self.assertEqual(case.p2, 'p2') self.assertEqual(3, len(primary_actions(case))) # create + update
def reset_case_state(self): """ Clear known case properties, and all dynamic properties """ dynamic_properties = set([ k for action in self.case.actions for k in action.updated_unknown_properties.keys() ]) for k in dynamic_properties: try: delattr(self.case, k) except KeyError: pass except AttributeError: # 'case_id' is not a valid property so don't worry about spamming # this error. if k != 'case_id': logging.error( "Cannot delete attribute '%(attribute)s' from case '%(case_id)s'" % { 'case_id': self.case.case_id, 'attribute': k, } ) # Clear indices and attachments self.case.indices = [] self.case.case_attachments = {} # already deleted means it was explicitly set to "deleted", # as opposed to getting set to that because it has no actions already_deleted = self.case.doc_type == 'CommCareCase-Deleted' and primary_actions(self.case) if not already_deleted: self.case.doc_type = 'CommCareCase' # hard-coded normal properties (from a create block) for prop, default_value in KNOWN_PROPERTIES.items(): setattr(self.case, prop, default_value) self.case.closed = False self.case.modified_on = None self.case.closed_on = None self.case.closed_by = '' return self
def testRebuildCreateCase(self): case_id = post_util(create=True) post_util(case_id=case_id, p1='p1', p2='p2') # delete initial case case = CommCareCase.get(case_id) case.delete() case = rebuild_case(case_id) self.assertEqual(case.p1, 'p1') self.assertEqual(case.p2, 'p2') self.assertEqual(2, len(primary_actions(case))) # create + update case.delete() try: CommCareCase.get(case_id) self.fail('get should fail on deleted cases') except ResourceNotFound: pass case = CommCareCase.get_with_rebuild(case_id) self.assertEqual(case.p1, 'p1') self.assertEqual(case.p2, 'p2')
def testFormArchiving(self): now = datetime.utcnow() # make sure we timestamp everything so they have the right order case_id = _post_util(create=True, p1='p1-1', p2='p2-1', form_extras={'received_on': now}) _post_util(case_id=case_id, p2='p2-2', p3='p3-2', p4='p4-2', form_extras={'received_on': now + timedelta(seconds=1)}) _post_util(case_id=case_id, p4='p4-3', p5='p5-3', close=True, form_extras={'received_on': now + timedelta(seconds=2)}) case = CommCareCase.get(case_id) closed_by = case.closed_by closed_on = case.closed_on self.assertNotEqual('', closed_by) self.assertNotEqual(None, closed_on) def _check_initial_state(case): self.assertTrue(case.closed) self.assertEqual(closed_by, case.closed_by) self.assertEqual(closed_on, case.closed_on) self.assertEqual(case.p1, 'p1-1') # original self.assertEqual(case.p2, 'p2-2') # updated in second post self.assertEqual(case.p3, 'p3-2') # new in second post self.assertEqual(case.p4, 'p4-3') # updated in third post self.assertEqual(case.p5, 'p5-3') # new in third post self.assertEqual(5, len(primary_actions(case))) # create + 3 updates + close _check_initial_state(case) # verify xform/action states [create, u1, u2, u3, close] = case.actions [f1, f2, f3] = case.xform_ids self.assertEqual(f1, create.xform_id) self.assertEqual(f1, u1.xform_id) self.assertEqual(f2, u2.xform_id) self.assertEqual(f3, u3.xform_id) # todo: should this be the behavior for archiving the create form? f1_doc = XFormInstance.get(f1) f1_doc.archive() case = CommCareCase.get(case_id) self.assertEqual(3, len(primary_actions(case))) [u2, u3] = case.xform_ids self.assertEqual(f2, u2) self.assertEqual(f3, u3) self.assertTrue(case.closed) # no change self.assertFalse('p1' in case._doc) # should disappear entirely self.assertEqual(case.p2, 'p2-2') # no change self.assertEqual(case.p3, 'p3-2') # no change self.assertEqual(case.p4, 'p4-3') # no change self.assertEqual(case.p5, 'p5-3') # no change def _reset(form_id): form_doc = XFormInstance.get(form_id) form_doc.unarchive() case = CommCareCase.get(case_id) _check_initial_state(case) _reset(f1) f2_doc = XFormInstance.get(f2) f2_doc.archive() case = CommCareCase.get(case_id) self.assertEqual(4, len(primary_actions(case))) [u1, u3] = case.xform_ids self.assertEqual(f1, u1) self.assertEqual(f3, u3) self.assertTrue(case.closed) # no change self.assertEqual(case.p1, 'p1-1') # original self.assertEqual(case.p2, 'p2-1') # loses second form update # self.assertFalse('p3' in case._doc) # todo: should disappear entirely self.assertEqual(case.p4, 'p4-3') # no change self.assertEqual(case.p5, 'p5-3') # no change _reset(f2) f3_doc = XFormInstance.get(f3) f3_doc.archive() case = CommCareCase.get(case_id) self.assertEqual(3, len(primary_actions(case))) [u1, u2] = case.xform_ids self.assertEqual(f1, u1) self.assertEqual(f2, u2) self.assertFalse(case.closed) # reopened! self.assertEqual('', case.closed_by) self.assertEqual(None, case.closed_on) self.assertEqual(case.p1, 'p1-1') # original self.assertEqual(case.p2, 'p2-2') # original self.assertEqual(case.p3, 'p3-2') # new in second post self.assertEqual(case.p4, 'p4-2') # loses third form update # self.assertFalse('p5' in case._doc) # todo: should disappear entirely _reset(f3)
def test_form_archiving(self): now = datetime.utcnow() # make sure we timestamp everything so they have the right order case_id = _post_util(create=True, p1='p1-1', p2='p2-1', form_extras={'received_on': now}) _post_util(case_id=case_id, p2='p2-2', p3='p3-2', p4='p4-2', form_extras={'received_on': now + timedelta(seconds=1)}) _post_util(case_id=case_id, p4='p4-3', p5='p5-3', close=True, form_extras={'received_on': now + timedelta(seconds=2)}) case_accessors = CaseAccessors(REBUILD_TEST_DOMAIN) case = case_accessors.get_case(case_id) closed_by = case.closed_by closed_on = case.closed_on self.assertNotEqual('', closed_by) self.assertNotEqual(None, closed_on) def _check_initial_state(case): self.assertTrue(case.closed) self.assertEqual(closed_by, case.closed_by) self.assertEqual(closed_on, case.closed_on) self.assertEqual(case.get_case_property('p1'), 'p1-1') # original self.assertEqual(case.get_case_property('p2'), 'p2-2') # updated in second post self.assertEqual(case.get_case_property('p3'), 'p3-2') # new in second post self.assertEqual(case.get_case_property('p4'), 'p4-3') # updated in third post self.assertEqual(case.get_case_property('p5'), 'p5-3') # new in third post if should_use_sql_backend(REBUILD_TEST_DOMAIN): # SQL stores one transaction per form self.assertEqual(3, len(primary_actions(case))) # create + update + close else: self.assertEqual(5, len(primary_actions(case))) # create + 3 updates + close _check_initial_state(case) # verify xform/action states [f1, f2, f3] = case.xform_ids if should_use_sql_backend(REBUILD_TEST_DOMAIN): [create, update, close] = case.actions self.assertEqual(f1, create.form_id) self.assertEqual(f2, update.form_id) self.assertEqual(f3, close.form_id) else: [create, u1, u2, u3, close] = case.actions self.assertEqual(f1, create.form_id) self.assertEqual(f1, u1.form_id) self.assertEqual(f2, u2.form_id) self.assertEqual(f3, u3.form_id) # todo: should this be the behavior for archiving the create form? form_acessors = FormAccessors(REBUILD_TEST_DOMAIN) f1_doc = form_acessors.get_form(f1) with capture_kafka_changes_context(topics.CASE_SQL) as change_context: f1_doc.archive() if should_use_sql_backend(case.domain): self.assertEqual([case.case_id], [change.id for change in change_context.changes]) case = case_accessors.get_case(case_id) if should_use_sql_backend(REBUILD_TEST_DOMAIN): self.assertEqual(2, len(primary_actions(case))) else: self.assertEqual(3, len(primary_actions(case))) [u2, u3] = case.xform_ids self.assertEqual(f2, u2) self.assertEqual(f3, u3) self.assertTrue(case.closed) # no change self.assertFalse('p1' in case.dynamic_case_properties()) # should disappear entirely self.assertEqual(case.get_case_property('p2'), 'p2-2') # no change self.assertEqual(case.get_case_property('p3'), 'p3-2') # no change self.assertEqual(case.get_case_property('p4'), 'p4-3') # no change self.assertEqual(case.get_case_property('p5'), 'p5-3') # no change def _reset(form_id): form_doc = form_acessors.get_form(form_id) form_doc.unarchive() case = case_accessors.get_case(case_id) _check_initial_state(case) _reset(f1) f2_doc = form_acessors.get_form(f2) f2_doc.archive() case = case_accessors.get_case(case_id) if should_use_sql_backend(REBUILD_TEST_DOMAIN): self.assertEqual(2, len(primary_actions(case))) else: self.assertEqual(4, len(primary_actions(case))) [u1, u3] = case.xform_ids self.assertEqual(f1, u1) self.assertEqual(f3, u3) self.assertTrue(case.closed) # no change self.assertEqual(case.get_case_property('p1'), 'p1-1') # original self.assertEqual(case.get_case_property('p2'), 'p2-1') # loses second form update self.assertFalse('p3' in case.dynamic_case_properties()) # should disappear entirely self.assertEqual(case.get_case_property('p4'), 'p4-3') # no change self.assertEqual(case.get_case_property('p5'), 'p5-3') # no change _reset(f2) f3_doc = form_acessors.get_form(f3) f3_doc.archive() case = case_accessors.get_case(case_id) if should_use_sql_backend(REBUILD_TEST_DOMAIN): self.assertEqual(2, len(primary_actions(case))) else: self.assertEqual(3, len(primary_actions(case))) [u1, u2] = case.xform_ids self.assertEqual(f1, u1) self.assertEqual(f2, u2) self.assertFalse(case.closed) # reopened! self.assertEqual('', case.closed_by) self.assertEqual(None, case.closed_on) self.assertEqual(case.get_case_property('p1'), 'p1-1') # original self.assertEqual(case.get_case_property('p2'), 'p2-2') # original self.assertEqual(case.get_case_property('p3'), 'p3-2') # new in second post self.assertEqual(case.get_case_property('p4'), 'p4-2') # loses third form update self.assertFalse('p5' in case.dynamic_case_properties()) # should disappear entirely _reset(f3)