Skip to content

Commit

Permalink
refactor(communications): cache changes, added live field
Browse files Browse the repository at this point in the history
  • Loading branch information
calummackervoy committed Aug 1, 2024
1 parent df11357 commit f62aa5a
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 61 deletions.
4 changes: 2 additions & 2 deletions itou/communications/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ class AnnouncementItemInline(ItouTabularInline):

@admin.register(models.AnnouncementCampaign)
class AnnouncementCampaignAdmin(ItouModelAdmin):
list_display = ("start_date", "end_date")
fields = ("max_items", "start_date")
list_display = ("start_date", "end_date", "live")
fields = ("max_items", "start_date", "live")
inlines = (AnnouncementItemInline,)
14 changes: 1 addition & 13 deletions itou/communications/apps.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
from django.apps import AppConfig
from django.conf import settings
from django.db import OperationalError, ProgrammingError, transaction
from django.db.models.signals import post_delete, post_migrate, post_save
from django.db.models.signals import post_migrate


class CommunicationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "itou.communications"

def ready(self):
from itou.communications.models import AnnouncementCampaign, AnnouncementItem
from itou.communications.signals import (
update_cached_announcement_on_campaign_changes,
update_cached_announcement_on_item_changes,
)

self.module.autodiscover()
post_migrate.connect(post_communications_migrate_handler, sender=self)

post_save.connect(update_cached_announcement_on_campaign_changes, sender=AnnouncementCampaign)
post_delete.connect(update_cached_announcement_on_campaign_changes, sender=AnnouncementCampaign)

post_save.connect(update_cached_announcement_on_item_changes, sender=AnnouncementItem)
post_delete.connect(update_cached_announcement_on_item_changes, sender=AnnouncementItem)


def post_communications_migrate_handler(sender, app_config, **kwargs):
sync_notifications(app_config.get_model("NotificationRecord"))
Expand Down
11 changes: 4 additions & 7 deletions itou/communications/cache.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import date, datetime, timedelta
from datetime import date, datetime

from django.core.cache import cache

Expand All @@ -9,12 +9,9 @@


def update_active_announcement_cache():
today = date.today()
last_edition_boundary = today.replace(day=1) - timedelta(days=1)

campaign = AnnouncementCampaign.objects.filter(
start_date__lte=today, start_date__gt=last_edition_boundary
).prefetch_related("items")
campaign = AnnouncementCampaign.objects.filter(start_date=date.today().replace(day=1), live=True).prefetch_related(
"items"
)

def get_cache_expiration():
if not len(campaign):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ class Migration(migrations.Migration):
(
"start_date",
models.DateField(
help_text=(
"la date de lancement des articles sur le site. "
"La date de fin est toujours le dernier jour du mois"
),
verbose_name="date de début",
help_text="le mois des nouveautés. Automatiquement fixé au premier du mois saisi",
verbose_name="mois concerné",
),
),
(
"live",
models.BooleanField(
default=True, help_text="les modifications sont toujours possible", verbose_name="prêt"
),
),
],
Expand Down Expand Up @@ -94,7 +97,7 @@ class Migration(migrations.Migration):
),
),
migrations.AlterUniqueTogether(
name='announcementitem',
unique_together={('campaign', 'priority')},
name="announcementitem",
unique_together={("campaign", "priority")},
),
]
50 changes: 37 additions & 13 deletions itou/communications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Q
Expand Down Expand Up @@ -128,8 +127,13 @@ class AnnouncementCampaign(models.Model):
)
start_date = models.DateField(
null=False,
verbose_name="date de début",
help_text="la date de lancement des articles sur le site. La date de fin est toujours le dernier jour du mois",
verbose_name="mois concerné",
help_text="le mois des nouveautés. Automatiquement fixé au premier du mois saisi",
)
live = models.BooleanField(
default=True,
verbose_name="prêt",
help_text="les modifications sont toujours possible",
)

class Meta:
Expand Down Expand Up @@ -160,18 +164,23 @@ def __str__(self):
return f"Campagne d'annonce du { self.start_date.strftime('%m/%Y') }"

def clean(self):
# prevent campaigns from sharing start_date
queryset = AnnouncementCampaign.objects.annotate(
year=models.functions.ExtractYear("start_date"), month=models.functions.ExtractMonth("start_date")
).filter(year=self.start_date.year, month=self.start_date.month)
self.start_date = self.start_date.replace(day=1)
return super().clean()

if self.pk:
queryset = queryset.exclude(pk=self.pk)
def _update_cached_active_announcement(self):
from itou.communications.cache import get_cached_active_announcement, update_active_announcement_cache

if queryset.exists():
raise ValidationError("Maximum 1 campagne par mois")
campaign = get_cached_active_announcement()
if campaign is None or self.pk == campaign.pk:
update_active_announcement_cache()

return super().clean()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self._update_cached_active_announcement()

def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
self._update_cached_active_announcement()

def items_for_template(self):
return self.items.all()[: self.max_items]
Expand Down Expand Up @@ -199,7 +208,22 @@ class AnnouncementItem(models.Model):
class Meta:
verbose_name = "article d'annonce"
ordering = ["-campaign__start_date", "priority", "pk"]
unique_together = [('campaign', 'priority')]
unique_together = [("campaign", "priority")]

def __str__(self):
return self.title

def _update_cached_active_announcement(self):
from itou.communications.cache import get_cached_active_announcement, update_active_announcement_cache

campaign = get_cached_active_announcement()
if campaign is None or self.campaign == campaign:
update_active_announcement_cache()

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self._update_cached_active_announcement()

def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
self._update_cached_active_announcement()
13 changes: 0 additions & 13 deletions itou/communications/signals.py

This file was deleted.

3 changes: 2 additions & 1 deletion tests/communications/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Meta:
model = AnnouncementCampaign
skip_postgeneration_save = True

start_date = factory.LazyFunction(date.today)
start_date = factory.LazyFunction(lambda: date.today().replace(day=1))

@factory.post_generation
def with_item(obj, create, extracted, **kwargs):
Expand All @@ -25,3 +25,4 @@ class Meta:
campaign = factory.SubFactory(AnnouncementCampaignFactory)
title = factory.Faker("sentence", locale="fr_FR")
description = factory.Faker("paragraph", locale="fr_FR")
priority = factory.Sequence(lambda n: n + 1)
8 changes: 5 additions & 3 deletions tests/communications/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def test_active_announcement_campaign_context_processor_cached(self):
active_announcement_campaign(None)["active_campaign_announce"] == campaign

# test cached value is kept up-to-date
campaign.start_date = campaign.start_date - timedelta(days=1)
campaign.max_items += 1
campaign.save()

with assertNumQueries(0):
assert active_announcement_campaign(None)["active_campaign_announce"].start_date == campaign.start_date
assert active_announcement_campaign(None)["active_campaign_announce"].max_items == campaign.max_items

item = AnnouncementItemFactory(campaign=campaign)
with assertNumQueries(0):
Expand All @@ -37,7 +37,9 @@ def test_active_announcement_campaign_context_processor_cached(self):
assert active_announcement_campaign(None)["active_campaign_announce"].items.count() == 1

# test cache does not become invalidated when saving a new campaign
new_campaign = AnnouncementCampaignFactory(with_item=True)
new_campaign = AnnouncementCampaignFactory(
with_item=True, start_date=(campaign.start_date + timedelta(days=40))
)
with assertNumQueries(0):
active_announcement_campaign(None)["active_campaign_announce"] == campaign

Expand Down
14 changes: 12 additions & 2 deletions tests/communications/test_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Meta:
fields = "__all__"

def test_valid_campaign(self):
expected_form_fields = ["max_items", "start_date"]
expected_form_fields = ["max_items", "start_date", "live"]
assert list(self.TestForm().fields.keys()) == expected_form_fields

form = self.TestForm(model_to_dict(AnnouncementCampaignFactory.build()))
Expand All @@ -29,7 +29,7 @@ def test_start_date_conflict(self):
campaign = AnnouncementCampaignFactory.build(start_date=date(2024, 1, 20))
form = self.TestForm(model_to_dict(campaign))

expected_form_errors = ["Maximum 1 campagne par mois"]
expected_form_errors = ["La contrainte « unique_announcement_campaign_month_year » n’est pas respectée."]
assert form.errors["__all__"] == expected_form_errors

campaign.start_date = date(2024, 2, 1)
Expand Down Expand Up @@ -75,9 +75,19 @@ def test_campaign_rendered_dashboard(self, client, snapshot):
assert "Item D" not in str(content)

def test_campaign_not_rendered_without_items(self, client):
cache.delete(CACHE_ACTIVE_ANNOUNCEMENTS_KEY)
AnnouncementCampaignFactory()

response = client.get(reverse("search:employers_home"))
assert response.status_code == 200
content = parse_response_to_soup(response)
assert len(content.select("#news-modal")) == 0

def test_campaign_not_rendered_draft(self, client):
cache.delete(CACHE_ACTIVE_ANNOUNCEMENTS_KEY)
AnnouncementCampaignFactory(live=False, with_item=True)

response = client.get(reverse("search:employers_home"))
assert response.status_code == 200
content = parse_response_to_soup(response)
assert len(content.select("#news-modal")) == 0

0 comments on commit f62aa5a

Please sign in to comment.