From a50a809d02aa2c9cff850ca7653cd2d520bf77c2 Mon Sep 17 00:00:00 2001 From: Stefan R <109965488+stefrado@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:56:49 +0100 Subject: [PATCH] :sparkles: 990 Implement new header (#1006) --- src/sdg/conf/base.py | 1 + src/sdg/organisaties/views/notificaties.py | 12 ++- src/sdg/organisaties/views/roles.py | 9 ++ .../migrations/0059_notificationviewed.py | 38 +++++++ ...ter_notificationviewed_last_viewed_date.py | 18 ++++ src/sdg/producten/models/__init__.py | 1 + src/sdg/producten/models/notification.py | 12 +++ src/sdg/scss/components/_header.scss | 80 ++------------ src/sdg/scss/components/_index.scss | 2 + src/sdg/scss/components/_nav.scss | 31 ++++++ src/sdg/scss/components/_user-dropdown.scss | 86 +++++++++++++++ .../templates/core/_municipality_switch.html | 7 -- src/sdg/templates/core/base_home.html | 69 +----------- src/sdg/templates/core/index.html | 6 -- src/sdg/templates/navigation/nav_item.html | 11 ++ src/sdg/templates/navigation/navigation.html | 39 +++++++ .../templates/navigation/user_dropdown.html | 23 ++++ .../templates/organisaties/roles/update.html | 7 +- src/sdg/utils/context_processors.py | 35 ++++++ src/sdg/utils/templatetags/utils.py | 101 +++++++++++++++++- 20 files changed, 431 insertions(+), 157 deletions(-) create mode 100644 src/sdg/producten/migrations/0059_notificationviewed.py create mode 100644 src/sdg/producten/migrations/0060_alter_notificationviewed_last_viewed_date.py create mode 100644 src/sdg/producten/models/notification.py create mode 100644 src/sdg/scss/components/_nav.scss create mode 100644 src/sdg/scss/components/_user-dropdown.scss delete mode 100644 src/sdg/templates/core/_municipality_switch.html create mode 100644 src/sdg/templates/navigation/nav_item.html create mode 100644 src/sdg/templates/navigation/navigation.html create mode 100644 src/sdg/templates/navigation/user_dropdown.html diff --git a/src/sdg/conf/base.py b/src/sdg/conf/base.py index 273c856ca..ec8240f97 100644 --- a/src/sdg/conf/base.py +++ b/src/sdg/conf/base.py @@ -183,6 +183,7 @@ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "sdg.utils.context_processors.settings", + "sdg.utils.context_processors.has_new_notifications", ], "loaders": TEMPLATE_LOADERS, }, diff --git a/src/sdg/organisaties/views/notificaties.py b/src/sdg/organisaties/views/notificaties.py index 208dd12a9..5138ad5ce 100644 --- a/src/sdg/organisaties/views/notificaties.py +++ b/src/sdg/organisaties/views/notificaties.py @@ -1,4 +1,5 @@ from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils import timezone from django.utils.timezone import now from django.utils.translation import gettext as _ from django.views.generic import ListView @@ -6,7 +7,7 @@ from dateutil.relativedelta import relativedelta from sdg.core.views.mixins import BreadcrumbsMixin -from sdg.producten.models import ProductVersie +from sdg.producten.models import NotificationViewed, ProductVersie class ProductVersieListView( @@ -41,3 +42,12 @@ def get_queryset(self): .published() .order_by("-gewijzigd_op")[: self.limit] ) + + def get(self, request, *args, **kwargs): + # Call the parent class's get method to fetch the queryset + response = super().get(request, *args, **kwargs) + + # Update or create the NotificationViewed instance for the current user + NotificationViewed.objects.update_or_create(gebruiker=request.user) + + return response diff --git a/src/sdg/organisaties/views/roles.py b/src/sdg/organisaties/views/roles.py index cfcabd969..705622a08 100644 --- a/src/sdg/organisaties/views/roles.py +++ b/src/sdg/organisaties/views/roles.py @@ -79,6 +79,15 @@ def get_form_class(self): raise PermissionDenied() + def get_success_url(self): + # Stay on the same page if the user is editing own settings and is not an admin. + if ( + self.request.user.email == self.object.user.email + and self.object.is_beheerder is not True + ): + return self.request.get_full_path() + return super().get_success_url() + def form_valid(self, form, *args, **kwargs): response = super().form_valid(form) diff --git a/src/sdg/producten/migrations/0059_notificationviewed.py b/src/sdg/producten/migrations/0059_notificationviewed.py new file mode 100644 index 000000000..6e1e12807 --- /dev/null +++ b/src/sdg/producten/migrations/0059_notificationviewed.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.23 on 2024-10-21 14:12 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("producten", "0058_alter_localizedproduct_decentrale_procedure_link"), + ] + + operations = [ + migrations.CreateModel( + name="NotificationViewed", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("last_viewed_date", models.DateTimeField(blank=True, null=True)), + ( + "gebruiker", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/src/sdg/producten/migrations/0060_alter_notificationviewed_last_viewed_date.py b/src/sdg/producten/migrations/0060_alter_notificationviewed_last_viewed_date.py new file mode 100644 index 000000000..a91b7a576 --- /dev/null +++ b/src/sdg/producten/migrations/0060_alter_notificationviewed_last_viewed_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-11-15 16:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("producten", "0059_notificationviewed"), + ] + + operations = [ + migrations.AlterField( + model_name="notificationviewed", + name="last_viewed_date", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/src/sdg/producten/models/__init__.py b/src/sdg/producten/models/__init__.py index 43ffa6f41..9e2a1778b 100644 --- a/src/sdg/producten/models/__init__.py +++ b/src/sdg/producten/models/__init__.py @@ -1,2 +1,3 @@ from .localized import * # noqa +from .notification import * # noqa from .product import * # noqa diff --git a/src/sdg/producten/models/notification.py b/src/sdg/producten/models/notification.py new file mode 100644 index 000000000..c1286e92e --- /dev/null +++ b/src/sdg/producten/models/notification.py @@ -0,0 +1,12 @@ +from django.contrib.auth import get_user_model +from django.db import models + +User = get_user_model() + + +class NotificationViewed(models.Model): + gebruiker = models.OneToOneField(User, on_delete=models.CASCADE) + last_viewed_date = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"User: {self.gebruiker_id} - Notification last viewed on: {self.last_viewed_date}" diff --git a/src/sdg/scss/components/_header.scss b/src/sdg/scss/components/_header.scss index a1259c019..0cafe1cc1 100644 --- a/src/sdg/scss/components/_header.scss +++ b/src/sdg/scss/components/_header.scss @@ -1,48 +1,24 @@ -@import 'colors'; +@import "colors"; .header { - --shadow-color: rgba(0, 0, 0, 0.121722); - width: 100%; background: rgb(57, 106, 136); - background: linear-gradient(353.15deg, var(--org-theme-bg-darkest) -68.3%, var(--org-theme-bg) 179.94%); + background: linear-gradient( + 353.15deg, + var(--org-theme-bg-darkest) -68.3%, + var(--org-theme-bg) 179.94% + ); padding: 24px 0 0; justify-content: space-between; display: flex; flex-direction: column; - &.header--inset { - padding-bottom: 88px; - } - - .header__top { + &__top { justify-content: space-between; display: flex; flex-direction: row; } - .header__user { - display: flex; - align-items: center; - - & > .svg-inline--fa { - height: 50px; - width: 50px; - border-radius: 50%; - background-color: $color_primary_lightest; - color: $color_primary; - padding-top: 4px; - overflow: hidden; - margin-right: 8px; - border: 3px solid $color_secondary_dark; - z-index: 1; - - path { - color: $color_primary; - } - } - } - .header__username { background-color: var(--org-theme-bg-darker); border: 1px solid transparent; @@ -59,36 +35,10 @@ color: #000; } - .header__title { - color: $color_secondary_dark; - font-size: 40px; - - .switch-icon { - color: $color_accent; - margin-left: 8px; - } - - a:hover svg { - color: $color_accent_dark; - transition: all 0.3s ease-in-out; - } - - } - - .header__subtitle { - color: $color_secondary_darker; - font-size: 14px; - margin-bottom: 8px; - } - .header__dropdown { position: relative; padding: 12px; color: #fff; - - .svg-inline--fa:hover { - transform: scale(1.05); - } } .header__dropdown *:hover ~ .header__dropdown-list, @@ -126,20 +76,4 @@ text-decoration: underline; } } - - .header__dropdown-action .svg-inline--fa { - float: left; - margin-top: 2px; - margin-right: 8px; - width: 1em; - } - - .header__dropdown-text { - padding: 12px; - color: #000; - display: block; - text-align: left; - text-decoration: none; - margin-right: 8px; - } } diff --git a/src/sdg/scss/components/_index.scss b/src/sdg/scss/components/_index.scss index d86e35bd3..e41a6574a 100644 --- a/src/sdg/scss/components/_index.scss +++ b/src/sdg/scss/components/_index.scss @@ -34,3 +34,5 @@ @import 'url_label_field'; @import 'choices'; @import 'publications'; +@import 'nav'; +@import 'user-dropdown'; diff --git a/src/sdg/scss/components/_nav.scss b/src/sdg/scss/components/_nav.scss new file mode 100644 index 000000000..bbac36c81 --- /dev/null +++ b/src/sdg/scss/components/_nav.scss @@ -0,0 +1,31 @@ +.nav { + display: flex; + justify-content: space-between; + + &__list { + display: flex; + list-style: none; + font-size: 1rem; + color: $color-white; + margin: 0; + gap: $grid-margin-3; + } + + &__item { + color: $color-white; + } + + &__item--active &__link { + color: $color_accent; + text-decoration: underline; + } + + &__link { + color: $color-white; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } +} diff --git a/src/sdg/scss/components/_user-dropdown.scss b/src/sdg/scss/components/_user-dropdown.scss new file mode 100644 index 000000000..f3e48b54f --- /dev/null +++ b/src/sdg/scss/components/_user-dropdown.scss @@ -0,0 +1,86 @@ +.user-dropdown { + --shadow-color: rgba(0, 0, 0, 0.121722); + --border-color: #{$color_grey_dark}; + + align-items: center; + background-color: var(--org-theme-bg-darker); + border: 1px solid transparent; + border-radius: 9999px; + color: $color-white; + display: flex; + gap: $grid-margin-0; + height: 40px; + text-decoration: none; + padding-right: $grid-margin-2; + position: relative; + transition: all 0.3s ease-in-out; + + &:hover &__list, + & > *:hover ~ &__list, + &__list:hover { + opacity: 1; + transition-delay: 0s; + visibility: visible; + } + + &:hover { + background-color: $color_grey_light; + border-color: var(--border-color); + color: #212121; + } + + &__icon { + background-color: $color_primary_lightest; + border: 2px solid var(--border-color); + border-radius: 50%; + color: $color_primary; + flex-shrink: 0; + height: 50px; + left: -4px; + overflow: hidden; + padding-top: 4px; + position: relative; + width: 50px; + + > .svg-inline--fa { + color: $color_primary; + height: 100%; + width: 100%; + } + } + + &__list { + background-color: $color_grey_light; + border: 1px solid var(--border-color); + border-radius: 10px; + box-shadow: 1px 1px 4px var(--shadow-color); + color: #fff; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: calc(100% + 1rem); + transition: all 0.3s ease-in-out; + transition-delay: 0s; + visibility: hidden; + z-index: 20; + } + + &__action { + align-items: center; + color: $color_primary_dark; + display: flex; + gap: 0.5rem; + padding: 12px; + text-align: left; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + hr { + border: 0.5px solid var(--border-color); + } +} diff --git a/src/sdg/templates/core/_municipality_switch.html b/src/sdg/templates/core/_municipality_switch.html deleted file mode 100644 index 7c8cc7597..000000000 --- a/src/sdg/templates/core/_municipality_switch.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load i18n %} - -{% if user.roles.count > 1 %} - - - -{% endif %} diff --git a/src/sdg/templates/core/base_home.html b/src/sdg/templates/core/base_home.html index 136df1b56..81be3c80a 100644 --- a/src/sdg/templates/core/base_home.html +++ b/src/sdg/templates/core/base_home.html @@ -17,75 +17,10 @@

voor {{ org_type_cfg.name_plural }}

-
- - - - {{ request.user }} - - -
- {% with pk=lokaleoverheid.pk %} - - {% if not pk %} - - {% trans "Selecteer eerst een organisatie voor meer opties." %} - - {% else %} - {% if request.user|is_manager:lokaleoverheid %} - - {% trans "Organisatie instellingen" %} - - - {% trans "Locaties" %} - - - {% trans "Bevoegde organisaties" %} - - {% endif %} - - {% trans "Gebruikersbeheer" %} - - {% endif %} - - {% if siteconfig.documentatie_link %} - - {{ siteconfig.documentatie_titel|capfirst|default:_("Documentatie") }} - - {% endif %} - -
- - - {% trans "Notificaties" %} - - -
- - - {% trans "Uitloggen" %} - - - {% endwith %} -
-
+ {% user_dropdown %}
-

{% block container_subtitle %}{% endblock container_subtitle %}

-

- {% block container_title %} - {% if lokaleoverheid %} - {{ lokaleoverheid }} - {% include "core/_municipality_switch.html" %} - {% else %} -

- SDG {% trans "Invoervoorziening" %} -
- {% endif %} - {% endblock container_title %} -

+ {% navigation %}
{% endblock %} diff --git a/src/sdg/templates/core/index.html b/src/sdg/templates/core/index.html index ec74e9c8b..6017b32f0 100644 --- a/src/sdg/templates/core/index.html +++ b/src/sdg/templates/core/index.html @@ -5,12 +5,6 @@ {% block head_title %}{% trans "Home" %}{% endblock %} -{% block container_title %} -
- SDG {% trans "Invoervoorziening" %} -
-{% endblock container_title %} - {% block inner %}
diff --git a/src/sdg/templates/navigation/nav_item.html b/src/sdg/templates/navigation/nav_item.html new file mode 100644 index 000000000..4bc95ede7 --- /dev/null +++ b/src/sdg/templates/navigation/nav_item.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/src/sdg/templates/navigation/navigation.html b/src/sdg/templates/navigation/navigation.html new file mode 100644 index 000000000..f0a99aaf3 --- /dev/null +++ b/src/sdg/templates/navigation/navigation.html @@ -0,0 +1,39 @@ +{% load i18n utils %} + +{% with pk=lokaleoverheid.pk %} + +{% endwith %} \ No newline at end of file diff --git a/src/sdg/templates/navigation/user_dropdown.html b/src/sdg/templates/navigation/user_dropdown.html new file mode 100644 index 000000000..d47f9a3f7 --- /dev/null +++ b/src/sdg/templates/navigation/user_dropdown.html @@ -0,0 +1,23 @@ +{% load i18n %} +
+
+ +
+ +

{{ request.user }}

+ +
+ {% if lokaleoverheid.pk %} + + {% trans "Mijn instellingen" %} + +
+ {% endif %} + + {% trans "Uitloggen" %} + +
+
diff --git a/src/sdg/templates/organisaties/roles/update.html b/src/sdg/templates/organisaties/roles/update.html index 299bfe5ec..e10faca3c 100644 --- a/src/sdg/templates/organisaties/roles/update.html +++ b/src/sdg/templates/organisaties/roles/update.html @@ -23,7 +23,12 @@
diff --git a/src/sdg/utils/context_processors.py b/src/sdg/utils/context_processors.py index 9811c069c..386a07d8a 100644 --- a/src/sdg/utils/context_processors.py +++ b/src/sdg/utils/context_processors.py @@ -1,6 +1,10 @@ from django.conf import settings as django_settings +from django.utils.timezone import now + +from dateutil.relativedelta import relativedelta from sdg.conf.utils import org_type_cfg +from sdg.producten.models import NotificationViewed, ProductVersie def settings(request): @@ -25,3 +29,34 @@ def settings(request): context.update(dsn=django_settings.SENTRY_CONFIG.get("public_dsn", "")) return context + + +def has_new_notifications(request): + user = request.user + has_new_notifications = False + + if user and user.is_anonymous is not True: + try: + notification_viewed = NotificationViewed.objects.get(gebruiker=user) + last_viewed_date = notification_viewed.last_viewed_date + except NotificationViewed.DoesNotExist: + notification_viewed = None + # By default, the last_viewed_date is set to 12 months ago. + last_viewed_date = now() - relativedelta(months=12) + + # Get the latest product versie (notifications are based on ProductVersie) later than last_viewed_date. + latest_notification = ( + ProductVersie.objects.select_related("product") + .filter( + product__referentie_product=None, + gewijzigd_op__gt=last_viewed_date, + ) + .order_by("-gewijzigd_op") + .first() + ) + + has_new_notifications = bool(latest_notification) + + return { + "has_new_notifications": has_new_notifications, + } diff --git a/src/sdg/utils/templatetags/utils.py b/src/sdg/utils/templatetags/utils.py index a54de4372..bf54c53f6 100644 --- a/src/sdg/utils/templatetags/utils.py +++ b/src/sdg/utils/templatetags/utils.py @@ -2,8 +2,6 @@ from django.conf import settings from django.utils.html import format_html -from sdg.producten.types import Language - register = template.Library() @@ -141,3 +139,102 @@ def is_manager(user, local_government): ] ) return None + + +@register.inclusion_tag("navigation/navigation.html", takes_context=True) +def navigation(context): + """ + Navigation element. + + Args: + - context + """ + + lokaleoverheid = context.get("lokaleoverheid") + request = context.get("request") + siteconfig = context.get("siteconfig") + has_new_notifications = context.get("has_new_notifications") + + return { + "context": context, + "lokaleoverheid": lokaleoverheid, + "request": request, + "siteconfig": siteconfig, + "has_new_notifications": has_new_notifications, + } + + +@register.inclusion_tag("navigation/nav_item.html", takes_context=True) +def nav_item(context, href, title, **kwargs): + """ + Generic nav_item element, built for the navigation component. + + Args: + - context + - link, href of the link + - title, label of the link (can also be an element) + + Kwargs: + - icon, can be an element. + - id, set an id on the element + - blank_target, set the target to `_blank` + - show_icon, boolean to render the icon + """ + + request = context.get("request") + + def check_active_link(): + if href == "/": + # Equality operator instead of partial check (disables always true on home route) + return href == request.path + else: + return href in request.path + + # Validate if the link is active. + active_link = check_active_link() + + # Get kwargs vars. + icon = kwargs.get("icon", None) + show_icon = kwargs.get("show_icon", None) + id = kwargs.get("id", None) + blank_target = kwargs.get("blank_target", False) + + return { + **kwargs, + "context": context, + "href": href, + "title": title, + "icon": icon, + "show_icon": show_icon, + "blank_target": blank_target, + "id": id, + "active_link": active_link, + } + + +@register.inclusion_tag("navigation/user_dropdown.html", takes_context=True) +def user_dropdown(context): + """ + Dropdown element inside the header. + + Args: + - context + """ + + lokaleoverheid = context.get("lokaleoverheid") + request = context.get("request") + + role_pk = None + if lokaleoverheid: + role_pk = ( + request.user.roles.all() + .get(lokale_overheid=lokaleoverheid, user=request.user) + .pk + ) + + return { + "context": context, + "lokaleoverheid": lokaleoverheid, + "request": request, + "role_pk": role_pk, + }