Example #1
0
class VersionAutomationRule(PolymorphicModel, TimeStampedModel):

    """Versions automation rules for projects."""

    ACTIVATE_VERSION_ACTION = 'activate-version'
    DELETE_VERSION_ACTION = 'delete-version'
    HIDE_VERSION_ACTION = 'hide-version'
    MAKE_VERSION_PUBLIC_ACTION = 'make-version-public'
    MAKE_VERSION_PRIVATE_ACTION = 'make-version-private'
    SET_DEFAULT_VERSION_ACTION = 'set-default-version'

    ACTIONS = (
        (ACTIVATE_VERSION_ACTION, _('Activate version')),
        (HIDE_VERSION_ACTION, _('Hide version')),
        (MAKE_VERSION_PUBLIC_ACTION, _('Make version public')),
        (MAKE_VERSION_PRIVATE_ACTION, _('Make version private')),
        (SET_DEFAULT_VERSION_ACTION, _('Set version as default')),
        (DELETE_VERSION_ACTION, _('Delete version (on branch/tag deletion)')),
    )

    allowed_actions_on_create = {}
    allowed_actions_on_delete = {}

    project = models.ForeignKey(
        Project,
        related_name='automation_rules',
        on_delete=models.CASCADE,
    )
    priority = models.IntegerField(
        _('Rule priority'),
        help_text=_('A lower number (0) means a higher priority'),
    )
    description = models.CharField(
        _('Description'),
        max_length=255,
        null=True,
        blank=True,
    )
    match_arg = models.CharField(
        _('Match argument'),
        help_text=_('Value used for the rule to match the version'),
        max_length=255,
    )
    predefined_match_arg = models.CharField(
        _('Predefined match argument'),
        help_text=_(
            'Match argument defined by us, it is used if is not None, '
            'otherwise match_arg will be used.'
        ),
        max_length=255,
        choices=PREDEFINED_MATCH_ARGS,
        null=True,
        blank=True,
        default=None,
    )
    action = models.CharField(
        _('Action'),
        help_text=_('Action to apply to matching versions'),
        max_length=32,
        choices=ACTIONS,
    )
    action_arg = models.CharField(
        _('Action argument'),
        help_text=_('Value used for the action to perfom an operation'),
        max_length=255,
        null=True,
        blank=True,
    )
    version_type = models.CharField(
        _('Version type'),
        help_text=_('Type of version the rule should be applied to'),
        max_length=32,
        choices=VERSION_TYPES,
    )

    objects = VersionAutomationRuleManager()

    class Meta:
        unique_together = (('project', 'priority'),)
        ordering = ('priority', '-modified', '-created')

    def get_match_arg(self):
        """Get the match arg defined for `predefined_match_arg` or the match from user."""
        match_arg = PREDEFINED_MATCH_ARGS_VALUES.get(
            self.predefined_match_arg,
        )
        return match_arg or self.match_arg

    def run(self, version, **kwargs):
        """
        Run an action if `version` matches the rule.

        :type version: readthedocs.builds.models.Version
        :returns: True if the action was performed
        """
        if version.type != self.version_type:
            return False

        match, result = self.match(version, self.get_match_arg())
        if match:
            self.apply_action(version, result)
            AutomationRuleMatch.objects.register_match(
                rule=self,
                version=version,
            )
            return True
        return False

    def match(self, version, match_arg):
        """
        Returns True and the match result if the version matches the rule.

        :type version: readthedocs.builds.models.Version
        :param str match_arg: Additional argument to perform the match
        :returns: A tuple of (boolean, match_resul).
                  The result will be passed to `apply_action`.
        """
        return False, None

    def apply_action(self, version, match_result):
        """
        Apply the action from allowed_actions_on_*.

        :type version: readthedocs.builds.models.Version
        :param any match_result: Additional context from the match operation
        :raises: NotImplementedError if the action
                 isn't implemented or supported for this rule.
        """
        action = (
            self.allowed_actions_on_create.get(self.action)
            or self.allowed_actions_on_delete.get(self.action)
        )
        if action is None:
            raise NotImplementedError
        action(version, match_result, self.action_arg)

    def move(self, steps):
        """
        Change the priority of this Automation Rule.

        This is done by moving it ``n`` steps,
        relative to the other priority rules.
        The priority from the other rules are updated too.

        :param steps: Number of steps to be moved
                      (it can be negative)
        :returns: True if the priority was changed
        """
        total = self.project.automation_rules.count()
        current_priority = self.priority
        new_priority = (current_priority + steps) % total

        if current_priority == new_priority:
            return False

        # Move other's priority
        if new_priority > current_priority:
            # It was moved down
            rules = (
                self.project.automation_rules
                .filter(priority__gt=current_priority, priority__lte=new_priority)
                # We sort the queryset in asc order
                # to be updated in that order
                # to avoid hitting the unique constraint (project, priority).
                .order_by('priority')
            )
            expression = F('priority') - 1
        else:
            # It was moved up
            rules = (
                self.project.automation_rules
                .filter(priority__lt=current_priority, priority__gte=new_priority)
                .exclude(pk=self.pk)
                # We sort the queryset in desc order
                # to be updated in that order
                # to avoid hitting the unique constraint (project, priority).
                .order_by('-priority')
            )
            expression = F('priority') + 1

        # Put an impossible priority to avoid
        # the unique constraint (project, priority)
        # while updating.
        self.priority = total + 99
        self.save()

        # We update each object one by one to
        # avoid hitting the unique constraint (project, priority).
        for rule in rules:
            rule.priority = expression
            rule.save()

        # Put back new priority
        self.priority = new_priority
        self.save()
        return True

    def delete(self, *args, **kwargs):  # pylint: disable=arguments-differ
        """Override method to update the other priorities after delete."""
        current_priority = self.priority
        project = self.project
        super().delete(*args, **kwargs)

        rules = (
            project.automation_rules
            .filter(priority__gte=current_priority)
            # We sort the queryset in asc order
            # to be updated in that order
            # to avoid hitting the unique constraint (project, priority).
            .order_by('priority')
        )
        # We update each object one by one to
        # avoid hitting the unique constraint (project, priority).
        for rule in rules:
            rule.priority = F('priority') - 1
            rule.save()

    def get_description(self):
        if self.description:
            return self.description
        return f'{self.get_action_display()}'

    def get_edit_url(self):
        raise NotImplementedError

    def __str__(self):
        class_name = self.__class__.__name__
        return (
            f'({self.priority}) '
            f'{class_name}/{self.get_action_display()} '
            f'for {self.project.slug}:{self.get_version_type_display()}'
        )
Example #2
0
class VersionAutomationRule(PolymorphicModel, TimeStampedModel):
    """Versions automation rules for projects."""

    ACTIVATE_VERSION_ACTION = 'activate-version'
    SET_DEFAULT_VERSION_ACTION = 'set-default-version'
    ACTIONS = (
        (ACTIVATE_VERSION_ACTION, _('Activate version on match')),
        (SET_DEFAULT_VERSION_ACTION, _('Set as default version on match')),
    )

    project = models.ForeignKey(
        Project,
        related_name='automation_rules',
        on_delete=models.CASCADE,
    )
    priority = models.IntegerField(
        _('Rule priority'),
        help_text=_('A lower number (0) means a higher priority'),
    )
    description = models.CharField(
        _('Description'),
        max_length=255,
        null=True,
        blank=True,
    )
    match_arg = models.CharField(
        _('Match argument'),
        help_text=_('Value used for the rule to match the version'),
        max_length=255,
    )
    action = models.CharField(
        _('Action'),
        max_length=32,
        choices=ACTIONS,
    )
    action_arg = models.CharField(
        _('Action argument'),
        help_text=_('Value used for the action to perfom an operation'),
        max_length=255,
        null=True,
        blank=True,
    )
    version_type = models.CharField(
        _('Version type'),
        max_length=32,
        choices=VERSION_TYPES,
    )

    objects = VersionAutomationRuleManager()

    class Meta:
        unique_together = (('project', 'priority'), )
        ordering = ('priority', '-modified', '-created')

    def run(self, version, *args, **kwargs):
        """
        Run an action if `version` matches the rule.

        :type version: readthedocs.builds.models.Version
        :returns: True if the action was performed
        """
        if version.type == self.version_type:
            match, result = self.match(version, self.match_arg)
            if match:
                self.apply_action(version, result)
                return True
        return False

    def match(self, version, match_arg):
        """
        Returns True and the match result if the version matches the rule.

        :type version: readthedocs.builds.models.Version
        :param str match_arg: Additional argument to perform the match
        :returns: A tuple of (boolean, match_resul).
                  The result will be passed to `apply_action`.
        """
        return False, None

    def apply_action(self, version, match_result):
        """
        Apply the action from allowed_actions.

        :type version: readthedocs.builds.models.Version
        :param any match_result: Additional context from the match operation
        :raises: NotImplementedError if the action
                 isn't implemented or supported for this rule.
        """
        action = self.allowed_actions.get(self.action)
        if action is None:
            raise NotImplementedError
        action(version, match_result, self.action_arg)

    def get_description(self):
        if self.description:
            return self.description
        return f'{self.get_action_display()}'

    def __str__(self):
        class_name = self.__class__.__name__
        return (f'({self.priority}) '
                f'{class_name}/{self.get_action_display()} '
                f'for {self.project.slug}:{self.get_version_type_display()}')