Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GEN-129] UX/UI: page nouveautés #4562

Merged
merged 1 commit into from
Sep 9, 2024
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
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,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 @@ -74,6 +74,7 @@
path("signup/", include("itou.www.signup.urls")),
path("stats/", include("itou.www.stats.urls")),
path("users/", include("itou.www.users_views.urls")),
path("announcements/", include("itou.www.announcements.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
calummackervoy marked this conversation as resolved.
Show resolved Hide resolved
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,)
39 changes: 39 additions & 0 deletions itou/communications/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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


def user_kind_tag_choices():
valid_choices = [UserKind.JOB_SEEKER, UserKind.PRESCRIBER, UserKind.EMPLOYER]
return [(u.value, u.label) for u in valid_choices]


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

class Meta:
model = AnnouncementItem
fields = ["priority", "title", "description", "user_kind_tags", "image", "image_alt_text", "link"]

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(
calummackervoy marked this conversation as resolved.
Show resolved Hide resolved
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;
}
72 changes: 72 additions & 0 deletions itou/templates/announcements/news.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{% 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">
<ul class="list-group list-group-collapse list-group-flush">
{% 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-{{ forloop.counter }}"
type="button"
aria-expanded="{% if forloop.counter > 1 %}false{% else %}true{% endif %}"
aria-controls="news-{{ forloop.counter }}">
<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">{{ month_news.count_items }}
<span class="visually-hidden">article{{ month_news.count_items|pluralize }}</span>
</span>
</div>
</button>
<article class="mt-3 collapse{% if forloop.counter == 1 %} show{% endif %}" id="news-{{ forloop.counter }}" aria-controls="news-{{ forloop.counter }}">
{% 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 }}"
alt="{{ news_item.image_alt_text }}"
{% if not news_item.image_alt_text %}aria-hidden="true"{% endif %}
class="img-fitcover img-thumbnail"
height="{{ news_item.image.height }}"
width="{{ news_item.image.width }}"
{% if forloop.counter > 1 %}loading="lazy"{% endif %} />
{% else %}
{# image height and width will depend on the space given to the SVG on the user's device #}
{# defaults set to prevent image seeming to "jump" during loading #}
<img src="{% static_theme_images 'ico-bicro-important.svg' %}" loading="lazy" alt="" height="396" width="420" class="img-fitcover img-thumbnail img-muted" aria-hidden="true" />
{% endif %}
</div>
<div class="col-12 col-md-8">
{% if news_item.user_kind_tags %}
<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>
{% endif %}
<p class="h4 my-2">{{ news_item.title }}</p>
<p>{{ news_item.description|linebreaksbr }}</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 %}
</article>
</li>
{% endfor %}
</ul>
</div>
{% include "includes/pagination.html" with page=news_page %}
</section>
{% endblock %}
7 changes: 1 addition & 6 deletions itou/templates/layout/_header_help_dropdown_content.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@
{% 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)"
{% matomo_event "help" "clic" "header_nouveautes" %}>Nouveautés</a>
<a href="{% url 'announcements:news' %}" 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>
Loading
Loading