Skip to content

Implement UI for automation rules #5996

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 51 commits into from
Nov 12, 2019
Merged
Show file tree
Hide file tree
Changes from 32 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions media/css/core.css
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,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
14 changes: 8 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
100 changes: 97 additions & 3 deletions readthedocs/builds/forms.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-

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

import re

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

from readthedocs.builds.models import Version
from readthedocs.builds.constants import 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 +38,96 @@ 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):

ALL_VERSIONS_REGEX = r'.*'
SEMVER_REGEX = r'^v?(\d+\.)(\d+\.)(\d)(-.+)?$'
MATCH_CHOICES = (
(ALL_VERSIONS_REGEX, 'All versions'),
(SEMVER_REGEX, 'SemVer versions'),
(None, 'Custom match'),
)

predefined_match = forms.ChoiceField(
label='Match',
choices=MATCH_CHOICES,
initial=ALL_VERSIONS_REGEX,
required=False,
help_text=_('Versions the rule should be applied to'),
)

match_arg = forms.CharField(
label='Custom match',
help_text=_(
'A <a href="https://docs.python.org/3/library/re.html">Python regular expression</a>'
),
required=False,
)

class Meta:
model = RegexAutomationRule
fields = [
'description',
'predefined_match',
'match_arg',
'version_type',
'action',
]

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),
]

# Set initial value of `predefined_match` if `match_arg`
# is one predefined match.
match_options = set(v[0] for v in self.MATCH_CHOICES)
if self.instance.pk:
match_arg = self.instance.match_arg
if match_arg and match_arg in match_options:
self.initial['match'] = self.instance.match_arg
else:
self.initial['match'] = None

def clean_match_arg(self):
"""Use value from predefined_match if a custom match isn't given."""
match_arg = self.cleaned_data['match_arg']
match = self.cleaned_data['predefined_match']
if match:
match_arg = match
if 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'],
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2019-08-14 14:25
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('builds', '0010_add-description-field-to-automation-rule'),
]

operations = [
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'),
),
]
15 changes: 13 additions & 2 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,8 +949,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 @@ -975,6 +975,7 @@ class VersionAutomationRule(PolymorphicModel, TimeStampedModel):
)
action = models.CharField(
_('Action'),
help_text=_('Action to apply to matching versions'),
max_length=32,
choices=ACTIONS,
)
Expand All @@ -987,6 +988,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 Down Expand Up @@ -1122,6 +1124,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 @@ -1150,3 +1155,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],
)
6 changes: 2 additions & 4 deletions readthedocs/builds/views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-

"""Views for builds app."""

import logging
import textwrap
from urllib.parse import urlparse

from django.contrib import messages
from django.contrib.auth.decorators import login_required
Expand All @@ -17,12 +16,11 @@
from django.utils.decorators import method_decorator
from django.views.generic import DetailView, ListView
from requests.utils import quote
from urllib.parse import urlparse

from readthedocs.doc_builder.exceptions import BuildEnvironmentError
from readthedocs.builds.models import Build, Version
from readthedocs.core.permissions import AdminPermission
from readthedocs.core.utils import trigger_build
from readthedocs.doc_builder.exceptions import BuildEnvironmentError
from readthedocs.projects.models import Project


Expand Down
37 changes: 35 additions & 2 deletions readthedocs/projects/urls/private.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Project URLs for authenticated users."""

from django.conf.urls import url
Expand All @@ -8,6 +6,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 All @@ -27,6 +28,8 @@
ProjectAdvertisingUpdate,
ProjectDashboard,
ProjectUpdate,
RegexAutomationRuleCreate,
RegexAutomationRuleUpdate,
)


Expand Down Expand Up @@ -268,3 +271,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
68 changes: 65 additions & 3 deletions readthedocs/projects/views/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,21 @@
from django.utils.translation import ugettext_lazy as _
from django.views.generic import ListView, TemplateView, View
from formtools.wizard.views import SessionWizardView
from vanilla import CreateView, DeleteView, DetailView, GenericView, UpdateView
from vanilla import (
CreateView,
DeleteView,
DetailView,
GenericModelView,
GenericView,
UpdateView,
)

from readthedocs.builds.forms import VersionForm
from readthedocs.builds.models import Version
from readthedocs.builds.forms import RegexAutomationRuleForm, VersionForm
from readthedocs.builds.models import (
RegexAutomationRule,
Version,
VersionAutomationRule,
)
from readthedocs.core.mixins import ListViewWithForm, LoginRequiredMixin
from readthedocs.core.utils import broadcast, trigger_build
from readthedocs.integrations.models import HttpExchange, Integration
Expand Down Expand Up @@ -918,6 +929,57 @@ def get(self, request, *args, **kwargs):
return self.http_method_not_allowed(request, *args, **kwargs)


class AutomationRuleMixin(ProjectAdminMixin, PrivateViewMixin):

model = VersionAutomationRule
lookup_url_kwarg = 'automation_rule_pk'

def get_success_url(self):
return reverse(
'projects_automation_rule_list',
args=[self.get_project().slug],
)


class AutomationRuleList(AutomationRuleMixin, ListView):
pass


class AutomationRuleMove(AutomationRuleMixin, GenericModelView):

http_method_names = ['post']

def post(self, request, *args, **kwargs):
rule = self.get_object()
steps = int(self.kwargs.get('steps', 0))
rule.move(steps)
return HttpResponseRedirect(
reverse(
'projects_automation_rule_list',
args=[self.get_project().slug],
)
)


class AutomationRuleDelete(AutomationRuleMixin, DeleteView):

http_method_names = ['post']


class RegexAutomationRuleMixin(AutomationRuleMixin):

model = RegexAutomationRule
form_class = RegexAutomationRuleForm


class RegexAutomationRuleCreate(RegexAutomationRuleMixin, CreateView):
pass


class RegexAutomationRuleUpdate(RegexAutomationRuleMixin, UpdateView):
pass


@login_required
def search_analytics_view(request, project_slug):
"""View for search analytics."""
Expand Down
Loading