Skip to content
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

Implement UI for automation rules #5996

Merged
merged 51 commits into from Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1164ba4
Add form
stsewd Jul 25, 2019
9ee90f8
Implement views
stsewd Jul 25, 2019
48e8bc9
Add templates
stsewd Jul 25, 2019
511edeb
Urls!
stsewd Jul 25, 2019
4347997
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Aug 2, 2019
2e0831b
Tests
stsewd Aug 5, 2019
9db6f58
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Aug 12, 2019
4ba11d3
Fix merge
stsewd Aug 12, 2019
cb72dff
Fix test
stsewd Aug 13, 2019
3c4109a
Little touch of JS
stsewd Aug 13, 2019
9048368
More js
stsewd Aug 13, 2019
d9575fe
Improve defaults
stsewd Aug 13, 2019
10bc350
Improve initial values
stsewd Aug 13, 2019
47ec2ca
Add up/down arrows to UI
stsewd Aug 13, 2019
f7d2c7d
Remove content
stsewd Aug 13, 2019
ccc02c3
Refactor
stsewd Aug 13, 2019
21fe1f0
Migration
stsewd Aug 13, 2019
e7ca032
Update help text
stsewd Aug 13, 2019
ffe2891
Merge branch 'update-automation-rules-model' into implement-ui-for-au…
stsewd Aug 13, 2019
eca2d14
Add link to python regex
stsewd Aug 13, 2019
09319c8
Update help text
stsewd Aug 14, 2019
5640dae
Merge branch 'add-move-method' into implement-ui-for-automation-rules
stsewd Aug 14, 2019
e001105
Merge branch 'add-move-method' into implement-ui-for-automation-rules
stsewd Aug 14, 2019
89ccea3
Merge branch 'add-move-method' into implement-ui-for-automation-rules
stsewd Aug 14, 2019
9c1cbfc
Rename
stsewd Aug 14, 2019
6b5c4b1
Add move view
stsewd Aug 14, 2019
5d1aa57
Tests
stsewd Aug 14, 2019
b93ef89
Add test for views
stsewd Aug 14, 2019
1779d6b
Merge branch 'add-move-method' into implement-ui-for-automation-rules
stsewd Aug 26, 2019
cdd28a3
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Aug 26, 2019
156b467
Merge branch 'add-move-method' into implement-ui-for-automation-rules
stsewd Aug 26, 2019
576bde5
Merge branch 'add-move-method' into implement-ui-for-automation-rules
stsewd Aug 26, 2019
5d17ab3
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Sep 6, 2019
4d82191
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Nov 5, 2019
5a802a9
Update migration
stsewd Nov 5, 2019
556f676
Add predefined_match_arg field
stsewd Nov 6, 2019
28915d2
Update form
stsewd Nov 6, 2019
0dcd88b
Update UI
stsewd Nov 6, 2019
abcb822
Update migration
stsewd Nov 6, 2019
b206bc0
Fix initial values
stsewd Nov 6, 2019
d41bce7
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Nov 6, 2019
43c4114
Tests
stsewd Nov 6, 2019
5d6d839
Fix tests
stsewd Nov 6, 2019
062b3fc
Fix linter
stsewd Nov 6, 2019
146c816
Apply suggestions from code review
stsewd Nov 11, 2019
9d78e74
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Nov 11, 2019
5ff19bd
Improve a11y
stsewd Nov 11, 2019
ba0ccfb
Move js to a file
stsewd Nov 11, 2019
e8d356f
Fix eslint
stsewd Nov 11, 2019
3c293d6
Merge branch 'master' into implement-ui-for-automation-rules
stsewd Nov 12, 2019
ce3349a
Add comment about knockoutjs
stsewd Nov 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions media/css/core.css
Expand Up @@ -1307,6 +1307,13 @@ div.module.project-subprojects li.subproject a.subproject-edit:before {
content: "\f044";
}

/* Automation Rules */

li.automation-rule input[type="submit"] {
font-family: FontAwesome;
font-weight: normal;
}


/* Pygments */
div.highlight pre .hll { background-color: #ffffcc }
Expand Down
34 changes: 28 additions & 6 deletions readthedocs/builds/constants.py
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Constants for the builds app."""

from django.conf import settings
Expand Down Expand Up @@ -37,16 +35,20 @@
# Manager name for External Versions or Builds.
# ie: Only pull request/merge request Versions and Builds.
EXTERNAL = 'external'
EXTERNAL_TEXT = _('External')

BRANCH = 'branch'
BRANCH_TEXT = _('Branch')
TAG = 'tag'
TAG_TEXT = _('Tag')
UNKNOWN = 'unknown'
UNKNOWN_TEXT = _('Unknown')

VERSION_TYPES = (
(BRANCH, _('Branch')),
(TAG, _('Tag')),
(EXTERNAL, _('External')),
(UNKNOWN, _('Unknown')),
(BRANCH, BRANCH_TEXT),
(TAG, TAG_TEXT),
(EXTERNAL, EXTERNAL_TEXT),
(UNKNOWN, UNKNOWN_TEXT),
)

LATEST = settings.RTD_LATEST
Expand Down Expand Up @@ -101,3 +103,23 @@
GITHUB_EXTERNAL_VERSION_NAME = 'Pull Request'
GITLAB_EXTERNAL_VERSION_NAME = 'Merge Request'
GENERIC_EXTERNAL_VERSION_NAME = 'External Version'


# Automation rules

ALL_VERSIONS = 'all-versions'
ALL_VERSIONS_REGEX = r'.*'
SEMVER_VERSIONS = 'semver-versions'
SEMVER_VERSIONS_REGEX = r'^v?(\d+\.)(\d+\.)(\d+)(-.+)?$'


PREDEFINED_MATCH_ARGS = (
(ALL_VERSIONS, _('Any version')),
(SEMVER_VERSIONS, _('SemVer versions')),
(None, _('Custom match')),
)

PREDEFINED_MATCH_ARGS_VALUES = {
ALL_VERSIONS: ALL_VERSIONS_REGEX,
SEMVER_VERSIONS: SEMVER_VERSIONS_REGEX,
}
103 changes: 100 additions & 3 deletions readthedocs/builds/forms.py
@@ -1,11 +1,19 @@
# -*- coding: utf-8 -*-

"""Django forms for the builds app."""

import re
import textwrap

from django import forms
from django.utils.translation import ugettext_lazy as _

from readthedocs.builds.models import Version
from readthedocs.builds.constants import (
ALL_VERSIONS,
BRANCH,
BRANCH_TEXT,
TAG,
TAG_TEXT,
)
from readthedocs.builds.models import RegexAutomationRule, Version
from readthedocs.core.mixins import HideProtectedLevelMixin
from readthedocs.core.utils import trigger_build

Expand Down Expand Up @@ -37,3 +45,92 @@ def save(self, commit=True):
if obj.active and not obj.built and not obj.uploaded:
trigger_build(project=obj.project, version=obj)
return obj


class RegexAutomationRuleForm(forms.ModelForm):

match_arg = forms.CharField(
label='Custom match',
help_text=_(textwrap.dedent(
"""
A
<a href="https://docs.readthedocs.io/page/automation-rules.html#user-defined-matches">
regular expression
</a>
to match the version.
stsewd marked this conversation as resolved.
Show resolved Hide resolved
"""
)),
required=False,
)

class Meta:
model = RegexAutomationRule
fields = [
'description',
'predefined_match_arg',
'match_arg',
'version_type',
'action',
]
# Don't pollute the UI with help texts
help_texts = {
'version_type': '',
'action': '',
}
labels = {
'predefined_match_arg': 'Match',
}

def __init__(self, *args, **kwargs):
self.project = kwargs.pop('project', None)
super().__init__(*args, **kwargs)

# Only list supported types
self.fields['version_type'].choices = [
stsewd marked this conversation as resolved.
Show resolved Hide resolved
(None, '-' * 9),
(BRANCH, BRANCH_TEXT),
(TAG, TAG_TEXT),
]

if not self.instance.pk:
self.initial['predefined_match_arg'] = ALL_VERSIONS
# Allow users to start from the pattern of the predefined match
# if they want to use a custom one.
if self.instance.pk and self.instance.predefined_match_arg:
self.initial['match_arg'] = self.instance.get_match_arg()

def clean_match_arg(self):
"""Check that a custom match was given if a predefined match wasn't used."""
match_arg = self.cleaned_data['match_arg']
predefined_match = self.cleaned_data['predefined_match_arg']
if predefined_match:
match_arg = ''
if not predefined_match and not match_arg:
raise forms.ValidationError(
_('Custom match should not be empty.'),
)

try:
re.compile(match_arg)
except Exception:
raise forms.ValidationError(
_('Invalid Python regular expression.'),
)
return match_arg

def save(self, commit=True):
if self.instance.pk:
rule = super().save(commit=commit)
else:
rule = RegexAutomationRule.objects.add_rule(
project=self.project,
description=self.cleaned_data['description'],
match_arg=self.cleaned_data['match_arg'],
predefined_match_arg=self.cleaned_data['predefined_match_arg'],
version_type=self.cleaned_data['version_type'],
action=self.cleaned_data['action'],
)
if not rule.description:
rule.description = rule.get_description()
rule.save()
return rule
3 changes: 2 additions & 1 deletion readthedocs/builds/managers.py
Expand Up @@ -195,7 +195,7 @@ class VersionAutomationRuleManager(PolymorphicManager):

def add_rule(
self, *, project, description, match_arg, version_type,
action, action_arg=None,
action, action_arg=None, predefined_match_arg=None,
):
"""
Append an automation rule to `project`.
Expand All @@ -219,6 +219,7 @@ def add_rule(
priority=priority,
description=description,
match_arg=match_arg,
predefined_match_arg=predefined_match_arg,
version_type=version_type,
action=action,
action_arg=action_arg,
Expand Down
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-11-05 23:54
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('builds', '0011_version-media-availability'),
]

operations = [
migrations.AddField(
model_name='versionautomationrule',
name='predefined_match_arg',
field=models.CharField(blank=True, choices=[('all-versions', 'Any version'), ('semver-versions', 'SemVer versions'), (None, 'Custom match')], default=None, help_text='Match argument defined by us, it is used if is not None, otherwise match_arg will be used.', max_length=255, null=True, verbose_name='Predefined match argument'),
),
migrations.AlterField(
model_name='versionautomationrule',
name='action',
field=models.CharField(choices=[('activate-version', 'Activate version'), ('set-default-version', 'Set version as default')], help_text='Action to apply to matching versions', max_length=32, verbose_name='Action'),
),
migrations.AlterField(
model_name='versionautomationrule',
name='version_type',
field=models.CharField(choices=[('branch', 'Branch'), ('tag', 'Tag'), ('external', 'External'), ('unknown', 'Unknown')], help_text='Type of version the rule should be applied to', max_length=32, verbose_name='Version type'),
),
]
39 changes: 35 additions & 4 deletions readthedocs/builds/models.py
Expand Up @@ -31,6 +31,8 @@
INTERNAL,
LATEST,
NON_REPOSITORY_VERSIONS,
PREDEFINED_MATCH_ARGS,
PREDEFINED_MATCH_ARGS_VALUES,
STABLE,
TAG,
VERSION_TYPES,
Expand Down Expand Up @@ -77,7 +79,6 @@
from readthedocs.projects.models import APIProject, Project
from readthedocs.projects.version_handling import determine_stable_version


log = logging.getLogger(__name__)


Expand Down Expand Up @@ -953,8 +954,8 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel):
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')),
(ACTIVATE_VERSION_ACTION, _('Activate version')),
(SET_DEFAULT_VERSION_ACTION, _('Set version as default')),
)

project = models.ForeignKey(
Expand All @@ -977,8 +978,21 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel):
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,
)
Expand All @@ -991,6 +1005,7 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel):
)
version_type = models.CharField(
_('Version type'),
help_text=_('Type of version the rule should be applied to'),
max_length=32,
choices=VERSION_TYPES,
)
Expand All @@ -1001,6 +1016,13 @@ 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, *args, **kwargs):
"""
Run an action if `version` matches the rule.
Expand All @@ -1009,7 +1031,7 @@ def run(self, version, *args, **kwargs):
:returns: True if the action was performed
"""
if version.type == self.version_type:
match, result = self.match(version, self.match_arg)
match, result = self.match(version, self.get_match_arg())
if match:
self.apply_action(version, result)
return True
Expand Down Expand Up @@ -1126,6 +1148,9 @@ def get_description(self):
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 (
Expand Down Expand Up @@ -1154,3 +1179,9 @@ def match(self, version, match_arg):
except Exception as e:
log.info('Error parsing regex: %s', e)
return False, None

def get_edit_url(self):
return reverse(
'projects_automation_rule_regex_edit',
args=[self.project.slug, self.pk],
)
35 changes: 35 additions & 0 deletions readthedocs/projects/urls/private.py
Expand Up @@ -8,6 +8,9 @@
from readthedocs.projects.backends.views import ImportDemoView, ImportWizardView
from readthedocs.projects.views import private
from readthedocs.projects.views.private import (
AutomationRuleDelete,
AutomationRuleList,
AutomationRuleMove,
DomainCreate,
DomainDelete,
DomainList,
Expand Down Expand Up @@ -38,6 +41,8 @@
ProjectUsersDelete,
ProjectVersionDeleteHTML,
ProjectVersionDetail,
RegexAutomationRuleCreate,
RegexAutomationRuleUpdate,
SearchAnalytics,
)

Expand Down Expand Up @@ -293,3 +298,33 @@
]

urlpatterns += environmentvariable_urls

automation_rule_urls = [
url(
r'^(?P<project_slug>[-\w]+)/rules/$',
AutomationRuleList.as_view(),
name='projects_automation_rule_list',
),
url(
r'^(?P<project_slug>[-\w]+)/rules/(?P<automation_rule_pk>[-\w]+)/move/(?P<steps>-?\d+)/$',
AutomationRuleMove.as_view(),
name='projects_automation_rule_move',
),
url(
r'^(?P<project_slug>[-\w]+)/rules/(?P<automation_rule_pk>[-\w]+)/delete/$',
AutomationRuleDelete.as_view(),
name='projects_automation_rule_delete',
),
url(
r'^(?P<project_slug>[-\w]+)/rules/regex/create/$',
RegexAutomationRuleCreate.as_view(),
name='projects_automation_rule_regex_create',
),
url(
r'^(?P<project_slug>[-\w]+)/rules/regex/(?P<automation_rule_pk>[-\w]+)/$',
RegexAutomationRuleUpdate.as_view(),
name='projects_automation_rule_regex_edit',
),
]

urlpatterns += automation_rule_urls