Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.2 on 2026-04-29 16:43

from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models


class Migration(migrations.Migration):

atomic = False

dependencies = [
("addons", "0027_visualaddon"),
]

operations = [
AddIndexConcurrently(
model_name="addonevent",
index=models.Index(
models.F("parameters__site"),
condition=models.Q(("parameters__has_key", "site")),
name="addonevent_param_site_idx",
),
),
]
10 changes: 10 additions & 0 deletions documentcloud/addons/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.conf import settings
from django.core.cache import cache
from django.db import models, transaction
from django.db.models import F, Q
from django.utils.translation import gettext_lazy as _

# Standard Library
Expand Down Expand Up @@ -568,6 +569,15 @@ class AddOnEvent(models.Model):
help_text=_("Timestamp of when the add-on event was last updated"),
)

class Meta:
indexes = [
models.Index(
F("parameters__site"),
name="addonevent_param_site_idx",
condition=Q(parameters__has_key="site"),
),
]

def __str__(self):
return f"Event: {self.addon_id} - {self.event}"

Expand Down
13 changes: 13 additions & 0 deletions documentcloud/addons/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Third Party
import factory

# DocumentCloud
from documentcloud.addons.choices import Event


class AddOnFactory(factory.django.DjangoModelFactory):
name = factory.Sequence(lambda n: f"Add-On {n}")
Expand Down Expand Up @@ -34,6 +37,16 @@ class Meta:
model = "addons.AddOnRun"


class AddOnEventFactory(factory.django.DjangoModelFactory):
addon = factory.SubFactory("documentcloud.addons.tests.factories.AddOnFactory")
user = factory.SubFactory("documentcloud.users.tests.factories.UserFactory")
event = Event.disabled
parameters = {}

class Meta:
model = "addons.AddOnEvent"


class GitHubAccountFactory(factory.django.DjangoModelFactory):

user = factory.SubFactory("documentcloud.users.tests.factories.UserFactory")
Expand Down
84 changes: 83 additions & 1 deletion documentcloud/addons/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
# DocumentCloud
from documentcloud.addons.models import AddOn, AddOnRun
from documentcloud.addons.serializers import AddOnRunSerializer, AddOnSerializer
from documentcloud.addons.tests.factories import AddOnFactory, AddOnRunFactory
from documentcloud.addons.tests.factories import (
AddOnEventFactory,
AddOnFactory,
AddOnRunFactory,
)
from documentcloud.documents.choices import Access
from documentcloud.users.tests.factories import UserFactory

Expand Down Expand Up @@ -268,3 +272,81 @@ def test_destroy(self, client, mocker):
response = client.delete(f"/api/addon_runs/{run.uuid}/")
assert response.status_code == status.HTTP_204_NO_CONTENT
assert cancel.called_once()

def test_filter_site(self, client):
"""Filter runs by event parameters.site"""
user = UserFactory()
site = "https://www.example.com"
matching_event = AddOnEventFactory(
user=user, parameters={"site": site, "selector": "*"}
)
other_event = AddOnEventFactory(
user=user, parameters={"site": "https://www.other.com"}
)
no_site_event = AddOnEventFactory(user=user, parameters={"selector": "*"})
matching_run = AddOnRunFactory(user=user, event=matching_event)
AddOnRunFactory(user=user, event=other_event)
AddOnRunFactory(user=user, event=no_site_event)
AddOnRunFactory(user=user, event=None)
client.force_authenticate(user=user)
response = client.get("/api/addon_runs/", {"site": site})
assert response.status_code == status.HTTP_200_OK
uuids = [r["uuid"] for r in response.json()["results"]]
assert uuids == [str(matching_run.uuid)]

def test_filter_site_absent_is_noop(self, client):
"""Omitting the site filter returns all viewable runs"""
user = UserFactory()
with_site = AddOnEventFactory(
user=user, parameters={"site": "https://www.example.com"}
)
without_site = AddOnEventFactory(user=user, parameters={})
AddOnRunFactory(user=user, event=with_site)
AddOnRunFactory(user=user, event=without_site)
AddOnRunFactory(user=user, event=None)
client.force_authenticate(user=user)
response = client.get("/api/addon_runs/")
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["results"]) == 3


@pytest.mark.django_db()
class TestAddOnEventAPI:
def test_filter_site(self, client):
"""Filter events by parameters.site"""
user = UserFactory()
site = "https://www.example.com"
matching = AddOnEventFactory(
user=user, parameters={"site": site, "selector": "*"}
)
AddOnEventFactory(
user=user, parameters={"site": "https://www.other.com", "selector": "*"}
)
AddOnEventFactory(user=user, parameters={"selector": "*"})
client.force_authenticate(user=user)
response = client.get("/api/addon_events/", {"site": site})
assert response.status_code == status.HTTP_200_OK
ids = [r["id"] for r in response.json()["results"]]
assert ids == [matching.pk]

def test_filter_site_absent_is_noop(self, client):
"""Omitting the site filter returns all viewable events"""
user = UserFactory()
AddOnEventFactory(user=user, parameters={"site": "https://www.example.com"})
AddOnEventFactory(user=user, parameters={})
client.force_authenticate(user=user)
response = client.get("/api/addon_events/")
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["results"]) == 2

def test_filter_site_no_match(self, client):
"""A site filter that matches nothing returns an empty list"""
user = UserFactory()
AddOnEventFactory(user=user, parameters={"site": "https://www.example.com"})
AddOnEventFactory(user=user, parameters={})
client.force_authenticate(user=user)
response = client.get(
"/api/addon_events/", {"site": "https://nope.example.com"}
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["results"] == []
11 changes: 10 additions & 1 deletion documentcloud/addons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from django.db.models import Q
from django.db.models.aggregates import Count
from django.db.models.expressions import Case, Exists, F, OuterRef, Value, When
from django.db.models.fields.related import ForeignKey
from django.db.models.functions.text import Concat
from django.http.response import (
Http404,
Expand Down Expand Up @@ -741,6 +740,11 @@ class Filter(django_filters.FilterSet):
model=AddOn, help_text="Filter runs by a specific add-on ID."
)
dismissed = django_filters.BooleanFilter(help_text="Was this run dismissed?")
site = django_filters.CharFilter(
field_name="event__parameters__site",
lookup_expr="exact",
Comment thread
duckduckgrayduck marked this conversation as resolved.
help_text="Filter runs by the `site` value in the event's parameters.",
)

class Meta:
model = AddOnRun
Expand Down Expand Up @@ -971,6 +975,11 @@ class Filter(django_filters.FilterSet):
lookup_expr="exact",
help_text="Filter events by a specific add-on ID.",
)
site = django_filters.CharFilter(
field_name="parameters__site",
lookup_expr="exact",
help_text="Filter events by the `site` value in their parameters.",
)

class Meta:
model = AddOnEvent
Expand Down
Loading