Skip to content

Commit

Permalink
Merge pull request #5996 from stsewd/implement-ui-for-automation-rules
Browse files Browse the repository at this point in the history
Implement UI for automation rules
  • Loading branch information
stsewd authored Nov 12, 2019
2 parents 1062600 + ce3349a commit f46405f
Show file tree
Hide file tree
Showing 17 changed files with 920 additions and 17 deletions.
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var sources = {
projects: {
'js/tools.js': {},
'js/import.js': {},
'js/automation-rules.js': {},
'css/import.less': {},
'css/admin.less': {},
},
Expand Down
7 changes: 7 additions & 0 deletions media/css/core.css
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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,
}
102 changes: 99 additions & 3 deletions readthedocs/builds/forms.py
Original file line number Diff line number Diff line change
@@ -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,91 @@ 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 regular expression to match the version.
<a href="https://docs.readthedocs.io/page/automation-rules.html#user-defined-matches">
Check the documentation for valid patterns.
</a>
"""
)),
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 = [
(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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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'),
),
]
38 changes: 35 additions & 3 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
INTERNAL,
LATEST,
NON_REPOSITORY_VERSIONS,
PREDEFINED_MATCH_ARGS,
PREDEFINED_MATCH_ARGS_VALUES,
STABLE,
TAG,
VERSION_TYPES,
Expand Down Expand Up @@ -954,8 +956,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 @@ -978,8 +980,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 @@ -992,6 +1007,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 @@ -1002,6 +1018,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 @@ -1010,7 +1033,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 @@ -1127,6 +1150,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 @@ -1177,3 +1203,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],
)
29 changes: 29 additions & 0 deletions readthedocs/projects/static-src/projects/js/automation-rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// TODO: use knockoutjs instead, and for new code as well.

var $ = require('jquery');

function set_help_text(value) {
var help_texts = {
'all-versions': 'All versions will be matched.',
'semver-versions': 'Versions incremented based on semantic versioning rules will be matched.',
'': ''
};
$('#id_predefined_match_arg').siblings('.helptext').text(help_texts[value]);
}

$(function () {
var value = $('#id_predefined_match_arg').val();
if (value !== '') {
$('#id_match_arg').parent().hide();
}
set_help_text(value);

$('#id_predefined_match_arg').bind('change', function (ev) {
if (this.value === '') {
$('#id_match_arg').parent().show();
} else {
$('#id_match_arg').parent().hide();
}
set_help_text(this.value);
});
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f46405f

Please sign in to comment.