From 71a256dc52ed3391638dcf9669cf57d75475d326 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Thu, 6 Apr 2023 18:36:47 +0200 Subject: Display of a changelog with alert display when updates are made --- ishtar_common/context_processors.py | 70 ++++++++++++++++++++-- .../migrations/0227_auto_20230404_1112.py | 29 --------- .../migrations/0227_auto_20230406_1834.py | 39 ++++++++++++ ishtar_common/models.py | 6 +- ishtar_common/templates/ishtar/changelog.html | 18 ++++++ ishtar_common/templates/navbar.html | 3 + ishtar_common/tests.py | 48 ++++++++++++++- ishtar_common/urls.py | 3 +- ishtar_common/views.py | 50 +++++++++++++++- 9 files changed, 227 insertions(+), 39 deletions(-) delete mode 100644 ishtar_common/migrations/0227_auto_20230404_1112.py create mode 100644 ishtar_common/migrations/0227_auto_20230406_1834.py create mode 100644 ishtar_common/templates/ishtar/changelog.html (limited to 'ishtar_common') diff --git a/ishtar_common/context_processors.py b/ishtar_common/context_processors.py index c1d1224ea..8131e0382 100644 --- a/ishtar_common/context_processors.py +++ b/ishtar_common/context_processors.py @@ -18,6 +18,8 @@ # See the file COPYING for details. import datetime +import os +import re from django.conf import settings from django.core.cache import cache @@ -33,6 +35,45 @@ from bootstrap_datepicker.widgets import DatePicker from .menus import Menu +def _get_changelog_version_from_file(): + changelog_dir = os.path.join( + settings.SHARE_BASE_PATH, + "changelog", + settings.LANGUAGE_CODE.split('-')[0] + ) + if not os.path.exists(changelog_dir): + return "no-version" + changelog_file = os.path.join(changelog_dir, f"changelog_1.md") + if not os.path.exists(changelog_file): + return "no-version" + current_version = None + with open(changelog_file, "r") as changelog: + for line in changelog.readlines(): + m = re.match(r"v(\d+)\.(\d+)\.(\d+)", line) + if not m: + continue + g = m.groups() + if len(g) != 3: + continue + current_version = ".".join(g) + break + if not current_version: + return "no-version" + return current_version + + +def get_changelog_version(): + cache_key = f"{settings.PROJECT_SLUG}-news-version" + current_version = cache.get(cache_key) + if current_version: + return current_version + + if not current_version: + current_version = _get_changelog_version_from_file() + cache.set(cache_key, current_version, settings.CACHE_LONGTIMEOUT) + return current_version + + def get_base_context(request): dct = { "URL_PATH": settings.URL_PATH, @@ -83,15 +124,36 @@ def get_base_context(request): cache.set(key, password_expired, settings.CACHE_TIMEOUT) if password_expired and not request.path.endswith("password_change/"): msg = str(_("Your password has expired. Please update it using this " - "form.")) + "
.")) msg = msg.replace( - str(_("form")), + "", f'' - f' ' - f'{_("form")}' + f'{_("form")} ' + f'' + '' ) dct["MESSAGES"].append((msg, "warning")) + # check changelog + if request.user.ishtaruser.display_news: + user_version = request.user.ishtaruser.latest_news_version + current_version = get_changelog_version() + if current_version != user_version and "changelog" not in dct["CURRENT_PATH"]: + if user_version: + msg = str(_("Ishtar have been updated from version to . " + "Check the .")) + else: + msg = str(_("Ishtar have been updated to version . Check the " + ".")) + msg = msg.replace( + "", + f'' + f'{_("changelog")} ' + f'' + f'' + ).replace("", user_version).replace("", current_version) + dct["MESSAGES"].append((msg, "info")) + # external sources if ( request.user.ishtaruser.current_profile diff --git a/ishtar_common/migrations/0227_auto_20230404_1112.py b/ishtar_common/migrations/0227_auto_20230404_1112.py deleted file mode 100644 index 1ba74f292..000000000 --- a/ishtar_common/migrations/0227_auto_20230404_1112.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.2.24 on 2023-04-04 11:12 - -import datetime -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ishtar_common', '0226_auto_20230316_1115'), - ] - - operations = [ - migrations.AddField( - model_name='ishtaruser', - name='password_last_update', - field=models.DateField(default=datetime.date.today, verbose_name='Password last update'), - ), - migrations.AlterField( - model_name='customform', - name='header', - field=models.TextField(blank=True, default='', help_text='You can use markdown syntax.', verbose_name='Header text'), - ), - migrations.AlterField( - model_name='ishtarsiteprofile', - name='footer', - field=models.TextField(blank=True, default='', help_text='You can use markdown syntax.', verbose_name='Footer text'), - ), - ] diff --git a/ishtar_common/migrations/0227_auto_20230406_1834.py b/ishtar_common/migrations/0227_auto_20230406_1834.py new file mode 100644 index 000000000..a365c0bc8 --- /dev/null +++ b/ishtar_common/migrations/0227_auto_20230406_1834.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.24 on 2023-04-06 18:34 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ishtar_common', '0226_auto_20230316_1115'), + ] + + operations = [ + migrations.AddField( + model_name='ishtaruser', + name='display_news', + field=models.BooleanField(default=True, verbose_name='Display news'), + ), + migrations.AddField( + model_name='ishtaruser', + name='latest_news_version', + field=models.CharField(blank=True, default='', max_length=20, verbose_name='Latest news version'), + ), + migrations.AddField( + model_name='ishtaruser', + name='password_last_update', + field=models.DateField(default=datetime.date.today, verbose_name='Password last update'), + ), + migrations.AlterField( + model_name='customform', + name='header', + field=models.TextField(blank=True, default='', help_text='You can use markdown syntax.', verbose_name='Header text'), + ), + migrations.AlterField( + model_name='ishtarsiteprofile', + name='footer', + field=models.TextField(blank=True, default='', help_text='You can use markdown syntax.', verbose_name='Footer text'), + ), + ] diff --git a/ishtar_common/models.py b/ishtar_common/models.py index efa061431..f7baebfe4 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -3416,6 +3416,10 @@ class IshtarUser(FullSearch): advanced_shortcut_menu = models.BooleanField( _("Advanced shortcut menu"), default=False ) + # latest news read by the user + latest_news_version = models.CharField(_("Latest news version"), default="", blank=True, + max_length=20) + display_news = models.BooleanField(_("Display news"), default=True) class Meta: verbose_name = _("Ishtar user") @@ -3500,7 +3504,7 @@ class IshtarUser(FullSearch): @post_importer_action def import_create_profile(self, context, value): UserProfile.objects.get_or_create(person=self.person, profile_type=value, - defaults={"name":value.label}) + defaults={"name": value.label}) post_save.connect(cached_label_changed, sender=IshtarUser) diff --git a/ishtar_common/templates/ishtar/changelog.html b/ishtar_common/templates/ishtar/changelog.html new file mode 100644 index 000000000..e8fb94ea8 --- /dev/null +++ b/ishtar_common/templates/ishtar/changelog.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} +
+

{% trans "Changelog" %}

+ {% if next %} + + {% endif %} + {{changelog|safe}} + {% if previous %} + + {% endif %} +
+{% endblock %} diff --git a/ishtar_common/templates/navbar.html b/ishtar_common/templates/navbar.html index d8e368bbc..3e7190645 100644 --- a/ishtar_common/templates/navbar.html +++ b/ishtar_common/templates/navbar.html @@ -33,6 +33,9 @@ {% trans "Change password" %} + + {% trans "Changelog" %} + {% trans "Log out" %} diff --git a/ishtar_common/tests.py b/ishtar_common/tests.py index 774fab2a2..97002f13d 100644 --- a/ishtar_common/tests.py +++ b/ishtar_common/tests.py @@ -1348,9 +1348,9 @@ class UserProfileTest(TestCase): self.user.set_password(self.password) self.user.save() self.client = Client() - self.client.login(username=self.username, password=self.password) def test_profile_edit(self): + self.client.login(username=self.username, password=self.password) base_url = "/profile/" base_profile = self.user.ishtaruser.current_profile response = self.client.get(base_url) @@ -1407,6 +1407,52 @@ class UserProfileTest(TestCase): self.assertEqual(self.user.ishtaruser.person.profiles.count(), 1) + def _check_changelog_alert(self, content, check_ok=True): + soup = Soup(content, "lxml") + messages = soup.findAll("div", {"id": "message_list"}) + if not check_ok and not len(messages): + return + self.assertEqual(len(messages), 1) + for div in messages: + content = str(div.extract()) + if check_ok: + self.assertIn("/changelog/", content) + else: + self.assertNotIn("/changelog/", content) + + def test_changelog(self): + url = "/changelog/" + response = self.client.get(url) + self.assertRedirects(response, "/accounts/login/?next={}".format(url)) + self.client.login(username=self.username, password=self.password) + user = models.IshtarUser.objects.get(pk=self.user.pk) + self.assertEqual(user.latest_news_version, "") + user.display_news = False + user.save() + + response = self.client.get("/") + self.assertEqual(response.status_code, 200) + response = self.client.get("/") + # 2 times because the first page on server start do not have the version in cache + # not a big deal at all... + + # display_news set to False + self._check_changelog_alert(response.content.decode(), check_ok=False) + user.display_news = True + user.save() + response = self.client.get("/") + self._check_changelog_alert(response.content.decode()) + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + user = models.IshtarUser.objects.get(pk=self.user.pk) + # checked version updated + self.assertNotEqual(user.latest_news_version, "") + # now no link to changelog displayed + response = self.client.get("/") + self._check_changelog_alert(response.content.decode(), check_ok=False) + + class AcItem: def __init__( self, diff --git a/ishtar_common/urls.py b/ishtar_common/urls.py index c5b3816a4..ad7e22019 100644 --- a/ishtar_common/urls.py +++ b/ishtar_common/urls.py @@ -422,8 +422,7 @@ urlpatterns += [ views.autocomplete_organization, name="autocomplete-organization", ), - # TODO v4: suppression - #url(r"admin-globalvar/", views.GlobalVarEdit.as_view(), name="admin-globalvar"), + url(r"changelog/(?:(?P\d+)/)?", views.ChangelogView.as_view(), name="changelog"), url(r"person-merge/(?:(?P\d+)/)?$", views.person_merge, name="person_merge"), url( r"person-manual-merge/$", diff --git a/ishtar_common/views.py b/ishtar_common/views.py index f5a58afad..ac4e995d1 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -24,6 +24,7 @@ from jinja2 import TemplateSyntaxError import json import logging import os +import re import unicodedata import urllib.parse @@ -1115,11 +1116,11 @@ organization_merge = merge_action( ) -class IshtarMixin(object): +class IshtarMixin: page_name = "" def get_context_data(self, **kwargs): - context = super(IshtarMixin, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context["page_name"] = self.page_name return context @@ -1149,6 +1150,51 @@ class AdminLoginRequiredMixin(LoginRequiredMixin): return super(AdminLoginRequiredMixin, self).dispatch(request, *args, **kwargs) +class ChangelogView(IshtarMixin, LoginRequiredMixin, TemplateView): + template_name = "ishtar/changelog.html" + page_name = _("Changelog") + current_url = "changelog" + + def update_read_version(self): + if not self.request.user or not hasattr(self.request.user, "ishtaruser") \ + or not self.request.user.ishtaruser: + return + cache_key = f"{settings.PROJECT_SLUG}-news-version" + current_version = cache.get(cache_key) + if not current_version: + return + ishtar_user = models.IshtarUser.objects.get(pk=self.request.user.ishtaruser.pk) + if not ishtar_user.latest_news_version \ + or ishtar_user.latest_news_version != current_version: + ishtar_user.latest_news_version = current_version + ishtar_user.save() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + changelog_dir = os.path.join( + settings.SHARE_BASE_PATH, + "changelog", + settings.LANGUAGE_CODE.split('-')[0] + ) + if not os.path.exists(changelog_dir): + raise Http404() + page_number = int(kwargs.get("page", 0) or 0) or 1 + if page_number == 1: + self.update_read_version() + changelog_file = os.path.join(changelog_dir, f"changelog_{page_number}.md") + if not os.path.exists(changelog_file): + raise Http404() + with open(changelog_file, "r") as changelog: + context["changelog"] = markdown(changelog.read()) + if page_number > 1: + context["next"] = page_number - 1 + + changelog_file_next = os.path.join(changelog_dir, f"changelog_{page_number + 1}.md") + if os.path.exists(changelog_file_next): + context["previous"] = page_number + 1 + return context + + class ProfileEdit(LoginRequiredMixin, FormView): template_name = "ishtar/forms/profile.html" form_class = forms.ProfilePersonForm -- cgit v1.2.3