class NodeRequestSerializer(JSONAPISerializer): class Meta: type_ = 'node-requests' filterable_fields = frozenset( ['creator', 'request_type', 'machine_state', 'created', 'id']) id = ser.CharField(source='_id', read_only=True) request_type = ser.ChoiceField(read_only=True, required=False, choices=RequestTypes.choices()) machine_state = ser.ChoiceField(read_only=True, required=False, choices=DefaultStates.choices()) comment = ser.CharField(required=False, allow_blank=True, max_length=65535) created = VersionedDateTimeField(read_only=True) modified = VersionedDateTimeField(read_only=True) date_last_transitioned = VersionedDateTimeField(read_only=True) target = RelationshipField( read_only=True, related_view='nodes:node-detail', related_view_kwargs={'node_id': '<target._id>'}, filter_key='target___id', ) creator = RelationshipField( read_only=True, related_view='users:user-detail', related_view_kwargs={'user_id': '<creator._id>'}, filter_key='creator___id', ) links = LinksField({ 'self': 'get_absolute_url', 'target': 'get_target_url' }) def get_absolute_url(self, obj): return absolute_reverse( 'requests:node-request-detail', kwargs={ 'request_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def get_target_url(self, obj): return absolute_reverse( 'nodes:node-detail', kwargs={ 'node_id': obj.target._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def create(self, validated_data): raise NotImplementedError()
class MachineableMixin(models.Model): class Meta: abstract = True # NOTE: machine_state should rarely/never be modified directly -- use the state transition methods below machine_state = models.CharField(max_length=15, db_index=True, choices=DefaultStates.choices(), default=DefaultStates.INITIAL.value) date_last_transitioned = models.DateTimeField(null=True, blank=True, db_index=True) @property def MachineClass(self): raise NotImplementedError() def run_submit(self, user): """Run the 'submit' state transition and create a corresponding Action. Params: user: The user triggering this transition. """ return self.__run_transition(DefaultTriggers.SUBMIT.value, user=user) def run_accept(self, user, comment): """Run the 'accept' state transition and create a corresponding Action. Params: user: The user triggering this transition. comment: Text describing why. """ return self.__run_transition(DefaultTriggers.ACCEPT.value, user=user, comment=comment) def run_reject(self, user, comment): """Run the 'reject' state transition and create a corresponding Action. Params: user: The user triggering this transition. comment: Text describing why. """ return self.__run_transition(DefaultTriggers.REJECT.value, user=user, comment=comment) def run_edit_comment(self, user, comment): """Run the 'edit_comment' state transition and create a corresponding Action. Params: user: The user triggering this transition. comment: New comment text. """ return self.__run_transition(DefaultTriggers.EDIT_COMMENT.value, user=user, comment=comment) def __run_transition(self, trigger, **kwargs): machine = self.MachineClass(self, 'machine_state') trigger_fn = getattr(machine, trigger) with transaction.atomic(): result = trigger_fn(**kwargs) action = machine.action if not result or action is None: valid_triggers = machine.get_triggers(self.machine_state) raise InvalidTriggerError(trigger, self.machine_state, valid_triggers) return action
class BaseAction(ObjectIDMixin, BaseModel): class Meta: abstract = True objects = IncludeManager() creator = models.ForeignKey('OSFUser', related_name='+', on_delete=models.CASCADE) trigger = models.CharField(max_length=31, choices=DefaultTriggers.choices()) from_state = models.CharField(max_length=31, choices=DefaultStates.choices()) to_state = models.CharField(max_length=31, choices=DefaultStates.choices()) comment = models.TextField(blank=True) is_deleted = models.BooleanField(default=False) @property def target(self): raise NotImplementedError()
class BaseActionSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'trigger', 'from_state', 'to_state', 'date_created', 'date_modified', 'target', ]) id = ser.CharField(source='_id', read_only=True) trigger = ser.ChoiceField(choices=DefaultTriggers.choices()) comment = ser.CharField(max_length=65535, required=False) from_state = ser.ChoiceField(choices=DefaultStates.choices(), read_only=True) to_state = ser.ChoiceField(choices=DefaultStates.choices(), read_only=True) date_created = ser.DateTimeField(source='created', read_only=True) date_modified = ser.DateTimeField(source='modified', read_only=True) auto = ser.BooleanField(read_only=True) creator = RelationshipField( read_only=True, related_view='users:user-detail', related_view_kwargs={'user_id': '<creator._id>'}, filter_key='creator__guids___id', always_embed=True, ) links = LinksField({ 'self': 'get_action_url', }) def get_absolute_url(self, obj): return self.get_action_url(obj) def get_action_url(self, obj): return utils.absolute_reverse( 'actions:action-detail', kwargs={ 'action_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def create(self, validated_data): trigger = validated_data.pop('trigger') user = validated_data.pop('user') target = validated_data.pop('target') comment = validated_data.pop('comment', '') permissions = validated_data.pop('permissions', '') visible = validated_data.pop('visible', '') try: if trigger == DefaultTriggers.ACCEPT.value: return target.run_accept(user=user, comment=comment, permissions=permissions, visible=visible) if trigger == DefaultTriggers.REJECT.value: return target.run_reject(user, comment) if trigger == DefaultTriggers.EDIT_COMMENT.value: return target.run_edit_comment(user, comment) if trigger == DefaultTriggers.SUBMIT.value: return target.run_submit(user) except InvalidTriggerError as e: # Invalid transition from the current state raise Conflict(e.message) else: raise JSONAPIAttributeException(attribute='trigger', detail='Invalid trigger.') class Meta: type_ = 'actions' abstract = True
class BaseActionSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'trigger', 'from_state', 'to_state', 'date_created', 'date_modified', 'target', ]) id = ser.CharField(source='_id', read_only=True) trigger = ser.ChoiceField(choices=DefaultTriggers.choices()) comment = HideIfProviderCommentsPrivate(ser.CharField(max_length=65535, required=False)) from_state = ser.ChoiceField(choices=DefaultStates.choices(), read_only=True) to_state = ser.ChoiceField(choices=DefaultStates.choices(), read_only=True) date_created = ser.DateTimeField(source='created', read_only=True) date_modified = ser.DateTimeField(source='modified', read_only=True) creator = RelationshipField( read_only=True, related_view='users:user-detail', related_view_kwargs={'user_id': '<creator._id>'}, filter_key='creator__guids___id', always_embed=True, ) links = LinksField( { 'self': 'get_action_url', } ) @property def get_action_url(self): raise NotImplementedError() def get_absolute_url(self, obj): return self.get_action_url(obj) def create(self, validated_data): trigger = validated_data.pop('trigger') user = validated_data.pop('user') target = validated_data.pop('target') comment = validated_data.pop('comment', '') try: if trigger == DefaultTriggers.ACCEPT.value: return target.run_accept(user, comment) if trigger == DefaultTriggers.REJECT.value: return target.run_reject(user, comment) if trigger == DefaultTriggers.EDIT_COMMENT.value: return target.run_edit_comment(user, comment) if trigger == DefaultTriggers.SUBMIT.value: return target.run_submit(user) except InvalidTriggerError as e: # Invalid transition from the current state raise Conflict(e.message) else: raise JSONAPIAttributeException(attribute='trigger', detail='Invalid trigger.') class Meta: type_ = 'actions' abstract = True