diff --git a/src/api/urls.py b/src/api/urls.py index 3380c8feddd87d2ff933b48ea371b43cdd5e1501..666bda2725bef79cca5a911e64f0e94e23dceee4 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -41,6 +41,8 @@ urlpatterns = [ path('map/assemblies/poi.json', maps.AssembliesPoiExportView.as_view(), name='map-assemblies-poi'), path('map/assemblies/areas.json', maps.AssembliesAreasExportView.as_view(), name='map-assemblies-areas'), path('badges/redeem_token', badges.redeem_badge_token, name='badge-redeem'), + path('badge/<uuid:pk>/award/user', badges.reward_badge_to_username, name='badge-award-username'), + path('badge/<uuid:pk>/award/dect', badges.reward_badge_to_dect, name='badge-award-dect'), path('rooms', rooms.ConferenceRoomList.as_view(), name='room-list'), path('room/<uuid:pk>/', rooms.ConferenceRoomDetail.as_view(), name='room-detail'), path('room/<uuid:pk>/schedule', schedule.RoomSchedule.as_view(), name='room-schedule'), diff --git a/src/api/views/badges.py b/src/api/views/badges.py index a9d9789ac1f67e03a80319b836dcc9016061c83a..e19fc17f1cdf320195f31537bf6e09d0706f73f7 100644 --- a/src/api/views/badges.py +++ b/src/api/views/badges.py @@ -8,9 +8,9 @@ from rest_framework.response import Response from django.http import HttpResponse from django.shortcuts import get_object_or_404 -from core.models.badges import Badge, BadgeToken +from core.models.badges import Badge, BadgeToken, UserBadge from core.models.conference import Conference -from core.models.users import PlatformUser +from core.models.users import PlatformUser, UserCommunicationChannel from api.permissions import HasIssuingToken, IsAssemblyManager, IsSuperUser from api.serializers import BadgeSerializer, BadgeTokenSerializer @@ -79,3 +79,34 @@ def redeem_badge_token(request, conference, **kwargs): badge_token.redeem(user, False) return Response({'badge': f'{badge_token.badge}', 'user': f'{user.username}'}) + + +@api_view(['POST']) +def reward_badge_to_username(request, **kwargs): + badge = get_object_or_404(Badge, id=kwargs.get('pk')) + if not badge.issuing_assembly.user_can_manage(request.user): + return HttpResponse(status=403) + username = request.data.get('username', None) + user = get_object_or_404(PlatformUser, username__iexact=username, is_active=True) + UserBadge.objects.redeem_badge(user=user, badge=badge, issuer=request.user) + return Response({'badge': f'{badge.name}', 'rewarded': True}) + + +@api_view(['POST']) +def reward_badge_to_dect(request, **kwargs): + badge = get_object_or_404(Badge, pk=kwargs.get('pk')) + if not badge.issuing_assembly.user_can_manage(request.user): + return HttpResponse(status=403) + dect = request.data.get('dect', None) + if dect is None: + return HttpResponse(status=400) + try: + dect = int(dect) + except ValueError: + return HttpResponse(status=400) + try: + user = get_object_or_404(UserCommunicationChannel, address=dect).user + except UserCommunicationChannel.MultipleObjectsReturned: + return Response({'error': 'Unique user cannot be found through dect'}, status=400) + UserBadge.objects.redeem_badge(user=user, badge=badge, issuer=request.user) + return Response({'badge': f'{badge.name}', 'rewarded': True}) diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 5f859b6dbd58c8f833ba236dd80a63ccaa6c2602..8e142a60abb8524b7f265c0fe8d1fd02acbbfeb1 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -2351,6 +2351,9 @@ msgstr "Adresse öffentlich anzeigen" msgid "UserCommunicationChannel__show_public" msgstr "öffentlich" +msgid "UserCommunicationChannel__cannot_notify__dect" +msgstr "Dieser Kanal kann nicht für Benachrichtigungen benutzt werden." + msgid "UserCommunicationChannel__cannot_notify__phone" msgstr "Eine Benachrichtigung per Telefon wird nicht unterstützt." diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index a2be9e62b7a53bc5502bfd2de34bbe866affb35d..1703787ba58c3ea7cc8ec4e037041a0b7aa1d8ad 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -2349,6 +2349,9 @@ msgstr "show this address publicly" msgid "UserCommunicationChannel__show_public" msgstr "public" +msgid "UserCommunicationChannel__cannot_notify__dect" +msgstr "cannot use this channel for notifications" + msgid "UserCommunicationChannel__cannot_notify__phone" msgstr "notifications via phone/mobile are not supported" diff --git a/src/core/models/users.py b/src/core/models/users.py index 0ab6c716e25656a7d98b1e212303066f19a94872..9b73e9633d196cfb91c0430909d832796d49793e 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -563,6 +563,17 @@ class UserCommunicationChannel(models.Model): if url.host is None or url.path is None: raise ValidationError({'address': 'Expected valid URL.'}) + @staticmethod + def validate_dect(address): + try: + if len(str(address)) > 10: + raise ValidationError({'address': 'Dect may not be longer than 10 chars.'}) + if address[0] == '0': + raise ValidationError({'address': 'Dect must not start with a 0.'}) + int(address) + except ValueError: + raise ValidationError({'address': 'Dect must be a number.'}) + @property def can_notify(self): """Signals whether this channel can be used for notifications.""" @@ -571,19 +582,21 @@ class UserCommunicationChannel(models.Model): def clean(self): super().clean() - if self.channel == self.Channel.MAIL: - self.validate_email(self.address) - - elif self.channel == self.Channel.MATRIX: - self.validate_matrix_room_url(self.address) - - elif self.channel == self.Channel.PHONE: - self.validate_phone_number(self.address) - if self.use_for_notifications: - raise ValidationError({'use_for_notifications': _('UserCommunicationChannel__cannot_notify__phone')}) - - elif self.channel == self.Channel.ACTIVITYPUB: - self.validate_activitypub_url(self.address) + match self.channel: + case self.Channel.MAIL: + self.validate_email(self.address) + case self.Channel.MATRIX: + self.validate_matrix_room_url(self.address) + case self.Channel.DECT: + self.validate_dect(self.address) + if self.use_for_notifications: + raise ValidationError({'use_for_notifications': _('UserCommunicationChannel__cannot_notify__dect')}) + case self.Channel.PHONE: + self.validate_phone_number(self.address) + if self.use_for_notifications: + raise ValidationError({'use_for_notifications': _('UserCommunicationChannel__cannot_notify__phone')}) + case self.Channel.ACTIVITYPUB: + self.validate_activitypub_url(self.address) # TODO: verify the other channel types for correct syntax as well diff --git a/src/plainui/forms.py b/src/plainui/forms.py index 2f62412aabd617da3e5de06bbd872d2510183a48..bb7c18cc802f79fb1564501792e6dc24b9a7629c 100644 --- a/src/plainui/forms.py +++ b/src/plainui/forms.py @@ -92,6 +92,8 @@ class ExampleForm(forms.Form): class ProfileEditForm(TranslatedFieldsForm): + dect = forms.IntegerField(required=False) + class Meta: model = PlatformUser fields = [ @@ -99,6 +101,29 @@ class ProfileEditForm(TranslatedFieldsForm): 'timezone', ] + def __init__(self, *args, instance, **kwargs): + super().__init__(*args, instance=instance, **kwargs) + + dect_channel = UserCommunicationChannel.objects.filter( + user=instance, + channel=UserCommunicationChannel.Channel.DECT, + ).first() + if dect_channel: + self.fields['dect'].initial = dect_channel.address + + def clean_dect(self): + dect = self.cleaned_data['dect'] + # TODO: De-Duplicate with UserCommunicationChannel + try: + if len(str(dect)) > 10: + raise ValidationError('Dect may not be longer than 10 chars.') + if str(dect)[0] == '0': + raise ValidationError('Dect must not start with a 0.') + int(dect) + except ValueError: + raise ValidationError('Dect must be a number.') + return dect + class ProfileDescriptionEditForm(TranslatedFieldsForm): class Meta: diff --git a/src/plainui/jinja2/plainui/components/form_elements.html.j2 b/src/plainui/jinja2/plainui/components/form_elements.html.j2 index 6da3b51b269bedaed4a4c2530e6c9b94e09292d1..facb377b6000a23112675155a29cc4dd31ee6a6f 100644 --- a/src/plainui/jinja2/plainui/components/form_elements.html.j2 +++ b/src/plainui/jinja2/plainui/components/form_elements.html.j2 @@ -37,6 +37,9 @@ {% macro password(form, name) -%} {{ input(form, name, 'password') }} {%- endmacro %} +{% macro number(form, name) -%} + {{ input(form, name, 'number') }} +{%- endmacro %} {% macro textarea(form, name) -%} {% set el = form[name] -%} {% set my_id = unique_id() -%} @@ -132,6 +135,8 @@ {{ text(form, field_name) }} {% elif field.widget_type == 'password' -%} {{ password(form, field_name) }} + {% elif field.widget_type == 'number' -%} + {{ number(form, field_name) }} {% elif field.widget_type == 'checkbox' -%} {{ checkbox(form, field_name) }} {% elif field.widget_type == 'select' -%} diff --git a/src/plainui/tests/test_views.py b/src/plainui/tests/test_views.py index 373612288a64e6f8ff33971397c0008aeffa76c6..daef90c5d063f83c0ba48c23a374ea360c708c6f 100644 --- a/src/plainui/tests/test_views.py +++ b/src/plainui/tests/test_views.py @@ -47,6 +47,7 @@ from core.models import ( StaticPageRevision, TagItem, UserBadge, + UserCommunicationChannel, UserDereferrerAllowlist, ) from core.templatetags.hub_absolute import hub_absolute @@ -1297,6 +1298,7 @@ class ViewsTest(ViewsTestBase): 'default_badge_visibility': 'private', 'pronouns': 'they', 'timezone': 'Europe/Berlin', + 'dect': '1337', }, ) self.assertRedirects(resp, reverse('plainui:userprofile')) @@ -1307,6 +1309,7 @@ class ViewsTest(ViewsTestBase): self.assertEqual(self.user.pronouns, 'they') self.assertEqual(self.user.timezone.key, 'Europe/Berlin') self.assertSetsMessage(resp, 'Updated Profile') + self.assertTrue(UserCommunicationChannel.objects.filter(user=self.user, channel=UserCommunicationChannel.Channel.DECT, address='1337').exists()) @override_locale('en') def test_ModifyThemeView_get(self): diff --git a/src/plainui/views/user_profile.py b/src/plainui/views/user_profile.py index 2ba4f6639f95afdd467d5096e9c27499613882b2..cbf955451a7ed6d18c2e22f6fce1f5fe6e397992 100644 --- a/src/plainui/views/user_profile.py +++ b/src/plainui/views/user_profile.py @@ -24,6 +24,7 @@ from core.models import ( Event, Project, UserBadge, + UserCommunicationChannel, UserDereferrerAllowlist, ) from core.sso import SSO @@ -120,6 +121,20 @@ class ProfileView(ConferenceRequiredMixin, UpdateView): form1.instance.timezone = form1.cleaned_data['timezone'] form1.instance.save() + if form1.cleaned_data['dect']: + UserCommunicationChannel.objects.update_or_create( + user=self.request.user, + channel=UserCommunicationChannel.Channel.DECT, + defaults={'address': form1.cleaned_data['dect']}, + ) + else: + dect_channel = UserCommunicationChannel.objects.filter( + user=self.request.user, + channel=UserCommunicationChannel.Channel.DECT, + ) + if dect_channel.exists(): + dect_channel.delete() + # TODO: Update after deciding oh one or more conferences in #648 cm = form1.instance.conferences.filter(conference=self.conf).first() if cm: