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

Automation Rules: keep history of recent matches #7658

Merged
merged 7 commits into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions readthedocs/builds/managers.py
Original file line number Diff line number Diff line change
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):
stsewd marked this conversation as resolved.
Show resolved Hide resolved
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/0030_add_automation_rule_matches.py
Original file line number Diff line number Diff line change
@@ -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', '0029_add_time_fields'),
]

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'), ('delete-version', 'Delete version (on branch/tag deletion)')], 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='Matched rule')),
],
options={
'ordering': ('-modified', '-created'),
},
),
]
45 changes: 39 additions & 6 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
VERSION_TYPES,
)
from readthedocs.builds.managers import (
AutomationRuleMatchManager,
BuildManager,
ExternalBuildManager,
ExternalVersionManager,
Expand Down Expand Up @@ -1047,18 +1048,24 @@ def get_match_arg(self):
)
return match_arg or self.match_arg

def run(self, version, *args, **kwargs):
def run(self, version, **kwargs):
"""
Run an action if `version` matches the rule.

:type version: readthedocs.builds.models.Version
: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)
AutomationRuleMatch.objects.register_match(
rule=self,
version=version,
)
return True
return False

def match(self, version, match_arg):
Expand Down Expand Up @@ -1241,3 +1248,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=_('Matched rule'),
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(
stsewd marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
48 changes: 48 additions & 0 deletions readthedocs/rtd_tests/tests/test_automation_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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 Down Expand Up @@ -318,6 +319,53 @@ def test_version_make_private_action(self, trigger_build):
assert version.privacy_level == PRIVATE
trigger_build.assert_not_called()

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
25 changes: 25 additions & 0 deletions readthedocs/templates/builds/versionautomationrule_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,29 @@
</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 date=match.created|timesince url=match.rule.get_edit_url %}
{{ type }} "{{ version }}"
matched the pattern <span>"{{ pattern }}"</span>,
and the action <a href="{{ url }}">{{ action }}</a>
was performed {{ date }} ago.
{% endblocktrans %}
</li>
{% empty %}
<li class="module-item">
<span class="quiet">
{% trans 'There is no recent activity' %}
</span>
</li>
{% endfor %}
</ul>
</div>

{% endblock %}