diff --git a/django_project_base/account/rest/project_profiles.py b/django_project_base/account/rest/project_profiles.py index fff58af7..a0275a0d 100644 --- a/django_project_base/account/rest/project_profiles.py +++ b/django_project_base/account/rest/project_profiles.py @@ -1,3 +1,4 @@ +import json import re from io import BytesIO @@ -10,6 +11,7 @@ from dynamicforms import fields, serializers from dynamicforms.action import TableAction, TablePosition from dynamicforms.viewsets import SingleRecordViewSet +from rest_framework.decorators import action from rest_framework.exceptions import NotFound, PermissionDenied from rest_framework.request import Request from rest_framework.response import Response @@ -88,11 +90,11 @@ def get_fields(self): return res def translate_relation_fields(self, field_name: str) -> str: - res = super().get_fields() - if field_name in res: - return field_name - else: + ProjectMember = swapper.load_model("django_project_base", "ProjectMember") + if field_name in ProjectMember().project_members_fields_names: return f"projects__{field_name}" + else: + return field_name def get_visible_fields(self): return [(name, field) for (name, field) in self.fields.items() if field.display == fields.DisplayMode.FULL] @@ -109,52 +111,54 @@ def filter_queryset_field(self, queryset, field, value): return queryset.filter(projects__state=value) return super().filter_queryset_field(queryset, field, value) - def save_club_member_data(self, request: Request, user): + def save_club_member_data(self, request: Request, user, **kwargs): if user is None: return - ProjectMember = swapper.load_model("django_project_base", "ProjectMember") - - data = { - name: request.data.pop(name, None) - for name in ProjectMember().project_members_fields_names - if name in request.data - } - project = request.selected_project club_member = None + ProjectMember = swapper.load_model("django_project_base", "ProjectMember") if project: club_member = ProjectMember.objects.filter(member=user).filter(project=project).first() # if club member data cant be retrieved, we can't save anything if club_member: - for name, value in data.items(): + for name, value in kwargs.items(): setattr(club_member, name, value) club_member.save() @transaction.atomic def create(self, request: Request, *args, **kwargs) -> Response: + ProjectMember = swapper.load_model("django_project_base", "ProjectMember") + Profile = swapper.load_model("django_project_base", "Profile") + data = { + name: request.data.pop(name, None) + for name in ProjectMember().project_members_fields_names + if name in request.data + } # hash password if it exists if "password" in request.data: request.data["password"] = make_password(request.data["password"]) response = super().create(request, *args, **kwargs) - - ProjectMember = swapper.load_model("django_project_base", "ProjectMember") - Profile = swapper.load_model("django_project_base", "Profile") - user = Profile.objects.get(pk=response.data["id"]) try: ProjectMember.objects.create(project=request.selected_project, member=user) except ProjectNotSelectedError as e: raise PermissionDenied(e.message) - self.save_club_member_data(request, user) + self.save_club_member_data(request, user, **data) return response @transaction.atomic def update(self, request: Request, *args, **kwargs) -> Response: - self.save_club_member_data(request, self.get_object()) + ProjectMember = swapper.load_model("django_project_base", "ProjectMember") + data = { + name: request.data.pop(name, None) + for name in ProjectMember().project_members_fields_names + if name in request.data + } + self.save_club_member_data(request, self.get_object(), **data) # hash password if it exists if "password" in request.data: request.data["password"] = make_password(request.data["password"]) @@ -196,9 +200,38 @@ class ProfileExportViewSet(SingleRecordViewSet): serializer_class = ProfileExportSerializer def new_object(self): - return dict(template=None, profile_fields=None, print_filter=True, filter_data=None) + filter_data = self.request.query_params.get("filter_data", None) + if filter_data: + filter_data = json.loads(filter_data) + return dict(template=None, profile_fields=None, print_filter=True, filter_data=filter_data) def create(self, request, *args, **kwargs): + ser = self.get_serializer(data=request.data) # type: ProfileExportSerializer + ser.is_valid(raise_exception=True) + + filter_data = ser.data.get("filter_data", None) + print_filter = ser.data.get("print_filter", False) + + file_name = "members" + + if filter_data: + active_filters = {name: value for name, value in filter_data.items() if value is not None} + if print_filter and len(active_filters) in [1, 2]: + file_name += "-" + ",".join(["-".join([name, str(value)]) for name, value in active_filters.items()]) + + data = dict(ser.data) + data["file_name"] = file_name + + return Response(data) + + @action( + methods=["POST"], + detail=False, + url_path="download", + url_name="profile-export-download", + permission_classes=[], + ) + def download(self, request: Request, **kwargs): profile_serializer = ProjectProfilesSerializer(None, context=self.get_serializer_context(), data=request.data) visible_profile_fields = { name: profile_serializer.translate_relation_fields(name) @@ -214,18 +247,11 @@ def create(self, request, *args, **kwargs): profile_fields = ser.data.get("profile_fields", None) template = ser.data.get("template", None) filter_data = ser.data.get("filter_data", None) - print_filter = ser.data.get("print_filter", False) + file_name = ser.data.get("file_name", "members") profile_items_q = get_project_members(self.request) - file_name = "members" - if filter_data: - active_filters = {name: value for name, value in filter_data.items() if value is not None} - print(len(active_filters), print_filter) - if print_filter and len(active_filters) in [1, 2]: - file_name += "-" + ",".join(["-".join([name, str(value)]) for name, value in active_filters.items()]) - pass for field_name, value in filter_data.items(): profile_items_q = filter_project_members_fields(profile_items_q, field_name, value) diff --git a/django_project_base/notifications/base/channels/integrations/provider_integration.py b/django_project_base/notifications/base/channels/integrations/provider_integration.py index 7950d928..3e21f852 100644 --- a/django_project_base/notifications/base/channels/integrations/provider_integration.py +++ b/django_project_base/notifications/base/channels/integrations/provider_integration.py @@ -1,8 +1,10 @@ import re from abc import ABC, abstractmethod +from html import unescape from typing import Union import swapper +from django.urls import reverse from django.utils.html import strip_tags from django_project_base.constants import SEND_NOTIFICATION_SMS @@ -58,17 +60,20 @@ def get_message(self, notification: DjangoProjectBaseNotification) -> Union[dict def get_send_notification_sms_text(self, notification: DjangoProjectBaseNotification, host_url: str) -> str: if notification.send_notification_sms: - template: str = swapper.load_model("django_project_base", "ProjectSettings").objects.get( - name=SEND_NOTIFICATION_SMS, project__slug=notification.project_slug).python_value + template: str = ( + swapper.load_model("django_project_base", "ProjectSettings") + .objects.get(name=SEND_NOTIFICATION_SMS, project__slug=notification.project_slug) + .python_value + ) return template.replace( "__LINK__", - f"{host_url}notification/{str(notification.pk)}/info" - f"{'/' if getattr(self.settings, 'APPEND_SLASH', False) else ''}") + f"{host_url.rstrip('/')}" + f"{reverse('notification-notification-login', kwargs=dict(pk=str(notification.pk)))}", + ) return "" @abstractmethod def _get_sms_message(self, notification: DjangoProjectBaseNotification) -> Union[dict, str]: - if notification.send_notification_sms: return notification.send_notification_sms_text @@ -77,9 +82,14 @@ def _get_sms_message(self, notification: DjangoProjectBaseNotification) -> Union if notification.message.subject: message += "\n\n" - message += notification.message.body - + message += ( + notification.message.body.replace("
", "\n") + .replace("", "\n") + .replace("\n ", "\n") + .replace(" ", "\n") + ) text_only = re.sub("[ \t]+", " ", strip_tags(message)) # Strip single spaces in the beginning of each line - message = text_only.replace("\n ", "\n").replace("\n", "\r\n").strip() - return message + message = text_only.replace("\n ", "\n") + message = message.replace("\n", "\r\n") + return unescape(message) diff --git a/django_project_base/notifications/base/channels/sms_channel.py b/django_project_base/notifications/base/channels/sms_channel.py index b851b6c7..b52e86d4 100644 --- a/django_project_base/notifications/base/channels/sms_channel.py +++ b/django_project_base/notifications/base/channels/sms_channel.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import List from django.conf import settings @@ -16,9 +16,6 @@ class SmsChannel(Channel): provider_setting_name = "SMS_PROVIDER" - def get_message(self, notification: DjangoProjectBaseNotification) -> Union[dict, str]: - return self._get_sms_message(notification) - def get_recipients(self, notification: DjangoProjectBaseNotification, unique_identifier=""): return list(set(super().get_recipients(notification, unique_identifier="phone_number"))) diff --git a/django_project_base/notifications/migrations/0006_djangoprojectbasenotification_send_notification_sms.py b/django_project_base/notifications/migrations/0006_djangoprojectbasenotification_send_notification_sms.py index b45733df..907fccc0 100644 --- a/django_project_base/notifications/migrations/0006_djangoprojectbasenotification_send_notification_sms.py +++ b/django_project_base/notifications/migrations/0006_djangoprojectbasenotification_send_notification_sms.py @@ -14,7 +14,7 @@ def forwards_func(apps, schema_editor): project=project, name=SEND_NOTIFICATION_SMS, defaults=dict( - description="Text for notification detail view. __LINK__ is url placeholder.", + description=gettext("Text for notification detail view. __LINK__ is url placeholder."), value=gettext("You received a notification. Clik link to view it: __LINK__"), value_type="char", ), diff --git a/django_project_base/notifications/rest/notification.py b/django_project_base/notifications/rest/notification.py index c9bc9e89..f405cd80 100644 --- a/django_project_base/notifications/rest/notification.py +++ b/django_project_base/notifications/rest/notification.py @@ -5,13 +5,13 @@ import pytz import swapper -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.cache import cache from django.db.models import ForeignKey, QuerySet from django.http import Http404, HttpResponse from django.shortcuts import render +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from drf_spectacular.utils import extend_schema from dynamicforms import fields @@ -350,7 +350,7 @@ def notification_login(self, request, pk=None) -> HttpResponse: "notification_login.html", dict( identifier=pk, - url=f"/notification/info-view{'/' if getattr(settings, 'APPEND_SLASH', False) else ''}", + url=reverse("notification-notification-view"), title=swapper.load_model("django_project_base", "Project") .objects.get(slug=DjangoProjectBaseNotification.objects.get(pk=pk).project_slug) .name, diff --git a/vue/components/notifications-editor.vue b/vue/components/notifications-editor.vue index 3354400b..45a0fba2 100644 --- a/vue/components/notifications-editor.vue +++ b/vue/components/notifications-editor.vue @@ -3,7 +3,7 @@ import { APIConsumer, ComponentDisplay, ConsumerLogicApi, - FormConsumerApiOneShot, gettext, + FormConsumerOneShotApi, gettext, useActionHandler, } from '@velis/dynamicforms'; import { onMounted, onUnmounted, ref } from 'vue'; @@ -50,12 +50,12 @@ const notificationLogic = ref(new ConsumerLogicApi( notificationLogic.value.getFullDefinition(); const actionViewLicense = async (): Promise