Skip to content

Commit

Permalink
Automation Rules: keep history of recent matches
Browse files Browse the repository at this point in the history
Ref #7653
Close #6393
  • Loading branch information
stsewd committed Nov 10, 2020
1 parent abd3b60 commit 3777436
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 11 deletions.
16 changes: 16 additions & 0 deletions readthedocs/builds/managers.py
Expand Up @@ -225,3 +225,19 @@ def add_rule(
action_arg=action_arg,
)
return rule


class AutomationRuleMatchManager(models.Manager):

def register_match(self, rule, version, max_registers=15):
created = self.create(
rule=rule,
match_arg=rule.get_match_arg(),
action=rule.action,
version_name=version.verbose_name,
version_type=version.type,
)

for match in self.filter(rule__project=rule.project)[max_registers:]:
match.delete()
return created
31 changes: 31 additions & 0 deletions readthedocs/builds/migrations/0028_add_automation_rule_matches.py
@@ -0,0 +1,31 @@
# Generated by Django 2.2.16 on 2020-11-10 22:42

from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields


class Migration(migrations.Migration):

dependencies = [
('builds', '0027_add_privacy_level_automation_rules'),
]

operations = [
migrations.CreateModel(
name='AutomationRuleMatch',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('version_name', models.CharField(max_length=255)),
('match_arg', models.CharField(max_length=255)),
('action', models.CharField(choices=[('activate-version', 'Activate version'), ('hide-version', 'Hide version'), ('make-version-public', 'Make version public'), ('make-version-private', 'Make version private'), ('set-default-version', 'Set version as default')], max_length=255)),
('version_type', models.CharField(choices=[('branch', 'Branch'), ('tag', 'Tag'), ('external', 'External'), ('unknown', 'Unknown')], max_length=32)),
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='builds.VersionAutomationRule', verbose_name='Automation rule match')),
],
options={
'ordering': ('-modified', '-created'),
},
),
]
48 changes: 42 additions & 6 deletions readthedocs/builds/models.py
Expand Up @@ -41,6 +41,7 @@
VERSION_TYPES,
)
from readthedocs.builds.managers import (
AutomationRuleMatchManager,
BuildManager,
ExternalBuildManager,
ExternalVersionManager,
Expand Down Expand Up @@ -1025,18 +1026,27 @@ def get_match_arg(self):
)
return match_arg or self.match_arg

def run(self, version, *args, **kwargs):
def run(self, version, register=True, **kwargs):
"""
Run an action if `version` matches the rule.
:type version: readthedocs.builds.models.Version
:param register: If ``True`` a register of the match is created
:returns: True if the action was performed
"""
if version.type == self.version_type:
match, result = self.match(version, self.get_match_arg())
if match:
self.apply_action(version, result)
return True
if version.type != self.version_type:
return False

match, result = self.match(version, self.get_match_arg())
if match:
self.apply_action(version, result)

if register:
AutomationRuleMatch.objects.register_match(
rule=self,
version=version,
)
return True
return False

def match(self, version, match_arg):
Expand Down Expand Up @@ -1212,3 +1222,29 @@ def get_edit_url(self):
'projects_automation_rule_regex_edit',
args=[self.project.slug, self.pk],
)


class AutomationRuleMatch(TimeStampedModel):
rule = models.ForeignKey(
VersionAutomationRule,
verbose_name=_('Automation rule match'),
related_name='matches',
on_delete=models.CASCADE,
)

# Metadata from when the match happened.
version_name = models.CharField(max_length=255)
match_arg = models.CharField(max_length=255)
action = models.CharField(
max_length=255,
choices=VersionAutomationRule.ACTIONS,
)
version_type = models.CharField(
max_length=32,
choices=VERSION_TYPES,
)

objects = AutomationRuleMatchManager()

class Meta:
ordering = ('-modified', '-created')
10 changes: 9 additions & 1 deletion readthedocs/projects/views/private.py
Expand Up @@ -35,6 +35,7 @@
from readthedocs.analytics.models import PageView
from readthedocs.builds.forms import RegexAutomationRuleForm, VersionForm
from readthedocs.builds.models import (
AutomationRuleMatch,
RegexAutomationRule,
Version,
VersionAutomationRule,
Expand Down Expand Up @@ -938,7 +939,14 @@ def get_success_url(self):


class AutomationRuleList(AutomationRuleMixin, ListView):
pass

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['matches'] = (
AutomationRuleMatch.objects
.filter(rule__project=self.get_project())
)
return context


class AutomationRuleMove(AutomationRuleMixin, GenericModelView):
Expand Down
61 changes: 57 additions & 4 deletions readthedocs/rtd_tests/tests/test_automation_rules.py
Expand Up @@ -72,8 +72,9 @@ def setup_method(self):
]
)
@pytest.mark.parametrize('version_type', [BRANCH, TAG])
@mock.patch('readthedocs.builds.automation_actions.trigger_build')
def test_match(
self, version_name, regex, result, version_type):
self, trigger_build, version_name, regex, result, version_type):
version = get(
Version,
verbose_name=version_name,
Expand All @@ -91,6 +92,7 @@ def test_match(
version_type=version_type,
)
assert rule.run(version) is result
assert rule.matches.all().count() == (1 if result else 0)

@pytest.mark.parametrize(
'version_name,result',
Expand All @@ -107,7 +109,8 @@ def test_match(
]
)
@pytest.mark.parametrize('version_type', [BRANCH, TAG])
def test_predefined_match_all_versions(self, version_name, result, version_type):
@mock.patch('readthedocs.builds.automation_actions.trigger_build')
def test_predefined_match_all_versions(self, trigger_build, version_name, result, version_type):
version = get(
Version,
verbose_name=version_name,
Expand Down Expand Up @@ -143,7 +146,8 @@ def test_predefined_match_all_versions(self, version_name, result, version_type)
]
)
@pytest.mark.parametrize('version_type', [BRANCH, TAG])
def test_predefined_match_semver_versions(self, version_name, result, version_type):
@mock.patch('readthedocs.builds.automation_actions.trigger_build')
def test_predefined_match_semver_versions(self, trigger_build, version_name, result, version_type):
version = get(
Version,
verbose_name=version_name,
Expand Down Expand Up @@ -183,7 +187,8 @@ def test_action_activation(self, trigger_build):
assert version.active is True
trigger_build.assert_called_once()

def test_action_set_default_version(self):
@mock.patch('readthedocs.builds.automation_actions.trigger_build')
def test_action_set_default_version(self, trigger_build):
version = get(
Version,
verbose_name='v2',
Expand Down Expand Up @@ -272,6 +277,54 @@ def test_version_make_private_action(self, trigger_build):
assert version.privacy_level == PRIVATE
trigger_build.assert_not_called()

@mock.patch('readthedocs.builds.automation_actions.trigger_build')
def test_matches_history(self, trigger_build):
version = get(
Version,
verbose_name='test',
project=self.project,
active=False,
type=TAG,
built=False,
)

rule = get(
RegexAutomationRule,
project=self.project,
priority=0,
match_arg='^test',
action=VersionAutomationRule.ACTIVATE_VERSION_ACTION,
version_type=TAG,
)

assert rule.run(version) is True
assert rule.matches.all().count() == 1

match = rule.matches.first()
assert match.version_name == 'test'
assert match.version_type == TAG
assert match.action == VersionAutomationRule.ACTIVATE_VERSION_ACTION
assert match.match_arg == '^test'

for i in range(1, 31):
version.verbose_name = f'test {i}'
version.save()
assert rule.run(version) is True

assert rule.matches.all().count() == 15

match = rule.matches.first()
assert match.version_name == 'test 30'
assert match.version_type == TAG
assert match.action == VersionAutomationRule.ACTIVATE_VERSION_ACTION
assert match.match_arg == '^test'

match = rule.matches.last()
assert match.version_name == 'test 16'
assert match.version_type == TAG
assert match.action == VersionAutomationRule.ACTIVATE_VERSION_ACTION
assert match.match_arg == '^test'


@pytest.mark.django_db
class TestAutomationRuleManager:
Expand Down
30 changes: 30 additions & 0 deletions readthedocs/templates/builds/versionautomationrule_list.html
Expand Up @@ -80,4 +80,34 @@
</ul>
</div>
</div>

<p>
<h3>{% trans "Recent Activity" %}</h3>
</p>
<div class="module-list-wrapper">
<ul>
{% for match in matches %}
<li class="module-item">
{% blocktrans trimmed with version=match.version_name pattern=match.match_arg type=match.get_version_type_display action=match.get_action_display %}
{{ type }} "{{ version }}"
matched the pattern <span>"{{ pattern }}"</span>,
and the action <span>"{{ action }}"</span> was performed
{% endblocktrans %}
<a href="{{ match.rule.get_edit_url }}">
{% blocktrans trimmed with date=match.created|timesince %}
{{ date }} ago
{% endblocktrans %}
</a>
.
</li>
{% empty %}
<li class="module-item">
<span class="quiet">
{% trans 'There is no recent activity' %}
</span>
</li>
{% endfor %}
</ul>
</div>

{% endblock %}

0 comments on commit 3777436

Please sign in to comment.