diff --git a/itou/communications/admin.py b/itou/communications/admin.py index 19a0105da41..a18bce43121 100644 --- a/itou/communications/admin.py +++ b/itou/communications/admin.py @@ -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,) diff --git a/itou/communications/apps.py b/itou/communications/apps.py index 92b7db5a8a3..8a5f04ee882 100644 --- a/itou/communications/apps.py +++ b/itou/communications/apps.py @@ -1,7 +1,7 @@ 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): @@ -9,21 +9,9 @@ class CommunicationsConfig(AppConfig): 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")) diff --git a/itou/communications/cache.py b/itou/communications/cache.py index 1551675698d..efff5d866a7 100644 --- a/itou/communications/cache.py +++ b/itou/communications/cache.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timedelta +from datetime import date, datetime from django.core.cache import cache @@ -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): diff --git a/itou/communications/migrations/0002_announcementcampaign_announcementitem_and_more.py b/itou/communications/migrations/0002_announcementcampaign_announcementitem_and_more.py index dabaf819066..271ce901b22 100644 --- a/itou/communications/migrations/0002_announcementcampaign_announcementitem_and_more.py +++ b/itou/communications/migrations/0002_announcementcampaign_announcementitem_and_more.py @@ -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" ), ), ], @@ -94,7 +97,7 @@ class Migration(migrations.Migration): ), ), migrations.AlterUniqueTogether( - name='announcementitem', - unique_together={('campaign', 'priority')}, + name="announcementitem", + unique_together={("campaign", "priority")}, ), ] diff --git a/itou/communications/models.py b/itou/communications/models.py index 9ef990531f6..9b2e0544cab 100644 --- a/itou/communications/models.py +++ b/itou/communications/models.py @@ -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 @@ -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: @@ -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] @@ -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() diff --git a/itou/communications/signals.py b/itou/communications/signals.py deleted file mode 100644 index 2e82fe058df..00000000000 --- a/itou/communications/signals.py +++ /dev/null @@ -1,13 +0,0 @@ -from itou.communications.cache import get_cached_active_announcement, update_active_announcement_cache - - -def update_cached_announcement_on_campaign_changes(sender, instance, *args, **kwargs): - campaign = get_cached_active_announcement() - if campaign is None or instance.pk == campaign.pk: - update_active_announcement_cache() - - -def update_cached_announcement_on_item_changes(sender, instance, *args, **kwargs): - campaign = get_cached_active_announcement() - if campaign is None or instance.campaign == campaign: - update_active_announcement_cache() diff --git a/tests/communications/factories.py b/tests/communications/factories.py index cdb50508804..e98f3ebb984 100644 --- a/tests/communications/factories.py +++ b/tests/communications/factories.py @@ -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): @@ -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) diff --git a/tests/communications/test_cache.py b/tests/communications/test_cache.py index 4f95a0d5652..01ad0469238 100644 --- a/tests/communications/test_cache.py +++ b/tests/communications/test_cache.py @@ -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): @@ -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 diff --git a/tests/communications/test_campaigns.py b/tests/communications/test_campaigns.py index 38e6f760d1d..1d8f3e60a3b 100644 --- a/tests/communications/test_campaigns.py +++ b/tests/communications/test_campaigns.py @@ -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())) @@ -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) @@ -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