def _further_deduplicate(action_list): def actions_match(a1, a2): # if everything but the server_date match, the actions match. # this will allow for multiple case blocks to be submitted # against the same case in the same form so long as they # are different a1doc = copy.copy(a1._doc) a2doc = copy.copy(a2._doc) a2doc['server_date'] = a1doc['server_date'] a2doc['date'] = a1doc['date'] return a1doc == a2doc ret = [] for a in action_list: found_actions = [ other for other in ret if actions_match(a, other) ] if found_actions: if len(found_actions) != 1: error = (u"Case {0} action conflicts " u"with multiple other actions: {1}") raise ReconciliationError(error.format(self.get_id, a)) match = found_actions[0] # when they disagree, choose the _earlier_ one as this is # the one that is likely timestamped with the form's date # (and therefore being processed later in absolute time) ret[ret.index( match )] = a if a.server_date < match.server_date else match else: ret.append(a) return ret
def _check_preconditions(): error = None for a in self.actions: if a.server_date is None: error = u"Case {0} action server_date is None: {1}" elif a.xform_id is None: error = u"Case {0} action xform_id is None: {1}" if error: raise ReconciliationError(error.format(self.get_id, a))
def rebuild(self, strict=True): """ Rebuilds the case state from its actions. If strict is True, this will enforce that the first action must be a create. TODO: this implementation has a number of flaws: - it starts from whatever the cases current state is, not a clean slate - it simply ignores all case create blocks, except to report whether they're in order; it does not apply their changes. """ # try to re-sort actions if necessary try: self.actions = sorted(self.actions, key=_action_sort_key_function(self)) except MissingServerDate: # only worry date reconciliation if in strict mode if strict: raise actions = copy.deepcopy(list(self.actions)) if strict: if actions[0].action_type != const.CASE_ACTION_CREATE: error = u"Case {0} first action not create action: {1}" raise ReconciliationError( error.format(self.get_id, self.actions[0])) actions.pop(0) else: actions = [ a for a in actions if a.action_type != const.CASE_ACTION_CREATE ] for a in actions: self._apply_action(a) self.xform_ids = [] for a in self.actions: if a.xform_id not in self.xform_ids: self.xform_ids.append(a.xform_id)
def rebuild(self, strict=True, xforms=None): """ Rebuilds the case state in place from its actions. If strict is True, this will enforce that the first action must be a create. """ from casexml.apps.case.cleanup import reset_state xforms = xforms or {} reset_state(self) # try to re-sort actions if necessary try: self.actions = sorted(self.actions, key=_action_sort_key_function(self)) except MissingServerDate: # only worry date reconciliation if in strict mode if strict: raise # remove all deprecated actions during rebuild. self.actions = [a for a in self.actions if not a.deprecated] actions = copy.deepcopy(list(self.actions)) if strict: if actions[0].action_type != const.CASE_ACTION_CREATE: error = u"Case {0} first action not create action: {1}" raise ReconciliationError( error.format(self.get_id, self.actions[0])) for a in actions: self._apply_action(a, xforms.get(a.xform_id)) self.xform_ids = [] for a in self.actions: if a.xform_id and a.xform_id not in self.xform_ids: self.xform_ids.append(a.xform_id)
def reconcile_actions(self, rebuild=False, xforms=None): """ Runs through the action list and tries to reconcile things that seem off (for example, out-of-order submissions, duplicate actions, etc.). This method raises a ReconciliationError if anything goes wrong. """ def _check_preconditions(): error = None for a in self.actions: if a.server_date is None: error = u"Case {0} action server_date is None: {1}" elif a.xform_id is None: error = u"Case {0} action xform_id is None: {1}" if error: raise ReconciliationError(error.format(self.get_id, a)) _check_preconditions() # this would normally work except we only recently started using the # form timestamp as the modification date so we have to do something # fancier to deal with old data deduplicated_actions = list(set(self.actions)) def _further_deduplicate(action_list): def actions_match(a1, a2): # if everything but the server_date match, the actions match. # this will allow for multiple case blocks to be submitted # against the same case in the same form so long as they # are different a1doc = copy.copy(a1._doc) a2doc = copy.copy(a2._doc) a2doc['server_date'] = a1doc['server_date'] a2doc['date'] = a1doc['date'] return a1doc == a2doc ret = [] for a in action_list: found_actions = [ other for other in ret if actions_match(a, other) ] if found_actions: if len(found_actions) != 1: error = (u"Case {0} action conflicts " u"with multiple other actions: {1}") raise ReconciliationError(error.format(self.get_id, a)) match = found_actions[0] # when they disagree, choose the _earlier_ one as this is # the one that is likely timestamped with the form's date # (and therefore being processed later in absolute time) ret[ret.index( match )] = a if a.server_date < match.server_date else match else: ret.append(a) return ret deduplicated_actions = _further_deduplicate(deduplicated_actions) sorted_actions = sorted(deduplicated_actions, key=_action_sort_key_function(self)) if sorted_actions: if sorted_actions[0].action_type != const.CASE_ACTION_CREATE: error = u"Case {0} first action not create action: {1}" raise ReconciliationError( error.format(self.get_id, sorted_actions[0])) self.actions = sorted_actions if rebuild: # it's pretty important not to block new case changes # just because previous case changes have been bad self.rebuild(strict=False, xforms=xforms)