Skip to content

Commit ebceeda

Browse files
authored
Merge pull request #3075 from laws-africa/follow-journal
Add following for law reports and journals
2 parents 5383561 + c5c609b commit ebceeda

File tree

13 files changed

+276
-7
lines changed

13 files changed

+276
-7
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Generated by Django 4.2.29 on 2026-03-19 07:44
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("peachjam", "0286_remove_coredocument_content_html_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name="userfollowing",
16+
name="journal",
17+
field=models.ForeignKey(
18+
blank=True,
19+
null=True,
20+
on_delete=django.db.models.deletion.CASCADE,
21+
related_name="followers",
22+
to="peachjam.journal",
23+
verbose_name="journal",
24+
),
25+
),
26+
migrations.AddField(
27+
model_name="userfollowing",
28+
name="law_report",
29+
field=models.ForeignKey(
30+
blank=True,
31+
null=True,
32+
on_delete=django.db.models.deletion.CASCADE,
33+
related_name="followers",
34+
to="peachjam.lawreport",
35+
verbose_name="law report",
36+
),
37+
),
38+
migrations.AddConstraint(
39+
model_name="userfollowing",
40+
constraint=models.UniqueConstraint(
41+
condition=models.Q(("journal__isnull", False)),
42+
fields=("user", "journal"),
43+
name="unique_user_journal",
44+
),
45+
),
46+
migrations.AddConstraint(
47+
model_name="userfollowing",
48+
constraint=models.UniqueConstraint(
49+
condition=models.Q(("law_report__isnull", False)),
50+
fields=("user", "law_report"),
51+
name="unique_user_law_report",
52+
),
53+
),
54+
]

peachjam/models/journals_books.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.contrib.contenttypes.fields import GenericRelation
22
from django.db import models
3+
from django.urls import reverse
34
from django.utils.translation import gettext_lazy as _
45
from django_lifecycle import BEFORE_SAVE
56
from markdown.extensions.toc import slugify
@@ -55,6 +56,9 @@ class Meta:
5556
def __str__(self):
5657
return self.title
5758

59+
def get_absolute_url(self):
60+
return reverse("journal_detail", args=[self.slug])
61+
5862

5963
class JournalArticle(CoreDocument):
6064

peachjam/models/user_following.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ class UserFollowing(models.Model):
8181
related_name="followers",
8282
verbose_name=_("taxonomy"),
8383
)
84+
journal = models.ForeignKey(
85+
"peachjam.Journal",
86+
null=True,
87+
blank=True,
88+
on_delete=models.CASCADE,
89+
related_name="followers",
90+
verbose_name=_("journal"),
91+
)
92+
law_report = models.ForeignKey(
93+
"peachjam.LawReport",
94+
null=True,
95+
blank=True,
96+
on_delete=models.CASCADE,
97+
related_name="followers",
98+
verbose_name=_("law report"),
99+
)
84100
saved_search = models.ForeignKey(
85101
SavedSearch,
86102
null=True,
@@ -111,6 +127,8 @@ class UserFollowing(models.Model):
111127
"country",
112128
"locality",
113129
"taxonomy",
130+
"journal",
131+
"law_report",
114132
"saved_search",
115133
"saved_document",
116134
]
@@ -152,6 +170,16 @@ class Meta:
152170
condition=models.Q(taxonomy__isnull=False),
153171
name="unique_user_taxonomy",
154172
),
173+
models.UniqueConstraint(
174+
fields=["user", "journal"],
175+
condition=models.Q(journal__isnull=False),
176+
name="unique_user_journal",
177+
),
178+
models.UniqueConstraint(
179+
fields=["user", "law_report"],
180+
condition=models.Q(law_report__isnull=False),
181+
name="unique_user_law_report",
182+
),
155183
models.UniqueConstraint(
156184
fields=["user", "saved_search"],
157185
condition=models.Q(saved_search__isnull=False),
@@ -190,6 +218,8 @@ def is_new_docs(self):
190218
"country",
191219
"locality",
192220
"taxonomy",
221+
"journal",
222+
"law_report",
193223
]
194224

195225
@property
@@ -256,6 +286,14 @@ def documents_for_followed_topic(self):
256286
topics = [self.taxonomy] + list(self.taxonomy.get_descendants())
257287
return qs.filter(taxonomies__topic__in=topics)
258288

289+
if self.journal:
290+
return qs.filter(journalarticle__journal=self.journal)
291+
292+
if self.law_report:
293+
return qs.filter(
294+
judgment__law_report_entries__law_report_volume__law_report=self.law_report
295+
).distinct()
296+
259297
return qs.none()
260298

261299
def documents_for_followed_search(self):

peachjam/templates/peachjam/journal/journal_detail.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends 'peachjam/layouts/document_list.html' %}
2-
{% load i18n %}
2+
{% load i18n peachjam %}
33
{% block title %}{{ journal.title }}{% endblock %}
44
{% block breadcrumbs %}
55
<div class="container">
@@ -17,7 +17,15 @@
1717
</div>
1818
{% endblock %}
1919
{% block page-title %}
20-
{% if not entity_profile %}<h1 class="my-4">{{ journal.title }}</h1>{% endif %}
20+
{% if not entity_profile %}
21+
<div class="my-4">
22+
<h1 class="mb-0">{{ journal.title }}</h1>
23+
{% block follow-button %}
24+
<div hx-get="{% url 'user_following_button' %}?{{ journal|get_follow_params }}"
25+
hx-trigger="load"></div>
26+
{% endblock %}
27+
</div>
28+
{% endif %}
2129
{% endblock %}
2230
{% block page-header %}
2331
{{ block.super }}

peachjam/templates/peachjam/law_report/law_report_detail.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
{% if not entity_profile %}
2222
<div class="my-4">
2323
<h1>{{ page_title }}</h1>
24-
{% block follow-button %}{% endblock %}
24+
{% block follow-button %}
25+
<div hx-get="{% url 'user_following_button' %}?{{ law_report|get_follow_params }}"
26+
hx-trigger="load"></div>
27+
{% endblock %}
2528
</div>
2629
{% endif %}
2730
{% endblock %}

peachjam/templatetags/peachjam.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@ def split(value, sep=None):
121121
@register.filter
122122
def get_follow_params(obj):
123123
# this would be better as a model method
124-
return f"{obj._meta.model_name}={obj.pk}"
124+
field_map = {
125+
"courtclass": "court_class",
126+
"courtregistry": "court_registry",
127+
"lawreport": "law_report",
128+
}
129+
field_name = field_map.get(obj._meta.model_name, obj._meta.model_name)
130+
return f"{field_name}={obj.pk}"
125131

126132

127133
@register.simple_tag

peachjam/tests/test_journals.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import datetime
2+
3+
from countries_plus.models import Country
4+
from django.test import TestCase
5+
from django.urls import reverse
6+
from languages_plus.models import Language
7+
8+
from peachjam.models import Journal, JournalArticle
9+
10+
11+
class JournalModelAndViewTests(TestCase):
12+
fixtures = [
13+
"tests/countries",
14+
"tests/languages",
15+
"documents/sample_documents",
16+
]
17+
18+
def setUp(self):
19+
super().setUp()
20+
self.journal = Journal.objects.create(
21+
title="Regional Law Journal",
22+
slug="regional-law-journal",
23+
)
24+
self.article = JournalArticle.objects.create(
25+
title="Fresh journal article",
26+
journal=self.journal,
27+
publisher="Publisher",
28+
date=datetime.date(2025, 1, 1),
29+
language=Language.objects.first(),
30+
jurisdiction=Country.objects.first(),
31+
)
32+
33+
def test_journal_absolute_url(self):
34+
self.assertEqual(
35+
reverse("journal_detail", args=[self.journal.slug]),
36+
self.journal.get_absolute_url(),
37+
)
38+
39+
def test_journal_detail_view_includes_follow_button(self):
40+
response = self.client.get(self.journal.get_absolute_url())
41+
42+
self.assertEqual(response.status_code, 200)
43+
self.assertContains(response, self.article.title)
44+
self.assertContains(
45+
response,
46+
reverse("user_following_button") + f"?journal={self.journal.pk}",
47+
)

peachjam/tests/test_law_reports.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ def test_law_report_detail_view_shows_law_report_judgments_and_non_empty_volumes
125125
self.assertIn(self.volume_1, response.context["law_report_volumes"])
126126
self.assertIn(self.volume_2, response.context["law_report_volumes"])
127127
self.assertNotIn(self.empty_volume, response.context["law_report_volumes"])
128-
self.assertTrue(response.context["hide_follow_button"])
128+
self.assertContains(
129+
response,
130+
reverse("user_following_button") + f"?law_report={self.law_report.pk}",
131+
)
129132

130133
def test_law_report_volume_detail_view_filters_to_selected_volume(self):
131134
response = self.client.get(self.volume_1.get_absolute_url())

peachjam/tests/test_timeline.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from peachjam.models import (
1111
Court,
1212
ExtractedCitation,
13+
Journal,
14+
JournalArticle,
1315
Judgment,
16+
LawReport,
17+
LawReportEntry,
18+
LawReportVolume,
1419
Legislation,
1520
Locality,
1621
Predicate,
@@ -157,6 +162,84 @@ def test_send_new_documents_email_includes_first_topic_in_subject(self):
157162
request = mailer.call_args[0][0]
158163
self.assertEqual(f"New documents for {topic}", str(request.subject))
159164

165+
def test_journal_follow_creates_new_documents_timeline_event(self):
166+
journal = Journal.objects.create(
167+
title="Regional Law Journal",
168+
slug="regional-law-journal",
169+
)
170+
follow = UserFollowing.objects.create(user=self.user, journal=journal)
171+
follow.last_alerted_at = self.last_alerted_at
172+
follow.save(update_fields=["last_alerted_at"])
173+
article = JournalArticle.objects.create(
174+
title="Fresh journal article",
175+
journal=journal,
176+
publisher="Publisher",
177+
date=datetime(2025, 10, 1),
178+
language=Language.objects.get(pk="en"),
179+
jurisdiction=Country.objects.get(pk="ZA"),
180+
)
181+
182+
UserFollowing.update_follows_for_user(self.user)
183+
184+
event = TimelineEvent.objects.get(user_following=follow)
185+
self.assertEqual(TimelineEvent.EventTypes.NEW_DOCUMENTS, event.event_type)
186+
self.assertIn(article.work, event.subject_works.all())
187+
188+
def test_law_report_follow_creates_new_documents_timeline_event(self):
189+
law_report = LawReport.objects.create(
190+
title="Regional Law Reports",
191+
slug="regional-law-reports",
192+
)
193+
volume = LawReportVolume.objects.create(
194+
title="Volume 1",
195+
slug="volume-1",
196+
law_report=law_report,
197+
year=2025,
198+
)
199+
follow = UserFollowing.objects.create(user=self.user, law_report=law_report)
200+
follow.last_alerted_at = self.last_alerted_at
201+
follow.save(update_fields=["last_alerted_at"])
202+
judgment = Judgment.objects.create(
203+
case_name="Reported case",
204+
court=self.court,
205+
date=datetime(2025, 10, 1),
206+
language=Language.objects.get(pk="en"),
207+
jurisdiction=Country.objects.get(pk="ZA"),
208+
)
209+
LawReportEntry.objects.create(judgment=judgment, law_report_volume=volume)
210+
211+
UserFollowing.update_follows_for_user(self.user)
212+
213+
event = TimelineEvent.objects.get(user_following=follow)
214+
self.assertEqual(TimelineEvent.EventTypes.NEW_DOCUMENTS, event.event_type)
215+
self.assertIn(judgment.work, event.subject_works.all())
216+
217+
def test_send_new_documents_email_includes_journal_in_subject(self):
218+
journal = Journal.objects.create(
219+
title="Regional Law Journal",
220+
slug="regional-law-journal",
221+
)
222+
follow = UserFollowing.objects.create(user=self.user, journal=journal)
223+
doc = Judgment.objects.first()
224+
TimelineEvent.add_new_documents_event(follow, [doc])
225+
226+
with (
227+
override_settings(
228+
PEACHJAM={
229+
**settings.PEACHJAM,
230+
"EMAIL_ALERTS_ENABLED": True,
231+
"CUSTOMERIO_EMAIL_API_KEY": "test",
232+
},
233+
TEMPLATED_EMAIL_BACKEND="peachjam.emails.CustomerIOTemplateBackend",
234+
),
235+
patch("peachjam.emails.APIClient.send_email") as mailer,
236+
):
237+
TimelineEmailService.send_new_documents_email(self.user)
238+
239+
self.assertEqual(1, mailer.call_count)
240+
request = mailer.call_args[0][0]
241+
self.assertEqual(f"New documents for {journal}", str(request.subject))
242+
160243

161244
class TimelineRelationshipTests(TestCase):
162245
fixtures = [

peachjam/views/journals.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ def add_facets(self, context):
9292

9393
def get_context_data(self, **kwargs):
9494
context = super().get_context_data(**kwargs)
95-
context["hide_follow_button"] = True
9695
context["entity_profile"] = self.journal.entity_profile.first()
9796
context["journal"] = self.journal
9897
volumes = list(self.journal.volumes.all())

0 commit comments

Comments
 (0)