diff --git a/readthedocs/builds/managers.py b/readthedocs/builds/managers.py index 83b2769798d..0ce18634a10 100644 --- a/readthedocs/builds/managers.py +++ b/readthedocs/builds/managers.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- - """Build and Version class model Managers.""" import logging -from django.db import models from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from polymorphic.managers import PolymorphicManager from readthedocs.core.utils.extend import ( SettingsOverrideObject, @@ -14,14 +13,15 @@ from .constants import ( BRANCH, + EXTERNAL, LATEST, LATEST_VERBOSE_NAME, STABLE, STABLE_VERBOSE_NAME, TAG, - EXTERNAL, ) -from .querysets import VersionQuerySet, BuildQuerySet +from .querysets import BuildQuerySet, VersionQuerySet + log = logging.getLogger(__name__) @@ -179,3 +179,48 @@ class InternalBuildManager(SettingsOverrideObject): class ExternalBuildManager(SettingsOverrideObject): _default_class = ExternalBuildManagerBase + + +class VersionAutomationRuleManager(PolymorphicManager): + + """ + Mananger for VersionAutomationRule. + + .. note:: + + This manager needs to inherit from PolymorphicManager, + since the model is a PolymorphicModel. + See https://django-polymorphic.readthedocs.io/page/managers.html + """ + + def add_rule( + self, *, project, description, match_arg, version_type, + action, action_arg=None, + ): + """ + Append an automation rule to `project`. + + The rule is created with a priority lower than the last rule + in `project`. + """ + last_priority = ( + project.automation_rules + .values_list('priority', flat=True) + .order_by('priority') + .last() + ) + if last_priority is None: + priority = 0 + else: + priority = last_priority + 1 + + rule = self.create( + project=project, + priority=priority, + description=description, + match_arg=match_arg, + version_type=version_type, + action=action, + action_arg=action_arg, + ) + return rule diff --git a/readthedocs/builds/migrations/0010_add-description-field-to-automation-rule.py b/readthedocs/builds/migrations/0010_add-description-field-to-automation-rule.py new file mode 100644 index 00000000000..51b9bc77e51 --- /dev/null +++ b/readthedocs/builds/migrations/0010_add-description-field-to-automation-rule.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-25 17:24 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('builds', '0009_added_external_version_type'), + ] + + operations = [ + migrations.AddField( + model_name='versionautomationrule', + name='description', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Description'), + ), + ] diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index cf20de76657..b7cbb0041a1 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -17,52 +17,31 @@ from polymorphic.models import PolymorphicModel import readthedocs.builds.automation_actions as actions -from readthedocs.config import LATEST_CONFIGURATION_VERSION -from readthedocs.core.utils import broadcast -from readthedocs.projects.constants import ( - BITBUCKET_COMMIT_URL, - BITBUCKET_URL, - GITHUB_BRAND, - GITHUB_COMMIT_URL, - GITHUB_URL, - GITHUB_PULL_REQUEST_URL, - GITHUB_PULL_REQUEST_COMMIT_URL, - GITLAB_BRAND, - GITLAB_COMMIT_URL, - GITLAB_MERGE_REQUEST_URL, - GITLAB_MERGE_REQUEST_COMMIT_URL, - GITLAB_URL, - PRIVACY_CHOICES, - PRIVATE, - MEDIA_TYPES, -) -from readthedocs.projects.models import APIProject, Project -from readthedocs.projects.version_handling import determine_stable_version - from readthedocs.builds.constants import ( BRANCH, BUILD_STATE, BUILD_STATE_FINISHED, BUILD_STATE_TRIGGERED, BUILD_TYPES, + EXTERNAL, GENERIC_EXTERNAL_VERSION_NAME, GITHUB_EXTERNAL_VERSION_NAME, GITLAB_EXTERNAL_VERSION_NAME, INTERNAL, LATEST, NON_REPOSITORY_VERSIONS, - EXTERNAL, STABLE, TAG, VERSION_TYPES, ) from readthedocs.builds.managers import ( - VersionManager, - InternalVersionManager, - ExternalVersionManager, BuildManager, - InternalBuildManager, ExternalBuildManager, + ExternalVersionManager, + InternalBuildManager, + InternalVersionManager, + VersionAutomationRuleManager, + VersionManager, ) from readthedocs.builds.querysets import ( BuildQuerySet, @@ -75,7 +54,27 @@ get_gitlab_username_repo, ) from readthedocs.builds.version_slug import VersionSlugField -from readthedocs.oauth.models import RemoteRepository +from readthedocs.config import LATEST_CONFIGURATION_VERSION +from readthedocs.core.utils import broadcast +from readthedocs.projects.constants import ( + BITBUCKET_COMMIT_URL, + BITBUCKET_URL, + GITHUB_BRAND, + GITHUB_COMMIT_URL, + GITHUB_PULL_REQUEST_COMMIT_URL, + GITHUB_PULL_REQUEST_URL, + GITHUB_URL, + GITLAB_BRAND, + GITLAB_COMMIT_URL, + GITLAB_MERGE_REQUEST_COMMIT_URL, + GITLAB_MERGE_REQUEST_URL, + GITLAB_URL, + MEDIA_TYPES, + PRIVACY_CHOICES, + PRIVATE, +) +from readthedocs.projects.models import APIProject, Project +from readthedocs.projects.version_handling import determine_stable_version log = logging.getLogger(__name__) @@ -962,6 +961,12 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel): _('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'), @@ -985,6 +990,8 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel): choices=VERSION_TYPES, ) + objects = VersionAutomationRuleManager() + class Meta: unique_together = (('project', 'priority'),) ordering = ('priority', '-modified', '-created') @@ -1028,6 +1035,11 @@ def apply_action(self, version, match_result): 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 ( diff --git a/readthedocs/rtd_tests/tests/test_automation_rules.py b/readthedocs/rtd_tests/tests/test_automation_rules.py index 58ec7a6435a..fd89eb5e85a 100644 --- a/readthedocs/rtd_tests/tests/test_automation_rules.py +++ b/readthedocs/rtd_tests/tests/test_automation_rules.py @@ -125,3 +125,61 @@ def test_action_set_default_version(self): assert self.project.get_default_version() == LATEST assert rule.run(version) is True assert self.project.get_default_version() == version.slug + + +@pytest.mark.django_db +class TestAutomationRuleManager: + + @pytest.fixture(autouse=True) + def setup_method(self): + self.project = get(Project) + + def test_add_rule_regex(self): + assert not self.project.automation_rules.all() + + rule = RegexAutomationRule.objects.add_rule( + project=self.project, + description='First rule', + match_arg='.*', + version_type=TAG, + action=VersionAutomationRule.ACTIVATE_VERSION_ACTION, + ) + + # First rule gets added with priority 0 + assert self.project.automation_rules.count() == 1 + assert rule.priority == 0 + + # Adding a second rule + rule = RegexAutomationRule.objects.add_rule( + project=self.project, + description='Second rule', + match_arg='.*', + version_type=BRANCH, + action=VersionAutomationRule.ACTIVATE_VERSION_ACTION, + ) + assert self.project.automation_rules.count() == 2 + assert rule.priority == 1 + + # Adding a rule with a not secuencial priority + rule = get( + RegexAutomationRule, + description='Third rule', + project=self.project, + priority=9, + match_arg='.*', + version_type=TAG, + action=VersionAutomationRule.ACTIVATE_VERSION_ACTION, + ) + assert self.project.automation_rules.count() == 3 + assert rule.priority == 9 + + # Adding a new rule + rule = RegexAutomationRule.objects.add_rule( + project=self.project, + description='Fourth rule', + match_arg='.*', + version_type=BRANCH, + action=VersionAutomationRule.ACTIVATE_VERSION_ACTION, + ) + assert self.project.automation_rules.count() == 4 + assert rule.priority == 10