Skip to content

Commit 9461768

Browse files
authored
Automation Rules: keep history of recent matches (#7658)
Ref #7653 Close #6393
1 parent 2b9d0fb commit 9461768

File tree

6 files changed

+168
-7
lines changed

6 files changed

+168
-7
lines changed

readthedocs/builds/managers.py

+16
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,19 @@ def add_rule(
225225
action_arg=action_arg,
226226
)
227227
return rule
228+
229+
230+
class AutomationRuleMatchManager(models.Manager):
231+
232+
def register_match(self, rule, version, max_registers=15):
233+
created = self.create(
234+
rule=rule,
235+
match_arg=rule.get_match_arg(),
236+
action=rule.action,
237+
version_name=version.verbose_name,
238+
version_type=version.type,
239+
)
240+
241+
for match in self.filter(rule__project=rule.project)[max_registers:]:
242+
match.delete()
243+
return created
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 2.2.16 on 2020-11-10 22:42
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django_extensions.db.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('builds', '0029_add_time_fields'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='AutomationRuleMatch',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
20+
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
21+
('version_name', models.CharField(max_length=255)),
22+
('match_arg', models.CharField(max_length=255)),
23+
('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'), ('delete-version', 'Delete version (on branch/tag deletion)')], max_length=255)),
24+
('version_type', models.CharField(choices=[('branch', 'Branch'), ('tag', 'Tag'), ('external', 'External'), ('unknown', 'Unknown')], max_length=32)),
25+
('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matches', to='builds.VersionAutomationRule', verbose_name='Matched rule')),
26+
],
27+
options={
28+
'ordering': ('-modified', '-created'),
29+
},
30+
),
31+
]

readthedocs/builds/models.py

+39-6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
VERSION_TYPES,
4646
)
4747
from readthedocs.builds.managers import (
48+
AutomationRuleMatchManager,
4849
BuildManager,
4950
ExternalBuildManager,
5051
ExternalVersionManager,
@@ -1045,18 +1046,24 @@ def get_match_arg(self):
10451046
)
10461047
return match_arg or self.match_arg
10471048

1048-
def run(self, version, *args, **kwargs):
1049+
def run(self, version, **kwargs):
10491050
"""
10501051
Run an action if `version` matches the rule.
10511052
10521053
:type version: readthedocs.builds.models.Version
10531054
:returns: True if the action was performed
10541055
"""
1055-
if version.type == self.version_type:
1056-
match, result = self.match(version, self.get_match_arg())
1057-
if match:
1058-
self.apply_action(version, result)
1059-
return True
1056+
if version.type != self.version_type:
1057+
return False
1058+
1059+
match, result = self.match(version, self.get_match_arg())
1060+
if match:
1061+
self.apply_action(version, result)
1062+
AutomationRuleMatch.objects.register_match(
1063+
rule=self,
1064+
version=version,
1065+
)
1066+
return True
10601067
return False
10611068

10621069
def match(self, version, match_arg):
@@ -1239,3 +1246,29 @@ def get_edit_url(self):
12391246
'projects_automation_rule_regex_edit',
12401247
args=[self.project.slug, self.pk],
12411248
)
1249+
1250+
1251+
class AutomationRuleMatch(TimeStampedModel):
1252+
rule = models.ForeignKey(
1253+
VersionAutomationRule,
1254+
verbose_name=_('Matched rule'),
1255+
related_name='matches',
1256+
on_delete=models.CASCADE,
1257+
)
1258+
1259+
# Metadata from when the match happened.
1260+
version_name = models.CharField(max_length=255)
1261+
match_arg = models.CharField(max_length=255)
1262+
action = models.CharField(
1263+
max_length=255,
1264+
choices=VersionAutomationRule.ACTIONS,
1265+
)
1266+
version_type = models.CharField(
1267+
max_length=32,
1268+
choices=VERSION_TYPES,
1269+
)
1270+
1271+
objects = AutomationRuleMatchManager()
1272+
1273+
class Meta:
1274+
ordering = ('-modified', '-created')

readthedocs/projects/views/private.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from readthedocs.analytics.models import PageView
3636
from readthedocs.builds.forms import RegexAutomationRuleForm, VersionForm
3737
from readthedocs.builds.models import (
38+
AutomationRuleMatch,
3839
RegexAutomationRule,
3940
Version,
4041
VersionAutomationRule,
@@ -935,7 +936,14 @@ def get_success_url(self):
935936

936937

937938
class AutomationRuleList(AutomationRuleMixin, ListView):
938-
pass
939+
940+
def get_context_data(self, **kwargs):
941+
context = super().get_context_data(**kwargs)
942+
context['matches'] = (
943+
AutomationRuleMatch.objects
944+
.filter(rule__project=self.get_project())
945+
)
946+
return context
939947

940948

941949
class AutomationRuleMove(AutomationRuleMixin, GenericModelView):

readthedocs/rtd_tests/tests/test_automation_rules.py

+48
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def test_match(
9494
version_type=version_type,
9595
)
9696
assert rule.run(version) is result
97+
assert rule.matches.all().count() == (1 if result else 0)
9798

9899
@pytest.mark.parametrize(
99100
'version_name,result',
@@ -318,6 +319,53 @@ def test_version_make_private_action(self, trigger_build):
318319
assert version.privacy_level == PRIVATE
319320
trigger_build.assert_not_called()
320321

322+
def test_matches_history(self, trigger_build):
323+
version = get(
324+
Version,
325+
verbose_name='test',
326+
project=self.project,
327+
active=False,
328+
type=TAG,
329+
built=False,
330+
)
331+
332+
rule = get(
333+
RegexAutomationRule,
334+
project=self.project,
335+
priority=0,
336+
match_arg='^test',
337+
action=VersionAutomationRule.ACTIVATE_VERSION_ACTION,
338+
version_type=TAG,
339+
)
340+
341+
assert rule.run(version) is True
342+
assert rule.matches.all().count() == 1
343+
344+
match = rule.matches.first()
345+
assert match.version_name == 'test'
346+
assert match.version_type == TAG
347+
assert match.action == VersionAutomationRule.ACTIVATE_VERSION_ACTION
348+
assert match.match_arg == '^test'
349+
350+
for i in range(1, 31):
351+
version.verbose_name = f'test {i}'
352+
version.save()
353+
assert rule.run(version) is True
354+
355+
assert rule.matches.all().count() == 15
356+
357+
match = rule.matches.first()
358+
assert match.version_name == 'test 30'
359+
assert match.version_type == TAG
360+
assert match.action == VersionAutomationRule.ACTIVATE_VERSION_ACTION
361+
assert match.match_arg == '^test'
362+
363+
match = rule.matches.last()
364+
assert match.version_name == 'test 16'
365+
assert match.version_type == TAG
366+
assert match.action == VersionAutomationRule.ACTIVATE_VERSION_ACTION
367+
assert match.match_arg == '^test'
368+
321369

322370
@pytest.mark.django_db
323371
class TestAutomationRuleManager:

readthedocs/templates/builds/versionautomationrule_list.html

+25
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,29 @@
8080
</ul>
8181
</div>
8282
</div>
83+
84+
<p>
85+
<h3>{% trans "Recent Activity" %}</h3>
86+
</p>
87+
<div class="module-list-wrapper">
88+
<ul>
89+
{% for match in matches %}
90+
<li class="module-item">
91+
{% blocktrans trimmed with version=match.version_name pattern=match.match_arg type=match.get_version_type_display action=match.get_action_display date=match.created|timesince url=match.rule.get_edit_url %}
92+
{{ type }} "{{ version }}"
93+
matched the pattern <span>"{{ pattern }}"</span>,
94+
and the action <a href="{{ url }}">{{ action }}</a>
95+
was performed {{ date }} ago.
96+
{% endblocktrans %}
97+
</li>
98+
{% empty %}
99+
<li class="module-item">
100+
<span class="quiet">
101+
{% trans 'There is no recent activity' %}
102+
</span>
103+
</li>
104+
{% endfor %}
105+
</ul>
106+
</div>
107+
83108
{% endblock %}

0 commit comments

Comments
 (0)