Skip to content

Add move method to automation rule #5998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions readthedocs/builds/managers.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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__)

Expand Down Expand Up @@ -179,3 +179,37 @@ class InternalBuildManager(SettingsOverrideObject):

class ExternalBuildManager(SettingsOverrideObject):
_default_class = ExternalBuildManagerBase


class VersionAutomationRuleManager(PolymorphicManager):

def append_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 higher than the last rule
in `project`.
"""
last_priority = (
project.automation_rules
.values_list('priority', flat=True)
.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
Original file line number Diff line number Diff line change
@@ -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'),
),
]
121 changes: 98 additions & 23 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from django.conf import settings
from django.db import models
from django.db.models import F
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext
Expand All @@ -17,47 +18,30 @@
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_COMMIT_URL,
GITHUB_URL,
GITHUB_PULL_REQUEST_URL,
GITHUB_PULL_REQUEST_COMMIT_URL,
GITLAB_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,
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,
Expand All @@ -70,7 +54,24 @@
get_gitlab_username_repo,
)
from readthedocs.builds.version_slug import VersionSlugField
from readthedocs.config import LATEST_CONFIGURATION_VERSION
from readthedocs.core.utils import broadcast
from readthedocs.oauth.models import RemoteRepository
from readthedocs.projects.constants import (
BITBUCKET_COMMIT_URL,
BITBUCKET_URL,
GITHUB_COMMIT_URL,
GITHUB_PULL_REQUEST_COMMIT_URL,
GITHUB_PULL_REQUEST_URL,
GITHUB_URL,
GITLAB_COMMIT_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__)
Expand Down Expand Up @@ -928,6 +929,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'),
Expand All @@ -951,6 +958,8 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel):
choices=VERSION_TYPES,
)

objects = VersionAutomationRuleManager()

class Meta:
unique_together = (('project', 'priority'),)
ordering = ('priority', '-modified', '-created')
Expand Down Expand Up @@ -994,6 +1003,72 @@ def apply_action(self, version, match_result):
raise NotImplementedError
action(version, match_result, self.action_arg)

def move(self, steps):
"""
Move the rule n steps.

: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 imposible 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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we hit an exception here? The rule will be stuck with a broken priority. We likely need this to happen in some kind of DB transaction to avoid bad states here.

This feels really complicated and brittle. I don't have a better suggestion currently, but I'm wondering if this concept of priority is something we need to enforce so strongly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have transactions per request

ATOMIC_REQUESTS = True

So, we need to have a unique priority in all rules of a project, that way they get sorted how the user wants.

We can remove the unique constraint as Manuel suggested, and maybe check for inconsistencies on save?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I guess we can keep this for now, but I'm guessing it will cause issues at some point :)


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

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

@property
def edit_url(self):
raise NotImplementedError

def __str__(self):
class_name = self.__class__.__name__
return (
Expand Down
Loading