def patch_draft(self, user, patch, **options): """Operation specific to draft, before applying generic patch.""" try: params = dict(patch) except Exception as exc: log.info(exc) raise err.PatchError(message=exc.message) # silently remove unexpected props within patch if not in strict mode strict_patch = Configuration('global').get('apiV1.strict_patch', False) if not strict_patch: allowed_properties = [ "body", "current_state", "user_identities", "message_id", "parent_id", "participants", "subject", "privacy_features", ] for key, value in params.items(): if key not in allowed_properties: del (params[key]) for key, value in params["current_state"].items(): if key not in allowed_properties: del (params["current_state"][key]) try: self.get_db() self.unmarshall_db() except Exception as exc: log.info("patch_draft() failed to get msg from db: {}".format(exc)) raise exc if not self.is_draft: raise err.PatchUnprocessable(message="this message is not a draft") try: current_state = params.pop("current_state") draft_param = Draft(params, strict=strict_patch) except Exception as exc: log.info(exc) raise err.PatchError(message=exc.message) # add missing params to be able to check consistency self_dict = self.marshall_dict() if "message_id" not in params and self.message_id: draft_param.message_id = UUIDType().to_native(self.message_id) if "parent_id" not in params and self.parent_id: draft_param.parent_id = UUIDType().to_native(self.parent_id) if "subject" not in params: draft_param.subject = self.subject if "participants" not in params and self.participants: for participant in self_dict['participants']: indexed = IndexedParticipant(participant) draft_param.participants.append(indexed) if "user_identities" not in params and self.user_identities: draft_param.user_identities = self_dict["user_identities"] # make sure the <from> participant is present # and is consistent with selected user's identity try: new_discussion_id = draft_param.validate_consistency(user, False) except Exception as exc: log.info("consistency validation failed with err : {}".format(exc)) raise err.PatchError(message=exc.message) validated_draft = draft_param.serialize() validated_params = copy.deepcopy(params) if "participants" in params: validated_params["participants"] = validated_draft["participants"] if new_discussion_id != self.discussion_id: # discussion_id has changed, update draft's discussion_id current_state["discussion_id"] = self.discussion_id validated_params["discussion_id"] = new_discussion_id # remove empty ids from current state if any if "parent_id" in current_state and current_state["parent_id"] == "": del (current_state["parent_id"]) # handle body key mapping to body_plain or body_html # TODO: handle plain/html flag to map to right field if "body" in validated_params: validated_params["body_plain"] = validated_params["body"] del (validated_params["body"]) if "body" in current_state: current_state["body_plain"] = current_state["body"] del (current_state["body"]) # date should reflect last edit time current_state["date"] = self.date current_state["date_sort"] = self.date_sort validated_params["date"] = validated_params["date_sort"] = \ datetime.datetime.now(tz=pytz.utc) validated_params["current_state"] = current_state if "participants" in current_state and self.participants: # replace participants' label and contact_ids # because frontend has up-to-date data for these properties # which are probably not the one stored in db db_parts = {} for p in self_dict['participants']: db_parts[p['protocol'] + p['type'] + p['address']] = p for i, p in enumerate(current_state['participants']): current_state['participants'][i] = db_parts[p['protocol'] + p['type'] + p['address']] try: self.apply_patch(validated_params, **options) except Exception as exc: log.info("apply_patch() failed with error : {}".format(exc)) raise exc
def apply_patch(self, patch, **options): """ Update self attributes with patch rfc7396 and Caliopen's specifications if, and only if, patch is consistent with current obj db instance :param patch: json-dict object describing the patch to apply with a "current_state" key. see caliopen rfc for explanation :param options: whether patch should be propagated to db and/or index :return: Exception or None """ if patch is None or "current_state" not in patch: raise main_errors.PatchUnprocessable() patch_current = patch.pop("current_state") # build 3 siblings : 2 from patch and last one from db obj_patch_new = self.__class__(user_id=self.user_id) obj_patch_old = self.__class__(user_id=self.user_id) try: obj_patch_new.unmarshall_json_dict(patch) except Exception as exc: log.info(exc) raise main_errors.PatchUnprocessable(message= \ "unable to unmarshall patch into object <{}>".format( exc)) try: obj_patch_old.unmarshall_json_dict(patch_current) except Exception as exc: log.info(exc) raise main_errors.PatchUnprocessable(message= \ "unable to unmarshall patch into object <{}>".format( exc)) self.get_db() # TODO : manage protected attributes, to prevent patch on them if "tags" in patch.keys(): raise main_errors.ForbiddenAction( message="patching tags through parent object is forbidden") # check if patch is consistent with db current state # if it is, squash self attributes self.unmarshall_db() for key in patch.keys(): current_attr = self._attrs[key] try: self._check_key_consistency(current_attr, key, obj_patch_old, obj_patch_new) except Exception as exc: log.info("key consistency checking failed: {}".format(exc)) raise exc # all controls passed, we can actually set the new attribute create_sub_object = False if key not in patch_current.keys(): create_sub_object = True else: if patch_current[key] in (None, [], {}): create_sub_object = True if isinstance( patch_current[key], list) and len(patch[key]) > len(patch_current[key]): create_sub_object = True if patch[key] is not None: unmarshall_item(patch, key, self, self._attrs[key], create_sub_object) if "db" in options and options["db"] is True: # apply changes to db model and update db if "with_validation" in options and options[ "with_validation"] is True: d = self.marshall_dict() try: self._json_model(d).validate() except Exception as exc: log.info("document is not valid: {}".format(exc)) raise main_errors.PatchUnprocessable( message="document is not valid," " can't insert it into db: <{}>".format(exc)) self.marshall_db() try: self.update_db() except Exception as exc: log.info(exc) raise main_errors.PatchError(message="Error when updating db")
def patch_draft(self, patch, **options): """Operation specific to draft, before applying generic patch.""" try: params = dict(patch) except Exception as exc: log.info(exc) raise err.PatchError(message=exc.message) # silently remove unexpected props within patch if not in strict mode strict_patch = Configuration('global').get('apiV1.strict_patch', False) if not strict_patch: allowed_properties = [ "body", "current_state", "identities", "message_id", "parent_id", "participants", "subject", ] for key, value in params.items(): if key not in allowed_properties: del (params[key]) for key, value in params["current_state"].items(): if key not in allowed_properties: del (params["current_state"][key]) try: self.get_db() self.unmarshall_db() except Exception as exc: log.info("patch_draft() failed to get msg from db: {}".format(exc)) raise exc if not self.is_draft: raise err.PatchUnprocessable(message="this message is not a draft") try: current_state = params.pop("current_state") draft_param = Draft(params, strict=strict_patch) except Exception as exc: log.info(exc) raise err.PatchError(message=exc.message) # add missing params to be able to check consistency self_dict = self.marshall_dict() if "message_id" not in params and self.message_id: draft_param.message_id = UUIDType().to_native(self.message_id) if "discussion_id" not in params and self.discussion_id: draft_param.discussion_id = UUIDType().to_native( self.discussion_id) if "parent_id" not in params and self.parent_id: draft_param.parent_id = UUIDType().to_native(self.parent_id) if "subject" not in params: draft_param.subject = self.subject if "participants" not in params and self.participants: for participant in self_dict['participants']: draft_param.participants.append( IndexedParticipant(participant)) if "identities" not in params and self.identities: draft_param.identities = self_dict["identities"] try: draft_param.validate_consistency(str(self.user_id), False) except Exception as exc: log.info("consistency validation failed with err : {}".format(exc)) raise err.PatchError(message=exc.message) # make sure the <from> participant is present # and is consistent with selected user's identity validated_draft = draft_param.serialize() validated_params = copy.deepcopy(params) if "participants" in params: validated_params["participants"] = validated_draft["participants"] # remove empty UUIDs from current state if any if "parent_id" in current_state and current_state["parent_id"] == "": del (current_state["parent_id"]) if "discussion_id" in current_state and current_state[ "discussion_id"] == "": del (current_state["discussion_id"]) # handle body key mapping to body_plain or body_html # TODO: handle plain/html flag to map to right field if "body" in validated_params: validated_params["body_plain"] = validated_params["body"] del (validated_params["body"]) if "body" in current_state: current_state["body_plain"] = current_state["body"] del (current_state["body"]) # date should reflect last edit time current_state["date"] = self.date current_state["date_sort"] = self.date_sort validated_params["date"] = validated_params["date_sort"] = \ datetime.datetime.now(tz=pytz.utc) validated_params["current_state"] = current_state try: self.apply_patch(validated_params, **options) except Exception as exc: log.info("apply_patch() failed with error : {}".format(exc)) raise exc
def _check_discussion_consistency(self, user): from caliopen_main.message.objects.message import Message new_discussion = False if not hasattr(self, 'discussion_id') or self.discussion_id == "" \ or self.discussion_id is None: # no discussion_id provided. Try to find one with draft's parent_id # or create new discussion if hasattr(self, 'parent_id') \ and self.parent_id is not None \ and self.parent_id != "": parent_msg = Message(user, message_id=self.parent_id) try: parent_msg.get_db() parent_msg.unmarshall_db() except NotFound: raise err.PatchError(message="parent message not found") self.discussion_id = parent_msg.discussion_id else: discussion = Discussion.create_from_message(user, self) self.discussion_id = discussion.discussion_id new_discussion = True if not new_discussion: dim = DIM(user) d_id = self.discussion_id last_message = dim.get_last_message(d_id, -10, 10, True) if last_message == {}: raise err.PatchError( message='No such discussion {}'.format(d_id)) is_a_reply = (str(last_message.message_id) != str(self.message_id)) if is_a_reply: # check participants consistency if hasattr(self, "participants") and len(self.participants) > 0: participants = [p['address'] for p in self.participants] last_msg_participants = [ p['address'] for p in last_message.participants ] if len(participants) != len(last_msg_participants): raise err.PatchError( message="list of participants " "is not consistent for this discussion") participants.sort() last_msg_participants.sort() for i, participant in enumerate(participants): if participant != last_msg_participants[i]: raise err.PatchConflict( message="list of participants " "is not consistent for this discussion") else: self.build_participants_for_reply(user) # check parent_id consistency if 'parent_id' in self and self.parent_id != "" \ and self.parent_id is not None: if not dim.message_belongs_to( discussion_id=self.discussion_id, message_id=self.parent_id): raise err.PatchConflict(message="provided message " "parent_id does not belong " "to this discussion") else: self.parent_id = last_message.parent_id self.update_external_references(user) else: last_message = None else: last_message = None return last_message
def patch_draft(self, patch, **options): """Operation specific to draft, before applying generic patch.""" try: self.get_db() self.unmarshall_db() except Exception as exc: log.info("patch_draft() failed to get msg from db: {}".format(exc)) raise exc if not self.is_draft: raise err.PatchUnprocessable(message="this message is not a draft") try: params = dict(patch) current_state = params.pop("current_state") draft_param = Draft(params) except Exception as exc: log.info(exc) raise err.PatchError(message=exc.message) # add missing params to be able to check consistency self_dict = self.marshall_dict() if "message_id" not in params and self.message_id: draft_param.message_id = UUIDType().to_native(self.message_id) if "discussion_id" not in params and self.discussion_id: draft_param.discussion_id = UUIDType().to_native( self.discussion_id) if "parent_id" not in params and self.parent_id: draft_param.parent_id = UUIDType().to_native(self.parent_id) if "subject" not in params: draft_param.subject = self.subject if "participants" not in params and self.participants: for participant in self_dict['participants']: draft_param.participants.append( IndexedParticipant(participant)) if "identities" not in params and self.identities: draft_param.identities = self_dict["identities"] try: draft_param.validate_consistency(str(self.user_id), False) except Exception as exc: log.info("consistency validation failed with err : {}".format(exc)) raise err.PatchError(message=exc.message) # make sure the <from> participant is present # and is consistent with selected user's identity validated_draft = draft_param.serialize() validated_params = copy.deepcopy(params) if "participants" in params: validated_params["participants"] = validated_draft["participants"] # handle body key mapping to body_plain or body_html # TODO: handle plain/html flag to map to right field if "body" in validated_params: validated_params["body_plain"] = validated_params["body"] del (validated_params["body"]) if "body" in current_state: current_state["body_plain"] = current_state["body"] del (current_state["body"]) validated_params["current_state"] = current_state try: self.apply_patch(validated_params, **options) except Exception as exc: log.info("apply_patch() failed with error : {}".format(exc)) raise exc