Skip to content

Commit

Permalink
feat(communications): page nouveautés
Browse files Browse the repository at this point in the history
feat(communications): render news modal on first device visit

feat(communications/admin): AnnouncementCampaign admin and validation

fix(AnnouncementCampaignAdminForm): modifying date range

feat(communications): render news modal on first device visit

feat(communications): implemented a cache for AnnouncementCampaign

greatly restricts database usage

refactor(CommunicationsConfig): django signals safe to import at module level

refactor(communications): model changes requested in feedback

refactor(communications): implemented requested cache changes

fix: make quality

fix(static): delete unused image file

refactor(communications): cache changes, added live field

feat(communications): render news modal on first device visit

feat(communications/admin): AnnouncementCampaign admin and validation

fix(AnnouncementCampaignAdminForm): modifying date range

feat(communications): render news modal on first device visit

feat(communications): implemented a cache for AnnouncementCampaign

greatly restricts database usage

refactor(communications): implemented requested cache changes

feat(communications): news item model fields, admin and form

WIP: page nouveautés

feat(news.html): styling for news items

fix: merge error

requirements: merge errors

fix: test and code quality

feat(news): limit candidate news to relevant news only

feat(news.html): collapse icon on monthly news summaries

fix: rebase errors

feat: links to news page

fix: code quality

fix(tests): reset_model_sequence for snapshot

fix: some ui adjusments

refactor(communications/admin.py): change import style

refactor(communications/forms.py): relying more on Python core

feat(communications/models.py): alt_text field, other model changes

refactor(configure_bucket): more concise version of configuration

WIP: diverse requested changes

feat(news): pagination

refactor: enhance testing

fix(settings): more specific addition to CSP_IMG_SRC policy

fix: tests

fix: tests

fix: content policy

fix: conflict

fix: link to news in header content
  • Loading branch information
calummackervoy committed Aug 27, 2024
1 parent 99d3d87 commit 58dda1b
Show file tree
Hide file tree
Showing 36 changed files with 959 additions and 32 deletions.
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@
"https://*.tile.openstreetmap.org",
"*.hotjar.com",
"https://cdn.redoc.ly",
f"{AWS_S3_ENDPOINT_URL}{AWS_STORAGE_BUCKET_NAME}/news-images/",
]
CSP_STYLE_SRC = [
"'self'",
Expand Down
1 change: 1 addition & 0 deletions config/settings/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
FORCE_IC_LOGIN = os.getenv("FORCE_IC_LOGIN", "True") == "True"

AWS_STORAGE_BUCKET_NAME = "dev"
CSP_IMG_SRC.append(f"{AWS_S3_ENDPOINT_URL}{AWS_STORAGE_BUCKET_NAME}/news-images/") # noqa: F405

# Don't use json formatter in dev
del LOGGING["handlers"]["console"]["formatter"] # noqa: F405
1 change: 1 addition & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
path("signup/", include("itou.www.signup.urls")),
path("stats/", include("itou.www.stats.urls")),
path("users/", include("itou.www.users_views.urls")),
path("news/", include("itou.www.news.urls")),
path("versions/", include("itou.www.releases.urls")),
# Enable Anymail’s status tracking
# https://anymail.readthedocs.io/en/stable/esps/mailjet/#status-tracking-webhooks
Expand Down
10 changes: 7 additions & 3 deletions itou/communications/admin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from django.contrib import admin

from itou.communications import models
from itou.utils.admin import ItouModelAdmin, ItouTabularInline
from itou.communications.forms import AnnouncementItemForm
from itou.utils.admin import ItouModelAdmin, ItouStackedInline


class AnnouncementItemInline(ItouTabularInline):
class AnnouncementItemInline(ItouStackedInline):
model = models.AnnouncementItem
fields = ("priority", "title", "description")
form = AnnouncementItemForm
extra = 0


@admin.register(models.AnnouncementCampaign)
class AnnouncementCampaignAdmin(ItouModelAdmin):
class Media:
css = {"all": ("css/itou-admin.css",)}

list_display = ("start_date", "end_date", "live")
fields = ("max_items", "start_date", "live")
inlines = (AnnouncementItemInline,)
34 changes: 34 additions & 0 deletions itou/communications/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pathlib
import uuid

from django import forms

from itou.communications.models import AnnouncementItem
from itou.files.forms import ItouAdminImageInput
from itou.users.enums import UserKind
from itou.utils import constants as global_constants


class AnnouncementItemForm(forms.ModelForm):
class Meta:
model = AnnouncementItem
fields = ["priority", "title", "description", "user_kind_tags", "image", "image_alt_text", "link"]

user_kind_tags = forms.MultipleChoiceField(
required=False,
widget=forms.CheckboxSelectMultiple,
choices=UserKind.choices,
label="Utilisateurs concernés",
)
image = forms.ImageField(
required=False,
widget=ItouAdminImageInput(attrs={"accept": global_constants.SUPPORTED_IMAGE_FILE_TYPES}),
label="Capture d'écran",
)

def clean_image(self):
image = self.cleaned_data.get("image")
if image:
extension = pathlib.Path(image.name).suffix
image.name = f"{uuid.uuid4()}{extension}"
return image
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Generated by Django 5.0.7 on 2024-07-29 12:26

import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models

import itou.communications.models
import itou.utils.storage.s3


class Migration(migrations.Migration):
dependencies = [
("communications", "0002_announcementcampaign_announcementitem_and_more"),
("files", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="announcementitem",
name="image",
field=models.ImageField(
blank=True,
storage=itou.utils.storage.s3.PublicStorage(),
upload_to="news-images/",
verbose_name="capture d'écran",
help_text="1200x600 recommandé",
),
),
migrations.AddField(
model_name="announcementitem",
name="image_alt_text",
field=models.TextField(
blank=True,
help_text=(
"la description est importante pour les utilisateurs de lecteurs d'écran,"
" et lorsque l'image ne se télécharge pas"
),
verbose_name="description de l'image",
),
),
migrations.AddField(
model_name="announcementitem",
name="link",
field=models.URLField(
help_text="URL d'une page où l'utilisateur peut obtenir plus d'informations sur l'article",
blank=True,
verbose_name="lien externe",
),
),
migrations.AddField(
model_name="announcementitem",
name="user_kind_tags",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("job_seeker", "candidat"),
("prescriber", "prescripteur"),
("employer", "employeur"),
("labor_inspector", "inspecteur du travail"),
("itou_staff", "administrateur"),
]
),
default=list,
size=None,
verbose_name="utilisateurs concernés",
),
),
migrations.AddField(
model_name="announcementitem",
name="image_storage",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="files.file",
),
),
]
58 changes: 57 additions & 1 deletion itou/communications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.files.storage import storages
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Q

from itou.files.models import File
from itou.users.enums import UserKind


class NotificationRecordQuerySet(models.QuerySet):
def actives(self):
Expand Down Expand Up @@ -158,7 +163,8 @@ def __str__(self):
return f"Campagne d'annonce du { self.start_date.strftime('%m/%Y') }"

def clean(self):
self.start_date = self.start_date.replace(day=1)
if self.start_date:
self.start_date = self.start_date.replace(day=1)
return super().clean()

def _update_cached_active_announcement(self):
Expand Down Expand Up @@ -196,6 +202,38 @@ class AnnouncementItem(models.Model):
description = models.TextField(
null=False, blank=False, verbose_name="description", help_text="détail du nouveauté ; le contenu"
)
user_kind_tags = ArrayField(
default=list,
base_field=models.CharField(choices=UserKind.choices),
verbose_name="utilisateurs concernés",
)
image = models.ImageField(
blank=True,
upload_to="news-images/",
storage=storages["public"],
verbose_name="capture d'écran",
help_text="1200x600 recommandé",
)
image_alt_text = models.TextField(
blank=True,
verbose_name="description de l'image",
help_text=(
"la description est importante pour les utilisateurs de lecteurs d'écran,"
" et lorsque l'image ne se télécharge pas"
),
)
image_storage = models.OneToOneField(
File,
null=True,
blank=True,
on_delete=models.SET_NULL,
)
link = models.URLField(
blank=True,
max_length=200,
verbose_name="lien externe",
help_text="URL d'une page où l'utilisateur peut obtenir plus d'informations sur l'article",
)

objects = AnnouncementItemQuerySet.as_manager()

Expand All @@ -216,8 +254,26 @@ def _update_cached_active_announcement(self):

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

def get_image_storage_key():
return self.image.name

def create_image_storage():
self.image_storage = File.objects.create(key=get_image_storage_key())

if self.image:
if self.image_storage is None:
create_image_storage()
elif self.image_storage.key != get_image_storage_key():
self.image_storage.delete()
create_image_storage()

self._update_cached_active_announcement()

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

@property
def user_kind_labels(self):
return [UserKind(u).label for u in self.user_kind_tags]
9 changes: 9 additions & 0 deletions itou/files/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ def get_context(self, name, value, attrs):
return context


class ItouAdminImageInput(forms.FileInput):
template_name = "utils/widgets/news_image_input.html"

def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context["selected_img_src"] = value.url if value and value.url else None
return context


class ContentTypeValidator:
content_type = None
extension = None
Expand Down
5 changes: 3 additions & 2 deletions itou/files/management/commands/configure_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ def handle(self, *args, autoexpire=False, **options):
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "s3:GetObject",
"Resource": f"arn:aws:s3:::{bucket}/resume/*",
},
"Resource": f"arn:aws:s3:::{bucket}/{path}/*",
}
for path in ["resume", "news-images"]
]
client.put_bucket_policy(
Bucket=bucket,
Expand Down
4 changes: 4 additions & 0 deletions itou/static/css/itou-admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ form .aligned ul.inline li,
form .form-row ul.inline li {
list-style-type: none;
}

.img-preview-admin {
max-width: 300px
}
8 changes: 8 additions & 0 deletions itou/static/css/itou.css
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,11 @@ an input field being invalid, generating an uncontrolled red box-shadow. */
.w-lg-400px .select2-selection__rendered {
white-space: nowrap !important;
}

/* News Page
--------------------------------------------------------------------------- */
/* utility class soon included in the itou theme */
.img-muted {
filter: grayscale(100%);
opacity: 0.3;
}
8 changes: 3 additions & 5 deletions itou/templates/layout/_header_help_dropdown_content.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@
{% endif %}
{% endif %}
<li>
<a href="{{ ITOU_HELP_CENTER_URL }}/categories/25225629682321--Nouveaut%C3%A9s"
rel="noopener"
class="dropdown-item has-external-link"
target="_blank"
aria-label="Les nouveautés du site des emplois de l'inclusion (ouverture dans un nouvel onglet)"
<a href="{% url 'news:home' %}"
class="dropdown-item"
aria-label="Les nouveautés du site des emplois de l'inclusion"
{% matomo_event "help" "clic" "header_nouveautes" %}>Nouveautés</a>
</li>
</ul>
3 changes: 2 additions & 1 deletion itou/templates/layout/_news_modal.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% load static %}
{% load theme_inclusion %}
{% load django_bootstrap5 %}
{% load matomo %}

<input id="news-modal-start-date" type="hidden" value="{{ active_campaign_announce.start_date|date:'Y-m-d' }}" />

Expand All @@ -21,7 +22,7 @@ <h6>{{ forloop.counter }}. {{ item.title }}</h6>
{% endfor %}
</div>
<div class="modal-footer">
<a href="https://aide.emplois.inclusion.beta.gouv.fr/hc/fr/categories/25225629682321--Nouveaut%C3%A9s" class="btn btn-sm btn-primary">Voir toutes les nouveautés</a>
<a href="{% url 'news:home' %}" class="btn btn-sm btn-primary" {% matomo_event "modale-nouveautes" "clic" "toutes-les-nouveautes" %}>Voir toutes les nouveautés</a>
</div>
</div>
</div>
Expand Down
64 changes: 64 additions & 0 deletions itou/templates/news/news.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{% extends "layout/base.html" %}
{% load theme_inclusion %}

{% block title %}Nouveautés {{ block.super }}{% endblock %}

{% block title_prevstep %}
{% include "layout/previous_step.html" with back_url=back_url only %}
{% endblock %}

{% block title_content %}<h1>Nouveautés</h1>{% endblock %}

{% block content %}
<section class="s-section">
<div class="s-section__container container" id="news">
<ul class="list-group list-group-collapse list-group-flush" id="news-list">
{% for month_news in news_page %}
<li class="list-group-item list-group-item-action">
<button class="d-block w-100"
data-bs-toggle="collapse"
data-bs-target="#news-{{ month_news.id }}"
type="button"
aria-expanded="{% if forloop.counter > 1 %}false{% else %}true{% endif %}"
aria-controls="news-{{ month_news.id }}">
<div class="h3 mb-0">
{{ month_news.start_date|date:"F Y"|capfirst }}
<span class="badge badge-sm bg-emploi-light text-info rounded-pill ms-2" aria-label="nombre d'articles">{{ month_news.count_items }}</span>
</div>
</button>
<div class="mt-3{% if forloop.counter > 1 %} collapse{% else %} collapse show{% endif %}" id="news-{{ month_news.id }}" aria-controls="news-{{ month_news.id }}">
{% for news_item in month_news.items.all %}
<div class="row mb-3">
<div class="col-12 col-md-4{% if news_item.image %} mb-3{% else %} d-none d-md-inline{% endif %}">
{% if news_item.image %}
<img src="{{ news_item.image.url }}"
loading="lazy"
alt="{{ news_item.image_alt_text|default:'' }}"
{% if not news_item.image_alt_text %}aria-hidden="true"{% endif %}
class="img-fluid img-fitcover img-thumbnail" />
{% else %}
<img src="{% static_theme_images 'ico-bicro-important.svg' %}" loading="lazy" alt="" class="img-fluid img-fitcover img-thumbnail img-muted" aria-hidden="true" />
{% endif %}
</div>
<div class="col-12 col-md-8">
<div aria-label="groupes d'utilisateurs concernés">
{% for tag in news_item.user_kind_labels %}
<span class="tag tag-base bg-info-lighter text-info">{{ tag|upper }}</span>
{% endfor %}
</div>
<p class="h4 my-2">{{ news_item.title }}</p>
<p>{{ news_item.description }}</p>
{% if news_item.link %}
<a href="{{ news_item.link }}" rel="noopener" target="_blank" class="btn-link has-external-link" aria-label="Plus d'informations sur {{ news_item.title }}">En savoir plus</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</div>
{% include "includes/pagination.html" with page=news_page boost=True %}
</section>
{% endblock %}
Loading

0 comments on commit 58dda1b

Please sign in to comment.