class WithAction(object): """Mixin for add/remove map/unmap actions processing""" _api_attrs = ApiAttributes( Attribute("actions", create=False, update=True, read=False)) _operation_order = [ "add_related", "remove_related", ] _object_map = { "Document": Document, "Evidence": Evidence, "Comment": Comment, "Snapshot": Snapshot, } _actions = None _added = None # collect added objects for signals sending _deleted = None # collect deleted objects fro signals sending _relationships_map = None def actions(self, value): """Save actions for further processing""" if value: self._actions = value.get("actions") def _validate_actions(self): """Validate operation types""" invalid_actions = ",".join( set(self._actions) - set(self._operation_order)) if invalid_actions: raise ValueError( "Invalid actions found: {}".format(invalid_actions)) def _build_relationships_map(self): """Build relationships map""" self._relationships_map = {(rel.destination_type, rel.destination_id): rel for rel in self.related_destinations} self._relationships_map.update({(rel.source_type, rel.source_id): rel for rel in self.related_sources}) def _process_operation(self, operation): """Process operation actions""" for action in self._actions[operation]: # get object class obj_type = action.get("type") if not obj_type: raise ValidationError('type is not defined') obj_class = self._object_map.get(obj_type) if not obj_class: raise ValueError( 'Invalid action type: {type}'.format(type=obj_type)) # get handler class action_type = '{type}Action'.format(type=obj_type) action_class = getattr(self, action_type, None) if not action_class: raise ValueError( 'Invalid action type: {type}'.format(type=obj_type)) # process action # pylint: disable=not-callable added, deleted = getattr(action_class(), operation)(self, action) # collect added/deleted objects self._added.extend(added) self._deleted.extend(deleted) def process_actions(self): """Process actions""" if not self._actions: return {}, [] self._validate_actions() self._added = [] self._deleted = [] for operation in self._operation_order: if operation not in self._actions: continue if not self._actions[operation]: continue self._build_relationships_map() self._process_operation(operation) # collect added/deleted objects for signals sending added = defaultdict(list) for obj in self._added: added[obj.__class__].append(obj) return added, self._deleted class BaseAction(object): """Base action""" AddRelated = namedtuple("AddRelated", ["id", "type"]) MapRelated = namedtuple("MapRelated", ["id", "type"]) RemoveRelated = namedtuple("RemoveRelated", ["id", "type"]) def add_related(self, parent, _action): """Add/map object to parent""" added = [] if _action.get("id"): action = self._validate(_action, self.MapRelated) obj = self._get(action) else: action = self._validate(_action, self.AddRelated) obj = self._create(parent, action) added.append(obj) rel = Relationship(source=parent, destination=obj, context=parent.context) added.append(rel) return added, [] @staticmethod def _validate(_action, ntuple): try: return ntuple(**_action) except TypeError: # According to documentation _fields is not private property # but public, '_' added to prevent conflicts with tuple field names # pylint: disable=protected-access missing_fields = set(ntuple._fields) - set(_action) raise ValidationError( "Fields {} are missing for action: {!r}".format( ", ".join(missing_fields), _action)) # pylint: disable=unused-argument,no-self-use def _create(self, parent, action): raise ValidationError( "Can't create {type} object".format(type=action.type)) def _get(self, action): """Get object specified in action""" if not action.id: raise ValueError("id is not defined") # pylint: disable=protected-access obj_class = WithAction._object_map[action.type] obj = obj_class.query.get(action.id) if not obj: raise ValueError('Object not found: {type} {id}'.format( type=action.type, id=action.id)) return obj def remove_related(self, parent, _action): """Remove relationship""" action = self._validate(_action, self.RemoveRelated) deleted = [] obj = self._get(action) # pylint: disable=protected-access rel = parent._relationships_map.get((obj.type, obj.id)) if rel: db.session.delete(rel) deleted.append(rel) return [], deleted def _check_related_permissions(self, obj): """Check permissions before deleting related Evidence or Document""" if not permissions.is_allowed_delete( obj.type, obj.id, obj.context_id) \ and not permissions.has_conditions("delete", obj.type): raise wzg_exceptions.Forbidden() if not permissions.is_allowed_delete_for(obj): raise wzg_exceptions.Forbidden() class DocumentAction(BaseAction): """Document action""" AddRelated = namedtuple("AddRelated", ["id", "type", "kind", "link", "title"]) @staticmethod def _validate_parent(parent): """Validates if paren in allowed parents""" from ggrc.models.object_document import Documentable if not isinstance(parent, Documentable): raise ValueError('Type "{}" is not Documentable.'.format( parent.type)) def _create(self, parent, action): self._validate_parent(parent) obj = Document(link=action.link, title=action.title, kind=action.kind, context=parent.context) return obj def remove_related(self, parent, _action): """Remove relationship""" action = self._validate(_action, self.RemoveRelated) deleted = [] obj = self._get(action) # pylint: disable=protected-access rel = parent._relationships_map.get((obj.type, obj.id)) self._check_related_permissions(obj) if rel: db.session.delete(rel) deleted.append(rel) return [], deleted class EvidenceAction(BaseAction): """Evidence action""" AddRelatedTuple = namedtuple( "AddRelated", ["id", "type", "kind", "link", "title", "source_gdrive_id"]) def add_related_wrapper(self, id, type, kind, link, title, source_gdrive_id=''): """Used to add 'default' value to the named tuple In case of Evidence.FILE source_gdrive_id is mandatory """ return self.AddRelatedTuple(id, type, kind, link, title, source_gdrive_id) AddRelated = add_related_wrapper AddRelated._fields = AddRelatedTuple._fields def _create(self, parent, action): obj = Evidence(link=action.link, title=action.title, kind=action.kind, source_gdrive_id=action.source_gdrive_id, context=parent.context) return obj def remove_related(self, parent, _action): """Remove relationship""" action = self._validate(_action, self.RemoveRelated) deleted = [] obj = self._get(action) # pylint: disable=protected-access rel = parent._relationships_map.get((obj.type, obj.id)) self._check_related_permissions(obj) if rel: db.session.delete(rel) deleted.append(rel) obj.status = Evidence.DEPRECATED return [], deleted class CommentAction(BaseAction): """Comment action""" AddRelated = namedtuple( "AddRelated", ["id", "type", "description", "custom_attribute_definition_id"]) def _create(self, parent, action): # get assignee type current_user = get_current_user() assignee_types = parent.assignees.get(current_user, []) assignee_type = ",".join(assignee_types) or None # create object cad_id = action.custom_attribute_definition_id if not cad_id: obj = Comment(description=action.description, assignee_type=assignee_type, context=parent.context) else: obj = Comment(description=action.description, custom_attribute_definition_id=cad_id, assignee_type=assignee_type, context=parent.context) return obj class SnapshotAction(BaseAction): """Snapshot action"""
class WithAction(object): """Mixin for add/remove map/unmap actions processing""" _api_attrs = ApiAttributes( Attribute("actions", create=False, update=True, read=False)) _operation_order = [ "add_related", "remove_related", ] _object_map = { "Document": Document, "Comment": Comment, "Snapshot": Snapshot, } _actions = None _added = None # collect added objects for signals sending _deleted = None # collect deleted objects fro signals sending _relationships_map = None def actions(self, value): """Save actions for further processing""" if value: self._actions = value.get("actions") def _validate_actions(self): """Validate operation types""" invalid_actions = ",".join( set(self._actions) - set(self._operation_order)) if invalid_actions: raise ValueError( "Invalid actions found: {}".format(invalid_actions)) def _build_relationships_map(self): """Build relationships map""" self._relationships_map = {(rel.destination_type, rel.destination_id): rel for rel in self.related_destinations} self._relationships_map.update({(rel.source_type, rel.source_id): rel for rel in self.related_sources}) def _process_operation(self, operation): """Process operation actions""" for action in self._actions[operation]: # get object class obj_type = action.get("type") if not obj_type: raise ValidationError('type is not defined') obj_class = self._object_map.get(obj_type) if not obj_class: raise ValueError( 'Invalid action type: {type}'.format(type=obj_type)) # get handler class action_type = '{type}Action'.format(type=obj_type) action_class = getattr(self, action_type, None) if not action_class: raise ValueError( 'Invalid action type: {type}'.format(type=obj_type)) # process action # pylint: disable=not-callable added, deleted = getattr(action_class(), operation)(self, action) # collect added/deleted objects self._added.extend(added) self._deleted.extend(deleted) def process_actions(self): """Process actions""" if not self._actions: return {}, [] self._validate_actions() self._added = [] self._deleted = [] for operation in self._operation_order: if operation not in self._actions: continue if not self._actions[operation]: continue self._build_relationships_map() self._process_operation(operation) # collect added/deleted objects for signals sending added = defaultdict(list) for obj in self._added: added[obj.__class__].append(obj) return added, self._deleted class BaseAction(object): """Base action""" AddRelated = namedtuple("AddRelated", ["id", "type"]) MapRelated = namedtuple("MapRelated", ["id", "type"]) RemoveRelated = namedtuple("RemoveRelated", ["id", "type"]) def add_related(self, parent, _action): """Add/map object to parent""" added = [] if _action.get("id"): action = self._validate(_action, self.MapRelated) obj = self._get(action) else: action = self._validate(_action, self.AddRelated) obj = self._create(parent, action) added.append(obj) rel = Relationship(source=parent, destination=obj, context=parent.context) added.append(rel) return added, [] @staticmethod def _validate(_action, ntuple): try: return ntuple(**_action) except TypeError: raise ValidationError("Missed action parameters") # pylint: disable=unused-argument,no-self-use def _create(self, parent, action): raise ValidationError( "Can't create {type} object".format(type=action.type)) def _get(self, action): """Get object specified in action""" if not action.id: raise ValueError("id is not defined") # pylint: disable=protected-access obj_class = WithAction._object_map[action.type] obj = obj_class.query.get(action.id) if not obj: raise ValueError('Object not found: {type} {id}'.format( type=action.type, id=action.id)) return obj def remove_related(self, parent, _action): """Remove relationship""" action = self._validate(_action, self.RemoveRelated) deleted = [] obj = self._get(action) # pylint: disable=protected-access rel = parent._relationships_map.get((obj.type, obj.id)) if rel: db.session.delete(rel) deleted.append(rel) return [], deleted class DocumentAction(BaseAction): """Document action""" AddRelated = namedtuple( "AddRelated", ["id", "type", "document_type", "link", "title"]) def _create(self, parent, action): obj = Document(link=action.link, title=action.title, document_type=action.document_type, context=parent.context) return obj class CommentAction(BaseAction): """Comment action""" AddRelated = namedtuple( "AddRelated", ["id", "type", "description", "custom_attribute_definition_id"]) def _create(self, parent, action): # get assignee type current_user = get_current_user() # pylint: disable=protected-access rel = parent._relationships_map.get( (current_user.type, current_user.id)) if rel: assignee_type = rel.attrs["AssigneeType"] else: assignee_type = None # create object cad_id = action.custom_attribute_definition_id if not cad_id: obj = Comment(description=action.description, assignee_type=assignee_type, context=parent.context) else: obj = Comment(description=action.description, custom_attribute_definition_id=cad_id, assignee_type=assignee_type, context=parent.context) return obj class SnapshotAction(BaseAction): """Snapshot action"""