From 68d4467ca79c73d830fe3565c5076b70b47c6d11 Mon Sep 17 00:00:00 2001 From: AndiEcker Date: Tue, 4 Jun 2024 18:27:18 +0100 Subject: [PATCH 1/2] V0.3.102: show member meeting texts formatting error to user (preventing telegram notification) fixes #7 M README.md M kairos/__init__.py M kairos/templatetags/kairos_tags.py M kairos/utils.py M kairos/views.py M mbr_announcements/views.py M mbr_meeting_plugin/models.py M mbr_meeting_plugin/views.py M mbr_messages/views.py --- README.md | 2 +- kairos/__init__.py | 2 +- kairos/templatetags/kairos_tags.py | 3 ++- kairos/utils.py | 10 +++++++--- kairos/views.py | 28 +++++++++++++++------------- mbr_announcements/views.py | 24 +++++++++++++----------- mbr_meeting_plugin/models.py | 8 ++++++-- mbr_meeting_plugin/views.py | 5 +++-- mbr_messages/views.py | 24 ++++++++++++++---------- 9 files changed, 62 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index caf4bbc..d08ecf2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ project, including the manuals for: * [administrators](https://gitlab.com/ae-group/kairos/-/blob/develop/docs/administrator_manual.rst) * [programmers](https://gitlab.com/ae-group/kairos/-/blob/develop/docs/programmer_manual.rst) -this includes also [a RST about the features of this website]( +this includes also [an RST about the features of this website]( https://gitlab.com/ae-group/kairos/-/blob/develop/docs/features_and_examples.rst) [how to contribute to this project is explained here]( diff --git a/kairos/__init__.py b/kairos/__init__.py index 620daa0..11cadce 100644 --- a/kairos/__init__.py +++ b/kairos/__init__.py @@ -86,4 +86,4 @@ TODO: """ -__version__ = '0.3.101' +__version__ = '0.3.102' diff --git a/kairos/templatetags/kairos_tags.py b/kairos/templatetags/kairos_tags.py index a1fe7eb..38e45c0 100644 --- a/kairos/templatetags/kairos_tags.py +++ b/kairos/templatetags/kairos_tags.py @@ -1,6 +1,7 @@ """ add flag icons to your templates. """ from django import template # type: ignore from django.contrib.auth import get_user_model # type: ignore +from django.contrib.auth.models import User # type: ignore from ae.django_utils import generic_language # type: ignore @@ -22,7 +23,7 @@ def flag_icon(language: str) -> str: @register.simple_tag -def full_name(member_rec_or_pk) -> str: +def full_name(member_rec_or_pk: User) -> str: """ return the full and unique name of a member. :param member_rec_or_pk: member user record or the primary key of the user record. diff --git a/kairos/utils.py b/kairos/utils.py index 6811f76..9d2a5fc 100644 --- a/kairos/utils.py +++ b/kairos/utils.py @@ -3,13 +3,15 @@ import os from typing import Iterable, Optional +from django.contrib import messages # type: ignore from django.contrib.auth.models import User # type: ignore +from django.http import HttpRequest # type: ignore from django.utils.translation import gettext as _ # type: ignore from ae.notify import Notifications # type: ignore -def member_full_name(member_rec) -> str: +def member_full_name(member_rec: User) -> str: """ return the full and unique display-name of a member. :param member_rec: member user record. @@ -28,7 +30,7 @@ notification_service = Notifications( ) -def send_chg_notif(mode: str, changes: Iterable[str], current_user: User, +def send_chg_notif(mode: str, changes: Iterable[str], current_user: User, request: HttpRequest, owner: Optional[User] = None, item: str = "", url: str = "", subject: Optional[str] = None, notify_members: Optional[bool] = True): """ send out change notifications of meeting/data-changes to log (MCN_LOGGERS) and all members (MCN_RECEIVERS). @@ -36,6 +38,7 @@ def send_chg_notif(mode: str, changes: Iterable[str], current_user: User, :param mode: data item change type: 'added', 'updated' or 'deleted' or 'meeting' for member meetings. :param changes: list/tuple of changes done to the data item / meeting text. :param current_user: user record of the authenticated user (current user or admin). + :param request: http request, needed for error notification. :param owner: data item owner user record, current user or None (specifying current user as modifier). :param item: name/description of the changed item (ignored if mode == 'meeting'). :param url: absolute url to show data item (ignored/not-used if mode in ('deleted', 'meeting')). @@ -60,7 +63,7 @@ def send_chg_notif(mode: str, changes: Iterable[str], current_user: User, msg = _(msg).format(owner=member_full_name(owner) if owner else username, item=item, url=url, mode=mode) if not owner or username != owner.username: - msg += " (" + _("by {admin}").format(admin=username) + ")" + msg += " (" + _("by {admin}").format(admin=member_full_name(current_user)) + ")" for change in changes: msg += f"

{change}" @@ -69,3 +72,4 @@ def send_chg_notif(mode: str, changes: Iterable[str], current_user: User, err_msg = notification_service.send_notification(msg, change_receiver, subject) if err_msg: print(f"*** send_chg_notif() error with message '{msg}' to '{change_receiver}': {err_msg}") + messages.error(request, err_msg) diff --git a/kairos/views.py b/kairos/views.py index 54bf4f3..dc3ebcb 100644 --- a/kairos/views.py +++ b/kairos/views.py @@ -4,11 +4,12 @@ from typing import Any, Optional from django.contrib import messages # type: ignore from django.contrib.auth import get_user_model # type: ignore -from django.contrib.auth.models import Group # type: ignore # noqa: E402 +from django.contrib.auth.models import Group, User # type: ignore # noqa: E402 from django.contrib.auth.tokens import PasswordResetTokenGenerator # type: ignore from django.contrib.auth.views import LoginView # type: ignore from django.db.models import IntegerField, Q # type: ignore from django.db.models.functions import Cast # type: ignore +from django.http import HttpRequest # type: ignore from django.shortcuts import render, redirect # type: ignore from django.template.loader import render_to_string # type: ignore from django.urls import reverse # type: ignore @@ -36,7 +37,7 @@ class _MemberSignupTokenGenerator(PasswordResetTokenGenerator): _member_signup_token = _MemberSignupTokenGenerator() -def _duplicate_members(username: str, phone: str, email: str, exclude_pk: int = 0): +def _duplicate_members(username: str, phone: str, email: str, exclude_pk: int = 0) -> list[User]: dup_filter = Q(pk__in=[]) # https://forum.djangoproject.com/t/improving-q-objects-with-true-false-and-none/851 if username: dup_filter |= Q(username=username) @@ -48,7 +49,8 @@ def _duplicate_members(username: str, phone: str, email: str, exclude_pk: int = return get_user_model().objects.exclude(Q(pk=exclude_pk) | Q(is_staff=True)).filter(dup_filter) -def _dup_member_messages(dup_members, request, msg_prefix: str, username: str, phone: str, email: str) -> bool: +def _dup_member_messages(dup_members: list[User], request: HttpRequest, + msg_prefix: str, username: str, phone: str, email: str) -> bool: for dup_mbr in dup_members or []: messages.error(request, f"{msg_prefix}: " + _("Member data duplicate error(s) with {name}:").format( name=member_full_name(dup_mbr))) @@ -62,7 +64,7 @@ def _dup_member_messages(dup_members, request, msg_prefix: str, username: str, p return bool(dup_members) -def _activate_member(member_pk: int, request, token: Optional[str] = None): +def _activate_member(member_pk: int, request: HttpRequest, token: Optional[str] = None) -> tuple[bool, User, str]: """ activate member account identified by member_pk, returning activation status, member record and msg prefix. """ user_model = get_user_model() username = "?" @@ -78,7 +80,7 @@ def _activate_member(member_pk: int, request, token: Optional[str] = None): msg_prefix = _("Admission Fee Payment Confirmation") activated = False - if not member_rec: + if not member_rec or not dup_members: messages.error(request, f"{msg_prefix}: " + _("Invalid member ids {}!").format(str(member_pk) + "/" + username)) elif token and not _member_signup_token.check_token(member_rec, token): messages.error(request, f"{msg_prefix}: " + _("Registration {} expired error!").format(token)) @@ -99,7 +101,7 @@ def _activate_member(member_pk: int, request, token: Optional[str] = None): mm_public=False, ) url = reverse('message-show', kwargs={'id': rec.pk}) + '#' + MSG_ANCHOR_ID_PREFIX + str(rec.pk) - send_chg_notif('added', [rec.mm_text], current_user, + send_chg_notif('added', [rec.mm_text], current_user, request, owner=current_user, item=_("private message"), url=request.build_absolute_uri(url)) activated = True @@ -107,7 +109,7 @@ def _activate_member(member_pk: int, request, token: Optional[str] = None): return activated, member_rec, msg_prefix -def _activate_and_confirm_member(member_pk: int, request, token: Optional[str] = None): +def _activate_and_confirm_member(member_pk: int, request: HttpRequest, token: Optional[str] = None): activated, member_rec, subject_prefix = _activate_member(member_pk, request, token) if activated: url = request.build_absolute_uri(reverse('login')) @@ -121,7 +123,7 @@ def _activate_and_confirm_member(member_pk: int, request, token: Optional[str] = return redirect(reverse('members-list') + ('#' + member_rec.username if activated else "")) -def signup_email_confirm(request, key: str, token: str): +def signup_email_confirm(request: HttpRequest, key: str, token: str): """ view from link in signup confirmation email to check. """ user_model = get_user_model() try: @@ -147,13 +149,13 @@ def signup_email_confirm(request, key: str, token: str): {'admin_name': admin_name, 'member': member_rec, 'act_url': url, 'mbr_url': mbr_url}) err_msg = notification_service.send_notification(msg, admin_data, _("Admission Fee Payment Confirmation")) if err_msg: - messages.warning(request, _("Message-Send-Error to '{admin}' on sign-up of '{name}': {err_msg}").format( + messages.error(request, _("Message-Send-Error to '{admin}' on sign-up of '{name}': {err_msg}").format( admin=f"{admin_name} ({admin_addr.split(':')[0]})", name=member_rec.first_name, err_msg=err_msg)) return render(request, 'registration/email_verification.html', {'member': member_rec}) -def signup_paid_confirm(request, keys: str, token: str): +def signup_paid_confirm(request: HttpRequest, keys: str, token: str): """ confirmation of signup admin that the new signup member has paid. """ admin_name, member_key = force_str(urlsafe_base64_decode(keys)).split(',') if not request.user.is_authenticated or member_full_name(request.user) != admin_name: @@ -170,12 +172,12 @@ class MemberEmailSignupView(View): form_class = MemberEmailSignupForm template_name = 'registration/signup_form.html' - def get(self, request, *_args, **_kwargs): + def get(self, request: HttpRequest, *_args, **_kwargs): """ GET request displaying empty form. """ form = self.form_class() return render(request, self.template_name, {'form': form}) - def post(self, request, *_args, **_kwargs): + def post(self, request: HttpRequest, *_args, **_kwargs): """ POST request from signup form submitted by new member. """ form = self.form_class(request.POST) @@ -240,7 +242,7 @@ class MembersListView(ListView): return super().get_queryset() @staticmethod - def post(request, *_args, **_kwargs): + def post(request: HttpRequest, *_args, **_kwargs): """ activate member. """ member_pk = int(request.POST.get('activate')) return _activate_and_confirm_member(member_pk, request) diff --git a/mbr_announcements/views.py b/mbr_announcements/views.py index 666ef87..c5bf1bd 100644 --- a/mbr_announcements/views.py +++ b/mbr_announcements/views.py @@ -1,11 +1,12 @@ """ announcements views. """ import datetime +from typing import Any, Optional from urllib.parse import urlparse from django.contrib.auth.models import User # type: ignore from django.db.models import Q # type: ignore -from django.http import HttpResponseRedirect # type: ignore +from django.http import HttpRequest, HttpResponseRedirect # type: ignore from django.urls import reverse # type: ignore from django.utils import timezone # type: ignore from django.utils.text import slugify # type: ignore @@ -23,14 +24,15 @@ EXC_ADD_MARKER = 'EmptyMemberAnnouncement' EXC_ANCHOR_ID_PREFIX = "MAnnAIdP" -def check_change_notification(log_desc: str, rec: MemberAnnouncement, current_user: User, - chk_desc: str = "", url: str = "", notify_members=True): +def check_change_notification(log_desc: str, rec: MemberAnnouncement, current_user: User, request: HttpRequest, + chk_desc: str = "", url: str = "", notify_members: Optional[bool] = True): """ check changes on MemberAnnouncement record and send notifications if yes. :param log_desc: announcement description text to be logged/send-in-notification, which is on update the new one and on deletion the actual one. :param rec: added/updated/deleted announcement record data (author, category and action). :param current_user: record of current user. + :param request: http request, needed for error notification. :param chk_desc: old announcement description text on update, unspecified or "" on deletion. :param url: absolute url to show the announcement (unspecified or "" on deletion). :param notify_members: pass False to not send notifications to all members (only send to MCN_LOGGERS). @@ -40,11 +42,11 @@ def check_change_notification(log_desc: str, rec: MemberAnnouncement, current_us item = _("{ma_cat_name}-{ma_action}-announcement").format(ma_cat_name=rec.ma_announce_category.ac_name, ma_action=rec.ma_action) mode = 'deleted' if chk_desc == "" else 'added' if EXC_ADD_MARKER in chk_desc else 'updated' - send_chg_notif(mode, [log_desc], current_user, owner=rec.ma_user, item=item, url=url, - notify_members=notify_members) + send_chg_notif(mode, [log_desc], current_user, request, + owner=rec.ma_user, item=item, url=url, notify_members=notify_members) -def extend_context(context, request, **kwargs): +def extend_context(context: dict[str, Any], request: HttpRequest, **kwargs): """ extend context for view and plugin (bundled here to avoid redundancies) """ context['EXC_ADD_MARKER'] = EXC_ADD_MARKER context['EXC_CAT_ACT_SEP'] = EXC_CAT_ACT_SEP @@ -58,7 +60,7 @@ def extend_context(context, request, **kwargs): user_cat_acts.append(ma_object.ma_announce_category.ac_name + EXC_CAT_ACT_SEP + ma_object.ma_action) -def get_queryset(request): +def get_queryset(request: HttpRequest): """ get full/filtered queryset of announcements/objects for view and plugin, bundled here to avoid redundancies. """ current_user = request.user obj_list = MemberAnnouncement.objects # pylint: disable=no-member # false positive @@ -107,10 +109,10 @@ class AnnouncementsListView(ListView): """ filter by currently selected language and order by category-name, action and newest changes first. """ return get_queryset(self.request) - def post(self, request, *_args, **kwargs): + def post(self, request: HttpRequest, *_args, **kwargs): """ handle member announcements add, delete and change of description. """ current_user = request.user - ma_id: int = kwargs.get('id') + ma_id: Optional[int] = kwargs.get('id') if not ma_id: cat_name, ma_action, ma_user = kwargs['cat'], kwargs['act'], current_user # pylint: disable=no-member # false positive .objects @@ -138,7 +140,7 @@ class AnnouncementsListView(ListView): ma_id = 0 url = reverse('announcements-list') if EXC_ADD_MARKER not in (desc := del_rec.ma_description) and desc.strip(): - check_change_notification(desc, del_rec, current_user, notify_members=notify_members) + check_change_notification(desc, del_rec, current_user, request, notify_members=notify_members) del_rec.delete() else: # update @@ -150,7 +152,7 @@ class AnnouncementsListView(ListView): upd_rec.ma_description = new_desc upd_rec.save() check_change_notification( - new_desc, upd_rec, current_user, + new_desc, upd_rec, current_user, request, chk_desc=old_desc, url=set_url_part(request.build_absolute_uri(url), EXC_ANCHOR_ID_PREFIX + str(ma_id)), notify_members=notify_members) diff --git a/mbr_meeting_plugin/models.py b/mbr_meeting_plugin/models.py index b7280ee..46289d2 100644 --- a/mbr_meeting_plugin/models.py +++ b/mbr_meeting_plugin/models.py @@ -1,5 +1,8 @@ """ member meetings model """ +from typing import cast + from django.conf import settings # type: ignore +from django.contrib.auth.models import User # type: ignore from django.db import models # type: ignore from kairos.utils import member_full_name @@ -13,7 +16,8 @@ class MemberMeeting(models.Model): mt_created = models.DateTimeField(auto_now_add=True) def __str__(self): - # noinspection PyUnresolvedReferences + # passing self.mt_author to member_full_name() results in + # .. AttributeError: 'ForwardManyToOneDescriptor' object has no attribute 'username' return (f"{self.__class__.__name__}-{self.mt_created}={self.mt_language}" - f"@{member_full_name(self.mt_author)}" + f"@{member_full_name(cast(User, self.mt_author))}" f":{self.mt_text}") diff --git a/mbr_meeting_plugin/views.py b/mbr_meeting_plugin/views.py index 537dfb4..b3eeb47 100644 --- a/mbr_meeting_plugin/views.py +++ b/mbr_meeting_plugin/views.py @@ -1,5 +1,6 @@ """ view to add a new member meeting text. """ from django.conf import settings # type: ignore +from django.http import HttpRequest # type: ignore from django.shortcuts import redirect # type: ignore from django.utils.translation import override # type: ignore from django.views import View # type: ignore @@ -16,7 +17,7 @@ MBR_MEET_ANCHOR_ID_PREFIX = 'MMeetAIdP' class MemberMeetingAddView(View): """ add new member meeting with text block, language, author, created date. """ @staticmethod - def post(request, *_args, **_kwargs): + def post(request: HttpRequest, *_args, **_kwargs): """ form post to add new member meeting text into the database. """ current_user = request.user updated_languages = [] @@ -31,7 +32,7 @@ class MemberMeetingAddView(View): updated_languages.append(lang) with override(lang): - send_chg_notif('meeting', [updated_text], current_user, + send_chg_notif('meeting', [updated_text], current_user, request, notify_members=request.POST.get(lang + '_notify') == 'on') return redirect(request.META.get('HTTP_REFERER', '/') + '#' + MBR_MEET_ANCHOR_ID_PREFIX diff --git a/mbr_messages/views.py b/mbr_messages/views.py index 3985f60..1fbeec0 100644 --- a/mbr_messages/views.py +++ b/mbr_messages/views.py @@ -1,12 +1,13 @@ """ member messages views """ import datetime import re -from typing import Optional + +from typing import Any, Optional from urllib.parse import urlparse from django.contrib.auth.models import User # type: ignore from django.db.models import Q # type: ignore -from django.http import HttpResponseRedirect # type: ignore +from django.http import HttpRequest, HttpResponseRedirect # type: ignore from django.urls import reverse # type: ignore from django.utils.translation import gettext as _ # type: ignore from django.views.generic import ListView # type: ignore @@ -25,7 +26,7 @@ MOBILE_AGENT_RE = re.compile(r".*(iphone|mobile|androidtouch)", re.IGNORECASE) def check_change_notification(old_public: bool, old_expired: Optional[datetime.date], old_text: str, new_public: bool, new_expired: Optional[datetime.date], new_text: str, - current_user: User, author: User, url: str, notify_members: bool): + current_user: User, author: User, url: str, notify_members: bool, request: HttpRequest): """ check for changes and if yes then send notifications to receivers. :param old_public: old publication status of message. @@ -38,6 +39,7 @@ def check_change_notification(old_public: bool, old_expired: Optional[datetime.d :param author: user record of message author. :param url: absolute url to show the changed message on the website. :param notify_members: pass False to not send notifications to all members (only send to MCN_LOGGERS). + :param request: http request, needed for error notification. """ action = 'added' if MSG_ADD_MARKER in old_text else 'updated' changes = [] @@ -66,10 +68,11 @@ def check_change_notification(old_public: bool, old_expired: Optional[datetime.d changes.append(new_text) if changes or not notify_members: - send_chg_notif(action, changes, current_user, owner=author, item=item, url=url, notify_members=notify_members) + send_chg_notif(action, changes, current_user, request, + owner=author, item=item, url=url, notify_members=notify_members) -def extend_context(context, request, **kwargs): +def extend_context(context: dict[str, Any], request: HttpRequest, **kwargs): """ extend context for view and plugin (to avoid redundancies) """ context['MSG_ADD_MARKER'] = MSG_ADD_MARKER context['MSG_ANCHOR_ID_PREFIX'] = MSG_ANCHOR_ID_PREFIX @@ -77,7 +80,7 @@ def extend_context(context, request, **kwargs): context['url_kwargs'] = kwargs -def get_queryset(request): +def get_queryset(request: HttpRequest): """ get queryset of all objects for view and plugin (to avoid redundancies). """ current_user = request.user obj_list = MemberMessage.objects # pylint: disable=no-member # false positive @@ -102,10 +105,10 @@ class MemberMessageListView(ListView): """ filter by currently selected language and order by category-name, action and newest changes first. """ return get_queryset(self.request) - def post(self, request, *_args, **kwargs): + def post(self, request: HttpRequest, *_args, **kwargs): """ handle member message edit. """ current_user = request.user - mm_id: int = kwargs.get('id') + mm_id: Optional[int] = kwargs.get('id') if not mm_id: # action == 'add' row = MemberMessage.objects.create( # pylint: disable=no-member # false positive mm_author=current_user, @@ -127,7 +130,8 @@ class MemberMessageListView(ListView): mm_id = 0 url = reverse('messages-list') if MSG_ADD_MARKER not in (txt := rec.mm_text) and txt.strip(): - send_chg_notif('deleted', ([f"- {rec.mm_expired}"] if rec.mm_expired else []) + [txt], current_user, + send_chg_notif('deleted', ([f"- {rec.mm_expired}"] if rec.mm_expired else []) + [txt], + current_user, request, owner=rec.mm_author, item=_("public message") if rec.mm_public else _("private message"), notify_members=notify_members) @@ -150,7 +154,7 @@ class MemberMessageListView(ListView): new_public, new_expired, new_text, current_user, rec.mm_author, set_url_part(request.build_absolute_uri(url), MSG_ANCHOR_ID_PREFIX + str(mm_id)), - notify_members) + notify_members, request) url = set_url_part(url, MSG_ANCHOR_ID_PREFIX + str(mm_id), urlparse(request.META.get('HTTP_REFERER')).query) -- GitLab From c2ec927eaa117e279f2bc64c38ae74c0d5ac7bf3 Mon Sep 17 00:00:00 2001 From: AndiEcker Date: Tue, 4 Jun 2024 18:37:22 +0100 Subject: [PATCH 2/2] V0.3.102: show member meeting texts formatting error preventing telegram notification closes #7 M kairos/templatetags/kairos_tags.py --- kairos/templatetags/kairos_tags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kairos/templatetags/kairos_tags.py b/kairos/templatetags/kairos_tags.py index 38e45c0..1f758fc 100644 --- a/kairos/templatetags/kairos_tags.py +++ b/kairos/templatetags/kairos_tags.py @@ -1,4 +1,6 @@ """ add flag icons to your templates. """ +from typing import Union + from django import template # type: ignore from django.contrib.auth import get_user_model # type: ignore from django.contrib.auth.models import User # type: ignore @@ -23,7 +25,7 @@ def flag_icon(language: str) -> str: @register.simple_tag -def full_name(member_rec_or_pk: User) -> str: +def full_name(member_rec_or_pk: Union[User, int]) -> str: """ return the full and unique name of a member. :param member_rec_or_pk: member user record or the primary key of the user record. -- GitLab