From cad2f7373a716750d234842d93849af3014b72ad Mon Sep 17 00:00:00 2001 From: Lucas Brandstaetter <lucas@brandstaetter.tech> Date: Sun, 26 Nov 2023 19:48:00 +0100 Subject: [PATCH] Format and fix with ruff rules * Reformat files * Format Imports * Fix PL rules * Fix ERA rules * Fix RSE rules * Fix SIM rules * Fix UP rules --- deployment/docker/check_psql.py | 6 +- deployment/local_settings.docker-example.py | 9 +- src/api/permissions.py | 3 +- src/api/schedule.py | 134 +- src/api/serializers.py | 81 +- src/api/tests/__init__.py | 2 +- src/api/tests/badges/create_redeem_token.py | 34 +- src/api/tests/bbb.py | 33 +- src/api/tests/map.py | 9 +- src/api/tests/metrics.py | 30 +- src/api/tests/permissions.py | 6 +- src/api/tests/schedule.py | 158 ++- src/api/tests/workadventure.py | 25 +- src/api/urls.py | 22 +- src/api/urls_sso.py | 19 +- src/api/views/__init__.py | 1 - src/api/views/assemblies.py | 6 +- src/api/views/badges.py | 49 +- src/api/views/bbb.py | 2 +- src/api/views/conferencemember.py | 15 +- src/api/views/events.py | 14 +- src/api/views/maps.py | 9 +- src/api/views/metanav.py | 4 +- src/api/views/metrics.py | 148 +- src/api/views/mixins.py | 2 +- src/api/views/rooms.py | 2 +- src/api/views/schedule.py | 38 +- src/api/views/sso.py | 18 +- src/api/views/users.py | 62 +- src/api/views/workadventure.py | 52 +- src/backoffice/forms.py | 51 +- src/backoffice/templatetags/c3assemblies.py | 4 +- src/backoffice/tests/__init__.py | 4 +- src/backoffice/tests/assemblies.py | 13 +- src/backoffice/tests/auth.py | 29 +- src/backoffice/tests/base.py | 6 +- src/backoffice/urls.py | 59 +- src/backoffice/views/assemblies.py | 148 +- src/backoffice/views/assemblyteam.py | 143 +- src/backoffice/views/auth.py | 2 +- src/backoffice/views/badges.py | 2 +- src/backoffice/views/channelteam.py | 9 +- src/backoffice/views/events.py | 22 +- src/backoffice/views/map.py | 40 +- src/backoffice/views/misc.py | 14 +- src/backoffice/views/mixins.py | 296 ++-- src/backoffice/views/schedules.py | 10 +- src/backoffice/views/users.py | 23 +- src/backoffice/views/utils.py | 14 +- src/backoffice/views/vouchers.py | 3 +- src/backoffice/views/wiki.py | 23 +- src/backoffice/views/workadventure.py | 63 +- src/core/abuse.py | 24 +- src/core/admin.py | 469 ++++--- src/core/apps.py | 1 - src/core/base_forms.py | 8 +- src/core/forms.py | 4 +- src/core/integrations/__init__.py | 3 +- src/core/integrations/bigbluebutton.py | 43 +- src/core/integrations/workadventure.py | 26 +- .../commands/assembly_contact_mails.py | 2 +- .../commands/bbb_integration_revisit.py | 10 +- .../management/commands/hangar_creation.py | 4 +- src/core/management/commands/housekeeping.py | 4 +- .../commands/import_mapservice_resultfile.py | 3 +- .../management/commands/rerender_markdown.py | 2 +- .../management/commands/sanitize_database.py | 32 +- .../commands/schedule_join_rooms.py | 2 +- src/core/management/commands/serviceusers.py | 1 + src/core/management/commands/stats.py | 24 +- .../management/commands/suggestion_tick.py | 32 +- src/core/markdown.py | 131 +- src/core/middleware.py | 5 +- ...4_prepare_primary_keys_badge_badgetoken.py | 2 +- src/core/models/__init__.py | 65 +- src/core/models/assemblies.py | 191 ++- src/core/models/badges.py | 3 +- src/core/models/board.py | 4 +- src/core/models/conference.py | 250 ++-- src/core/models/dereferrer.py | 1 + src/core/models/events.py | 156 +-- src/core/models/map.py | 31 +- src/core/models/messages.py | 64 +- src/core/models/pages.py | 117 +- src/core/models/rooms.py | 56 +- src/core/models/schedules.py | 177 +-- src/core/models/shared.py | 11 +- src/core/models/sso.py | 8 +- src/core/models/tags.py | 28 +- src/core/models/ticket.py | 4 +- src/core/models/users.py | 251 ++-- src/core/models/voucher.py | 72 +- src/core/models/workadventure.py | 28 +- src/core/schedules/__init__.py | 1 - src/core/schedules/base.py | 8 +- src/core/schedules/schedulejson.py | 50 +- src/core/schedules/schedulexml.py | 49 +- src/core/search.py | 20 +- src/core/sso.py | 10 +- src/core/tests/__init__.py | 8 +- src/core/tests/assemblies.py | 33 +- src/core/tests/badges.py | 2 +- src/core/tests/bigbluebutton.py | 122 +- src/core/tests/events.py | 10 +- src/core/tests/exportcache.py | 2 +- src/core/tests/markdown.py | 1 - src/core/tests/schedules.py | 19 +- src/core/tests/search.py | 8 +- src/core/tests/tags.py | 38 +- src/core/tests/tickets.py | 17 +- src/core/tests/users.py | 4 +- src/core/tests/utils.py | 8 +- src/core/tests/validators.py | 4 +- src/core/tests/workadventure.py | 22 +- src/core/tokens.py | 5 +- src/core/translation.py | 40 +- src/core/utils.py | 36 +- src/core/validators.py | 60 +- src/core/views/auth.py | 4 +- src/hub/settings/base.py | 130 +- src/hub/settings/build.py | 2 +- src/hub/settings/default.py | 25 +- src/hub/settings/dev.py | 16 +- src/hub/views.py | 5 +- src/manage.py | 4 +- src/plainui/forms.py | 113 +- src/plainui/jinja2.py | 61 +- .../management/commands/makemessages.py | 4 +- src/plainui/tests/test_views.py | 1228 ++++++++++------- src/plainui/urls.py | 8 +- src/plainui/utils.py | 25 +- src/plainui/views.py | 684 ++++----- tests/utils.py | 1 - 133 files changed, 3757 insertions(+), 3450 deletions(-) diff --git a/deployment/docker/check_psql.py b/deployment/docker/check_psql.py index e542e5c8c..1c2d2e41d 100755 --- a/deployment/docker/check_psql.py +++ b/deployment/docker/check_psql.py @@ -9,17 +9,17 @@ except ImportError: import psycopg2 as psycopg -url = os.getenv("DATABASE_URL") +url = os.getenv('DATABASE_URL') if url is None or url == '': print('No DATABASE_URL specified!', file=sys.stderr) sys.exit(2) try: if url.startswith('postgis://'): - url = 'postgresql://' + url[len('postgis://'):] + url = 'postgresql://' + url[len('postgis://') :] psycopg.connect(url) except Exception as err: print('ERROR', file=sys.stderr) print(' ', err, sep='', file=sys.stderr) - exit(1) + sys.exit(1) diff --git a/deployment/local_settings.docker-example.py b/deployment/local_settings.docker-example.py index a07fb65e1..3a1449f28 100644 --- a/deployment/local_settings.docker-example.py +++ b/deployment/local_settings.docker-example.py @@ -1,5 +1,5 @@ # Dies ist ein Beispiel für eine local_settings.py in einem Docker-Deployment. - +# ruff: noqa: ERA001 IS_ADMIN = True IS_API = True IS_BACKOFFICE = True @@ -11,10 +11,7 @@ WORKADVENTURE_URL_SCHEME = 'https://play.{assembly_slug}.at.rc3.world/' # other LOGGING = { 'version': 1, 'disable_existing_loggers': False, - - 'filters': { - }, - + 'filters': {}, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', @@ -25,7 +22,6 @@ LOGGING = { 'style': '{', }, }, - 'handlers': { 'file': { 'level': 'INFO', @@ -45,7 +41,6 @@ LOGGING = { # 'class': 'django.utils.log.AdminEmailHandler' # }, }, - 'loggers': { 'django': { 'handlers': ['file', 'console'], diff --git a/src/api/permissions.py b/src/api/permissions.py index 5c2c7aae6..431a9b180 100644 --- a/src/api/permissions.py +++ b/src/api/permissions.py @@ -1,6 +1,7 @@ +from rest_framework import permissions + from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from rest_framework import permissions from core.models.assemblies import Assembly from core.models.badges import Badge diff --git a/src/api/schedule.py b/src/api/schedule.py index 33da86f37..5c9ed35a2 100644 --- a/src/api/schedule.py +++ b/src/api/schedule.py @@ -1,40 +1,42 @@ +# ruff: noqa: PLW2901 import json -import re import logging - +import re from collections import OrderedDict from datetime import datetime, timedelta +from typing import Optional from uuid import UUID + from lxml import etree as ET -from typing import Optional -# from xml.etree import cElementTree as ET from django.conf import settings from core.models.conference import Conference -from core.models.rooms import Room from core.models.events import Event +from core.models.rooms import Room logger = logging.getLogger(__name__) # template for optimized attribute order -event_template = OrderedDict({ - 'guid': None, - 'id': None, - 'date': None, - 'start': None, - 'duration': None, - 'room': None, - 'slug': None, - 'url': None, - 'title': None, - 'subtitle': None, - 'language': None, - 'track': None, - 'type': 'other', - 'abstract': None, - 'description': None, -}) +event_template = OrderedDict( + { + 'guid': None, + 'id': None, + 'date': None, + 'start': None, + 'duration': None, + 'room': None, + 'slug': None, + 'url': None, + 'title': None, + 'subtitle': None, + 'language': None, + 'track': None, + 'type': 'other', + 'abstract': None, + 'description': None, + } +) class Day: @@ -82,7 +84,7 @@ class ScheduleEncoder(json.JSONEncoder): tz = None def encode_duration(self, duration: Optional[timedelta]) -> Optional[str]: - """ converts a python `timedelta` to the schedule xml timedelta string that represents this timedelta. ([d:]HH:mm) """ + """converts a python `timedelta` to the schedule xml timedelta string that represents this timedelta. ([d:]HH:mm)""" if duration is None: return None @@ -91,39 +93,44 @@ class ScheduleEncoder(json.JSONEncoder): hours = duration.seconds // 3600 minutes = (duration.seconds % 3600) // 60 if days: - return f"{days}:{hours:02d}:{minutes:02d}" - return f"{hours:02d}:{minutes:02d}" + return f'{days}:{hours:02d}:{minutes:02d}' + return f'{hours:02d}:{minutes:02d}' def encode_event(self, event: Event, tz=None): start = event.schedule_start.astimezone(tz or self.tz) if event.schedule_start is not None else None additional_data = event.additional_data or {} legacy_id = additional_data.get('id') or int(re.sub('[^0-9]+', '', str(event.id))[0:6]) slug = f'{event.conference.slug}-{legacy_id}-{event.slug}' - return OrderedDict({ - **event_template, - 'id': legacy_id, - 'description': event.description, # TODO: if the description also exists in additional_data it is overwritten due to concatination with abstract - **additional_data, - 'slug': slug, - 'url': event.get_absolute_url(), - 'guid': event.id, - 'date': start.isoformat() if start is not None else None, - 'start': start.strftime('%H:%M') if start is not None else None, - 'duration': self.encode_duration(event.schedule_duration), - 'room': event.room.name if event.room is not None else None, - 'title': event.name, - 'language': event.language, - 'track': event.track.name if event.track else None, - }) + return OrderedDict( + { + **event_template, + 'id': legacy_id, + # TODO: if the description also exists in additional_data it is overwritten due to concatenation with abstract + 'description': event.description, + **additional_data, + 'slug': slug, + 'url': event.get_absolute_url(), + 'guid': event.id, + 'date': start.isoformat() if start is not None else None, + 'start': start.strftime('%H:%M') if start is not None else None, + 'duration': self.encode_duration(event.schedule_duration), + 'room': event.room.name if event.room is not None else None, + 'title': event.name, + 'language': event.language, + 'track': event.track.name if event.track else None, + } + ) def encode_day(self, obj: Day): - return OrderedDict({ - 'index': obj.day.index, - 'date': obj.day.start.strftime('%Y-%m-%d'), - 'day_start': obj.day.start.isoformat(), - 'day_end': obj.day.end.isoformat(), - 'rooms': obj.rooms - }) + return OrderedDict( + { + 'index': obj.day.index, + 'date': obj.day.start.strftime('%Y-%m-%d'), + 'day_start': obj.day.start.isoformat(), + 'day_end': obj.day.end.isoformat(), + 'rooms': obj.rooms, + } + ) def encode_room(self, obj: RoomDay): return obj.events @@ -131,10 +138,7 @@ class ScheduleEncoder(json.JSONEncoder): def transform(self, obj): if isinstance(obj, Schedule): self.tz = obj.tz - return { - '$schema': 'https://c3voc.de/schedule/schema.json', - 'schedule': obj._schedule - } + return {'$schema': 'https://c3voc.de/schedule/schema.json', 'schedule': obj._schedule} if isinstance(obj, Day): return self.encode_day(obj) if isinstance(obj, RoomDay): @@ -151,9 +155,10 @@ class ScheduleEncoder(json.JSONEncoder): class Schedule: - ''' + """ Schedule class with import and export methods - ''' + """ + _schedule = None tz = None conference = None @@ -163,7 +168,7 @@ class Schedule: self.tz = conference.timezone self._schedule = { - 'version': datetime.now(self.tz).strftime("%Y-%m-%d %H:%M"), + 'version': datetime.now(self.tz).strftime('%Y-%m-%d %H:%M'), 'base_url': settings.PLAINUI_BASE_URL, # 'https://events.ccc.de/' + conference.slug + '/', 'conference': { 'acronym': conference.slug, @@ -174,7 +179,7 @@ class Schedule: 'timeslot_duration': '00:10', 'time_zone_name': self.tz.key if self.tz.key else datetime.now(self.tz).tzname(), 'days': [Day(day) for day in conference.days], - } + }, } def __getitem__(self, key): @@ -206,7 +211,7 @@ class Schedule: def add_events(self, events): for event in events: - try: + try: # noqa: SIM105 self.add_event(event) except Warning: # TODO log event and error @@ -242,7 +247,7 @@ class Schedule: def _set_attrib(tag, k, v): if isinstance(v, str): tag.set(k, v) - elif isinstance(v, int) or isinstance(v, UUID): + elif isinstance(v, (UUID, int)): tag.set(k, str(v)) elif v is not None: logger.error('unknown attribute type %s=%s', k, v) @@ -259,7 +264,7 @@ class Schedule: elif parent == 'person': node.text = d['public_name'] _set_attrib(node, 'id', d['id']) - elif isinstance(d, dict) or isinstance(d, OrderedDict): + elif isinstance(d, (OrderedDict, dict)): if parent == 'schedule' and 'base_url' in d: d['conference']['base_url'] = d['base_url'] del d['base_url'] @@ -315,10 +320,7 @@ class Schedule: continue elif k == 'do_not_record': k = 'recording' - v = OrderedDict([ - ('license', recording_license), - ('optout', 'true' if v else 'false') - ]) + v = OrderedDict([('license', recording_license), ('optout', 'true' if v else 'false')]) if isinstance(v, RoomDay): _set_attrib(node_, 'guid', v.room.id) for event in v.events: @@ -333,13 +335,9 @@ class Schedule: _to_etree(v, ET.SubElement(node_, k), k) else: raise Exception('unknown type', d, parent) - # assert isinstance(self._schedule, dict) and len(self._schedule) == 1 - # encoder = ScheduleEncoder() - # schedule_dict = encoder.default(self.json()) root_node = ET.Element('schedule') - root_node.set("{http://www.w3.org/2001/XMLSchema-instance}noNamespaceSchemaLocation", "https://c3voc.de/schedule/schema.xsd") + root_node.set('{http://www.w3.org/2001/XMLSchema-instance}noNamespaceSchemaLocation', 'https://c3voc.de/schedule/schema.xsd') _to_etree(self._schedule, root_node, 'schedule') return ET.tounicode(root_node, doctype='<?xml version="1.0"?>') - # return ET.tostring(root_node, xml_declaration=True, encoding='utf-8', pretty_print = True, ) diff --git a/src/api/serializers.py b/src/api/serializers.py index fa6856eb0..15310fb7c 100644 --- a/src/api/serializers.py +++ b/src/api/serializers.py @@ -1,15 +1,18 @@ -from django.core.exceptions import SuspiciousOperation, ValidationError +import contextlib + from rest_framework import serializers from rest_framework.relations import HyperlinkedIdentityField from rest_framework.reverse import reverse +from django.core.exceptions import SuspiciousOperation, ValidationError + +from core.models.assemblies import Assembly from core.models.badges import Badge, BadgeToken, BadgeTokenTimeConstraint from core.models.conference import Conference, ConferenceMember, ConferenceTrack from core.models.events import Event from core.models.metanavi import MetaNavItem from core.models.rooms import Room from core.models.users import UserTimelineEntry -from core.models.assemblies import Assembly class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField): @@ -21,11 +24,12 @@ class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField): taken from https://github.com/encode/django-rest-framework/issues/1024 """ + lookup_fields = (('pk', 'pk'),) def __init__(self, *args, **kwargs): self.lookup_fields = kwargs.pop('lookup_fields', self.lookup_fields) - super(ParameterisedHyperlinkedIdentityField, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_url(self, obj, view_name, request, format): """ @@ -46,14 +50,8 @@ class ParameterisedHyperlinkedIdentityField(HyperlinkedIdentityField): class ValidatingModelSerializer(serializers.ModelSerializer): def validate(self, data): - instance = self.Meta.model(**{ - field: value - for field, value in data.items() - if field in self.Meta.model._meta.fields - }) - for f, v in {field: value - for field, value in data.items() - if field in self.Meta.model._meta.fields}.items(): + instance = self.Meta.model(**{field: value for field, value in data.items() if field in self.Meta.model._meta.fields}) + for f, v in {field: value for field, value in data.items() if field in self.Meta.model._meta.fields}.items(): getattr(instance, f).set(v) try: instance.clean() @@ -81,13 +79,11 @@ class HubModelSerializer(ValidatingModelSerializer): self.request_user = request_user = self.context['request'].user self.conference_member = None if request_user.is_authenticated: - try: + with contextlib.suppress(ConferenceMember.DoesNotExist): self.conference_member = ConferenceMember.objects.select_related('conference').get( conference__slug=conference_slug, user=request_user, ) - except ConferenceMember.DoesNotExist: - pass # store if the request's user has staff permissions in the conference (either direct or globally) self.is_staff = request_user.is_superuser or request_user.is_staff or (self.conference_member is not None and self.conference_member.is_staff) @@ -104,18 +100,16 @@ class HubModelSerializer(ValidatingModelSerializer): class ConferenceSerializer(HubModelSerializer): - tracks = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='slug' - ) + tracks = serializers.SlugRelatedField(many=True, read_only=True, slug_field='slug') class Meta: model = Conference fields = [ - 'slug', 'name', + 'slug', + 'name', 'is_public', - 'start', 'end', + 'start', + 'end', 'registration_deadline', 'tracks', ] @@ -126,27 +120,19 @@ class ConferenceTrackSerializer(HubModelSerializer): class Meta: model = ConferenceTrack read_only_fields = ['id'] - fields = [ - 'conference', - 'slug', - 'name', - 'is_public', - 'id' - ] + fields = ['conference', 'slug', 'name', 'is_public', 'id'] staff_only_fields = ['is_public'] class AssemblySerializer(HubModelSerializer): - conference = serializers.SlugRelatedField( - read_only=True, - slug_field='slug' - ) + conference = serializers.SlugRelatedField(read_only=True, slug_field='slug') parent = serializers.SlugRelatedField(slug_field='slug', queryset=Assembly.objects.none) events_url = ParameterisedHyperlinkedIdentityField(view_name='api:assembly-events', lookup_fields=(('conference_slug', 'conference'), ('slug', 'assembly'))) rooms_url = ParameterisedHyperlinkedIdentityField(view_name='api:assembly-rooms', lookup_fields=(('conference_slug', 'conference'), ('slug', 'assembly'))) - badges_url = ParameterisedHyperlinkedIdentityField(view_name='api:assembly-badges-list-create', - lookup_fields=(('conference_slug', 'conference'), ('slug', 'assembly'))) + badges_url = ParameterisedHyperlinkedIdentityField( + view_name='api:assembly-badges-list-create', lookup_fields=(('conference_slug', 'conference'), ('slug', 'assembly')) + ) class Meta: model = Assembly @@ -178,11 +164,11 @@ class BadgeSerializer(HubModelSerializer): class Meta: model = Badge exclude = ['issuing_assembly', 'conference', 'issuing_token'] - badge_token_url = ParameterisedHyperlinkedIdentityField(view_name='api:badge-token-list-create', - lookup_fields=( - ('issuing_assembly.conference_slug', 'conference'), - ('issuing_assembly.slug', 'assembly'), - ('pk', 'pk'))) + + badge_token_url = ParameterisedHyperlinkedIdentityField( + view_name='api:badge-token-list-create', + lookup_fields=(('issuing_assembly.conference_slug', 'conference'), ('issuing_assembly.slug', 'assembly'), ('pk', 'pk')), + ) def create(self, validated_data): issuing_assembly = Assembly.objects.filter(slug=self.context['assembly']) @@ -223,15 +209,9 @@ class BadgeTokenUpdateSerializer(BadgeTokenSerializer): class RoomSerializer(HubModelSerializer): - conference = serializers.SlugRelatedField( - read_only=True, - slug_field='slug' - ) + conference = serializers.SlugRelatedField(read_only=True, slug_field='slug') - assembly = serializers.SlugRelatedField( - read_only=True, - slug_field='slug' - ) + assembly = serializers.SlugRelatedField(read_only=True, slug_field='slug') links = serializers.StringRelatedField( many=True, @@ -288,7 +268,7 @@ class EventSerializer(HubModelSerializer): def update(self, instance, validated_data): # is somebody trying to change the event ID? if (given_id := validated_data.get('id')) is not None and given_id != instance.id: - raise SuspiciousOperation('You cannot update an event\'s id.') + raise SuspiciousOperation("You cannot update an event's id.") # prevent some fields from being updated, no matter what for protected_field in ['id', 'conference']: @@ -312,10 +292,7 @@ class UserTimelineEntrySerializer(ValidatingModelSerializer): class MetaNavItemSerializer(HubModelSerializer): - conference = serializers.SlugRelatedField( - read_only=True, - slug_field='slug' - ) + conference = serializers.SlugRelatedField(read_only=True, slug_field='slug') class Meta: model = MetaNavItem diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py index e6a579db4..8fb7973a1 100644 --- a/src/api/tests/__init__.py +++ b/src/api/tests/__init__.py @@ -6,4 +6,4 @@ from .metrics import * # noqa: F401, F403 from .schedule import * # noqa: F401, F403 from .workadventure import * # noqa: F401, F403 -__all__ = '*' +__all__ = ('*',) # noqa: F405 diff --git a/src/api/tests/badges/create_redeem_token.py b/src/api/tests/badges/create_redeem_token.py index 9f8ea1f23..125f02f8e 100644 --- a/src/api/tests/badges/create_redeem_token.py +++ b/src/api/tests/badges/create_redeem_token.py @@ -1,11 +1,11 @@ from datetime import datetime +from http import HTTPStatus -from django.test import TestCase -from django.urls import reverse from rest_framework.authtoken.models import Token from zoneinfo import ZoneInfo -from http import HTTPStatus +from django.test import TestCase +from django.urls import reverse from core.models import Assembly, Badge, BadgeToken, Conference, PlatformUser @@ -14,7 +14,9 @@ class CreateRedeemTokenTests(TestCase): def setUp(self): tz = ZoneInfo('CET') self.conf = Conference( - slug='conf', name='TestConf', is_public=True, + slug='conf', + name='TestConf', + is_public=True, start=datetime(2020, 12, 27, 1, 23, 45, tzinfo=tz), end=datetime(2020, 12, 30, 23, 45, 00, tzinfo=tz), ) @@ -22,7 +24,11 @@ class CreateRedeemTokenTests(TestCase): self.assembly = Assembly(name='TestAssembly', slug='asmbly', conference=self.conf, state_assembly=Assembly.State.PLACED) self.assembly.save() - self.badge = Badge(conference=self.conf, issuing_assembly=self.assembly, name="Test Badge", ) + self.badge = Badge( + conference=self.conf, + issuing_assembly=self.assembly, + name='Test Badge', + ) self.badge_token = self.badge.reset_token() self.user = PlatformUser(username='bernd', is_active=True) @@ -32,27 +38,27 @@ class CreateRedeemTokenTests(TestCase): self.token.save() def test_create_redeem_token_invalid_issuing_token(self): - url = reverse('api:badge-token-create-with-token', kwargs={'conference': self.conf.slug, 'assembly': self.assembly, "issuing_token": "invalid"}) - resp = self.client.post(url, {"issuing_token": "test"}) + url = reverse('api:badge-token-create-with-token', kwargs={'conference': self.conf.slug, 'assembly': self.assembly, 'issuing_token': 'invalid'}) + resp = self.client.post(url, {'issuing_token': 'test'}) self.assertEqual(resp.status_code, 404) def test_create_redeem_invalid_conference(self): - url = reverse('api:badge-token-create-with-token', kwargs={'conference': "invalid", 'assembly': self.assembly, "issuing_token": self.badge_token}) - resp = self.client.post(url, {"issuing_token": self.badge_token, "badge_class": "single"}) + url = reverse('api:badge-token-create-with-token', kwargs={'conference': 'invalid', 'assembly': self.assembly, 'issuing_token': self.badge_token}) + resp = self.client.post(url, {'issuing_token': self.badge_token, 'badge_class': 'single'}) self.assertEqual(resp.status_code, 404) def test_create_redeem_token_limited(self): - url = reverse('api:badge-token-create-with-token', kwargs={'conference': self.conf.slug, 'assembly': self.assembly, "issuing_token": self.badge_token}) - resp = self.client.post(url, {"issuing_token": self.badge_token, "redeemable_count": 20}) + url = reverse('api:badge-token-create-with-token', kwargs={'conference': self.conf.slug, 'assembly': self.assembly, 'issuing_token': self.badge_token}) + resp = self.client.post(url, {'issuing_token': self.badge_token, 'redeemable_count': 20}) self.assertEqual(resp.status_code, HTTPStatus.CREATED) badge_token = BadgeToken.objects.get(badge=self.badge) - self.assertEqual(str(badge_token.token), resp.json()["token"]) + self.assertEqual(str(badge_token.token), resp.json()['token']) self.assertEqual(badge_token.redeemable_count, 20) def test_create_redeem_token(self): - url = reverse('api:badge-token-create-with-token', kwargs={'conference': self.conf.slug, 'assembly': self.assembly, "issuing_token": self.badge_token}) + url = reverse('api:badge-token-create-with-token', kwargs={'conference': self.conf.slug, 'assembly': self.assembly, 'issuing_token': self.badge_token}) resp = self.client.post(url, {}) self.assertEqual(resp.status_code, HTTPStatus.CREATED) badge_token = BadgeToken.objects.get(badge=self.badge) - self.assertEqual(str(badge_token.token), resp.json()["token"]) + self.assertEqual(str(badge_token.token), resp.json()['token']) self.assertEqual(badge_token.redeemable_count, 0) diff --git a/src/api/tests/bbb.py b/src/api/tests/bbb.py index c285cbc3e..8ad086159 100644 --- a/src/api/tests/bbb.py +++ b/src/api/tests/bbb.py @@ -1,4 +1,5 @@ from uuid import uuid4 + from django.test import TestCase from django.urls import reverse @@ -12,22 +13,32 @@ class BBBTest(TestCase): assembly = Assembly(name='TestAssembly', slug='asmbly', conference=conf) assembly.save() room = Room( - conference=conf, assembly=assembly, name='Room 1', - room_type=Room.RoomType.BIGBLUEBUTTON, backend_status=Room.BackendStatus.ACTIVE, - backend_link=str(uuid4()), backend_data={'close_secret': 'asdf'} + conference=conf, + assembly=assembly, + name='Room 1', + room_type=Room.RoomType.BIGBLUEBUTTON, + backend_status=Room.BackendStatus.ACTIVE, + backend_link=str(uuid4()), + backend_data={'close_secret': 'asdf'}, ) room.save() - self.client.get(reverse('api:bbb_meeting_end'), { - 'meetingID': room.backend_link, - 'close_secret': 'invalid', - }) + self.client.get( + reverse('api:bbb_meeting_end'), + { + 'meetingID': room.backend_link, + 'close_secret': 'invalid', + }, + ) room.refresh_from_db() self.assertEqual(room.backend_status, Room.BackendStatus.ACTIVE) - self.client.get(reverse('api:bbb_meeting_end'), { - 'meetingID': room.backend_link, - 'close_secret': 'asdf', - }) + self.client.get( + reverse('api:bbb_meeting_end'), + { + 'meetingID': room.backend_link, + 'close_secret': 'asdf', + }, + ) room.refresh_from_db() self.assertEqual(room.backend_status, Room.BackendStatus.INACTIVE) diff --git a/src/api/tests/map.py b/src/api/tests/map.py index 29667843b..7b30bb3c4 100644 --- a/src/api/tests/map.py +++ b/src/api/tests/map.py @@ -1,19 +1,22 @@ -from datetime import datetime import json +from datetime import datetime + from zoneinfo import ZoneInfo from django.contrib.gis.geos import Point from django.test import TestCase from django.urls import reverse -from core.models import Assembly, ConferenceExportCache, Conference, PlatformUser +from core.models import Assembly, Conference, ConferenceExportCache, PlatformUser class MapTest(TestCase): def setUp(self): tz = ZoneInfo('CET') self.conf = Conference( - slug='conf', name='TestConf', is_public=True, + slug='conf', + name='TestConf', + is_public=True, start=datetime(2020, 12, 27, 1, 23, 45, tzinfo=tz), end=datetime(2020, 12, 30, 23, 45, 00, tzinfo=tz), ) diff --git a/src/api/tests/metrics.py b/src/api/tests/metrics.py index 20e03fe68..6f56c2432 100644 --- a/src/api/tests/metrics.py +++ b/src/api/tests/metrics.py @@ -1,15 +1,14 @@ from uuid import uuid4 +from django.contrib.auth import get_user_model from django.test import TestCase, override_settings from django.urls import reverse -from django.contrib.auth import get_user_model from core.models import Assembly, Conference, Room, WorkadventureSession from core.models.conference import ConferenceMember class MetricsTest(TestCase): - def test_MetricsView_allow_ip_addresses(self): with self.settings(METRICS_SERVER_IPS=['1.2.3.4']): resp = self.client.get(reverse('metrics:index')) @@ -26,7 +25,7 @@ class MetricsTest(TestCase): # Our address is not 127.0.0.1 so we should get a `HTTP 200`. self.assertEqual(200, resp.status_code) - @override_settings(METRICS_SERVER_IPS="*") + @override_settings(METRICS_SERVER_IPS='*') def test_MetricsView(self): conf = Conference(slug='conf', name='TestConf', is_public=True) conf.save() @@ -38,24 +37,27 @@ class MetricsTest(TestCase): ) assembly.save() room = Room( - conference=conf, assembly=assembly, name='Room 1', - room_type=Room.RoomType.BIGBLUEBUTTON, backend_status=Room.BackendStatus.ACTIVE, - backend_link=str(uuid4()), backend_data={'close_secret': 'asdf'}, + conference=conf, + assembly=assembly, + name='Room 1', + room_type=Room.RoomType.BIGBLUEBUTTON, + backend_status=Room.BackendStatus.ACTIVE, + backend_link=str(uuid4()), + backend_data={'close_secret': 'asdf'}, ) room.save() - user = get_user_model()( - username="testuser" - ) + user = get_user_model()(username='testuser') user.save() - member = ConferenceMember( - conference=conf, active_angel=False, user=user - ) + member = ConferenceMember(conference=conf, active_angel=False, user=user) member.save() wa_room = Room( - conference=conf, assembly=assembly, name='Room 2', - room_type=Room.RoomType.WORKADVENTURE, backend_status=Room.BackendStatus.ACTIVE, + conference=conf, + assembly=assembly, + name='Room 2', + room_type=Room.RoomType.WORKADVENTURE, + backend_status=Room.BackendStatus.ACTIVE, ) wa_room.save() wa_sess1 = WorkadventureSession.create_for_conference_user(conf, user, wa_room) diff --git a/src/api/tests/permissions.py b/src/api/tests/permissions.py index 021da5eee..92fef7d75 100644 --- a/src/api/tests/permissions.py +++ b/src/api/tests/permissions.py @@ -1,8 +1,11 @@ from unittest.mock import Mock +from rest_framework.permissions import BasePermission + from django.contrib.auth.models import AnonymousUser from django.test import RequestFactory, TestCase, override_settings -from rest_framework.permissions import BasePermission + +from core.models import Assembly, AssemblyMember, Badge, Conference, ConferenceMember, PlatformUser from api.permissions import ( AssemblyPermission, @@ -16,7 +19,6 @@ from api.permissions import ( IsReadOnly, IsSuperUser, ) -from core.models import Assembly, AssemblyMember, Badge, Conference, ConferenceMember, PlatformUser class PermissionTestCase(TestCase): diff --git a/src/api/tests/schedule.py b/src/api/tests/schedule.py index 16ce0b4a7..24d394e40 100644 --- a/src/api/tests/schedule.py +++ b/src/api/tests/schedule.py @@ -1,21 +1,24 @@ -from datetime import datetime, timedelta import json import xml.etree.ElementTree as ET +from datetime import datetime, timedelta + +from rest_framework.authtoken.models import Token from zoneinfo import ZoneInfo from django.test import TestCase from django.urls import reverse from django.utils.timezone import now -from rest_framework.authtoken.models import Token -from core.models import Assembly, ConferenceExportCache, Conference, Event, PlatformUser, Room +from core.models import Assembly, Conference, ConferenceExportCache, Event, PlatformUser, Room class ScheduleTest(TestCase): def setUp(self): tz = ZoneInfo('CET') self.conf = Conference( - slug='conf', name='TestConf', is_public=True, + slug='conf', + name='TestConf', + is_public=True, start=datetime(2020, 12, 27, 1, 23, 45, tzinfo=tz), end=datetime(2020, 12, 30, 23, 45, 00, tzinfo=tz), ) @@ -110,27 +113,27 @@ class ScheduleTest(TestCase): event.save() update = { - "url": "https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html", - "id": 11583, - "guid": str(event.id), - "logo": None, - "date": "2020-12-27T12:20:00+01:00", - "start": "12:20", - "duration": "00:30", - "room": "foo room", - "slug": "rc3-11583-rc3_eroffnung", - "title": "#rC3 Er\u00f6ffnung", - "subtitle": "", - "track": "Community", - "type": "Talk", - "language": "de", - "abstract": "Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!", - "description": "", - "recording_license": "", - "do_not_record": False, - "persons": [{"id": 14151, "public_name": "blubbel"}], - "links": [], - "attachments": [] + 'url': 'https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html', + 'id': 11583, + 'guid': str(event.id), + 'logo': None, + 'date': '2020-12-27T12:20:00+01:00', + 'start': '12:20', + 'duration': '00:30', + 'room': 'foo room', + 'slug': 'rc3-11583-rc3_eroffnung', + 'title': '#rC3 Er\u00f6ffnung', + 'subtitle': '', + 'track': 'Community', + 'type': 'Talk', + 'language': 'de', + 'abstract': 'Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!', + 'description': '', + 'recording_license': '', + 'do_not_record': False, + 'persons': [{'id': 14151, 'public_name': 'blubbel'}], + 'links': [], + 'attachments': [], } url = reverse('api:event-schedule', kwargs={'conference': self.conf.slug, 'pk': event.pk}) @@ -146,9 +149,7 @@ class ScheduleTest(TestCase): self.assertEqual(timedelta(minutes=30), event.schedule_duration) self.assertIsNotNone(event.schedule_end) - update = { - "public": True - } + update = {'public': True} with self.modify_settings(API_USERS={'append': self.user.username}): resp = self.client.post(url, json.dumps(update), content_type='application/json', HTTP_AUTHORIZATION=f'Token {self.token.key}') @@ -166,35 +167,35 @@ class ScheduleTest(TestCase): another_room.save() update = { - "url": "https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html", - "id": 11583, - "guid": "d9334deb-f183-4aec-9c6c-137741f6ff73", - "logo": None, - "date": "2020-12-27T12:20:00+01:00", - "start": "12:20", - "duration": "01:30", - "room": "foo room", - "room_id": str(another_room.pk), - "slug": "rc3-11583-rc3_eroffnung", - "title": "#rC3 Er\u00f6ffnung", - "subtitle": "", - "type": "Talk", - "language": "de", - "abstract": "Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!", - "description": "", - "recording_license": "", - "do_not_record": False, - "persons": [{"id": 14151, "public_name": "blubbel"}], - "links": [], - "attachments": [], - "public": False + 'url': 'https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html', + 'id': 11583, + 'guid': 'd9334deb-f183-4aec-9c6c-137741f6ff73', + 'logo': None, + 'date': '2020-12-27T12:20:00+01:00', + 'start': '12:20', + 'duration': '01:30', + 'room': 'foo room', + 'room_id': str(another_room.pk), + 'slug': 'rc3-11583-rc3_eroffnung', + 'title': '#rC3 Er\u00f6ffnung', + 'subtitle': '', + 'type': 'Talk', + 'language': 'de', + 'abstract': 'Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!', + 'description': '', + 'recording_license': '', + 'do_not_record': False, + 'persons': [{'id': 14151, 'public_name': 'blubbel'}], + 'links': [], + 'attachments': [], + 'public': False, } self.assertFalse(Event.objects.filter(pk=update['guid']).exists()) url = reverse('api:event-schedule', kwargs={'conference': self.conf.slug, 'pk': update['guid']}) - with self.modify_settings(API_USERS={'append': self.user.username}), self.assertLogs("api.views.schedule", "WARNING"): + with self.modify_settings(API_USERS={'append': self.user.username}), self.assertLogs('api.views.schedule', 'WARNING'): resp = self.client.post(url, json.dumps(update), content_type='application/json', HTTP_AUTHORIZATION=f'Token {self.token.key}') self.assertEqual(201, resp.status_code, f'Unexpected result from POST: {resp.content}') @@ -209,10 +210,7 @@ class ScheduleTest(TestCase): self.assertEqual(another_room.pk, event.room_id, 'Expected import to prefer "room_id" over "room".') - update = { - "public": False, - "track": "Security" - } + update = {'public': False, 'track': 'Security'} with self.modify_settings(API_USERS={'append': self.user.username}): resp = self.client.post(url, json.dumps(update), content_type='application/json', HTTP_AUTHORIZATION=f'Token {self.token.key}') @@ -228,35 +226,35 @@ class ScheduleTest(TestCase): def testPushInvalidTrack(self): update = { - "url": "https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html", - "id": 11583, - "guid": "d9334deb-f183-4aec-9c6c-137741f6ff73", - "logo": None, - "date": "2020-12-27T12:20:00+01:00", - "start": "12:20", - "duration": "01:30", - "room": "foo room", - "slug": "rc3-11583-rc3_eroffnung", - "title": "#rC3 Er\u00f6ffnung", - "subtitle": "", - "type": "Talk", - "language": "de", - "abstract": "Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!", - "description": "", - "recording_license": "", - "do_not_record": False, - "persons": [{"id": 14151, "public_name": "blubbel"}], - "links": [], - "attachments": [], - "public": False, - "track": "Fnord" + 'url': 'https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html', + 'id': 11583, + 'guid': 'd9334deb-f183-4aec-9c6c-137741f6ff73', + 'logo': None, + 'date': '2020-12-27T12:20:00+01:00', + 'start': '12:20', + 'duration': '01:30', + 'room': 'foo room', + 'slug': 'rc3-11583-rc3_eroffnung', + 'title': '#rC3 Er\u00f6ffnung', + 'subtitle': '', + 'type': 'Talk', + 'language': 'de', + 'abstract': 'Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!', + 'description': '', + 'recording_license': '', + 'do_not_record': False, + 'persons': [{'id': 14151, 'public_name': 'blubbel'}], + 'links': [], + 'attachments': [], + 'public': False, + 'track': 'Fnord', } self.assertFalse(Event.objects.filter(pk=update['guid']).exists()) url = reverse('api:event-schedule', kwargs={'conference': self.conf.slug, 'pk': update['guid']}) - with self.modify_settings(API_USERS={'append': self.user.username}), self.assertLogs("api.views.schedule", "WARNING"): + with self.modify_settings(API_USERS={'append': self.user.username}), self.assertLogs('api.views.schedule', 'WARNING'): resp = self.client.post(url, json.dumps(update), content_type='application/json', HTTP_AUTHORIZATION=f'Token {self.token.key}') self.assertEqual(400, resp.status_code, f'Unexpected success result from POST: {resp.content}') @@ -282,8 +280,8 @@ class ScheduleTest(TestCase): for remaining_day in remaining_days: self.assertEqual(len(remaining_day), 0) - room, = day1 - event1, = room + (room,) = day1 + (event1,) = room self.assertEqual(event1.findtext('title'), ev1.name) self.assertEqual(event1.findtext('date'), '2020-12-27T01:23:45+01:00') self.assertEqual(event1.findtext('start'), '01:23') diff --git a/src/api/tests/workadventure.py b/src/api/tests/workadventure.py index 87d0498fb..c329b9b1e 100644 --- a/src/api/tests/workadventure.py +++ b/src/api/tests/workadventure.py @@ -1,9 +1,10 @@ import json from pathlib import Path +from rest_framework.authtoken.models import Token + from django.conf import settings from django.test import Client, TestCase, override_settings -from rest_framework.authtoken.models import Token from core.models import Assembly, Conference, ConferenceMember, PlatformUser, Room from core.sso import SSO @@ -103,7 +104,7 @@ class WorkAdventureMapServiceTestCase(_WorkAdventureTestCase): self.assertTrue(all(k in x for x in endpoint_data), f'The field "{k}" is missing on at least one of returned items.') self.assertTrue( all(x['assembly_url'].startswith('http://test.localhost/') for x in endpoint_data), - 'The assembly_url field is empty on at least one of returned items.' + 'The assembly_url field is empty on at least one of returned items.', ) def testPush(self): @@ -139,7 +140,7 @@ class WorkAdventureMapServiceTestCase(_WorkAdventureTestCase): room5a.save() # load import data and fill in assembly and room ids - with Path(__file__).with_name("wa_mapservice_import.json").open() as f: + with Path(__file__).with_name('wa_mapservice_import.json').open() as f: import_json = f.read() import_replacements = { 'ASSEMBLY_1_ID': self.assembly1a.id, @@ -182,7 +183,7 @@ class WorkAdventureMapServiceTestCase(_WorkAdventureTestCase): room2a.refresh_from_db() self.assertEqual(Room.BackendStatus.ERROR, room2a.backend_status, 'the previously active room should have error state now') room4a.refresh_from_db() - self.assertEqual(Room.BackendStatus.NEW, room4a.backend_status, 'the fresh room shouldn\'t have changed status') + self.assertEqual(Room.BackendStatus.NEW, room4a.backend_status, "the fresh room shouldn't have changed status") self.assertEqual('error', room4a.director_data.get('violation', {}).get('severity', '(not set)').lower()) # fetch complete list again and check what's there @@ -191,7 +192,7 @@ class WorkAdventureMapServiceTestCase(_WorkAdventureTestCase): self.assertEqual('active', x['backend_status']) -class WorkAdventureEndpointTestCase(object): # TODO: enable again -- _WorkAdventureTestCase): +class WorkAdventureEndpointTestCase: # TODO: enable again -- _WorkAdventureTestCase): def setUp(self): super().setUp() @@ -265,8 +266,8 @@ class WorkAdventureEndpointTestCase(object): # TODO: enable again -- _WorkAdven self.assertEqual( self.room1c.reserve_capacity, - data["reserve_capacity"], - f'WA endpoint returned room_capacity {data["reserve_capacity"]} when room capacity was {self.room1c.reserve_capacity}' + data['reserve_capacity'], + f'WA endpoint returned room_capacity {data["reserve_capacity"]} when room capacity was {self.room1c.reserve_capacity}', ) for k in ['room_id', 'assembly_id', 'blocked']: self.assertTrue(k in data, f'The field "{k}" is missing on at least one of returned items.') @@ -300,7 +301,7 @@ class WorkAdventureEndpointTestCase(object): # TODO: enable again -- _WorkAdven self.assertEqual(42, data.get('director_data', {}).get('fnord')) -class WorkAdventureBackendTestCase(object): # TODO: enable again -- _WorkAdventureTestCase): +class WorkAdventureBackendTestCase: # TODO: enable again -- _WorkAdventureTestCase): def setUp(self): super().setUp() @@ -312,9 +313,11 @@ class WorkAdventureBackendTestCase(object): # TODO: enable again -- _WorkAdvent self.service_token = Token(user=self.service_user) self.service_token.save() self.user_token = SSO.generate_token(self.human_user) - self.client.defaults.update({ - f'HTTP_{settings.SSO_HEADER}': str(self.user_token), - }) + self.client.defaults.update( + { + f'HTTP_{settings.SSO_HEADER}': str(self.user_token), + } + ) def testFetch(self): resp = self.client.get( diff --git a/src/api/urls.py b/src/api/urls.py index 86996c8c6..a0e94b2e6 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -1,15 +1,13 @@ -from django.urls import path -from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.authtoken import views as authtoken_views +from rest_framework.urlpatterns import format_suffix_patterns -from .views import api_root -from .views import assemblies, badges, bbb, conferencemember, conferences, events, maps, metanav, rooms, schedule, users, workadventure +from django.urls import path +from .views import api_root, assemblies, badges, bbb, conferencemember, conferences, events, maps, metanav, rooms, schedule, users, workadventure app_name = 'api' urlpatterns = [ path('', api_root, name='index'), - # user data/stuff path('me', users.profile, name='profile'), path('badges.zip', users.BadgeExportView.as_view(), name='badge-export'), @@ -17,7 +15,6 @@ urlpatterns = [ path('friends', users.friends, name='friends'), path('conferences', conferences.ConferenceList.as_view(), name='conference-list'), path('timeline', users.UserTimelineList.as_view(), name='timeline-list'), - # conference-specific views path('c/<slug:conference>/', conferences.ConferenceDetail.as_view(), name='conference-detail'), path('c/<slug:conference>/metanav', metanav.MetaNavView.as_view(), name='conference-metanav'), @@ -30,8 +27,11 @@ urlpatterns = [ path('c/<slug:conference>/assembly/<slug:assembly>/rooms', assemblies.ConferenceAssemblyRoomList.as_view(), name='assembly-rooms'), path('c/<slug:conference>/assembly/<slug:assembly>/schedule', schedule.AssemblySchedule.as_view(), name='assembly-schedule'), path('c/<slug:conference>/assembly/<slug:assembly>/badges', badges.BadgeListCreate.as_view(), name='assembly-badges-list-create'), - path('c/<slug:conference>/assembly/<slug:assembly>/issue_redeem_token/<str:issuing_token>', - badges.BadgeTokenListCreate.as_view(), name='badge-token-create-with-token'), + path( + 'c/<slug:conference>/assembly/<slug:assembly>/issue_redeem_token/<str:issuing_token>', + badges.BadgeTokenListCreate.as_view(), + name='badge-token-create-with-token', + ), path('c/<slug:conference>/assembly/<slug:assembly>/badges/<uuid:pk>', badges.BadgeTokenListCreate.as_view(), name='badge-token-list-create'), path('c/<slug:conference>/map/poi.json', maps.PoiExportView.as_view(), name='map-poi'), path('c/<slug:conference>/map/assemblies/poi.json', maps.AssembliesPoiExportView.as_view(), name='map-assemblies-poi'), @@ -44,26 +44,20 @@ urlpatterns = [ path('c/<slug:conference>/events', events.EventList.as_view(), name='event-list'), path('c/<slug:conference>/event/<uuid:pk>/', events.EventDetail.as_view(), name='event-detail'), path('c/<slug:conference>/event/<uuid:pk>/schedule', schedule.EventSchedule.as_view(), name='event-schedule'), - # WorkAdventure integration (mapservice) path('c/<slug:conference>/workadventure/maps', workadventure.MapServiceView.as_view()), path('c/<slug:conference>/workadventure/maps/raw', workadventure.MapServiceView.as_view(), name='wa-mapservice'), path('c/<slug:conference>/workadventure/map/<slug:assembly>/<slug:world>', workadventure.MapDetailView.as_view(), name='wa-map-detail'), path('c/<slug:conference>/workadventure/map/<slug:assembly>/<slug:world>/<slug:room>', workadventure.MapDetailView.as_view(), name='wa-map-detail'), - # WorkAdventure integration (exneuland) path('c/<slug:conference>/workadventure/maps/list', workadventure.MapBackendListView.as_view(), name='wa-backend-maps'), path('c/<slug:conference>/workadventure/login/<str:token>', workadventure.RegisterView.as_view(), name='wa-register'), path('c/<slug:conference>/workadventure/checkuser/<uuid:uid>', workadventure.UserInfoView.as_view(), name='wa-userinfo'), - path('c/<slug:conference>/workadventure/redeem_badge/<str:token>', workadventure.MapBackendRedeemBadgeTokenView.as_view(), name='wa-badgeredeem'), - path('c/<slug:conference>/workadventure/report/user', workadventure.MapBackendReportUserView.as_view(), name='wa-report-user'), path('c/<slug:conference>/workadventure/report/map', workadventure.MapBackendReportUserView.as_view(), name='wa-report-user'), - # integration with other components path('c/<slug:conference>/is_angel/<str:username>', conferencemember.AngelView.as_view(), name='user-angel'), - # BBB meeting ended callback path('bbb_meeting_end', bbb.MeetingEnded.as_view(), name='bbb_meeting_end'), ] diff --git a/src/api/urls_sso.py b/src/api/urls_sso.py index 294ec61ec..d0b90462b 100644 --- a/src/api/urls_sso.py +++ b/src/api/urls_sso.py @@ -1,21 +1,18 @@ -from django.urls import re_path from oauth2_provider import views -from .views import sso as sso_views +from django.urls import re_path +from .views import sso as sso_views -app_name = "oauth2_provider" +app_name = 'oauth2_provider' urlpatterns = [ # General Authorization Endpoint - re_path(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"), - + re_path(r'^authorize/$', views.AuthorizationView.as_view(), name='authorize'), # provide (or revoke/renew) access tokens - re_path(r"^token/$", views.TokenView.as_view(), name="token"), - re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"), - + re_path(r'^token/$', views.TokenView.as_view(), name='token'), + re_path(r'^revoke_token/$', views.RevokeTokenView.as_view(), name='revoke-token'), # check an existing token, needs 'introspection' scope - re_path(r"^introspect/$", sso_views.IntrospectTokenView.as_view(), name="introspect"), - + re_path(r'^introspect/$', sso_views.IntrospectTokenView.as_view(), name='introspect'), # static page for OOB token transmission - re_path(r"^out-of-band-display-token/$", sso_views.OutOfBandDisplayTokenView.as_view(), name="out-of-band-display-token"), + re_path(r'^out-of-band-display-token/$', sso_views.OutOfBandDisplayTokenView.as_view(), name='out-of-band-display-token'), ] diff --git a/src/api/views/__init__.py b/src/api/views/__init__.py index d498b70e1..cf012e6c0 100644 --- a/src/api/views/__init__.py +++ b/src/api/views/__init__.py @@ -4,7 +4,6 @@ from rest_framework.reverse import reverse from core.models.conference import Conference - __all__ = [ 'api_root', ] diff --git a/src/api/views/assemblies.py b/src/api/views/assemblies.py index a975e5970..98c5ec518 100644 --- a/src/api/views/assemblies.py +++ b/src/api/views/assemblies.py @@ -1,11 +1,11 @@ from rest_framework import generics +from core.models.assemblies import Assembly from core.models.events import Event from core.models.rooms import Room -from core.models.assemblies import Assembly -from ..serializers import AssemblySerializer, RoomSerializer, EventSerializer -from .mixins import ConferenceSlugMixin, ConferenceSlugAssemblyMixin +from ..serializers import AssemblySerializer, EventSerializer, RoomSerializer +from .mixins import ConferenceSlugAssemblyMixin, ConferenceSlugMixin class ConferenceAssemblyList(ConferenceSlugMixin, generics.ListAPIView): diff --git a/src/api/views/badges.py b/src/api/views/badges.py index 2e7260b15..63ad4f1b5 100644 --- a/src/api/views/badges.py +++ b/src/api/views/badges.py @@ -1,8 +1,5 @@ import logging -from django.conf import settings -from django.http import HttpResponse -from django.shortcuts import get_object_or_404 from rest_framework import authentication from rest_framework.decorators import api_view from rest_framework.exceptions import NotFound @@ -10,14 +7,19 @@ from rest_framework.generics import ListCreateAPIView from rest_framework.response import Response from rest_framework.views import APIView -from api.permissions import IsAssemblyManager, IsConferenceService, IsSuperUser, HasIssuingToken -from api.serializers import BadgeSerializer, BadgeTokenSerializer -from api.views.mixins import ConferenceSlugAssemblyMixin, ConferenceSlugMixin +from django.conf import settings +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 + from core.models.badges import Badge, BadgeToken from core.models.conference import Conference from core.models.users import PlatformUser from core.sso import SSO +from api.permissions import HasIssuingToken, IsAssemblyManager, IsConferenceService, IsSuperUser +from api.serializers import BadgeSerializer, BadgeTokenSerializer +from api.views.mixins import ConferenceSlugAssemblyMixin, ConferenceSlugMixin + logger = logging.getLogger(__name__) @@ -32,10 +34,7 @@ class BadgeListCreate(ListCreateAPIView): def get_serializer_context(self): context = super().get_serializer_context() - context.update({ - 'assembly': self.kwargs.get('assembly'), - 'conference': self.kwargs.get('conference') - }) + context.update({'assembly': self.kwargs.get('assembly'), 'conference': self.kwargs.get('conference')}) return context @@ -55,23 +54,21 @@ class BadgeTokenListCreate(ConferenceSlugAssemblyMixin, ListCreateAPIView): badge = self.kwargs.get('pk') else: try: - badge = Badge.objects.get(issuing_token=self.kwargs.get("issuing_token"), conference__slug=self.kwargs.get("conference")).pk + badge = Badge.objects.get(issuing_token=self.kwargs.get('issuing_token'), conference__slug=self.kwargs.get('conference')).pk except Badge.DoesNotExist: - raise NotFound("Badge matching issuing_token not found") + raise NotFound('Badge matching issuing_token not found') context = super().get_serializer_context() - context.update({ - 'badge': badge - }) + context.update({'badge': badge}) return context @api_view(['POST']) def redeem_badge_token(request, conference, **kwargs): - redeem_token = request.POST.get('token', request.data.get("token", None)) + redeem_token = request.POST.get('token', request.data.get('token', None)) if redeem_token is None: return HttpResponse(status=400) - username = request.POST.get('username', request.data.get("username", None)) + username = request.POST.get('username', request.data.get('username', None)) user = get_object_or_404(PlatformUser, username=username, is_active=True) # TODO: Proper Access Checking @@ -107,8 +104,8 @@ class RedeemBadgeMapTokenView(ConferenceSlugMixin, APIView): if self.target_user is None or not self.target_user.is_in_conference(self.conference): return HttpResponse(status=404) - redeem_token = request.POST.get('id', request.data.get("id", None)) - assembly = request.POST.get('assembly', request.data.get("assembly", None)) + redeem_token = request.POST.get('id', request.data.get('id', None)) + assembly = request.POST.get('assembly', request.data.get('assembly', None)) if assembly is None or redeem_token is None: return HttpResponse(status=400) @@ -119,10 +116,14 @@ class RedeemBadgeMapTokenView(ConferenceSlugMixin, APIView): if badge_token.map_token: # Redeem only MAP Tokens created = badge_token.redeem(self.target_user, False) - return Response({'badge_name': f'{badge_token.badge}', - 'badge_image': f'{badge_token.badge.image.url}', - 'issuing_assembly': f'{badge_token.badge.issuing_assembly}', - 'user': f'{self.target_user.username}', - 'created': created}) + return Response( + { + 'badge_name': f'{badge_token.badge}', + 'badge_image': f'{badge_token.badge.image.url}', + 'issuing_assembly': f'{badge_token.badge.issuing_assembly}', + 'user': f'{self.target_user.username}', + 'created': created, + } + ) else: return HttpResponse(status=415) diff --git a/src/api/views/bbb.py b/src/api/views/bbb.py index 79876416a..08e4f1588 100644 --- a/src/api/views/bbb.py +++ b/src/api/views/bbb.py @@ -1,5 +1,5 @@ -from django.shortcuts import get_object_or_404 from django.http import HttpResponse +from django.shortcuts import get_object_or_404 from django.views import View from core.models import Room diff --git a/src/api/views/conferencemember.py b/src/api/views/conferencemember.py index 328a99f1f..22eda299a 100644 --- a/src/api/views/conferencemember.py +++ b/src/api/views/conferencemember.py @@ -1,10 +1,11 @@ import logging -from django.conf import settings from rest_framework import authentication, status from rest_framework.response import Response from rest_framework.views import APIView +from django.conf import settings + from core.models.conference import ConferenceMember from core.sso import SSO @@ -64,11 +65,13 @@ class WorkadventureView(ConferenceSlugMixin, APIView): return Response({'active': False}, status=status.HTTP_400_BAD_REQUEST) result = self.target_user.get_avatar_json(self.conference) - result.update({ - 'active': self.target_user.is_active, - 'username': self.target_user.username, - 'user_id': self.target_user.id, # TODO: exchange for an ident after rC3 - }) + result.update( + { + 'active': self.target_user.is_active, + 'username': self.target_user.username, + 'user_id': self.target_user.id, # TODO: exchange for an ident after rC3 + } + ) return Response(result) def post(self, request, *get, format=None, **kwargs): diff --git a/src/api/views/events.py b/src/api/views/events.py index 74a2d7f12..a4232805b 100644 --- a/src/api/views/events.py +++ b/src/api/views/events.py @@ -1,7 +1,9 @@ -from core.models.events import Event -from django.shortcuts import get_object_or_404 from rest_framework import generics +from django.shortcuts import get_object_or_404 + +from core.models.events import Event + from ..serializers import EventSerializer from .mixins import ConferenceSlugMixin @@ -10,14 +12,12 @@ class EventList(ConferenceSlugMixin, generics.ListAPIView): serializer_class = EventSerializer def get_queryset(self, **kwargs): - return Event.objects.filter(conference=self.conference).order_by("name") + return Event.objects.filter(conference=self.conference).order_by('name') class EventDetail(ConferenceSlugMixin, generics.RetrieveAPIView): serializer_class = EventSerializer def get_object(self, **kwargs): - event_id = self.request.resolver_match.kwargs["pk"] - return get_object_or_404( - Event.objects.conference_accessible(conference=self.conference), pk=event_id - ) + event_id = self.request.resolver_match.kwargs['pk'] + return get_object_or_404(Event.objects.conference_accessible(conference=self.conference), pk=event_id) diff --git a/src/api/views/maps.py b/src/api/views/maps.py index ff9324649..509deb138 100644 --- a/src/api/views/maps.py +++ b/src/api/views/maps.py @@ -1,14 +1,15 @@ import abc import json -from django.contrib.gis.gdal import SpatialReference, CoordTransform from rest_framework.views import APIView +from django.contrib.gis.gdal import CoordTransform, SpatialReference + from core.models.assemblies import Assembly -from core.models.map import MapPOI from core.models.conference import ConferenceExportCache -from .mixins import ConferenceSlugMixin +from core.models.map import MapPOI +from .mixins import ConferenceSlugMixin _cts = {} # cache of CoordTransforms (if needed) @@ -31,7 +32,6 @@ class PoiExportView(ConferenceSlugMixin, APIView): features = [] result = { 'type': 'FeatureCollection', - # 'crs': {'type': 'name', 'properties': {'name': f'EPSG:{srid}'}}, # deprecated, not in RFC7946 'features': features, } @@ -80,7 +80,6 @@ class AssembliesExportView(ConferenceSlugMixin, APIView, metaclass=abc.ABCMeta): features = [] result = { 'type': 'FeatureCollection', - # 'crs': {'type': 'name', 'properties': {'name': f'EPSG:{srid}'}}, # deprecated, not in RFC7946 'features': features, } diff --git a/src/api/views/metanav.py b/src/api/views/metanav.py index 84168b88d..491a16fb1 100644 --- a/src/api/views/metanav.py +++ b/src/api/views/metanav.py @@ -2,10 +2,10 @@ import logging from rest_framework.generics import ListAPIView -from api.serializers import MetaNavItemSerializer -from api.views.mixins import ConferenceSlugMixin from core.models.metanavi import MetaNavItem +from api.serializers import MetaNavItemSerializer +from api.views.mixins import ConferenceSlugMixin logger = logging.getLogger(__name__) diff --git a/src/api/views/metrics.py b/src/api/views/metrics.py index 3f3d06280..4a8722c79 100644 --- a/src/api/views/metrics.py +++ b/src/api/views/metrics.py @@ -1,7 +1,8 @@ from django.conf import settings from django.http.response import HttpResponseForbidden from django.views.generic.base import TemplateView -from core.models import Room, Conference, ConferenceMember, PlatformUser, Badge, UserBadge + +from core.models import Badge, Conference, ConferenceMember, PlatformUser, Room, UserBadge from core.models.assemblies import Assembly from core.models.events import Event from core.models.ticket import ConferenceMemberTicket @@ -16,7 +17,7 @@ class MetricsView(TemplateView): """ Only allow IP addresses that are listed in settings.METRICS_SERVER_IPS. """ - remote_addr = request.META.get("REMOTE_ADDR") + remote_addr = request.META.get('REMOTE_ADDR') if remote_addr in settings.METRICS_SERVER_IPS or '*' in settings.METRICS_SERVER_IPS: return super().dispatch(request, *args, **kwargs) else: @@ -39,73 +40,21 @@ class MetricsView(TemplateView): '{user_type="service"}': PlatformUser.objects.filter(user_type=PlatformUser.Type.SERVICE).count(), '{user_type="bot"}': PlatformUser.objects.filter(user_type=PlatformUser.Type.BOT).count(), '{user_type="assembly"}': PlatformUser.objects.filter(user_type=PlatformUser.Type.ASSEMBLY).count(), - } - }, - 'hub_conference_members': { - 'help': 'members in the conference', - 'type': 'counter', - 'values': {} - }, - 'hub_conference_members_staff': { - 'help': 'staff count', - 'type': 'counter', - 'values': {} - }, - 'hub_conference_members_themes': { - 'help': 'used themes by members in the conference', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_tickets': { - 'help': 'registered tickets', - 'type': 'counter', - 'values': {} - }, - 'hub_conference_assemblies': { - 'help': 'conference\'s assemblies', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_channels': { - 'help': 'conference\'s channels', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_badges_public': { - 'help': 'conference\'s badges (public)', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_badges_hidden': { - 'help': 'conference\'s badges (non-public)', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_badges_accepted': { - 'help': 'conference\'s badges (accepted/assigned)', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_badges_redeemed': { - 'help': 'conference\'s badges (redeemed, but not accepted yet)', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_events': { - 'help': 'conference\'s events', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_rooms': { - 'help': 'conference\'s rooms', - 'type': 'gauge', - 'values': {} - }, - 'hub_conference_workadventure_sessions': { - 'help': 'conference\'s workadventure session', - 'type': 'gauge', - 'values': {} + }, }, + 'hub_conference_members': {'help': 'members in the conference', 'type': 'counter', 'values': {}}, + 'hub_conference_members_staff': {'help': 'staff count', 'type': 'counter', 'values': {}}, + 'hub_conference_members_themes': {'help': 'used themes by members in the conference', 'type': 'gauge', 'values': {}}, + 'hub_conference_tickets': {'help': 'registered tickets', 'type': 'counter', 'values': {}}, + 'hub_conference_assemblies': {'help': "conference's assemblies", 'type': 'gauge', 'values': {}}, + 'hub_conference_channels': {'help': "conference's channels", 'type': 'gauge', 'values': {}}, + 'hub_conference_badges_public': {'help': "conference's badges (public)", 'type': 'gauge', 'values': {}}, + 'hub_conference_badges_hidden': {'help': "conference's badges (non-public)", 'type': 'gauge', 'values': {}}, + 'hub_conference_badges_accepted': {'help': "conference's badges (accepted/assigned)", 'type': 'gauge', 'values': {}}, + 'hub_conference_badges_redeemed': {'help': "conference's badges (redeemed, but not accepted yet)", 'type': 'gauge', 'values': {}}, + 'hub_conference_events': {'help': "conference's events", 'type': 'gauge', 'values': {}}, + 'hub_conference_rooms': {'help': "conference's rooms", 'type': 'gauge', 'values': {}}, + 'hub_conference_workadventure_sessions': {'help': "conference's workadventure session", 'type': 'gauge', 'values': {}}, } for conference in Conference.objects.filter(is_public=True): @@ -129,56 +78,59 @@ class MetricsView(TemplateView): # hub_conference_rooms for room_type in Room.RoomType.values: - room_count = Room.objects.filter( - conference=conference, - room_type=Room.RoomType(room_type) - ).count() + room_count = Room.objects.filter(conference=conference, room_type=Room.RoomType(room_type)).count() metrics['hub_conference_rooms']['values'][f'{{conference="{slug}",room_type="{room_type}"}}'] = room_count # hub_conference_events for kind in Event.Kind.values: - event_count = Event.objects.filter( - conference=conference, - kind=Event.Kind(kind) - ).count() + event_count = Event.objects.filter(conference=conference, kind=Event.Kind(kind)).count() metrics['hub_conference_events']['values'][f'{{conference="{slug}",kind="{kind}"}}'] = event_count # hub_conference_badges_{public,private} - for visibility in {Badge.State.HIDDEN, Badge.State.PUBLIC}: - metrics['hub_conference_badges_' + visibility]['values'][f'{{conference="{slug}",badge_type="achievement"}}'] = \ - Badge.objects.filter(conference=conference, state=visibility).count() - metrics['hub_conference_badges_' + visibility]['values'][f'{{conference="{slug}",badge_type="sticker"}}'] = \ - Badge.objects.filter(conference=conference, state=visibility).count() + for visibility in (Badge.State.HIDDEN, Badge.State.PUBLIC): + metrics['hub_conference_badges_' + visibility]['values'][f'{{conference="{slug}",badge_type="achievement"}}'] = Badge.objects.filter( + conference=conference, state=visibility + ).count() + metrics['hub_conference_badges_' + visibility]['values'][f'{{conference="{slug}",badge_type="sticker"}}'] = Badge.objects.filter( + conference=conference, state=visibility + ).count() # hub_conference_badges_{accepted,redeemed} for userbadge_visibility in UserBadge.Visibility.values: - metrics['hub_conference_badges_accepted']['values'][f'{{conference="{slug}",visibility="{userbadge_visibility}"}}'] = \ - UserBadge.objects.filter(badge__conference=conference, visibility=userbadge_visibility, accepted_by_user=True).count() - metrics['hub_conference_badges_redeemed']['values'][f'{{conference="{slug}",visibility="{userbadge_visibility}"}}'] = \ - UserBadge.objects.filter(badge__conference=conference, visibility=userbadge_visibility, accepted_by_user=False).count() + metrics['hub_conference_badges_accepted']['values'][f'{{conference="{slug}",visibility="{userbadge_visibility}"}}'] = UserBadge.objects.filter( + badge__conference=conference, visibility=userbadge_visibility, accepted_by_user=True + ).count() + metrics['hub_conference_badges_redeemed']['values'][f'{{conference="{slug}",visibility="{userbadge_visibility}"}}'] = UserBadge.objects.filter( + badge__conference=conference, visibility=userbadge_visibility, accepted_by_user=False + ).count() # hub_conference_members{,_staff} - metrics['hub_conference_members']['values'][f'{{conference="{slug}",active_angel="true"}}'] = \ - ConferenceMember.objects.filter(conference=conference, active_angel=True).count() - metrics['hub_conference_members']['values'][f'{{conference="{slug}",active_angel="false"}}'] = \ - ConferenceMember.objects.filter(conference=conference, active_angel=False).count() - metrics['hub_conference_members_staff']['values'][f'{{conference="{slug}"}}'] = \ - ConferenceMember.objects.filter(conference=conference, is_staff=True).count() + metrics['hub_conference_members']['values'][f'{{conference="{slug}",active_angel="true"}}'] = ConferenceMember.objects.filter( + conference=conference, active_angel=True + ).count() + metrics['hub_conference_members']['values'][f'{{conference="{slug}",active_angel="false"}}'] = ConferenceMember.objects.filter( + conference=conference, active_angel=False + ).count() + metrics['hub_conference_members_staff']['values'][f'{{conference="{slug}"}}'] = ConferenceMember.objects.filter( + conference=conference, is_staff=True + ).count() # hub_conference_members_themes for theme in PlatformUser.Theme.values: - metrics['hub_conference_members_themes']['values'][f'{{conference="{slug}",theme="{theme}"}}'] = \ - ConferenceMember.objects.filter(conference=conference, user__theme=theme).count() + metrics['hub_conference_members_themes']['values'][f'{{conference="{slug}",theme="{theme}"}}'] = ConferenceMember.objects.filter( + conference=conference, user__theme=theme + ).count() # hub_conference_tickets - metrics['hub_conference_tickets']['values'][f'{{conference="{slug}"}}'] = \ - ConferenceMemberTicket.objects.filter(conference=conference).count() + metrics['hub_conference_tickets']['values'][f'{{conference="{slug}"}}'] = ConferenceMemberTicket.objects.filter(conference=conference).count() # hub_conference_workadventure_sessions - metrics['hub_conference_workadventure_sessions']['values'][f'{{conference="{slug}",active="true"}}'] = \ - WorkadventureSession.objects.filter(conference=conference, token=None).count() - metrics['hub_conference_workadventure_sessions']['values'][f'{{conference="{slug}",active="false"}}'] = \ + metrics['hub_conference_workadventure_sessions']['values'][f'{{conference="{slug}",active="true"}}'] = WorkadventureSession.objects.filter( + conference=conference, token=None + ).count() + metrics['hub_conference_workadventure_sessions']['values'][f'{{conference="{slug}",active="false"}}'] = ( WorkadventureSession.objects.filter(conference=conference).exclude(token=None).count() + ) context['metrics'] = metrics diff --git a/src/api/views/mixins.py b/src/api/views/mixins.py index a265b67c2..8b99f77aa 100644 --- a/src/api/views/mixins.py +++ b/src/api/views/mixins.py @@ -54,7 +54,7 @@ class ConferenceSlugAssemblyMixin(ConferenceSlugMixin): try: self._assembly = Assembly.objects.accessible_by_user(self.request.user, self.conference).get(slug=assembly_slug) except Assembly.DoesNotExist: - if issuing_token := self.kwargs.get("issuing_token", None): + if issuing_token := self.kwargs.get('issuing_token', None): try: self._assembly = Badge.objects.get(issuing_token=issuing_token).issuing_assembly except Badge.DoesNotExist: diff --git a/src/api/views/rooms.py b/src/api/views/rooms.py index 544c26cf1..b55800b38 100644 --- a/src/api/views/rooms.py +++ b/src/api/views/rooms.py @@ -1,12 +1,12 @@ import logging from rest_framework import generics + from core.models.rooms import Room from ..serializers import RoomSerializer from .mixins import ConferenceSlugMixin - logger = logging.getLogger(__name__) diff --git a/src/api/views/schedule.py b/src/api/views/schedule.py index b2226234f..d2b44ea3f 100644 --- a/src/api/views/schedule.py +++ b/src/api/views/schedule.py @@ -1,14 +1,15 @@ import logging from datetime import timedelta -from django.db.models import F -from django.http import JsonResponse, HttpResponse -from django.utils.dateparse import parse_datetime -from django.views.generic import View from rest_framework import authentication from rest_framework.response import Response from rest_framework.views import APIView +from django.db.models import F +from django.http import HttpResponse, JsonResponse +from django.utils.dateparse import parse_datetime +from django.views.generic import View + from core.models.assemblies import Assembly from core.models.conference import ConferenceExportCache, ConferenceTrack from core.models.events import Event @@ -58,12 +59,13 @@ class ConferenceSchedule(BaseScheduleView): return '' def get_events(self, **kwargs): - return Event.objects \ - .conference_accessible(conference=self.conference) \ - .exclude(schedule_duration=None) \ - .exclude(schedule_duration__lte=timedelta(minutes=5)) \ - .select_related('track', 'room', 'assembly') \ + return ( + Event.objects.conference_accessible(conference=self.conference) + .exclude(schedule_duration=None) + .exclude(schedule_duration__lte=timedelta(minutes=5)) + .select_related('track', 'room', 'assembly') .order_by(F('assembly__is_official').desc(nulls_last=True), F('room__capacity').desc(nulls_last=True), 'schedule_start') + ) class AssemblySchedule(BaseScheduleView): @@ -73,11 +75,12 @@ class AssemblySchedule(BaseScheduleView): def get_events(self): assembly_id = self.request.resolver_match.kwargs.get('assembly') - return Event.objects \ - .conference_accessible(conference=self.conference) \ - .select_related('track', 'room') \ - .filter(assembly_id=assembly_id) \ + return ( + Event.objects.conference_accessible(conference=self.conference) + .select_related('track', 'room') + .filter(assembly_id=assembly_id) .order_by('room__room_type', F('room__capacity').desc(nulls_last=True), 'room__name', 'schedule_start') + ) class RoomSchedule(BaseScheduleView): @@ -87,10 +90,7 @@ class RoomSchedule(BaseScheduleView): def get_events(self): room_id = self.request.resolver_match.kwargs.get('pk') - return Event.objects \ - .conference_accessible(conference=self.conference) \ - .filter(room_id=room_id) \ - .order_by('schedule_start') + return Event.objects.conference_accessible(conference=self.conference).filter(room_id=room_id).order_by('schedule_start') class EventSchedule(ConferenceSlugMixin, APIView): @@ -98,9 +98,7 @@ class EventSchedule(ConferenceSlugMixin, APIView): permission_classes = [IsApiUserOrReadOnly] def get(self, request, conference, pk, format=None, **kwargs): - event = Event.objects \ - .accessible_by_user(conference=self.conference, user=self.request.user) \ - .get(pk=pk) + event = Event.objects.accessible_by_user(conference=self.conference, user=self.request.user).get(pk=pk) return Response(ScheduleEncoder().encode_event(event, self.conference.timezone)) def post(self, request, conference, pk, format=None, **kwargs): diff --git a/src/api/views/sso.py b/src/api/views/sso.py index 80c5e849d..47543108c 100644 --- a/src/api/views/sso.py +++ b/src/api/views/sso.py @@ -2,15 +2,14 @@ import calendar import json import logging +from oauth2_provider.models import get_access_token_model +from oauth2_provider.views import ClientProtectedScopedResourceView + from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse from django.utils.decorators import method_decorator -from django.views.generic import TemplateView from django.views.decorators.csrf import csrf_exempt - -from oauth2_provider.models import get_access_token_model -from oauth2_provider.views import ClientProtectedScopedResourceView - +from django.views.generic import TemplateView logger = logging.getLogger(__name__) @@ -19,7 +18,7 @@ class OutOfBandDisplayTokenView(TemplateView): template_name = 'oauth2_provider/out-of-band-display-token.html' -@method_decorator(csrf_exempt, name="dispatch") +@method_decorator(csrf_exempt, name='dispatch') class IntrospectTokenView(ClientProtectedScopedResourceView): """ Implements an endpoint for token introspection based @@ -35,7 +34,7 @@ class IntrospectTokenView(ClientProtectedScopedResourceView): @staticmethod def get_token_response(query_user, token_value=None): try: - token = get_access_token_model().objects.select_related("user", "application").get(token=token_value) + token = get_access_token_model().objects.select_related('user', 'application').get(token=token_value) except ObjectDoesNotExist: return HttpResponse(content=json.dumps({'active': False}), status=401, content_type='application/json') @@ -57,7 +56,6 @@ class IntrospectTokenView(ClientProtectedScopedResourceView): 'active': True, 'client_id': token.application.client_id, 'exp': int(calendar.timegm(token.expires.timetuple())), - # 'scope': token.scope, } # prepare user details and preferences @@ -75,7 +73,7 @@ class IntrospectTokenView(ClientProtectedScopedResourceView): :param kwargs: :return: """ - return self.get_token_response(request.user, request.GET.get("token", None)) + return self.get_token_response(request.user, request.GET.get('token', None)) def post(self, request, *args, **kwargs): """ @@ -86,4 +84,4 @@ class IntrospectTokenView(ClientProtectedScopedResourceView): :param kwargs: :return: """ - return self.get_token_response(request.user, request.POST.get("token", None)) + return self.get_token_response(request.user, request.POST.get('token', None)) diff --git a/src/api/views/users.py b/src/api/views/users.py index 69699b4a4..5dd37ac3e 100644 --- a/src/api/views/users.py +++ b/src/api/views/users.py @@ -2,17 +2,19 @@ import io import json import zipfile -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpResponse -from django.views.generic import View from rest_framework import generics, permissions from rest_framework.decorators import api_view from rest_framework.response import Response -from ..serializers import UserTimelineEntrySerializer +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse +from django.views.generic import View + from core.models.badges import UserBadge from core.models.users import PlatformUser, UserTimelineEntry +from ..serializers import UserTimelineEntrySerializer + @api_view(['GET']) def profile(request, format=None): @@ -21,11 +23,13 @@ def profile(request, format=None): if not u.is_authenticated: u = PlatformUser.get_anonymous_user() - return Response({ - 'authenticated': u.is_authenticated, - 'username': u.username, - 'flags': PlatformUser.get_user_flags(u), - }) + return Response( + { + 'authenticated': u.is_authenticated, + 'username': u.username, + 'flags': PlatformUser.get_user_flags(u), + } + ) @api_view(['GET']) @@ -34,14 +38,19 @@ def friends(request, format=None): if not u.is_authenticated: return Response([]) - return Response([{ - 'user': c.contact.username, - 'pending': c.pending, - # status may only be shown if that is public or the contact has a allowed sharing to us - 'status': c.contact.status if c.contact.status_public or (c.reverse_contact_share_status(default=False) and not c.pending) else None, - 'receive_dms': c.receive_dms, - 'receive_dm_images': c.receive_dm_images, - } for c in u.contacts.all()]) + return Response( + [ + { + 'user': c.contact.username, + 'pending': c.pending, + # status may only be shown if that is public or the contact has a allowed sharing to us + 'status': c.contact.status if c.contact.status_public or (c.reverse_contact_share_status(default=False) and not c.pending) else None, + 'receive_dms': c.receive_dms, + 'receive_dm_images': c.receive_dm_images, + } + for c in u.contacts.all() + ] + ) @api_view(['GET']) @@ -50,13 +59,18 @@ def badges(request, format=None): if not u.is_authenticated: return Response([]) - return Response([{ - 'conference': ub.badge.conference.name, - 'assembly': ub.badge.issuing_assembly.name, - 'name': ub.badge.name, - 'image_url': ub.badge.image.url, - 'is_achievement': ub.badge.is_achievement, - } for ub in u.badges.filter(visibility=UserBadge.Visibility.PUBLIC)]) + return Response( + [ + { + 'conference': ub.badge.conference.name, + 'assembly': ub.badge.issuing_assembly.name, + 'name': ub.badge.name, + 'image_url': ub.badge.image.url, + 'is_achievement': ub.badge.is_achievement, + } + for ub in u.badges.filter(visibility=UserBadge.Visibility.PUBLIC) + ] + ) class BadgeExportView(LoginRequiredMixin, View): diff --git a/src/api/views/workadventure.py b/src/api/views/workadventure.py index 9e40b5ee6..57110681c 100644 --- a/src/api/views/workadventure.py +++ b/src/api/views/workadventure.py @@ -3,13 +3,14 @@ import logging from datetime import datetime from time import time +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response +from rest_framework.views import APIView + from django.conf import settings from django.http import Http404 from django.urls import NoReverseMatch, reverse from django.utils import timezone -from rest_framework.generics import get_object_or_404 -from rest_framework.response import Response -from rest_framework.views import APIView from core.abuse import report_content from core.integrations import WorkAdventureIntegration @@ -106,7 +107,9 @@ class MapServiceView(ConferenceSlugMixin, APIView): skipped[room_id] = 'Non-matching assembly id, skipped.' logging.warning( 'Got mapservice update for room id %s with non-matching assembly_id (got %s but expected %s).', - room_id, a_id, room.assembly_id, + room_id, + a_id, + room.assembly_id, ) continue @@ -126,7 +129,7 @@ class MapServiceView(ConferenceSlugMixin, APIView): if has_mapinfo and is_published: room.backend_status = Room.BackendStatus.ACTIVE update_fields.append('backend_status') - logging.info("setting WA room %s active per MapService update as we now have publishedSince>0", room_id) + logging.info('setting WA room %s active per MapService update as we now have publishedSince>0', room_id) elif room.backend_status in [Room.BackendStatus.ACTIVE]: # if 'publishedSince' is not set any more, we should signal an error @@ -164,6 +167,7 @@ class MapBackendListView(ConferenceSlugMixin, APIView): It is queried to fetch all currently active maps. """ + permission_classes = [IsConferenceService | IsSuperUser] required_service_classes = ['wa_backend'] @@ -201,15 +205,6 @@ class MapBackendReportView(ConferenceSlugMixin, APIView): class MapBackendReportUserView(MapBackendReportView): def post(self, request, *args, **kwargs): - # interface reportUser { - # reportedUserUuid: string; - # reportedUserIPAdress?: string; - # reporterUserUuid: string; - # orgSlug: string; - # worldSlug: string; - # roomSlug: string; - # comment?: string; - # } reporting_user, data, comment, solution = self.extract_map_report_data(request) reported_session = WorkadventureSession.objects.filter(conference=self.conference, id=data.get('reportedUserUuid')).first() @@ -231,18 +226,6 @@ class MapBackendReportUserView(MapBackendReportView): class MapBackendReportMapView(MapBackendReportView): def post(self, request, *args, **kwargs): - # interface reportMap { - # mapUrl: string; - # orgSlug: string; - # worldSlug: string; - # roomSlug: string; - # reporterUserUuid: string; - # comment?: string; - # position: { - # x: number: - # y: number; - # }; - # } reporting_user, data, comment, solution = self.extract_map_report_data(request) report_content( @@ -274,10 +257,13 @@ class MapBackendRedeemBadgeTokenView(ConferenceSlugMixin, APIView): created = badge_token.redeem(wa_session.user, False) if created: - return Response({ - 'msg': f'{badge_token.badge}', - 'icon': f'{badge_token.badge.image.url}', - }, status=201) + return Response( + { + 'msg': f'{badge_token.badge}', + 'icon': f'{badge_token.badge.image.url}', + }, + status=201, + ) else: return Response(status=204) @@ -323,7 +309,7 @@ class UserInfoView(ConferenceSlugMixin, APIView): try: wa_session = WorkadventureSession.objects.get(conference=self.conference, pk=uid) except WorkadventureSession.DoesNotExist: - raise Http404() + raise Http404 return Response(wa_session.export_userdata()) @@ -354,9 +340,9 @@ class RegisterView(ConferenceSlugMixin, APIView): wa_session = WorkadventureSession.objects.get(conference=self.conference, token=token) if wa_session.token_expiry < timezone.now(): logger.info('WA session %s: token had expired (%s)', wa_session.id, token) - raise Http404() + raise Http404 except WorkadventureSession.DoesNotExist: - raise Http404() + raise Http404 if wa_session.additional_data is None: wa_session.additional_data = {} diff --git a/src/backoffice/forms.py b/src/backoffice/forms.py index 062b6c3f5..16576eca3 100644 --- a/src/backoffice/forms.py +++ b/src/backoffice/forms.py @@ -45,10 +45,14 @@ class ProfileForm(forms.ModelForm): fields = [ 'show_name', # 'description', - 'status', 'status_public', + 'status', + 'status_public', 'timezone', - 'no_animations', 'colorblind', 'high_contrast', - 'receive_dms', 'receive_dm_images', + 'no_animations', + 'colorblind', + 'high_contrast', + 'receive_dms', + 'receive_dm_images', 'autoaccept_contacts', ] @@ -111,7 +115,7 @@ class AssemblyEditForm(TranslatedFieldsForm): ] def __init__(self, *args, staff_access: bool, staff_mode: bool, assembly_staff_access: bool, channel_staff_access: bool, **kwargs): - super(AssemblyEditForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # configure fields' widget customizations self.fields['assembly_link'].widget.attrs['placeholder'] = 'https://' @@ -159,8 +163,8 @@ class AssemblyEditForm(TranslatedFieldsForm): raise ValidationError(_('Assembly__tags__splitwithcomma')) for tag in split_tags: - tag = tag.strip() - validate_slug(tag) + stripped_tag = tag.strip() + validate_slug(stripped_tag) return tags @@ -367,14 +371,13 @@ class EditAssemblyRoomForm(TranslatedFieldsForm): self.fields['backend_status'] = forms.CharField(initial=self.instance.get_backend_status_display(), disabled=True) if self.instance.room_type not in Room.TYPES_WITH_CAPACITY: del self.fields['capacity'] - else: - if self.instance.room_type == Room.RoomType.BIGBLUEBUTTON: - self.fields['capacity'].label = _('Room-bigbluebutton_capacity') - self.fields['capacity'].help_text = _('Room-bigbluebutton_capacity__help') + elif self.instance.room_type == Room.RoomType.BIGBLUEBUTTON: + self.fields['capacity'].label = _('Room-bigbluebutton_capacity') + self.fields['capacity'].help_text = _('Room-bigbluebutton_capacity__help') - if self.instance.backend_status == Room.BackendStatus.ACTIVE: - self.fields['capacity'].help_text += ' ' + gettext('Room-bigbluebutton__activeroom') - self.fields['capacity'].disabled = True + if self.instance.backend_status == Room.BackendStatus.ACTIVE: + self.fields['capacity'].help_text += ' ' + gettext('Room-bigbluebutton__activeroom') + self.fields['capacity'].disabled = True if self.instance.room_type in [Room.RoomType.HANGAR]: self.fields['name'].disabled = True @@ -386,7 +389,7 @@ class EditAssemblyRoomForm(TranslatedFieldsForm): def clean_name(self): if Room.objects.filter(assembly=self.instance.assembly, name=self.cleaned_data['name']).exclude(pk=self.instance.pk).exists(): - raise ValidationError(_("Room-name-assembly-unique")) + raise ValidationError(_('Room-name-assembly-unique')) # update slug to be based on the new name iff the name changed. if self.instance.name != self.cleaned_data['name']: @@ -408,10 +411,10 @@ class EditAssemblyRoomForm(TranslatedFieldsForm): try: capacity = int(capacity) except ValueError: - raise ValidationError(_("Room-capacity-invalid")) + raise ValidationError(_('Room-capacity-invalid')) if capacity < 0: - raise ValidationError(_("Room-capacity-negative")) + raise ValidationError(_('Room-capacity-negative')) return capacity def save(self, commit=False): @@ -466,24 +469,21 @@ class EventForm(TranslatedFieldsForm): class Meta: model = Event fields = ['room', 'name', 'language', 'banner_image', 'is_public', 'abstract', 'description', 'schedule_start', 'schedule_duration'] - widgets = { - 'abstract': forms.Textarea(attrs={'rows': 4}), - 'is_public': forms.HiddenInput() - } + widgets = {'abstract': forms.Textarea(attrs={'rows': 4}), 'is_public': forms.HiddenInput()} def __init__(self, *args, conference, rooms=None, assembly=None, owner=None, create=False, publish=False, **kwargs): super().__init__(*args, **kwargs) self.conference = conference self.create = create self.sos = not assembly - self.event_type_name = _("SoS") if self.sos else _('event') + self.event_type_name = _('SoS') if self.sos else _('event') self.owner = owner self.publish = publish if not assembly: del self.fields['room'] del self.fields['banner_image'] del self.fields['abstract'] - self.fields["is_public"].initial = True + self.fields['is_public'].initial = True self.assembly = self.conference.self_organized_sessions_assembly self.kind = Event.Kind.SELF_ORGANIZED else: @@ -495,7 +495,7 @@ class EventForm(TranslatedFieldsForm): self.fields['schedule_start'].widget.attrs['placeholder'] = _('Event__schedule_start__placeholder') def clean(self): - if self.cleaned_data["schedule_duration"] is None: + if self.cleaned_data['schedule_duration'] is None: self.instance.schedule_end = None self.cleaned_data['is_public'] = not self.instance.is_public if self.publish else self.instance.is_public self.instance.conference = self.conference @@ -527,6 +527,7 @@ class BadgeForm(TranslatedFieldsForm): class Meta: model = Badge fields = ['name', 'state', 'category', 'image', 'description', 'location'] + create = False def __init__(self, *args, conference=None, assembly=None, **kwargs) -> None: @@ -570,10 +571,10 @@ class BadgeTokenTimeConstraintForm(forms.ModelForm): self.fields['date_time_range'].label = _('BadgeTokenTimeConstraint__date_time_range__label') self.fields['date_time_range'].widget.widgets[0].attrs['placeholder'] = _('BadgeTokenTimeConstraint__date_time_range__placeholder-start') # TODO: value bei Ausgabe richtig setzen, sonst wird existierender Wert nicht mehr angezeigt - # self.fields["date_time_range"].widget.widgets[0].input_type = "datetime-local" + # self.fields["date_time_range"].widget.widgets[0].input_type = "datetime-local" # noqa: ERA001 self.fields['date_time_range'].widget.widgets[1].attrs['placeholder'] = _('BadgeTokenTimeConstraint__date_time_range__placeholder-end') # TODO: dito, s.o. - # self.fields["date_time_range"].widget.widgets[1].input_type = "datetime-local" + # self.fields["date_time_range"].widget.widgets[1].input_type = "datetime-local" # noqa: ERA001 BadgeTokenTimeConstraintFormSet = inlineformset_factory(BadgeToken, BadgeTokenTimeConstraint, form=BadgeTokenTimeConstraintForm, extra=3) diff --git a/src/backoffice/templatetags/c3assemblies.py b/src/backoffice/templatetags/c3assemblies.py index f96c7c07a..15e30a999 100644 --- a/src/backoffice/templatetags/c3assemblies.py +++ b/src/backoffice/templatetags/c3assemblies.py @@ -1,10 +1,10 @@ -from datetime import datetime, timedelta import json +from datetime import datetime, timedelta from django.conf import settings from django.template.defaulttags import register -from django.utils.translation import get_language from django.utils.timezone import get_current_timezone +from django.utils.translation import get_language from core.models.assemblies import Assembly diff --git a/src/backoffice/tests/__init__.py b/src/backoffice/tests/__init__.py index 89c9be319..3169e36fc 100644 --- a/src/backoffice/tests/__init__.py +++ b/src/backoffice/tests/__init__.py @@ -1,5 +1,5 @@ -from .base import * # noqa: F401, F403 +from .base import * # noqa: F401, F403, I001 from .assemblies import * # noqa: F401, F403 from .auth import * # noqa: F401, F403 -__all__ = '*' +__all__ = ('*',) # noqa: F405 diff --git a/src/backoffice/tests/assemblies.py b/src/backoffice/tests/assemblies.py index f4740e5db..a931e439e 100644 --- a/src/backoffice/tests/assemblies.py +++ b/src/backoffice/tests/assemblies.py @@ -1,20 +1,19 @@ from django.urls import reverse -from backoffice.tests import BackOfficeTestCase from core.models import Assembly, AssemblyLink +from backoffice.tests import BackOfficeTestCase + class AssemblyListViewTest(BackOfficeTestCase): def setUp(self): super().setUp() - self.assemblies = [Assembly(slug=a, name=a, is_virtual=True, conference_id=self.conf.id, state_assembly=Assembly.State.ACCEPTED) - for a in ('a1', 'a2', 'a3')] + self.assemblies = [ + Assembly(slug=a, name=a, is_virtual=True, conference_id=self.conf.id, state_assembly=Assembly.State.ACCEPTED) for a in ('a1', 'a2', 'a3') + ] for a in self.assemblies: a.save() - self.assembly_links = [ - AssemblyLink(a=self.assemblies[0], b=self.assemblies[1]), - AssemblyLink(a=self.assemblies[0], b=self.assemblies[2]) - ] + self.assembly_links = [AssemblyLink(a=self.assemblies[0], b=self.assemblies[1]), AssemblyLink(a=self.assemblies[0], b=self.assemblies[2])] for al in self.assembly_links: al.save() diff --git a/src/backoffice/tests/auth.py b/src/backoffice/tests/auth.py index e6130b06b..429cdb13c 100644 --- a/src/backoffice/tests/auth.py +++ b/src/backoffice/tests/auth.py @@ -10,33 +10,35 @@ class PasswordResetTest(BackOfficeTestCase): @override_settings(LANGUAGE_CODE='en', AUTH_PASSWORD_VALIDATORS=[]) def test_password_reset(self): resp = self.client.get(reverse('backoffice:password_reset')) - self.assertNotContains(resp, "Reset password link invalid") + self.assertNotContains(resp, 'Reset password link invalid') @override_settings(LANGUAGE_CODE='en', AUTH_PASSWORD_VALIDATORS=[]) def test_invalid_password_reset_link(self): - from django.contrib.auth.tokens import default_token_generator from datetime import timedelta + + from django.contrib.auth.tokens import default_token_generator from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode + uidb = urlsafe_base64_encode(force_bytes(self.user.pk)) expired_token = default_token_generator._make_token_with_timestamp( - self.user, default_token_generator._num_seconds(default_token_generator._now() - timedelta(days=365)), secret=None) - expired_resp = self.client.get(reverse('backoffice:password_reset_confirm', - kwargs={'uidb64': uidb, 'token': expired_token}), follow=True) + self.user, default_token_generator._num_seconds(default_token_generator._now() - timedelta(days=365)), secret=None + ) + expired_resp = self.client.get(reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': expired_token}), follow=True) self.assertRedirects(expired_resp, f"{reverse('backoffice:password_reset')}?retry=True") self.assertContains(expired_resp, 'Reset password link invalid') - invalid_session_resp = self.client.get( - reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': 'set-password'})) + invalid_session_resp = self.client.get(reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': 'set-password'})) self.assertRedirects(invalid_session_resp, f"{reverse('backoffice:password_reset')}?retry=True") self.assertNotIn(INTERNAL_RESET_SESSION_TOKEN, self.client.session) @override_settings(LANGUAGE_CODE='en', AUTH_PASSWORD_VALIDATORS=[]) def test_PasswordResetConfirmView(self): - from django.utils.encoding import force_bytes - from django.utils.http import urlsafe_base64_encode from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.views import INTERNAL_RESET_SESSION_TOKEN + from django.utils.encoding import force_bytes + from django.utils.http import urlsafe_base64_encode + self.client.force_login(self.user) self.client.cookies = SimpleCookie() self.user.set_password('forgotten') @@ -46,14 +48,15 @@ class PasswordResetTest(BackOfficeTestCase): resp = self.client.get(reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': token})) self.assertEqual(self.client.session[INTERNAL_RESET_SESSION_TOKEN], token) - self.assertRedirects(resp, reverse('backoffice:password_reset_confirm', - kwargs={'uidb64': uidb, 'token': 'set-password'})) + self.assertRedirects(resp, reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': 'set-password'})) resp = self.client.post( - reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': 'set-password'}), { + reverse('backoffice:password_reset_confirm', kwargs={'uidb64': uidb, 'token': 'set-password'}), + { 'new_password1': '4', # chosen by fair dice roll 'new_password2': '4', # guranteed to be random - }) + }, + ) self.assertRedirects(resp, reverse('backoffice:password_reset_complete')) self.user.refresh_from_db() diff --git a/src/backoffice/tests/base.py b/src/backoffice/tests/base.py index b868f77e0..5d4ed2f71 100644 --- a/src/backoffice/tests/base.py +++ b/src/backoffice/tests/base.py @@ -1,9 +1,9 @@ import uuid -from datetime import datetime, UTC +from datetime import UTC, datetime -from django.test import override_settings, TestCase +from django.test import TestCase, override_settings -from core.models import Conference, PlatformUser, ConferenceMember +from core.models import Conference, ConferenceMember, PlatformUser TEST_CONF_ID = uuid.uuid4() diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 94e89525f..1c8857548 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -1,66 +1,44 @@ from django.urls import path, re_path - from django.views.i18n import set_language -from .views import \ - assemblies, \ - assemblyteam, \ - auth, \ - badges, \ - channelteam, \ - events, \ - map, \ - misc, \ - profile, \ - schedules, \ - users, \ - vouchers, \ - wiki, \ - workadventure - +from .views import assemblies, assemblyteam, auth, badges, channelteam, events, map, misc, profile, schedules, users, vouchers, wiki, workadventure app_name = 'backoffice' urlpatterns = [ path('', misc.IndexView.as_view(), name='index'), - path('accounts/profile/', profile.ProfileView.as_view(), name='profile'), path('accounts/signup/', auth.RegistrationView.as_view(), name='signup'), path('accounts/signup/done', auth.SignupDoneView.as_view(), name='account_activation_sent'), - path('accounts/change-password/', auth.PasswordChangeView.as_view(), name='password_change'), path('accounts/change-password/done', auth.PasswordChangeDoneView.as_view(), name='password_change_done'), - path('accounts/reset-password/', auth.PasswordResetView.as_view(), name='password_reset'), path('accounts/reset-password/done', auth.PasswordResetDoneView.as_view(), name='password_reset_done'), path('accounts/reset-password/confirm/<uidb64>/<token>/', auth.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('accounts/reset-password/complete', auth.PasswordResetCompleteView.as_view(), name='password_reset_complete'), - - re_path(r'^accounts/activate/(?P<uid_b64>[0-9A-Za-z_\-]+)/(?P<channel_id>\d+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,40})/$', auth.RegistrationActivationView.as_view(), name='signup_activate'), # noqa: E501 - + re_path( + r'^accounts/activate/(?P<uid_b64>[0-9A-Za-z_\-]+)/(?P<channel_id>\d+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,40})/$', + auth.RegistrationActivationView.as_view(), + name='signup_activate', + ), # noqa: E501 path('login', auth.LoginView.as_view(), name='login'), path('logout', auth.LogoutView.as_view(), name='logout'), path('auth_debug', auth.AuthDebugView.as_view()), - path('conferences', misc.ConferenceSelectionView.as_view(), name='conference_selection'), - path('wiki', wiki.WikiOverviewView.as_view(), name='wiki'), path('wiki/namespaces', wiki.NamespaceListView.as_view(), name='wiki-namespaces'), path('wiki/pages', wiki.PagesView.as_view(), name='wiki-pages'), path('wiki/page/<uuid:pk>', wiki.PageView.as_view(), name='wiki-page-detail'), path('wiki/page/<uuid:pk>/delete', wiki.PageDeleteView.as_view(), name='wiki-page-delete'), path('wiki/page/<uuid:pk>/delete-revision', wiki.PageRevisionDeleteView.as_view(), name='wiki-page-revision-delete'), - path('assemblies', assemblyteam.AssembliesView.as_view(), name='assemblies'), path('assemblies/list/<str:variant>', assemblyteam.AssembliesListsView.as_view(), name='assemblieslist'), path('channels', channelteam.ChannelsView.as_view(), name='channels'), path('channels/list/<str:variant>', channelteam.ChannelsListView.as_view(), name='channelslist'), - path('assemblyteam/<uuid:pk>', assemblyteam.AssemblyView.as_view(), name='assemblyteam-detail'), path('assemblyteam/<uuid:pk>/state', assemblyteam.AssemblyEditStateView.as_view(), name='assemblyteam-editstate'), path('assemblyteam/<uuid:pk>/hierarchy', assemblyteam.AssemblyEditHierarchyView.as_view(), name='assemblyteam-edithierarchy'), path('assemblyteam/<uuid:pk>/position', assemblyteam.AssemblyEditPlacementView.as_view(), name='assemblyteam-editposition'), path('assemblyteam/<uuid:pk>/message', assemblyteam.AssemblyMessageView.as_view(), name='assemblyteam-message'), - path('assembly/create', assemblies.CreateAssemblyView.as_view(), name='assembly-create'), path('assembly/<uuid:pk>', assemblies.AssemblyView.as_view(), name='assembly'), path('assembly/<uuid:pk>/edit', assemblies.EditAssemblyView.as_view(), name='assembly-edit'), @@ -70,43 +48,38 @@ urlpatterns = [ path('assembly/<uuid:pk>/members/add', assemblies.MembersAddView.as_view(), name='assembly-members-add'), path('assembly/<uuid:pk>/members/edit/<str:uname>', assemblies.MembersEditView.as_view(), name='assembly-members-edit'), path('assembly/<uuid:pk>/vouchers', assemblies.VouchersView.as_view(), name='assembly-vouchers'), - path('assembly/<uuid:assembly>/auth', assemblies.AuthView.as_view(), name='assembly-auth'), path('assembly/<uuid:assembly>/auth/app/<int:pk>', assemblies.AuthAppView.as_view(), name='assembly-auth-app'), path('assembly/<uuid:assembly>/auth/new_token', assemblies.AuthGetTokenView.as_view(), name='assembly-auth-gettoken'), - path('assembly/<uuid:assembly>/badges', badges.BadgesView.as_view(), name='assembly-badges'), path('assembly/<uuid:assembly>/badge/new', badges.CreateBadgeView.as_view(), name='assembly-create-badge'), path('assembly/<uuid:assembly>/badge/<uuid:pk>', badges.BadgeView.as_view(), name='assembly-badge'), path('assembly/<uuid:assembly>/badge/<uuid:pk>/renew_token', badges.RenewBadgeIssuingTokenView.as_view(), name='assembly-badge-renew'), path('assembly/<uuid:assembly>/badge/<uuid:pk>/remove', badges.RemoveBadgeView.as_view(), name='assembly-badge-remove'), path('assembly/<uuid:assembly>/badge/<uuid:pk>/award', badges.AwardBadgeView.as_view(), name='assembly-badge-award'), - path('assembly/<uuid:assembly>/badge/<uuid:badge>/redeem_token/new', - badges.CreateBadgeTokenView.as_view(), name='assembly-badge-create-redeem-token'), - path('assembly/<uuid:assembly>/badge/<uuid:badge>/redeem_token/<uuid:pk>', - badges.BadgeTokenEditView.as_view(), name='assembly-badge-redeem-token'), - path('assembly/<uuid:assembly>/badge/<uuid:badge>/redeem_token/<uuid:pk>/switch', - badges.BadgeTokenToggleActiveView.as_view(), name='assembly-badge-toggle-active-redeem-token'), - + path('assembly/<uuid:assembly>/badge/<uuid:badge>/redeem_token/new', badges.CreateBadgeTokenView.as_view(), name='assembly-badge-create-redeem-token'), + path('assembly/<uuid:assembly>/badge/<uuid:badge>/redeem_token/<uuid:pk>', badges.BadgeTokenEditView.as_view(), name='assembly-badge-redeem-token'), + path( + 'assembly/<uuid:assembly>/badge/<uuid:badge>/redeem_token/<uuid:pk>/switch', + badges.BadgeTokenToggleActiveView.as_view(), + name='assembly-badge-toggle-active-redeem-token', + ), path('assembly/<uuid:assembly>/new_event', events.AssemblyCreateEventView.as_view(), name='assembly-create-event'), path('assembly/<uuid:assembly>/events', events.AssemblyEventsView.as_view(), name='assembly-events'), path('assembly/<uuid:assembly>/new_room', assemblies.CreateRoomView.as_view(), name='assembly-create-room'), path('assembly/<uuid:assembly>/new_project', assemblies.CreateProjectView.as_view(), name='assembly-create-project'), - path('assembly/<uuid:assembly>/e/<uuid:pk>/', events.AssemblyEventView.as_view(), name='assembly-event'), path('assembly/<uuid:assembly>/e/<uuid:pk>/remove', events.AssemblyRemoveEventView.as_view(), name='assembly-event-remove'), path('assembly/<uuid:assembly>/r/<uuid:pk>/', assemblies.AssemblyRoomView.as_view(), name='assembly-room'), path('assembly/<uuid:assembly>/r/<uuid:room>/new_link', assemblies.CreateRoomLinkView.as_view(), name='roomlink-create'), path('assembly/<uuid:assembly>/r/<uuid:room>/remove_link', assemblies.RemoveRoomLinkView.as_view(), name='roomlink-remove'), path('assembly/<uuid:assembly>/r/<uuid:room>/remove', assemblies.RemoveRoomView.as_view(), name='assembly-remove-room'), - path('map/floors', map.FloorListView.as_view(), name='map-floor-list'), path('map/floor/new', map.FloorCreateView.as_view(), name='map-floor-create'), path('map/floor/<uuid:pk>', map.FloorUpdateView.as_view(), name='map-floor-edit'), path('map/pois', map.POIListView.as_view(), name='map-poi-list'), path('map/poi/new', map.POICreateView.as_view(), name='map-poi-create'), path('map/poi/<uuid:pk>', map.POIUpdateView.as_view(), name='map-poi-edit'), - path('schedule/', schedules.SchedulesIndexView.as_view(), name='schedules'), path('schedule/sources', schedules.ScheduleSourcesListView.as_view(), name='schedulesource-list'), path('schedule/source/add', schedules.ScheduleSourcesCreateView.as_view(), name='schedulesource-add'), @@ -115,12 +88,10 @@ urlpatterns = [ path('schedule/source/<uuid:pk>/import', schedules.ScheduleSourcesDoImportView.as_view(), name='schedulesource-import'), path('schedule/source/<uuid:pk>/update', schedules.ScheduleSourcesUpdateView.as_view(), name='schedulesource-edit'), path('schedule/import/<int:pk>', schedules.ScheduleSourceImportDetailView.as_view(), name='schedulesourceimport-detail'), - path('sos/', events.SoSIndexView.as_view(), name='sos'), path('sos/new', events.SoSCreateView.as_view(), name='sos-create'), path('sos/<uuid:pk>/', events.SosEditView.as_view(), name='sos-edit'), path('sos/<uuid:pk>/delete', events.SosDeleteView.as_view(), name='sos-delete'), - path('wa', workadventure.IndexView.as_view(), name='workadventure'), path('wa/maps', workadventure.MapsView.as_view(), name='workadventure-map-list'), path('wa/map/<uuid:pk>', workadventure.MapView.as_view(), name='workadventure-map-detail'), @@ -140,7 +111,6 @@ urlpatterns = [ path('wa/texture/<uuid:pk>/assembly_remove', workadventure.TextureAssemblyRemoveView.as_view(), name='workadventure-texture-assembly-remove'), path('wa/texture/<uuid:pk>/user_assign', workadventure.TextureUserAssignView.as_view(), name='workadventure-texture-user-assign'), path('wa/texture/<uuid:pk>/user_remove', workadventure.TextureUserRemoveView.as_view(), name='workadventure-texture-user-remove'), - path('users', users.UsersView.as_view(), name='users'), path('users/<int:pk>', users.UserView.as_view(), name='user-detail'), path('users/<int:pk>/block', users.UserBlockView.as_view(), name='user-block'), @@ -151,10 +121,7 @@ urlpatterns = [ path('users/<int:user_id>/board', users.UserBoardEntries.as_view(page=1), name='user-board'), path('users/<int:user_id>/board/page<int:page>', users.UserBoardEntries.as_view(), name='user-board-page'), path('users/<int:user_id>/board-hide/', users.UserBoardEntriesHide.as_view(), name='user-board-hide'), - path('vouchers', vouchers.VouchersView.as_view(), name='vouchers'), - path('set_language', set_language, name='set_language'), - path('_boom', misc.BoomView.as_view()), ] diff --git a/src/backoffice/views/assemblies.py b/src/backoffice/views/assemblies.py index 6e93cd3a8..2d7099ec3 100644 --- a/src/backoffice/views/assemblies.py +++ b/src/backoffice/views/assemblies.py @@ -1,22 +1,25 @@ import logging from datetime import date +from rest_framework.authtoken.models import Token + from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied -from django.http import HttpResponse, Http404 +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render -from django.utils.safestring import mark_safe -from django.views.generic import TemplateView, View -from django.views.generic.edit import CreateView, FormView, UpdateView from django.urls import reverse from django.utils import timezone from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.text import format_lazy -from django.utils.translation import get_language, gettext, gettext_lazy as _, gettext_noop -from rest_framework.authtoken.models import Token +from django.utils.translation import get_language, gettext, gettext_noop +from django.utils.translation import gettext_lazy as _ +from django.views.generic import TemplateView, View +from django.views.generic.edit import CreateView, FormView, UpdateView -from core.models.assemblies import Assembly, AssemblyMember, AssemblyLink +from core.integrations import BigBlueButton, Hangar, IntegrationError, WorkAdventure +from core.models.assemblies import Assembly, AssemblyLink, AssemblyMember from core.models.conference import ConferenceExportCache from core.models.events import Event from core.models.rooms import Room, RoomLink @@ -24,18 +27,23 @@ from core.models.sso import Application from core.models.tags import ConferenceTag from core.models.users import PlatformUser from core.models.voucher import Voucher -from core.integrations import BigBlueButton, Hangar, IntegrationError, WorkAdventure - -from ..forms import \ - AssemblyAddApplicationForm, AssemblyAddMemberForm, \ - AssemblyCreateForm, AssemblyCreateRoomGenericForm, AssemblyCreateRoomBigBlueButtonForm, AssemblyCreateRoomWorkAdventureForm, AssemblyCreateRoomHangarForm, \ - AssemblyEditForm, \ - AssemblyMemberEditForm, \ - CreateAssemblyRoomLinkForm, \ - EditAssemblyRoomForm, EditAssemblyRoomHangarForm, EditAssemblyRoomWorkAdventureForm - -from .mixins import ConferenceMixin, AssemblyMixin +from ..forms import ( + AssemblyAddApplicationForm, + AssemblyAddMemberForm, + AssemblyCreateForm, + AssemblyCreateRoomBigBlueButtonForm, + AssemblyCreateRoomGenericForm, + AssemblyCreateRoomHangarForm, + AssemblyCreateRoomWorkAdventureForm, + AssemblyEditForm, + AssemblyMemberEditForm, + CreateAssemblyRoomLinkForm, + EditAssemblyRoomForm, + EditAssemblyRoomHangarForm, + EditAssemblyRoomWorkAdventureForm, +) +from .mixins import AssemblyMixin, ConferenceMixin logger = logging.getLogger(__name__) @@ -225,8 +233,8 @@ class EditAssemblyView(AssemblyMixin, UpdateView): # update tags: go through supplied list of tags given_tags = form.cleaned_data.get('tags').split(',') - for tag in given_tags: - tag = tag.strip() # type: str + for raw_tag in given_tags: + tag = raw_tag.strip() # type: str if len(tag) == 0: # skip empty ones continue @@ -282,7 +290,7 @@ class EditAssemblyView(AssemblyMixin, UpdateView): if parent_id != assembly.parent_id: if parent is not None and parent.hierarchy == Assembly.Hierarchy.CLUSTER_RESTRICTED: - raise PermissionDenied() + raise PermissionDenied if parent is not None: logger.info( 'Assigning assembly "%(assembly_slug)s" (%(assembly_pk)s) to "%(parent_slug)s" (%(parent_pk)s) upon request by <%(user)s>.', @@ -328,14 +336,19 @@ class EditAssemblyView(AssemblyMixin, UpdateView): if settings.DEBUG: messages.info( self.request, - format_html('<b>DEBUG INFO:</b> <em>changes</em> on assembly "{0}"', assembly.slug) + - mark_safe('<table class="table table-border table-striped table-sm"><thead><tr><th>field</th><th>value</th></tr></thead><tbody>') + # noqa:E501 - mark_safe(''.join(format_html( - '<tr><td>{0}</td><td>{1}</td></tr>', - k, - format_html('<s>{}</s> {}', *v) if isinstance(v, tuple) else v, - ) for k, v in changes.items())) + - mark_safe('</tbody></table>') + format_html('<b>DEBUG INFO:</b> <em>changes</em> on assembly "{0}"', assembly.slug) + + mark_safe('<table class="table table-border table-striped table-sm"><thead><tr><th>field</th><th>value</th></tr></thead><tbody>') # noqa:E501 + + mark_safe( + ''.join( + format_html( + '<tr><td>{0}</td><td>{1}</td></tr>', + k, + format_html('<s>{}</s> {}', *v) if isinstance(v, tuple) else v, + ) + for k, v in changes.items() + ) + ) + + mark_safe('</tbody></table>'), ) assembly.save() @@ -382,11 +395,11 @@ class AssemblyEditChildrenView(AssemblyMixin, View): assembly = self.assembly # say 'Not Found' if assembly is not a cluster or clusters aren't supported at all if not assembly.is_cluster or not self.conference.support_clusters: - raise Http404() + raise Http404 # bail out if the current user is not associated as a contact if not assembly.user_can_manage(self.request.user, staff_can_manage=True): - raise PermissionDenied() + raise PermissionDenied return assembly @@ -445,9 +458,11 @@ class AssemblyEditChildrenView(AssemblyMixin, View): return redirect('backoffice:assembly-editchildren', pk=assembly.pk) def get(self, *args, **kwargs): - candidates_qs = Assembly.objects.accessible_by_user(conference=self.conference, user=self.request.user). \ - filter(hierarchy=Assembly.Hierarchy.REGULAR, parent=None). \ - exclude(state_assembly__in=[Assembly.State.NONE, Assembly.State.REJECTED, Assembly.State.HIDDEN, Assembly.State.PLANNED]) + candidates_qs = ( + Assembly.objects.accessible_by_user(conference=self.conference, user=self.request.user) + .filter(hierarchy=Assembly.Hierarchy.REGULAR, parent=None) + .exclude(state_assembly__in=[Assembly.State.NONE, Assembly.State.REJECTED, Assembly.State.HIDDEN, Assembly.State.PLANNED]) + ) candidates = list(candidates_qs.order_by('name')) context = self.get_context_data() @@ -464,7 +479,7 @@ class AssemblyEditLinksView(AssemblyMixin, View): # bail out if the current user is not associated as a contact if not assembly.user_can_manage(self.request.user, staff_can_manage=True): - raise PermissionDenied() + raise PermissionDenied return assembly @@ -510,17 +525,21 @@ class AssemblyEditLinksView(AssemblyMixin, View): messages.success(request, gettext('assemblyedit_removedlink').format(linked_name=linkee.name)) logger.info( 'Assembly "%s" (%s): removed link to "%s" (%s) upon request by <%s>', - assembly.slug, assembly.pk, - linkee.slug, linkee.pk, + assembly.slug, + assembly.pk, + linkee.slug, + linkee.pk, request.user.username, ) return redirect('backoffice:assembly-editlinks', pk=assembly.pk) def get(self, *args, **kwargs): - candidates_qs = Assembly.objects.accessible_by_user(conference=self.conference, user=self.request.user). \ - filter(hierarchy=Assembly.Hierarchy.REGULAR). \ - exclude(state_assembly__in=[Assembly.State.NONE, Assembly.State.PLANNED, Assembly.State.HIDDEN, Assembly.State.REJECTED]) + candidates_qs = ( + Assembly.objects.accessible_by_user(conference=self.conference, user=self.request.user) + .filter(hierarchy=Assembly.Hierarchy.REGULAR) + .exclude(state_assembly__in=[Assembly.State.NONE, Assembly.State.PLANNED, Assembly.State.HIDDEN, Assembly.State.REJECTED]) + ) candidates = list(candidates_qs.order_by('name')) context = self.get_context_data() context['candidates'] = candidates @@ -627,9 +646,9 @@ class MembersView(AssemblyMixin, TemplateView): assembly_management = True def get_queryset(self): - return AssemblyMember.objects.manageable_by_user_for_assembly(user=self.request.user, - assembly=self.assembly - ).prefetch_related('member__communication_channels') + return AssemblyMember.objects.manageable_by_user_for_assembly(user=self.request.user, assembly=self.assembly).prefetch_related( + 'member__communication_channels' + ) def get_context_data(self, *args, **kwargs): ctx = super().get_context_data(*args, **kwargs) @@ -638,11 +657,11 @@ class MembersView(AssemblyMixin, TemplateView): return ctx def post(self, *args, **kwargs): - for k in self.request.POST: - if '-' not in k: + for data_pair in self.request.POST: + if '-' not in data_pair: continue - k, v = k.split('-') + k, v = data_pair.split('-') if k == 'hide': m = self.get_queryset().select_related('member').get(member_id=int(v)) @@ -754,7 +773,7 @@ class MembersAddView(AssemblyMixin, FormView): }, ) - else: + else: # noqa: PLR5501 if m.member == self.request.user and not self.staff_access: messages.error(self.request, format_lazy(_('Assembly__members__cant_modify_self'))) else: @@ -833,11 +852,14 @@ class AuthView(AssemblyMixin, FormView): authorization_grant_type=data['grant_type'], ) app.save() - messages.success(self.request, format_html( - '{msg}:<br><strong><code>{secret}</code></strong>', - msg=_('Application__newclientsecret'), - secret=app.client_secret, - )) + messages.success( + self.request, + format_html( + '{msg}:<br><strong><code>{secret}</code></strong>', + msg=_('Application__newclientsecret'), + secret=app.client_secret, + ), + ) logger.info( 'New OAuth2 app "%(app_name)s" created for assembly %(assembly)s by %(user)s', {'app_name': app.nam, 'assembly': self.assembly, 'user': self.request.user}, @@ -885,7 +907,7 @@ class AuthAppView(AssemblyMixin, UpdateView): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) if obj.assembly_id != self.assembly.id: - raise Application.DoesNotExist() + raise Application.DoesNotExist return obj def form_valid(self, form): @@ -921,21 +943,21 @@ class CreateRoomView(AssemblyMixin, FormView): if self.room_type == Room.RoomType.BIGBLUEBUTTON: if BigBlueButton is None or not BigBlueButton.can_create_for_assembly(self.assembly): messages.error(self.request, 'BBB not available') - raise RoomNotAvailableError() + raise RoomNotAvailableError return AssemblyCreateRoomBigBlueButtonForm(self.request.POST, assembly=self.assembly) if self.room_type == Room.RoomType.WORKADVENTURE: if WorkAdventure is None or not WorkAdventure.can_create_for_assembly(self.assembly): messages.error(self.request, 'WA not available') - raise RoomNotAvailableError() + raise RoomNotAvailableError return AssemblyCreateRoomWorkAdventureForm(self.request.POST, assembly=self.assembly) if self.room_type == Room.RoomType.HANGAR: if Hangar is None or not Hangar.can_create_for_assembly(self.assembly): messages.error(self.request, 'Hangar not available') - raise RoomNotAvailableError() + raise RoomNotAvailableError return AssemblyCreateRoomHangarForm(self.request.POST, assembly=self.assembly) @@ -945,7 +967,7 @@ class CreateRoomView(AssemblyMixin, FormView): else: logger.warning('Unexpected room_type "%s" upon creating new room for %s.', self.room_type, self.assembly) messages.warning(self.request, _('internal_error_please_retry')) - raise RoomNotAvailableError() + raise RoomNotAvailableError def get_context_data(self, *args, **kwargs): ctx = super().get_context_data(*args, **kwargs) @@ -953,11 +975,13 @@ class CreateRoomView(AssemblyMixin, FormView): if not self.room_type: ctx['rooms_available'] = rooms_available = {k[0]: True for k in Room.RoomType.choices} - rooms_available.update({ - 'workadventure': WorkAdventure is not None and WorkAdventure.can_create_for_assembly(self.assembly), - 'bbb': BigBlueButton is not None and BigBlueButton.can_create_for_assembly(self.assembly), - 'hangar': Hangar is not None and Hangar.can_create_for_assembly(self.assembly), - }) + rooms_available.update( + { + 'workadventure': WorkAdventure is not None and WorkAdventure.can_create_for_assembly(self.assembly), + 'bbb': BigBlueButton is not None and BigBlueButton.can_create_for_assembly(self.assembly), + 'hangar': Hangar is not None and Hangar.can_create_for_assembly(self.assembly), + } + ) ctx['support_bbb'] = settings.INTEGRATIONS_BBB ctx['support_hangar'] = settings.INTEGRATIONS_HANGAR ctx['support_wa'] = settings.INTEGRATIONS_WORKADVENTURE diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py index 503704a7f..55affa7ab 100644 --- a/src/backoffice/views/assemblyteam.py +++ b/src/backoffice/views/assemblyteam.py @@ -1,27 +1,27 @@ import csv -from io import StringIO import json import logging +from io import StringIO from django.contrib import messages -from django.contrib.gis.geos import Point, MultiPolygon, Polygon +from django.contrib.gis.geos import MultiPolygon, Point, Polygon from django.contrib.postgres.aggregates import StringAgg -from django.db.models import Q, OuterRef, Subquery -from django.http import HttpResponse, Http404 +from django.db.models import OuterRef, Q, Subquery +from django.http import Http404, HttpResponse +from django.shortcuts import redirect, render from django.urls import reverse -from django.shortcuts import render, redirect from django.utils.html import format_html -from django.utils.translation import gettext, gettext_lazy as _ -from django.views.generic import ListView, View, DetailView +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ +from django.views.generic import DetailView, ListView, View from core.models import Room -from core.models.assemblies import Assembly, AssemblyMember, AssemblyLink +from core.models.assemblies import Assembly, AssemblyLink, AssemblyMember from core.models.conference import ConferenceExportCache from core.models.users import UserCommunicationChannel -from .mixins import ConferenceMixin, AssemblyMixin from ..templatetags.c3assemblies import get_language_item - +from .mixins import AssemblyMixin, ConferenceMixin logger = logging.getLogger(__name__) @@ -32,7 +32,7 @@ class AssemblyTeamMixin(ConferenceMixin): active_page = 'assemblies' base_view_name = 'backoffice:assemblies' list_view_name = 'backoffice:assemblieslist' - sidebar_caption = _("nav_assemblies") + sidebar_caption = _('nav_assemblies') status_field = 'state_assembly' MODES = { @@ -53,7 +53,6 @@ class AssemblyTeamMixin(ConferenceMixin): lists = [] context['sidebar'] = { 'title': self.sidebar_caption, - # 'title_link': reverse(self.base_view_name), 'items': [ { 'caption': _('Assemblys'), @@ -69,37 +68,47 @@ class AssemblyTeamMixin(ConferenceMixin): } for m, (q, t) in self.MODES.items(): - assemblies.append({ - 'mode': m, - 'caption': t, - 'count': self.conference.assemblies.filter(q).count(), - 'link': reverse(self.base_view_name) + '?mode=' + m, - }) - - lists.append({ - 'caption': 'slug, name, friends & WA', - 'link': reverse(self.list_view_name, kwargs={'variant': 'slugname'}) + '?mode=accepted', - 'variant': 'slugname', - }) - - lists.append({ - 'caption': 'contacts', - 'link': reverse(self.list_view_name, kwargs={'variant': 'assemblycontacts'}) + '?mode=accepted', - 'variant': 'assemblycontacts', - }) - - lists.append({ - 'caption': 'contact mails', - 'link': reverse(self.list_view_name, kwargs={'variant': 'contactsmail'}) + '?mode=accepted', - 'variant': 'contactsmail', - }) + assemblies.append( + { + 'mode': m, + 'caption': t, + 'count': self.conference.assemblies.filter(q).count(), + 'link': reverse(self.base_view_name) + '?mode=' + m, + } + ) + + lists.append( + { + 'caption': 'slug, name, friends & WA', + 'link': reverse(self.list_view_name, kwargs={'variant': 'slugname'}) + '?mode=accepted', + 'variant': 'slugname', + } + ) + + lists.append( + { + 'caption': 'contacts', + 'link': reverse(self.list_view_name, kwargs={'variant': 'assemblycontacts'}) + '?mode=accepted', + 'variant': 'assemblycontacts', + } + ) + + lists.append( + { + 'caption': 'contact mails', + 'link': reverse(self.list_view_name, kwargs={'variant': 'contactsmail'}) + '?mode=accepted', + 'variant': 'contactsmail', + } + ) if self.conference.additional_fields_schema is not None: - lists.append({ - 'caption': 'registration', - 'link': reverse(self.list_view_name, kwargs={'variant': 'registration'}) + '?mode=accepted', - 'variant': 'registration', - }) + lists.append( + { + 'caption': 'registration', + 'link': reverse(self.list_view_name, kwargs={'variant': 'registration'}) + '?mode=accepted', + 'variant': 'registration', + } + ) return context @@ -147,9 +156,7 @@ def build_nav_from_assembly(assembly): } if assmbly.is_cluster: - me['children'] = [ - _build_nav_from_assembly(a, way_up=False) for a in assmbly.children.order_by('slug').all() - ] + me['children'] = [_build_nav_from_assembly(a, way_up=False) for a in assmbly.children.order_by('slug').all()] me['expanded'] = True return [me] if way_up else me @@ -252,43 +259,54 @@ class AssembliesListsView(AssembliesListMixin, View): variant_fields = None if variant == 'slugname': + def wa_room_status(a): try: r = a.rooms.get(room_type=Room.RoomType.WORKADVENTURE) - return "x" if not r.blocked else "b" + return 'x' if not r.blocked else 'b' except Room.DoesNotExist: - return "" + return '' except Room.MultipleObjectsReturned: - return "+" + return '+' # all assemblies with slug + name + related assemblies qs = tuple( - (a.slug, a.name, a.parent, ", ".join(link.b.slug for link in AssemblyLink.objects.filter(a=a)), wa_room_status(a)) - for a in self.get_queryset() + (a.slug, a.name, a.parent, ', '.join(link.b.slug for link in AssemblyLink.objects.filter(a=a)), wa_room_status(a)) for a in self.get_queryset() ) variant_name = 'slug, name, parent, link, wa' variant_fields = [_('Assembly__slug'), _('Assembly__name'), _('Assembly__parent'), _('assembly_links'), _('Room__type-workadventure')] elif variant == 'contactsmail': # all assembly contacts' email addresses with duplicates removed - qs = UserCommunicationChannel.objects.filter( - channel=UserCommunicationChannel.Channel.MAIL, - is_verified=True, - user_id__in=AssemblyMember.objects.filter(assembly__in=self.get_queryset(), can_manage_assembly=True).values('member_id'), - ).values('address').distinct().order_by('address') + qs = ( + UserCommunicationChannel.objects.filter( + channel=UserCommunicationChannel.Channel.MAIL, + is_verified=True, + user_id__in=AssemblyMember.objects.filter(assembly__in=self.get_queryset(), can_manage_assembly=True).values('member_id'), + ) + .values('address') + .distinct() + .order_by('address') + ) variant_name = 'assembly contacts emails' variant_fields = [_('UserCommunicationChannel__address')] elif variant == 'assemblycontacts': # all assemblies with their associated contacts - user_mails = Subquery(UserCommunicationChannel.objects.filter( - user_id=OuterRef('member_id'), is_verified=True, channel=UserCommunicationChannel.Channel.MAIL, - ).values('address')) - qs = AssemblyMember.objects.filter(assembly__in=self.get_queryset(), can_manage_assembly=True) \ - .annotate(mail=StringAgg(user_mails, '; ')) \ - .values_list('assembly__slug', 'is_representative', 'mail') \ + user_mails = Subquery( + UserCommunicationChannel.objects.filter( + user_id=OuterRef('member_id'), + is_verified=True, + channel=UserCommunicationChannel.Channel.MAIL, + ).values('address') + ) + qs = ( + AssemblyMember.objects.filter(assembly__in=self.get_queryset(), can_manage_assembly=True) + .annotate(mail=StringAgg(user_mails, '; ')) + .values_list('assembly__slug', 'is_representative', 'mail') .order_by('assembly__slug', 'mail') + ) variant_name = 'contact emails (assembly managers)' variant_fields = [_('Assembly__slug'), _('AssemblyMember__is_representative'), _('UserCommunicationChannel__address')] @@ -431,7 +449,7 @@ class AssemblyEditHierarchyView(SingleAssemblyTeamMixin, View): # say 'Not Found' if clusters aren't supported at all if not self.conference.support_clusters: - raise Http404() + raise Http404 return assembly @@ -448,8 +466,7 @@ class AssemblyEditHierarchyView(SingleAssemblyTeamMixin, View): comment = request.POST.get('comment', '').strip() # don't allow changing cluster to regular if it has children - if value == Assembly.Hierarchy.REGULAR and \ - assembly.is_cluster and assembly.children.exists(): + if value == Assembly.Hierarchy.REGULAR and assembly.is_cluster and assembly.children.exists(): messages.error(request, gettext('assemblyedit_clusterstillhaschildren')) return redirect(reverse('backoffice:assemblyteam-edithierarchy', kwargs={'pk': assembly.pk}) + '?value=' + value) diff --git a/src/backoffice/views/auth.py b/src/backoffice/views/auth.py index 3c5c283a1..e453fbed6 100644 --- a/src/backoffice/views/auth.py +++ b/src/backoffice/views/auth.py @@ -5,7 +5,7 @@ from django.http import JsonResponse from django.urls import reverse_lazy from django.views.generic import TemplateView, View -from core.views import BaseLoginView, BaseRegistrationActivationView, BaseRegistrationView, BasePasswordResetView, BasePasswordResetConfirmView +from core.views import BaseLoginView, BasePasswordResetConfirmView, BasePasswordResetView, BaseRegistrationActivationView, BaseRegistrationView from .mixins import ConferenceMixin, PasswordMixin diff --git a/src/backoffice/views/badges.py b/src/backoffice/views/badges.py index 51a4f4e83..a2121e519 100644 --- a/src/backoffice/views/badges.py +++ b/src/backoffice/views/badges.py @@ -90,7 +90,7 @@ class RemoveBadgeView(AssemblyMixin, DeleteView): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) if obj.issuing_assembly != self.assembly: - raise self.model.DoesNotExist() + raise self.model.DoesNotExist return obj def delete(self, *args, **kwargs): diff --git a/src/backoffice/views/channelteam.py b/src/backoffice/views/channelteam.py index 1d352b968..bee9d64cf 100644 --- a/src/backoffice/views/channelteam.py +++ b/src/backoffice/views/channelteam.py @@ -1,13 +1,14 @@ -from django.utils.translation import gettext_lazy as _ from django.db.models import Q +from django.utils.translation import gettext_lazy as _ from core.models.assemblies import Assembly -from .assemblyteam import AssembliesView, AssembliesListsView +from .assemblyteam import AssembliesListsView, AssembliesView class ChannelsMixin: - """ sets options that configure the Assemblies views to work in Channels mode """ + """sets options that configure the Assemblies views to work in Channels mode""" + MODES = { 'all': (Q(), _('nav_channels_all')), 'accepted': (Q(state_channel__in=Assembly.PUBLIC_STATES), _('nav_channels_accepted')), @@ -19,7 +20,7 @@ class ChannelsMixin: active_page = 'channels' base_view_name = 'backoffice:channels' list_view_name = 'backoffice:channelslist' - sidebar_caption = _("nav_channels") + sidebar_caption = _('nav_channels') status_field = 'state_channel' diff --git a/src/backoffice/views/events.py b/src/backoffice/views/events.py index b3cf344ec..8a0a693d0 100644 --- a/src/backoffice/views/events.py +++ b/src/backoffice/views/events.py @@ -8,12 +8,13 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView from django.views.generic.edit import CreateView, DeleteView, ModelFormMixin, UpdateView -from backoffice.forms import EventForm from core.models.assemblies import Assembly from core.models.conference import ConferenceExportCache from core.models.events import Event from core.models.rooms import Room +from backoffice.forms import EventForm + from .mixins import AssemblyMixin, ConferenceMixin logger = logging.getLogger(__name__) @@ -46,8 +47,7 @@ class EventFormMixin(ModelFormMixin): result = super().form_valid(form) if form.publish: messages.success( - self.request, - _('Event__published %(event_id)s %(event_type)s') % {'event_id': form.instance.id, 'event_type': self.event_type_name} + self.request, _('Event__published %(event_id)s %(event_type)s') % {'event_id': form.instance.id, 'event_type': self.event_type_name} ) elif form.create: messages.success(self.request, _('Event__created %(event_id)s %(event_type)s') % {'event_id': form.instance.id, 'event_type': self.event_type_name}) @@ -66,9 +66,7 @@ class EventPublicationMixin(ModelFormMixin): try: self.get_object().clean(True) except ValidationError as error: - context.update({ - 'publication_errors': error.message_dict - }) + context.update({'publication_errors': error.message_dict}) return context def get_form_kwargs(self, *args, **kwargs): @@ -146,7 +144,7 @@ class AssemblyRemoveEventView(AssemblyMixin, DeleteView): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) if obj.assembly != self.assembly: - raise self.model.DoesNotExist() + raise self.model.DoesNotExist return obj def form_valid(self, *args, **kwargs): @@ -171,14 +169,16 @@ class SoSIndexView(ConferenceMixin, ListView): } def get_queryset(self, *args, **kwargs): - return Event.objects.manageable_by_user(user=self.request.user, conference=self.conference) \ - .filter(kind=Event.Kind.SELF_ORGANIZED).select_related('owner') + return ( + Event.objects.manageable_by_user(user=self.request.user, conference=self.conference).filter(kind=Event.Kind.SELF_ORGANIZED).select_related('owner') + ) class SosDeleteView(ConferenceMixin, DeleteView): def get_queryset(self, *args, **kwargs): - return Event.objects.manageable_by_user(user=self.request.user, conference=self.conference) \ - .filter(kind=Event.Kind.SELF_ORGANIZED).select_related('owner') + return ( + Event.objects.manageable_by_user(user=self.request.user, conference=self.conference).filter(kind=Event.Kind.SELF_ORGANIZED).select_related('owner') + ) def delete(self, *args, **kwargs): result = super().delete(*args, **kwargs) diff --git a/src/backoffice/views/map.py b/src/backoffice/views/map.py index f3c0ce91a..8ac2ed094 100644 --- a/src/backoffice/views/map.py +++ b/src/backoffice/views/map.py @@ -6,11 +6,12 @@ from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView from django.views.generic.detail import SingleObjectTemplateResponseMixin -from django.views.generic.edit import UpdateView, CreateView, ModelFormMixin +from django.views.generic.edit import CreateView, ModelFormMixin, UpdateView from core.models.map import MapFloor, MapPOI -from .mixins import ConferenceMixin, guess_active_sidebar_item + from ..forms import POIForm +from .mixins import ConferenceMixin, guess_active_sidebar_item logger = logging.getLogger(__name__) @@ -24,16 +25,18 @@ class MapAdminMixin(ConferenceMixin): ctx['active_page'] = 'map' ctx['uses_map'] = True - floors = [{ - 'caption': f'{floor["name"]} ({floor["index"]})', - 'link': reverse('backoffice:map-floor-edit', kwargs={'pk': floor["pk"]}), - } for floor in MapFloor.objects.filter(conference=self.conference).order_by('index').values('pk', 'name', 'index')] + floors = [ + { + 'caption': f'{floor["name"]} ({floor["index"]})', + 'link': reverse('backoffice:map-floor-edit', kwargs={'pk': floor['pk']}), + } + for floor in MapFloor.objects.filter(conference=self.conference).order_by('index').values('pk', 'name', 'index') + ] pois = [] poi_count = 0 ctx['sidebar'] = { 'title': _('nav_map'), - # 'title_link': reverse(self.base_view_name), 'items': [ { 'caption': _('MapFloors'), @@ -51,15 +54,20 @@ class MapAdminMixin(ConferenceMixin): for poi in MapPOI.objects.filter(conference=self.conference).values('id', 'visible', 'name').iterator(): poi_count += 1 - pois.append({ - 'caption': poi['name'] if poi['visible'] else format_html('<s>{}</s>', poi['name']), - 'link': reverse('backoffice:map-poi-edit', kwargs={'pk': poi['id']}), - }) - pois.insert(0, { - 'caption': format_html('<i>({all})</i>', all=_('all')), - 'link': reverse('backoffice:map-poi-list'), - 'count': poi_count, - }) + pois.append( + { + 'caption': poi['name'] if poi['visible'] else format_html('<s>{}</s>', poi['name']), + 'link': reverse('backoffice:map-poi-edit', kwargs={'pk': poi['id']}), + } + ) + pois.insert( + 0, + { + 'caption': format_html('<i>({all})</i>', all=_('all')), + 'link': reverse('backoffice:map-poi-list'), + 'count': poi_count, + }, + ) # try to guess 'active' sidebar item guess_active_sidebar_item(self.request, ctx['sidebar']['items'], with_query_string=False) diff --git a/src/backoffice/views/misc.py b/src/backoffice/views/misc.py index 788bf322e..bfdc982f0 100644 --- a/src/backoffice/views/misc.py +++ b/src/backoffice/views/misc.py @@ -1,6 +1,6 @@ +from django.shortcuts import redirect, render from django.views.generic import View from django.views.generic.edit import FormView -from django.shortcuts import redirect, render from core.models import Assembly, Conference @@ -35,14 +35,16 @@ class IndexView(ConferenceMixin, View): myassemblies = None ctx = self.get_context_data() - ctx.update({ - 'active_page': 'home', - 'myassemblies': myassemblies, - }) + ctx.update( + { + 'active_page': 'home', + 'myassemblies': myassemblies, + } + ) return render(self.request, 'backoffice/index.html', ctx) class BoomView(View): def get(self, *args, **kwargs): - raise Exception("Bazinga! Testing the error handling, are we?") + raise Exception('Bazinga! Testing the error handling, are we?') diff --git a/src/backoffice/views/mixins.py b/src/backoffice/views/mixins.py index c3b37a8fe..7151e278b 100644 --- a/src/backoffice/views/mixins.py +++ b/src/backoffice/views/mixins.py @@ -3,7 +3,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix from django.core.exceptions import PermissionDenied from django.http import HttpRequest from django.shortcuts import redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from core.models.assemblies import Assembly @@ -12,7 +12,6 @@ from core.models.conference import Conference, ConferenceMember from core.models.rooms import Room from core.models.sso import Application - _UNSET = object() @@ -66,9 +65,11 @@ class ConferenceMixin(LoginRequiredMixin, PermissionRequiredMixin): @property def is_channel_team(self): - return self.conference.support_channels and \ - self.request.user.is_authenticated and \ - self.request.user.has_conference_staffpermission(self.conference, 'core.channel_team') + return ( + self.conference.support_channels + and self.request.user.is_authenticated + and self.request.user.has_conference_staffpermission(self.conference, 'core.channel_team') + ) def dispatch(self, request, *args, **kwargs): if self.require_conference and self.conference is None: @@ -88,35 +89,41 @@ class ConferenceMixin(LoginRequiredMixin, PermissionRequiredMixin): conference_list = Conference.objects.accessible_by_user(self.request.user).all() - context.update({ - 'LANGUAGES': settings.LANGUAGES, - 'conference': self.conference, - 'conferencemember': self.conferencemember, - 'conferences': conference_list, - }) + context.update( + { + 'LANGUAGES': settings.LANGUAGES, + 'conference': self.conference, + 'conferencemember': self.conferencemember, + 'conferences': conference_list, + } + ) if self.request.user.is_authenticated: - context.update({ - 'has_sos': self.conferencemember is not None, - 'has_assemblies': self.is_assembly_team, - 'has_channel': self.is_channel_team, - 'has_pages': self.request.user.has_conference_staffpermission(self.conference, 'core.static_pages'), - 'has_map': self.request.user.has_conference_staffpermission(self.conference, 'core.map_edit'), - 'has_users': self.request.user.has_conference_staffpermission(self.conference, 'core.platformusers'), - 'has_schedules': self.request.user.has_conference_staffpermission(self.conference, 'core.scheduleadmin'), - 'has_workadventure': - settings.INTEGRATIONS_WORKADVENTURE and self.request.user.has_conference_staffpermission(self.conference, 'core.workadventure_admin'), - }) + context.update( + { + 'has_sos': self.conferencemember is not None, + 'has_assemblies': self.is_assembly_team, + 'has_channel': self.is_channel_team, + 'has_pages': self.request.user.has_conference_staffpermission(self.conference, 'core.static_pages'), + 'has_map': self.request.user.has_conference_staffpermission(self.conference, 'core.map_edit'), + 'has_users': self.request.user.has_conference_staffpermission(self.conference, 'core.platformusers'), + 'has_schedules': self.request.user.has_conference_staffpermission(self.conference, 'core.scheduleadmin'), + 'has_workadventure': settings.INTEGRATIONS_WORKADVENTURE + and self.request.user.has_conference_staffpermission(self.conference, 'core.workadventure_admin'), + } + ) else: - context.update({ - 'has_assemblies': False, - 'has_channel': False, - 'has_pages': False, - 'has_map': False, - 'has_users': False, - 'has_schedules': False, - 'has_workadventure': False, - }) + context.update( + { + 'has_assemblies': False, + 'has_channel': False, + 'has_pages': False, + 'has_map': False, + 'has_users': False, + 'has_schedules': False, + 'has_workadventure': False, + } + ) return context @@ -172,14 +179,16 @@ class AssemblyMixin(ConferenceMixin): if assembly.has_user(self.request.user): # don't set self._staff_mode = False here as this would prevent assembly team members to edit their own assemblies - if not self._staff_access and \ - assembly.state_assembly in [Assembly.State.NONE, Assembly.State.HIDDEN] and \ - assembly.state_channel in [Assembly.State.NONE, Assembly.State.HIDDEN]: - raise Assembly.DoesNotExist() + if ( + not self._staff_access + and assembly.state_assembly in [Assembly.State.NONE, Assembly.State.HIDDEN] + and assembly.state_channel in [Assembly.State.NONE, Assembly.State.HIDDEN] + ): + raise Assembly.DoesNotExist # neither owner/manager nor assembly team? go away elif not self._assembly_staff_access and not self._channels_staff_access: - raise PermissionDenied() + raise PermissionDenied self._assembly = assembly return assembly @@ -248,97 +257,132 @@ class AssemblyMixin(ConferenceMixin): organisation = [] sidebar.append({'caption': _('backoffice:assembly-organisational-data'), 'children': organisation}) - organisation.append({ - 'caption': _('backoffice:assembly-basic-data'), - 'link': reverse('backoffice:assembly-edit', kwargs={'pk': assembly.id}), - }) + organisation.append( + { + 'caption': _('backoffice:assembly-basic-data'), + 'link': reverse('backoffice:assembly-edit', kwargs={'pk': assembly.id}), + } + ) if assembly.is_cluster: - organisation.append({ - 'caption': 'Sub-Assemblies', - 'link': reverse('backoffice:assembly-editchildren', kwargs={'pk': assembly.id}), - }) - - organisation.append({ - 'caption': 'Links', - 'link': reverse('backoffice:assembly-editlinks', kwargs={'pk': assembly.id}), - }) - - organisation.append({ - 'caption': _('Assembly__members'), - 'link': reverse('backoffice:assembly-members', kwargs={'pk': assembly.id}), - 'count': assembly.members.count(), - }) + organisation.append( + { + 'caption': 'Sub-Assemblies', + 'link': reverse('backoffice:assembly-editchildren', kwargs={'pk': assembly.id}), + } + ) + + organisation.append( + { + 'caption': 'Links', + 'link': reverse('backoffice:assembly-editlinks', kwargs={'pk': assembly.id}), + } + ) + + organisation.append( + { + 'caption': _('Assembly__members'), + 'link': reverse('backoffice:assembly-members', kwargs={'pk': assembly.id}), + 'count': assembly.members.count(), + } + ) if can_manage: - apps = [{ - 'caption': f'{app["name"]}', - 'link': reverse('backoffice:assembly-auth-app', kwargs={'assembly': assembly.id, 'pk': app['id']}), - 'classes': [], - } for app in Application.objects.filter(assembly=self.assembly).values('id', 'name')] - sidebar.append({ - 'caption': _('Assembly__authentication'), - 'children': apps, - 'count': len(apps), - 'link': reverse('backoffice:assembly-auth', kwargs={'assembly': assembly.id}), - }) + apps = [ + { + 'caption': f'{app["name"]}', + 'link': reverse('backoffice:assembly-auth-app', kwargs={'assembly': assembly.id, 'pk': app['id']}), + 'classes': [], + } + for app in Application.objects.filter(assembly=self.assembly).values('id', 'name') + ] + sidebar.append( + { + 'caption': _('Assembly__authentication'), + 'children': apps, + 'count': len(apps), + 'link': reverse('backoffice:assembly-auth', kwargs={'assembly': assembly.id}), + } + ) if (voucher_count := assembly.get_voucher_count(with_always_public=True)) is not None: - organisation.append({ - 'caption': _('Vouchers'), - 'count': voucher_count, - 'link': reverse('backoffice:assembly-vouchers', kwargs={'pk': assembly.id}), - }) - - rooms = [{ - 'caption': f'{room["name"]} ({room["room_type"]})', - 'link': reverse('backoffice:assembly-room', kwargs={'assembly': assembly.id, 'pk': room['id']}), - 'classes': ['blocked'] if room['blocked'] else [], - } for room in assembly.rooms.exclude(room_type=Room.RoomType.PROJECT).values('id', 'name', 'room_type', 'blocked')] - sidebar.append({ - 'caption': _('backoffice:assembly-rooms'), - 'children': rooms, - 'count': len(rooms), - 'add_link': reverse('backoffice:assembly-create-room', kwargs={'assembly': assembly.id}) if can_manage else None, - }) - - projects = [{ - 'caption': prj["name"], - 'link': reverse('backoffice:assembly-room', kwargs={'assembly': assembly.id, 'pk': prj['id']}), - 'classes': ['blocked'] if prj['blocked'] else [], - } for prj in assembly.rooms.filter(room_type=Room.RoomType.PROJECT).values('id', 'name', 'blocked')] - sidebar.append({ - 'caption': _('backoffice:assembly-projects'), - 'children': projects, - 'count': len(projects), - 'add_link': reverse('backoffice:assembly-create-project', kwargs={'assembly': assembly.id}) if can_manage else None, - }) - - events = [{ - 'caption': ev["name"], - 'link': reverse('backoffice:assembly-event', kwargs={'assembly': assembly.id, 'pk': ev['id']}), - 'classes': ['blocked'] if not ev['is_public'] else [], - } for ev in assembly.events.values('id', 'name', 'is_public')] - sidebar.append({ - 'caption': 'Events', - 'link': reverse('backoffice:assembly-events', kwargs={'assembly': assembly.id}), - 'children': events, - 'count': len(events), - 'add_link': reverse('backoffice:assembly-create-event', kwargs={'assembly': assembly.id}) if can_manage else None, - }) - - badges = [{ - 'caption': b["name"], - 'link': reverse('backoffice:assembly-badge', kwargs={'assembly': assembly.id, 'pk': b['id']}), - 'classes': ['blocked'] if b['state'] == Badge.State.PLANNED else [], - } for b in assembly.badges.values('id', 'name', 'state')] - sidebar.append({ - 'caption': 'Badges', - 'link': reverse('backoffice:assembly-badges', kwargs={'assembly': assembly.id}), - 'children': badges, - 'count': len(badges), - 'add_link': reverse('backoffice:assembly-create-badge', kwargs={'assembly': assembly.id}) if can_manage else None, - }) + organisation.append( + { + 'caption': _('Vouchers'), + 'count': voucher_count, + 'link': reverse('backoffice:assembly-vouchers', kwargs={'pk': assembly.id}), + } + ) + + rooms = [ + { + 'caption': f'{room["name"]} ({room["room_type"]})', + 'link': reverse('backoffice:assembly-room', kwargs={'assembly': assembly.id, 'pk': room['id']}), + 'classes': ['blocked'] if room['blocked'] else [], + } + for room in assembly.rooms.exclude(room_type=Room.RoomType.PROJECT).values('id', 'name', 'room_type', 'blocked') + ] + sidebar.append( + { + 'caption': _('backoffice:assembly-rooms'), + 'children': rooms, + 'count': len(rooms), + 'add_link': reverse('backoffice:assembly-create-room', kwargs={'assembly': assembly.id}) if can_manage else None, + } + ) + + projects = [ + { + 'caption': prj['name'], + 'link': reverse('backoffice:assembly-room', kwargs={'assembly': assembly.id, 'pk': prj['id']}), + 'classes': ['blocked'] if prj['blocked'] else [], + } + for prj in assembly.rooms.filter(room_type=Room.RoomType.PROJECT).values('id', 'name', 'blocked') + ] + sidebar.append( + { + 'caption': _('backoffice:assembly-projects'), + 'children': projects, + 'count': len(projects), + 'add_link': reverse('backoffice:assembly-create-project', kwargs={'assembly': assembly.id}) if can_manage else None, + } + ) + + events = [ + { + 'caption': ev['name'], + 'link': reverse('backoffice:assembly-event', kwargs={'assembly': assembly.id, 'pk': ev['id']}), + 'classes': ['blocked'] if not ev['is_public'] else [], + } + for ev in assembly.events.values('id', 'name', 'is_public') + ] + sidebar.append( + { + 'caption': 'Events', + 'link': reverse('backoffice:assembly-events', kwargs={'assembly': assembly.id}), + 'children': events, + 'count': len(events), + 'add_link': reverse('backoffice:assembly-create-event', kwargs={'assembly': assembly.id}) if can_manage else None, + } + ) + + badges = [ + { + 'caption': b['name'], + 'link': reverse('backoffice:assembly-badge', kwargs={'assembly': assembly.id, 'pk': b['id']}), + 'classes': ['blocked'] if b['state'] == Badge.State.PLANNED else [], + } + for b in assembly.badges.values('id', 'name', 'state') + ] + sidebar.append( + { + 'caption': 'Badges', + 'link': reverse('backoffice:assembly-badges', kwargs={'assembly': assembly.id}), + 'children': badges, + 'count': len(badges), + 'add_link': reverse('backoffice:assembly-create-badge', kwargs={'assembly': assembly.id}) if can_manage else None, + } + ) # try to guess 'active' sidebar item guess_active_sidebar_item(self.request, context['sidebar']['items']) @@ -372,7 +416,7 @@ def guess_active_sidebar_item(request: HttpRequest, sidebar_items: dict, with_qu x['children'] = None -class PasswordMixin(): +class PasswordMixin: def get_context_data(self, *args, **kwargs): try: context = super().get_context_data(*args, **kwargs) @@ -380,8 +424,10 @@ class PasswordMixin(): # super() does not have .get_context_data(), e.g. if it's a plain View context = {} - context.update({ - 'LANGUAGES': settings.LANGUAGES, - }) + context.update( + { + 'LANGUAGES': settings.LANGUAGES, + } + ) return context diff --git a/src/backoffice/views/schedules.py b/src/backoffice/views/schedules.py index f000fc1d3..ed92c9481 100644 --- a/src/backoffice/views/schedules.py +++ b/src/backoffice/views/schedules.py @@ -2,7 +2,7 @@ from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse from django.views import View -from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView, TemplateView +from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView from django.views.generic.detail import SingleObjectMixin from core.models import ScheduleSource, ScheduleSourceImport @@ -62,16 +62,16 @@ class ScheduleSourcesDoImportView(ScheduleAdminMixin, SingleObjectMixin, View): if src.assembly is not None: messages.info(request, f"+ ScheduleSourceImport {job.pk} for '{src.assembly.slug}' ({src.pk})") else: - messages.info(request, f"+ ScheduleSourceImport {job.pk} for wildcard assembly ({src.pk})") + messages.info(request, f'+ ScheduleSourceImport {job.pk} for wildcard assembly ({src.pk})') try: result = job.do_import() if result: - messages.success(request, f"ScheduleSourceImport {job.pk} succeeded") + messages.success(request, f'ScheduleSourceImport {job.pk} succeeded') else: - messages.warning(request, f"ScheduleSourceImport {job.pk} failed") + messages.warning(request, f'ScheduleSourceImport {job.pk} failed') except Exception as err: - messages.error(request, f"ScheduleSourceImport {job.pk} threw exception: {err}") + messages.error(request, f'ScheduleSourceImport {job.pk} threw exception: {err}') return redirect('backoffice:schedulesourceimport-detail', pk=str(job.pk)) diff --git a/src/backoffice/views/users.py b/src/backoffice/views/users.py index c0423c445..02e6976dd 100644 --- a/src/backoffice/views/users.py +++ b/src/backoffice/views/users.py @@ -1,31 +1,29 @@ import logging +from oauth2_provider.models import AccessToken + from django.conf import settings from django.contrib import messages from django.contrib.sessions.exceptions import SuspiciousSession from django.contrib.sessions.models import Session -from django.core.mail import EmailMessage from django.core.exceptions import ObjectDoesNotExist +from django.core.mail import EmailMessage from django.db.models import F from django.shortcuts import redirect, render from django.urls import reverse from django.utils.translation import gettext from django.views import View -from django.views.generic import DetailView, TemplateView, ListView +from django.views.generic import DetailView, ListView, TemplateView from django.views.generic.detail import SingleObjectMixin -from oauth2_provider.models import AccessToken - from core.integrations import WorkAdventureIntegration from core.models import BulletinBoardEntry from core.models.conference import ConferenceMember from core.models.messages import DirectMessage from core.models.users import PlatformUser, UserCommunicationChannel - from .mixins import ConferenceMixin - logger = logging.getLogger(__name__) MAX_ROWS = 42 @@ -52,13 +50,14 @@ class UsersView(ConferenceMixin, TemplateView): qs = qs.filter(conferences__conference=self.conference) qs_base = qs.annotate(is_conference_staff=F('conferences__is_staff'), active_conference_angel=F('conferences__active_angel')) if '@' in search_term: - direct_matches += list(qs.filter(communication_channels__channel=UserCommunicationChannel.Channel.MAIL, - communication_channels__address=search_term)) + direct_matches += list( + qs.filter(communication_channels__channel=UserCommunicationChannel.Channel.MAIL, communication_channels__address=search_term) + ) direct_matches += list(qs.filter(username__iexact=search_term)) qs = qs_base.filter(username__icontains=search_term).exclude(pk__in=[direct_match.pk for direct_match in direct_matches]) qs = qs.order_by('username') - results = list(direct_matches) + list(qs[:MAX_ROWS + 1]) + results = list(direct_matches) + list(qs[: MAX_ROWS + 1]) more_results = len(results) > MAX_ROWS if more_results: @@ -242,7 +241,7 @@ class UserRenameView(ConferenceMixin, DetailView): user = self.get_object() new_name = self.request.POST.get('new_name', '').strip() - if new_name == '' or new_name == user.username: + if new_name in ('', user.username): return self.get(*args, **kwargs) if PlatformUser.objects.filter(username=new_name): @@ -323,7 +322,7 @@ class UserMailView(ConferenceMixin, DetailView): messages.info(self.request, f'Sent mail to "{user.username}" with subject "{subject}" ({len(addresses)} recipients(s)).') except ObjectDoesNotExist: - messages.error(self.request, "Mail not send because channel not marked to be used for notifications or not verified!") + messages.error(self.request, 'Mail not send because channel not marked to be used for notifications or not verified!') except Exception as e: messages.error(self.request, e) @@ -355,5 +354,5 @@ class UserBoardEntriesHide(ConferenceMixin, View): def post(self, request, user_id, **kwargs): BulletinBoardEntry.objects.filter(owner=user_id, pk=request.POST['pk']).update(hidden=True) - messages.success(request, gettext("BulletinBoardEntry--deleted")) + messages.success(request, gettext('BulletinBoardEntry--deleted')) return redirect(reverse('backoffice:user-board', kwargs={'user_id': user_id})) diff --git a/src/backoffice/views/utils.py b/src/backoffice/views/utils.py index af7afe2fb..08b16493d 100644 --- a/src/backoffice/views/utils.py +++ b/src/backoffice/views/utils.py @@ -25,11 +25,13 @@ def extend_context(request, context, conference=None): conference = get_conference(request) conference_list = Conference.objects.accessible_by_user(request.user).all() - context.update({ - 'LANGUAGES': settings.LANGUAGES, - 'conference': conference, - 'conferences': conference_list, - 'has_pages': request.user.has_pages(conference) if request.user.is_authenticated else False, - }) + context.update( + { + 'LANGUAGES': settings.LANGUAGES, + 'conference': conference, + 'conferences': conference_list, + 'has_pages': request.user.has_pages(conference) if request.user.is_authenticated else False, + } + ) return context diff --git a/src/backoffice/views/vouchers.py b/src/backoffice/views/vouchers.py index c49edc477..7a184b693 100644 --- a/src/backoffice/views/vouchers.py +++ b/src/backoffice/views/vouchers.py @@ -8,7 +8,6 @@ from core.models import VoucherEntry from .mixins import ConferenceMixin - logger = logging.getLogger(__name__) MAX_ROWS = 42 @@ -30,7 +29,7 @@ class VouchersView(ConferenceMixin, TemplateView): direct_matches = [] qs = VoucherEntry.objects.filter(content__icontains=search_term) - results = list(direct_matches) + list(qs[:MAX_ROWS + 1]) + results = list(direct_matches) + list(qs[: MAX_ROWS + 1]) more_results = len(results) > MAX_ROWS if more_results: diff --git a/src/backoffice/views/wiki.py b/src/backoffice/views/wiki.py index ed9dda4fd..8d0aa7268 100644 --- a/src/backoffice/views/wiki.py +++ b/src/backoffice/views/wiki.py @@ -1,17 +1,17 @@ -from datetime import datetime import json +from datetime import datetime from django.contrib import messages from django.db import models from django.db.models import OuterRef from django.db.models.expressions import F from django.db.models.functions import JSONObject -from django.views.generic import ListView, TemplateView -from django.views.generic.edit import UpdateView, DeleteView from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ +from django.views.generic import ListView, TemplateView +from django.views.generic.edit import DeleteView, UpdateView -from core.models import StaticPage, StaticPageRevision, StaticPageNamespace +from core.models import StaticPage, StaticPageNamespace, StaticPageRevision from ..forms import StaticPageEditForm from .mixins import ConferenceMixin, guess_active_sidebar_item @@ -89,11 +89,14 @@ class PagesView(WikiAdminMixin, ListView): template_name = 'backoffice/wiki_page_list.html' def get_queryset(self, *args, **kwargs): - last_revision_details = StaticPageRevision.objects.filter(page=OuterRef('pk'), pk=OuterRef('public_revision'))\ - .values(data=LastRevisionJSONObject(author=F('author__username'), timestamp=F('timestamp')))[0:1] - return StaticPage.objects.accessible_by_user(conference=self.conference, user=self.request.user, language=None)\ - .order_by('slug', 'language')\ + last_revision_details = StaticPageRevision.objects.filter(page=OuterRef('pk'), pk=OuterRef('public_revision')).values( + data=LastRevisionJSONObject(author=F('author__username'), timestamp=F('timestamp')) + )[0:1] + return ( + StaticPage.objects.accessible_by_user(conference=self.conference, user=self.request.user, language=None) + .order_by('slug', 'language') .annotate(last_revision_details=last_revision_details) + ) class PageView(WikiAdminMixin, UpdateView): @@ -130,7 +133,7 @@ class PageDeleteView(WikiAdminMixin, DeleteView): def form_valid(self, form): res = super().form_valid(form) - messages.success(self.request, _("StaticPage--deleted")) + messages.success(self.request, _('StaticPage--deleted')) return res @@ -147,5 +150,5 @@ class PageRevisionDeleteView(WikiAdminMixin, DeleteView): def form_valid(self, form): res = super().form_valid(form) - messages.success(self.request, _("StaticPageRevision--deleted")) + messages.success(self.request, _('StaticPageRevision--deleted')) return res diff --git a/src/backoffice/views/workadventure.py b/src/backoffice/views/workadventure.py index baa65f135..8a13ebe84 100644 --- a/src/backoffice/views/workadventure.py +++ b/src/backoffice/views/workadventure.py @@ -5,24 +5,22 @@ from uuid import UUID from django.conf import settings from django.contrib import messages from django.core.exceptions import ValidationError +from django.db.models import Q from django.http import HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import reverse from django.utils.html import format_html -from django.views.generic import CreateView, DetailView, ListView, TemplateView, UpdateView, View, DeleteView -from django.views.generic.detail import SingleObjectMixin - from django.utils.translation import gettext_lazy as _ -from django.db.models import Q +from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView, View +from django.views.generic.detail import SingleObjectMixin from core.integrations.workadventure import WorkAdventureIntegration -from core.models import PlatformUser, ConferenceMember, Assembly +from core.models import Assembly, ConferenceMember, PlatformUser from core.models.rooms import Room from core.models.workadventure import WorkadventureSession, WorkadventureTexture -from .mixins import ConferenceMixin, guess_active_sidebar_item from ..forms import CreateWorkadventureTextureForm - +from .mixins import ConferenceMixin, guess_active_sidebar_item logger = logging.getLogger(__name__) MAX_ROWS = 42 @@ -43,7 +41,7 @@ class WorkAdventureAdminMixin(ConferenceMixin): def get_context_data(self, *args, **kwargs): if not settings.INTEGRATIONS_WORKADVENTURE: - messages.warning(self.request, "WorkAdventure integration NOT active!") + messages.warning(self.request, 'WorkAdventure integration NOT active!') context = super().get_context_data(*args, **kwargs) context['active_page'] = 'workadventure' @@ -69,16 +67,18 @@ class WorkAdventureAdminMixin(ConferenceMixin): 'link': reverse('backoffice:workadventure-texture-list'), 'expanded': False, 'count': self.conference.workadventure_textures.count(), - } + }, ], } for m, (q, t) in self.BACKEND_STATUS.items(): - maps.append({ - 'caption': t, - 'count': self.conference.rooms.filter(q).count(), - 'link': reverse('backoffice:workadventure-map-list') + '?mode=' + m, - }) + maps.append( + { + 'caption': t, + 'count': self.conference.rooms.filter(q).count(), + 'link': reverse('backoffice:workadventure-map-list') + '?mode=' + m, + } + ) # try to guess 'active' sidebar item guess_active_sidebar_item(self.request, context['sidebar']['items'], with_query_string=True) @@ -218,17 +218,24 @@ class MapSyncView(WorkAdventureMapMixin, SingleObjectMixin, View): res = WorkAdventureIntegration.trigger_map_synchronization(room, force) msg = format_html( '<strong>sync room {id} (force={force}):</strong><br><pre class="small">{res}</pre>', - id=room.pk, force=force, res=json.dumps(res, indent=2), + id=room.pk, + force=force, + res=json.dumps(res, indent=2), ) if res.get('_errors'): messages.error(request, msg) else: messages.success(request, msg) except Exception as err: - messages.error(request, format_html( - '<strong>sync room {id} (force={force}):</strong><br><pre class="small">{err}</pre>', - id=room.pk, force=force, err=err, - )) + messages.error( + request, + format_html( + '<strong>sync room {id} (force={force}):</strong><br><pre class="small">{err}</pre>', + id=room.pk, + force=force, + err=err, + ), + ) return redirect('backoffice:workadventure-map-detail', pk=room.pk) @@ -290,7 +297,8 @@ class SessionPushDataView(SessionView): res = WorkAdventureIntegration.push_userinfo_session(obj) msg = format_html( '<strong>push userinfo {id} to backend:</strong><br><pre class="small">{res}</pre>', - id=obj.pk, res=json.dumps(res, indent=2), + id=obj.pk, + res=json.dumps(res, indent=2), ) if res.get('_errors'): messages.error(request, msg) @@ -312,7 +320,8 @@ class SessionDeleteView(SessionView): res = WorkAdventureIntegration.terminate_session(obj) msg = format_html( '<strong>termination of WA session {id} in backend:</strong><br><pre class="small">{res}</pre>', - id=obj_id, res=json.dumps(res, indent=2), + id=obj_id, + res=json.dumps(res, indent=2), ) if res.get('_errors'): messages.error(request, msg) @@ -328,10 +337,14 @@ class SessionDeleteView(SessionView): messages.success(request, 'WA session deletion failure (' + obj_id + ')') except Exception as err: - messages.error(request, format_html( - '<strong>delete session: general failure</strong> ({id})<br><pre class="small">{err}</pre>', - err=err, id=obj_id, - )) + messages.error( + request, + format_html( + '<strong>delete session: general failure</strong> ({id})<br><pre class="small">{err}</pre>', + err=err, + id=obj_id, + ), + ) else: messages.warning(request, 'session was already deleted.') diff --git a/src/core/abuse.py b/src/core/abuse.py index d92df660c..14f4216b4 100644 --- a/src/core/abuse.py +++ b/src/core/abuse.py @@ -1,17 +1,19 @@ +import contextlib + from django.contrib.sites.shortcuts import get_current_site from django.core.mail import send_mail from django.template import loader -from django.urls import reverse, NoReverseMatch +from django.urls import NoReverseMatch, reverse from django.utils.translation import gettext_lazy as _ - REPORT_CATEGORIES = { - 'abuse': (_("abuse_report_category-abuse"), 'Abuse', 'abuse@cccv.de'), - 'content': (_("abuse_report_category-content"), 'Content', 'report@cccv.de'), - 'person': (_("abuse_report_category-person"), 'Person', 'report@cccv.de'), - 'tech': (_("abuse_report_category-tech"), 'Technical', 'hub@cccv.de'), - # 'map': (_("abuse_report_category-map"), 'Map', 'world@cccv.de'), - 'unknown': (_("abuse_report_category-misc"), 'Unknown', 'report@cccv.de'), + 'abuse': (_('abuse_report_category-abuse'), 'Abuse', 'abuse@cccv.de'), + 'content': (_('abuse_report_category-content'), 'Content', 'report@cccv.de'), + 'person': (_('abuse_report_category-person'), 'Person', 'report@cccv.de'), + 'tech': (_('abuse_report_category-tech'), 'Technical', 'hub@cccv.de'), + # TODO: Make this depended on whether WA is enabled + # 'map': (_("abuse_report_category-map"), 'Map', 'world@cccv.de'),# noqa: ERA001 + 'unknown': (_('abuse_report_category-misc'), 'Unknown', 'report@cccv.de'), } @@ -40,12 +42,10 @@ def report_content(request, reporter, category, reported_content, problem_messag 'message2': proposed_solution, } - try: + with contextlib.suppress(NoReverseMatch): context['reporter_profile'] = reverse('plainui:user_by_uuid', kwargs={'uuid': str(reporter.uuid)}) - except NoReverseMatch: - pass - subject = "New %s Report" % (readable_category,) + subject = f'New {readable_category} Report' body = loader.render_to_string(EMAIL_TEMPLATE_NAME, context) send_mail( diff --git a/src/core/admin.py b/src/core/admin.py index 21a378a78..223482025 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -8,23 +8,47 @@ from django.contrib.gis.admin import GISModelAdmin from django.db.models import F from django.utils.translation import gettext_lazy as _ -from .models import \ - BulletinBoardEntry, \ - Conference, ConferenceMember, ConferenceNavigationItem, ConferenceTag, ConferenceTrack, \ - DereferrerStats, \ - Event, EventAttachment, EventParticipant, \ - PlatformUser, \ - Room, RoomLink, \ - Assembly, AssemblyLink, AssemblyMember, AssemblyLogEntry, \ - MapFloor, MapPOI, \ - MetaNavItem, \ - Badge, BadgeCategory, BadgeToken, BadgeTokenTimeConstraint, \ - ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping, \ - StaticPage, StaticPageRevision, StaticPageNamespace, \ - TagItem, \ - UserCommunicationChannel, UserContact, UserBadge, UserDereferrerAllowlist, \ - Voucher, VoucherEntry, \ - WorkadventureSession, WorkadventureTexture +from .models import ( + Assembly, + AssemblyLink, + AssemblyLogEntry, + AssemblyMember, + Badge, + BadgeCategory, + BadgeToken, + BadgeTokenTimeConstraint, + BulletinBoardEntry, + Conference, + ConferenceMember, + ConferenceNavigationItem, + ConferenceTag, + ConferenceTrack, + DereferrerStats, + Event, + EventAttachment, + EventParticipant, + MapFloor, + MapPOI, + MetaNavItem, + PlatformUser, + Room, + RoomLink, + ScheduleSource, + ScheduleSourceImport, + ScheduleSourceMapping, + StaticPage, + StaticPageNamespace, + StaticPageRevision, + TagItem, + UserBadge, + UserCommunicationChannel, + UserContact, + UserDereferrerAllowlist, + Voucher, + VoucherEntry, + WorkadventureSession, + WorkadventureTexture, +) logger = logging.getLogger(__name__) @@ -57,21 +81,21 @@ class ArrayFieldEntryFilter(FieldListFilter): # assemble available options v = self.value() yield { - "selected": v is None or v == '', - "query_string": changelist.get_query_string(remove=[self.parameter_name]), - "display": _("All"), + 'selected': v is None or v == '', + 'query_string': changelist.get_query_string(remove=[self.parameter_name]), + 'display': _('All'), } for value in sorted(values, key=lambda x: x.upper()): yield { - "selected": v == value, - "query_string": changelist.get_query_string({self.parameter_name: value}), - "display": value, + 'selected': v == value, + 'query_string': changelist.get_query_string({self.parameter_name: value}), + 'display': value, } def queryset(self, request, queryset): if query := self.value(): qs_filter = { - f"{self.field_path}__contains": query.split(','), + f'{self.field_path}__contains': query.split(','), } queryset = queryset.filter(**qs_filter) return queryset @@ -108,22 +132,22 @@ class UserAssemblyMemberInline(admin.TabularInline): class UserFavoriteEventInline(admin.TabularInline): model = Event.favorited_by.through extra = 0 - verbose_name = _("PlatformUser__favorite_event") - verbose_name_plural = _("PlatformUser__favorite_events") + verbose_name = _('PlatformUser__favorite_event') + verbose_name_plural = _('PlatformUser__favorite_events') class UserFavoriteAssemblyInline(admin.TabularInline): model = Assembly.favorited_by.through extra = 0 - verbose_name = _("PlatformUser__favorite_assembly") - verbose_name_plural = _("PlatformUser__favorite_assemblies") + verbose_name = _('PlatformUser__favorite_assembly') + verbose_name_plural = _('PlatformUser__favorite_assemblies') class UserPersonalCalendarEventInline(admin.TabularInline): model = Event.in_personal_calendar.through extra = 0 - verbose_name = _("PlatformUser__calendar_event") - verbose_name_plural = _("PlatformUser__calendar_events") + verbose_name = _('PlatformUser__calendar_event') + verbose_name_plural = _('PlatformUser__calendar_events') class PlatformUserAdmin(UserAdmin): @@ -140,7 +164,7 @@ class PlatformUserAdmin(UserAdmin): ('Accessibility', {'fields': ('theme', 'no_animations', 'colorblind', 'high_contrast', 'tag_ignorelist')}), ('Disturbance Settings', {'fields': ('receive_dms', 'receive_dm_images', 'receive_audio', 'receive_video', 'autoaccept_contacts')}), ('Permissions', {'fields': ('is_active', 'shadow_banned', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), - ('Notifications', {'fields': ('admin_notification', )}), + ('Notifications', {'fields': ('admin_notification',)}), ('Important dates', {'fields': ('last_login', 'date_joined')}), ('Security', {'fields': ('allow_reset_non_primary',)}), ) @@ -218,23 +242,29 @@ class ConferenceNavigationItemAdmin(admin.ModelAdmin): return qs.order_by('conference__name', F('parent__index').asc(nulls_first=True), 'index') fieldsets = ( - ('Organisation', { - 'fields': ['conference', 'parent', 'index'], - }), - ('Data', { - 'fields': [ - 'is_visible', - 'icon', - ('label_de', 'label_en'), - ('title_de', 'title_en'), - 'url', - ], - }), + ( + 'Organisation', + { + 'fields': ['conference', 'parent', 'index'], + }, + ), + ( + 'Data', + { + 'fields': [ + 'is_visible', + 'icon', + ('label_de', 'label_en'), + ('title_de', 'title_en'), + 'url', + ], + }, + ), ) def formfield_for_foreignkey(self, db_field, request, **kwargs): - if db_field.name == "parent": - kwargs["queryset"] = ConferenceNavigationItem.objects.filter(parent=None).order_by('index') + if db_field.name == 'parent': + kwargs['queryset'] = ConferenceNavigationItem.objects.filter(parent=None).order_by('index') return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -254,12 +284,18 @@ class ConferenceTrackAdmin(admin.ModelAdmin): readonly_fields = ['conference'] fieldsets = ( - ('Organisation', { - 'fields': ['conference', 'is_public'], - }), - ('Data', { - 'fields': ['slug', 'name', 'banner_image'], - }), + ( + 'Organisation', + { + 'fields': ['conference', 'is_public'], + }, + ), + ( + 'Data', + { + 'fields': ['slug', 'name', 'banner_image'], + }, + ), ) def get_readonly_fields(self, request, obj=None, **kwargs): @@ -328,18 +364,30 @@ class AssemblyAdmin(GISModelAdmin): inlines = [TagsInline, BadgeInline, AssemblyLinkInline, AssemblyMemberInline, AssemblyLogEntryInline] fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference', 'state_assembly', 'state_channel', 'hierarchy', 'parent', 'is_official'], - }), - ('Data', { - 'fields': ['is_physical', 'is_virtual', 'is_remote', 'slug', 'name', 'description', 'assembly_link', 'banner_image'], - }), - ('Registration', { - 'fields': ['registration_details'], - }), - ('Location', { - 'fields': ['assembly_location', 'location_point', 'location_boundaries'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'state_assembly', 'state_channel', 'hierarchy', 'parent', 'is_official'], + }, + ), + ( + 'Data', + { + 'fields': ['is_physical', 'is_virtual', 'is_remote', 'slug', 'name', 'description', 'assembly_link', 'banner_image'], + }, + ), + ( + 'Registration', + { + 'fields': ['registration_details'], + }, + ), + ( + 'Location', + { + 'fields': ['assembly_location', 'location_point', 'location_boundaries'], + }, + ), ) def formfield_for_foreignkey(self, db_field, request, **kwargs): @@ -356,12 +404,18 @@ class AssemblyAdmin(GISModelAdmin): def get_fieldsets(self, request, obj=None, **kwargs): if obj is None: return [ - ('Organisation', { - 'fields': ['id', 'conference', 'state_assembly', 'state_channel', 'hierarchy', 'is_official'], - }), - ('Data', { - 'fields': ['is_physical', 'is_virtual', 'is_remote', 'slug', 'name'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'state_assembly', 'state_channel', 'hierarchy', 'is_official'], + }, + ), + ( + 'Data', + { + 'fields': ['is_physical', 'is_virtual', 'is_remote', 'slug', 'name'], + }, + ), ] return super().get_fieldsets(request, obj, **kwargs) @@ -382,16 +436,25 @@ class AssemblyLogEntryAdmin(admin.ModelAdmin): search_fields = ['assembly__slug', 'assembly__name', 'comment', 'changes'] readonly_fields = ['timestamp'] fieldsets = [ - ('Organisation', { - 'fields': ['timestamp', 'assembly'], - }), - ('Data', { - 'fields': [ - ('kind', 'user',), - 'comment', - 'changes', - ], - }), + ( + 'Organisation', + { + 'fields': ['timestamp', 'assembly'], + }, + ), + ( + 'Data', + { + 'fields': [ + ( + 'kind', + 'user', + ), + 'comment', + 'changes', + ], + }, + ), ] @@ -403,12 +466,18 @@ class MapFloorAdmin(admin.ModelAdmin): ordering = ['conference', 'index'] fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference'], - }), - ('Data', { - 'fields': ['index', ('name_de', 'name_en')], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference'], + }, + ), + ( + 'Data', + { + 'fields': ['index', ('name_de', 'name_en')], + }, + ), ) def get_readonly_fields(self, request, obj=None, **kwargs): @@ -426,17 +495,24 @@ class MapPOIAdmin(GISModelAdmin): search_fields = ['name', 'description'] fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference'], - }), - ('Data', { - 'fields': ['visible', 'is_official', - ('name_de', 'name_en'), - ('description_de', 'description_en')], - }), - ('Location', { - 'fields': ['location_floor', 'location_point'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference'], + }, + ), + ( + 'Data', + { + 'fields': ['visible', 'is_official', ('name_de', 'name_en'), ('description_de', 'description_en')], + }, + ), + ( + 'Location', + { + 'fields': ['location_floor', 'location_point'], + }, + ), ) def get_readonly_fields(self, request, obj=None, **kwargs): @@ -454,7 +530,9 @@ class BadgeAdmin(admin.ModelAdmin): model = Badge fields = ['name', 'issuing_assembly', 'state', 'category', 'description', 'location', 'image', 'issuing_token'] - readonly_fields = ['issuing_assembly',] + readonly_fields = [ + 'issuing_assembly', + ] list_display = ['__str__', 'issuing_assembly', 'category'] list_display_links = ['__str__'] @@ -462,8 +540,8 @@ class BadgeAdmin(admin.ModelAdmin): class BadgeTokenTimeConstraintInline(admin.TabularInline): model = BadgeTokenTimeConstraint fields = ['date_time_range'] - verbose_name = _("BadgeToken__badge_token_time_constraint") - verbose_name_plural = _("BadgeToken__badge_token_time_constraints") + verbose_name = _('BadgeToken__badge_token_time_constraint') + verbose_name_plural = _('BadgeToken__badge_token_time_constraints') class BadgeTokenAdmin(admin.ModelAdmin): @@ -480,6 +558,7 @@ class BadgeTokenAdmin(admin.ModelAdmin): def valid(self, obj): return obj.valid + valid.boolean = True def time_constraints_list(self, obj): @@ -567,15 +646,24 @@ class EventAdmin(admin.ModelAdmin): readonly_fields = ['id', 'conference', 'get_is_imported'] fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference', 'track', 'assembly', 'room', 'kind', 'is_public'], - }), - ('Schedule', { - 'fields': ['schedule_start', 'schedule_duration', 'get_is_imported'], - }), - ('Data', { - 'fields': ['name', 'slug', 'language', 'description', 'banner_image', 'additional_data'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'track', 'assembly', 'room', 'kind', 'is_public'], + }, + ), + ( + 'Schedule', + { + 'fields': ['schedule_start', 'schedule_duration', 'get_is_imported'], + }, + ), + ( + 'Data', + { + 'fields': ['name', 'slug', 'language', 'description', 'banner_image', 'additional_data'], + }, + ), ) def formfield_for_foreignkey(self, db_field, request, **kwargs): @@ -596,12 +684,18 @@ class EventAdmin(admin.ModelAdmin): def get_fieldsets(self, request, obj=None, **kwargs): if obj is None: return [ - ('Organisation', { - 'fields': ['id', 'conference', 'assembly'], - }), - ('Data', { - 'fields': ['name'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'assembly'], + }, + ), + ( + 'Data', + { + 'fields': ['name'], + }, + ), ] return super().get_fieldsets(request, obj, **kwargs) @@ -639,15 +733,24 @@ class RoomAdmin(admin.ModelAdmin): ordering = ('-conference__id', F('assembly__is_official').desc(nulls_last=True), 'assembly__name', F('capacity').desc(nulls_last=True), 'name') fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference', 'assembly', 'is_public_fahrplan', 'blocked', 'reserve_capacity'], - }), - ('Data', { - 'fields': ['name', 'room_type', 'capacity', 'occupants', 'description'], - }), - ('Backend', { - 'fields': ['backend_link', 'backend_link_branch', 'backend_status', 'backend_data', 'director_data'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'assembly', 'is_public_fahrplan', 'blocked', 'reserve_capacity'], + }, + ), + ( + 'Data', + { + 'fields': ['name', 'room_type', 'capacity', 'occupants', 'description'], + }, + ), + ( + 'Backend', + { + 'fields': ['backend_link', 'backend_link_branch', 'backend_status', 'backend_data', 'director_data'], + }, + ), ) def formfield_for_foreignkey(self, db_field, request, **kwargs): @@ -665,12 +768,18 @@ class RoomAdmin(admin.ModelAdmin): def get_fieldsets(self, request, obj=None, **kwargs): if obj is None: return [ - ('Organisation', { - 'fields': ['id', 'conference', 'assembly', 'blocked'], - }), - ('Data', { - 'fields': ['name', 'room_type', 'backend_link', 'capacity'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'assembly', 'blocked'], + }, + ), + ( + 'Data', + { + 'fields': ['name', 'room_type', 'backend_link', 'capacity'], + }, + ), ] return super().get_fieldsets(request, obj, **kwargs) @@ -706,15 +815,24 @@ class StaticPageAdmin(admin.ModelAdmin): readonly_fields = ['id', 'conference', 'language', 'body_html', 'search_content'] fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference', 'slug', 'language'], - }), - ('Configuration', { - 'fields': ['public_revision', 'protection', 'privacy', 'remove_html', 'sanitize_html'], - }), - ('Data', { - 'fields': ['title', ('search_content', 'body_html')], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference', 'slug', 'language'], + }, + ), + ( + 'Configuration', + { + 'fields': ['public_revision', 'protection', 'privacy', 'remove_html', 'sanitize_html'], + }, + ), + ( + 'Data', + { + 'fields': ['title', ('search_content', 'body_html')], + }, + ), ) def get_inline_instances(self, request, obj=None, **kwargs): @@ -739,15 +857,24 @@ class StaticPageNamespaceAdmin(admin.ModelAdmin): readonly_fields = ['id', 'conference'] fieldsets = ( - ('Organisation', { - 'fields': ['id', 'conference'], - }), - ('Namespace', { - 'fields': ['prefix', 'groups'], - }), - ('Upstream', { - 'fields': ['upstream_url', 'upstream_image_base_url'], - }), + ( + 'Organisation', + { + 'fields': ['id', 'conference'], + }, + ), + ( + 'Namespace', + { + 'fields': ['prefix', 'groups'], + }, + ), + ( + 'Upstream', + { + 'fields': ['upstream_url', 'upstream_image_base_url'], + }, + ), ) def get_readonly_fields(self, request, obj=None, **kwargs): @@ -797,6 +924,7 @@ class VoucherEntryAdmin(admin.ModelAdmin): def is_assigned(self, instance): return instance.assigned is not None + is_assigned.boolean = True @@ -862,24 +990,33 @@ class MetaNavItemAdmin(admin.ModelAdmin): readonly_fields = ['pk', 'conference', 'graphic_light_current', 'graphic_dark_current'] fieldsets = [ - ('Metadata', { - 'fields': ['pk', 'conference', 'slug'], - }), - ('Display', { - 'fields': [ - 'index', - 'visible', - 'enabled', - ('title_de', 'title_en'), - ], - }), - ('Item', { - 'fields': [ - 'url', - ('graphic_light_current', 'graphic_light'), - ('graphic_dark_current', 'graphic_dark'), - ], - }), + ( + 'Metadata', + { + 'fields': ['pk', 'conference', 'slug'], + }, + ), + ( + 'Display', + { + 'fields': [ + 'index', + 'visible', + 'enabled', + ('title_de', 'title_en'), + ], + }, + ), + ( + 'Item', + { + 'fields': [ + 'url', + ('graphic_light_current', 'graphic_light'), + ('graphic_dark_current', 'graphic_dark'), + ], + }, + ), ] def get_readonly_fields(self, request, obj=None, **kwargs): @@ -890,11 +1027,13 @@ class MetaNavItemAdmin(admin.ModelAdmin): def graphic_light_current(self, obj: MetaNavItem): return obj.get_graphic_light_as_html() + graphic_light_current.allow_tags = True graphic_light_current.caption = _('MetaNavItem__graphic_light') def graphic_dark_current(self, obj: MetaNavItem): return obj.get_graphic_dark_as_html(default_to_light=False) + graphic_dark_current.allow_tags = True graphic_dark_current.verbose_name = _('MetaNavItem__graphic_dark') diff --git a/src/core/apps.py b/src/core/apps.py index 08c8a22a9..9e5e1e9e8 100644 --- a/src/core/apps.py +++ b/src/core/apps.py @@ -2,7 +2,6 @@ import logging from django.apps import AppConfig - logger = logging.getLogger(__name__) diff --git a/src/core/base_forms.py b/src/core/base_forms.py index bbea55ed3..4c5fe68dc 100644 --- a/src/core/base_forms.py +++ b/src/core/base_forms.py @@ -1,7 +1,7 @@ from django.forms import ModelForm from modeltranslation.fields import build_localized_fieldname from modeltranslation.settings import AVAILABLE_LANGUAGES -from modeltranslation.translator import translator, NotRegistered +from modeltranslation.translator import NotRegistered, translator class TranslatedFieldsForm(ModelForm): @@ -19,7 +19,11 @@ class TranslatedFieldsForm(ModelForm): fields_to_translate = set(translation_options.get_field_names()) - for attr in ('fields', 'exclude', 'localized_fields',): + for attr in ( + 'fields', + 'exclude', + 'localized_fields', + ): fields = getattr(meta, attr, None) if fields: new_fields = [] diff --git a/src/core/forms.py b/src/core/forms.py index bae3f6827..1f5c32531 100644 --- a/src/core/forms.py +++ b/src/core/forms.py @@ -3,14 +3,14 @@ from smtplib import SMTPException from typing import Any from django.conf import settings -from django.db.models import Q from django.contrib.auth import forms as auth_forms -from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.forms import PasswordResetForm as ContribPasswordResetForm +from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives +from django.db.models import Q from django.forms import ChoiceField, EmailField, EmailInput from django.template import loader from django.utils.encoding import force_bytes diff --git a/src/core/integrations/__init__.py b/src/core/integrations/__init__.py index c6d4856c1..ab02f4673 100644 --- a/src/core/integrations/__init__.py +++ b/src/core/integrations/__init__.py @@ -1,7 +1,8 @@ from django.conf import settings + from .bigbluebutton import BigBlueButtonIntegration -from .rc3hangar import HangarIntegration from .error import IntegrationError +from .rc3hangar import HangarIntegration from .workadventure import WorkAdventureIntegration if settings.BIGBLUEBUTTON_API_URL is not None: diff --git a/src/core/integrations/bigbluebutton.py b/src/core/integrations/bigbluebutton.py index ffe772d11..bbc8f7c90 100644 --- a/src/core/integrations/bigbluebutton.py +++ b/src/core/integrations/bigbluebutton.py @@ -1,21 +1,21 @@ -from hashlib import sha1 import logging -from random import SystemRandom -import requests import string +from hashlib import sha1 +from random import SystemRandom from typing import Dict, Union -from urllib.parse import urlencode, urljoin, quote +from urllib.parse import quote, urlencode, urljoin from uuid import uuid4 from xml.etree import ElementTree as ET +import requests + from django.utils.translation import gettext as _ +from core.models import BackendMixin, Event, PlatformUser, Room from core.models.assemblies import Assembly, AssemblyMember -from core.models import BackendMixin, Event, Room, PlatformUser from .error import IntegrationError - logger = logging.getLogger(__name__) PASSWORD_CHARS = string.ascii_letters + string.digits @@ -24,16 +24,16 @@ def _params_to_str(data: Dict[str, Union[str, int, bool]]): res = {} for k, v in data.items(): if isinstance(v, bool): - v = 'true' if v else 'false' + v = 'true' if v else 'false' # noqa: PLW2901 elif isinstance(v, int): - v = str(v) + v = str(v) # noqa: PLW2901 else: assert isinstance(v, str), f'{v!r} is no string!' res[k] = v return res -class BigBlueButtonIntegration(object): +class BigBlueButtonIntegration: """ This class talks with a BigBlueButton server's API. @@ -68,14 +68,14 @@ class BigBlueButtonIntegration(object): return resp if resp.status_code != 200: - logger.warning("Request for resource %r failed with status code %r", resource, resp.status_code) - raise IntegrationError(_("Request failed")) + logger.warning('Request for resource %r failed with status code %r', resource, resp.status_code) + raise IntegrationError(_('Request failed')) try: resp = ET.fromstring(resp.text) except UnicodeDecodeError: - logger.exception("Response decoding failed") - raise IntegrationError(_("Invalid Response")) + logger.exception('Response decoding failed') + raise IntegrationError(_('Invalid Response')) return resp @@ -137,7 +137,8 @@ class BigBlueButtonIntegration(object): f' <module name="presentation">' f' <document url="{self._initial_presentation_url}" />' f' </module>' - f'</modules>') + f'</modules>' + ) try: result = self._send_request('create', params, post_body=request_body) @@ -153,8 +154,8 @@ class BigBlueButtonIntegration(object): message = result.find('message') if message is not None: message = message.text - logger.warning("Request to create Room %s failed. Message: %r", room, message) - raise IntegrationError(_("Request failed")) + logger.warning('Request to create Room %s failed. Message: %r', room, message) + raise IntegrationError(_('Request failed')) room.backend_link = result.find('meetingID').text room.backend_status = Room.BackendStatus.ACTIVE @@ -196,8 +197,8 @@ class BigBlueButtonIntegration(object): message = result.find('message') if message is not None: message = message.text - logger.warning("Request to delete room %s failed. Message: %r", room, message) - raise IntegrationError(_("Request failed")) + logger.warning('Request to delete room %s failed. Message: %r', room, message) + raise IntegrationError(_('Request failed')) room.backend_status = Room.BackendStatus.INACTIVE room.save(update_fields=['backend_status']) @@ -213,11 +214,11 @@ class BigBlueButtonIntegration(object): retcode = resp.find('returncode') if retcode is not None and retcode.text != 'SUCCESS': message_key = resp.find('messageKey') - if message_key is not None and (message_key.text == 'notFound' or message_key.text == 'invalidMeetingIdentifier'): + if message_key is not None and (message_key.text in ('notFound', 'invalidMeetingIdentifier')): room.backend_status = Room.BackendStatus.INACTIVE else: - logger.warning("getMeetingInfo request for room %r failed with message %r", room, None if message_key is None else message_key.text) - raise IntegrationError(_("Request failed")) + logger.warning('getMeetingInfo request for room %r failed with message %r', room, None if message_key is None else message_key.text) + raise IntegrationError(_('Request failed')) try: room.occupants = int(resp.find('participantCount').text) diff --git a/src/core/integrations/workadventure.py b/src/core/integrations/workadventure.py index dd5a3c3c7..094bed765 100644 --- a/src/core/integrations/workadventure.py +++ b/src/core/integrations/workadventure.py @@ -1,9 +1,11 @@ +import contextlib import logging from json import JSONDecodeError +import requests + from django.conf import settings from django.utils.translation import gettext_lazy as _ -import requests from core.models.assemblies import Assembly from core.models.conference import Conference @@ -96,9 +98,9 @@ class WorkAdventureIntegration: return {conference.slug: maps} @staticmethod - def _send_request_for_conference(url_template, url_auth, conference: Conference, - method='POST', content=None, data=None, - url_replacements=None, no_verify=False, expect_json=True): + def _send_request_for_conference( + url_template, url_auth, conference: Conference, method='POST', content=None, data=None, url_replacements=None, no_verify=False, expect_json=True + ): if url_template is None or url_template == '': logging.warning('URL is not defined.') return None, 'No PUSH url configured.' @@ -117,7 +119,7 @@ class WorkAdventureIntegration: r = session.request(method, url, json=data, data=content.encode('utf-8') if content is not None else None) if not r.ok: logging.error('Request to %s failed with %s: %s', url, r.status_code, r.text) - errmsg = 'Request got response {code}: {msg}'.format(code=r.status_code, msg=r.text) + errmsg = f'Request got response {r.status_code}: {r.text}' try: return r.json(), errmsg except JSONDecodeError: @@ -125,7 +127,7 @@ class WorkAdventureIntegration: except Exception as err: logging.exception('Request to %s failed.', url) - return None, 'Request failed: {msg}'.format(msg=str(err)) + return None, f'Request failed: {err!s}' if r.status_code == 204: # server said OK but 'No Content' @@ -142,7 +144,7 @@ class WorkAdventureIntegration: try: maps = WorkAdventureIntegration.assemble_wa_backend_maplist(conference) except Exception as err: - result['_errors'].append('Failed to assemble maps for conference {confslug}: {msg}'.format(confslug=conference.slug, msg=str(err))) + result['_errors'].append(f'Failed to assemble maps for conference {conference.slug}: {err!s}') logging.exception('WA map assembly for export failed.') return result result['maps'] = len(maps) @@ -282,10 +284,8 @@ class WorkAdventureIntegration: # summarize violations violation = director_data.get('violation', {}) - try: + with contextlib.suppress(ValueError): linter_timestamp = unix2timestamp(int(violation.get('violationCheck'))) - except ValueError: - pass linter_commit = violation.get('violationCommitHash') if not violation: linter_status = 'success' @@ -303,7 +303,7 @@ class WorkAdventureIntegration: linter_results = violation.get('linterRes', {}).get('mapLints', {}) linter_missingassets = violation.get('linterRes', {}).get('missingAssets', []) linter_missingentrypoints = violation.get('linterRes', {}).get('missingDeps', []) - linter_exitgraph = violation.get('linterRes', {}).get('exitGraph', "") + linter_exitgraph = violation.get('linterRes', {}).get('exitGraph', '') return { 'wa_published': { 'commitHash': publish_commit if has_mapinfo else None, @@ -320,6 +320,6 @@ class WorkAdventureIntegration: 'timestamp': linter_timestamp, 'missingAssets': linter_missingassets, 'missingEntrypoints': linter_missingentrypoints, - 'exitGraph': linter_exitgraph - } + 'exitGraph': linter_exitgraph, + }, } diff --git a/src/core/management/commands/assembly_contact_mails.py b/src/core/management/commands/assembly_contact_mails.py index 361e5a037..4caf3905b 100644 --- a/src/core/management/commands/assembly_contact_mails.py +++ b/src/core/management/commands/assembly_contact_mails.py @@ -6,7 +6,7 @@ from ...models.users import UserCommunicationChannel class Command(BaseCommand): - help = 'List all assemblies\' contacts\' verified e-mail addresses' + help = "List all assemblies' contacts' verified e-mail addresses" def add_arguments(self, parser): parser.add_argument('conf_slug', help='conference slug') diff --git a/src/core/management/commands/bbb_integration_revisit.py b/src/core/management/commands/bbb_integration_revisit.py index af4041322..dd92c61ce 100644 --- a/src/core/management/commands/bbb_integration_revisit.py +++ b/src/core/management/commands/bbb_integration_revisit.py @@ -3,14 +3,16 @@ import time from django.core.management.base import BaseCommand from django.db import transaction -from core.models import Conference, Room from core.integrations import BigBlueButton, IntegrationError +from core.models import Conference, Room class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '-a', '--all', action='store_true', + '-a', + '--all', + action='store_true', help='Revisit all Rooms, not just failing ones', ) @@ -30,7 +32,7 @@ class Command(BaseCommand): BigBlueButton.create_room(room) room.save() except IntegrationError as e: - print(f"Refreshing room {room!s} failed: {e!s}") + print(f'Refreshing room {room!s} failed: {e!s}') elif revisit_active and room.backend_status in {Room.BackendStatus.ACTIVE, Room.BackendStatus.FULL}: n_healthy += 1 @@ -39,7 +41,7 @@ class Command(BaseCommand): BigBlueButton.room_status(room) room.save() except IntegrationError as e: - print(f"Refreshing room {room!s} failed: {e!s}") + print(f'Refreshing room {room!s} failed: {e!s}') run_time = time.time() - start print(f"Revisited {n_fail} failing rooms {f'and {n_healthy} healthy ones'} in {run_time} Seconds.") diff --git a/src/core/management/commands/hangar_creation.py b/src/core/management/commands/hangar_creation.py index a718cb650..201952c69 100644 --- a/src/core/management/commands/hangar_creation.py +++ b/src/core/management/commands/hangar_creation.py @@ -12,7 +12,7 @@ def create_hangar(room: Room): username = room.assembly.slug cmd = ['ssh', '-i', settings.HANGAR_KEY, settings.HANGAR_HOST, settings.HANGAR_CMD, username] - result = subprocess.run(cmd, capture_output=True, timeout=42, encoding='utf-8') + result = subprocess.run(cmd, capture_output=True, timeout=42, encoding='utf-8', check=False) if result.returncode != 0: room.backend_data = { 'timestamp': timezone.now().strftime('%Y-%m-%d %H:%M:%S'), @@ -61,7 +61,7 @@ class Command(BaseCommand): contacts = create_hangar(room) msg_subject = f'Hangar "{room.assembly.slug}"' - msg_text = '''Your hangar has been created. Please visit it in the Maschinenraum for access details.''' + msg_text = """Your hangar has been created. Please visit it in the Maschinenraum for access details.""" for c in contacts: c.notify_user(msg_subject, msg_text, details_link=reverse('backoffice:assembly-room', kwargs={'assembly': room.assembly_id, 'pk': room.id})) diff --git a/src/core/management/commands/housekeeping.py b/src/core/management/commands/housekeeping.py index a1645d065..8e7ab72ca 100644 --- a/src/core/management/commands/housekeeping.py +++ b/src/core/management/commands/housekeeping.py @@ -15,8 +15,8 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument('--forever', action='store_true', help='repeat the housekeeping forever (until Ctrl+C is pressed)') parser.add_argument('--forever-delay', type=int, default=300, help='seconds to wait between housekeeping runs') - parser.add_argument('--skip-schedule-imports', action='store_true', help='don\'t do schedule imports') - parser.add_argument('--skip-wiki-imports', action='store_true', help='don\'t import wiki namespaces from upstream') + parser.add_argument('--skip-schedule-imports', action='store_true', help="don't do schedule imports") + parser.add_argument('--skip-wiki-imports', action='store_true', help="don't import wiki namespaces from upstream") def _housekeeping_directmessages(self): # clear all direct messages which are after their expiry date diff --git a/src/core/management/commands/import_mapservice_resultfile.py b/src/core/management/commands/import_mapservice_resultfile.py index f55d27c4b..4c9956521 100644 --- a/src/core/management/commands/import_mapservice_resultfile.py +++ b/src/core/management/commands/import_mapservice_resultfile.py @@ -4,11 +4,12 @@ import json from django.core.management.base import BaseCommand from api.views.workadventure import MapServiceView + from ...models import Conference class Command(BaseCommand): - help = 'import the mapservice\'s result file directly (as it would have been sent via the maps endpoint)' + help = "import the mapservice's result file directly (as it would have been sent via the maps endpoint)" def add_arguments(self, parser): parser.add_argument('conference_slug', help='slug of the conference') diff --git a/src/core/management/commands/rerender_markdown.py b/src/core/management/commands/rerender_markdown.py index 77a0c86e7..9dce899de 100644 --- a/src/core/management/commands/rerender_markdown.py +++ b/src/core/management/commands/rerender_markdown.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand -from core.models import Event, Room, Assembly, ConferenceMember, StaticPage from core.markdown import render_markdown_ex, store_relationships +from core.models import Assembly, ConferenceMember, Event, Room, StaticPage class Command(BaseCommand): diff --git a/src/core/management/commands/sanitize_database.py b/src/core/management/commands/sanitize_database.py index 23e2fffd8..d151568ea 100644 --- a/src/core/management/commands/sanitize_database.py +++ b/src/core/management/commands/sanitize_database.py @@ -1,16 +1,22 @@ from django.core.management.base import BaseCommand -from ...models import \ - Assembly, AssemblyMember, \ - ConferenceMember, ConferenceMemberTicket, ConferenceTag, ConferenceTrack, \ - DirectMessage, \ - Event, \ - PlatformUser, \ - StaticPage, \ - TagItem, \ - UserBadge, \ - UserCommunicationChannel, \ - WorkadventureSession, UserDereferrerAllowlist +from ...models import ( + Assembly, + AssemblyMember, + ConferenceMember, + ConferenceMemberTicket, + ConferenceTag, + ConferenceTrack, + DirectMessage, + Event, + PlatformUser, + StaticPage, + TagItem, + UserBadge, + UserCommunicationChannel, + UserDereferrerAllowlist, + WorkadventureSession, +) class Command(BaseCommand): @@ -86,10 +92,6 @@ class Command(BaseCommand): print('WorkadventureSession: ', end='', flush=True) print_delete_stat(WorkadventureSession.objects.all().delete()) - # from plainui.models import BulletinBoardEntry - # print('BulletinBoardEntry: ', end='', flush=True) - # print_delete_stat(BulletinBoardEntry.objects.all().delete()) # cascade via PlatformUser - print('UserDereferrerAllowlist: ', end='', flush=True) print_delete_stat(UserDereferrerAllowlist.objects.all().delete()) diff --git a/src/core/management/commands/schedule_join_rooms.py b/src/core/management/commands/schedule_join_rooms.py index 100e10c8f..a9d4b9cd6 100644 --- a/src/core/management/commands/schedule_join_rooms.py +++ b/src/core/management/commands/schedule_join_rooms.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand, CommandError from django.db import transaction -from ...models import Room, Event, ScheduleSourceMapping, RoomLink +from ...models import Event, Room, RoomLink, ScheduleSourceMapping class Command(BaseCommand): diff --git a/src/core/management/commands/serviceusers.py b/src/core/management/commands/serviceusers.py index a898830b6..2d857bf11 100644 --- a/src/core/management/commands/serviceusers.py +++ b/src/core/management/commands/serviceusers.py @@ -1,4 +1,5 @@ from rest_framework.authtoken.models import Token + from django.core.management.base import BaseCommand from ...models.users import PlatformUser diff --git a/src/core/management/commands/stats.py b/src/core/management/commands/stats.py index 84f09ee9b..08368fe71 100644 --- a/src/core/management/commands/stats.py +++ b/src/core/management/commands/stats.py @@ -8,15 +8,21 @@ class Command(BaseCommand): for conf in Conference.objects.all(): print('#', conf.slug, '(' + conf.name + ')') - print('- assemblies:', - Assembly.objects.filter(conference=conf).count(), 'total,', - Assembly.objects.filter(conference=conf, state__in=Assembly.PUBLIC_STATES).count(), 'accepted', - ) - - print('- events:', - Event.objects.filter(conference=conf).count(), 'total,', - Event.objects.filter(conference=conf, is_public=True).count(), 'visible', - ) + print( + '- assemblies:', + Assembly.objects.filter(conference=conf).count(), + 'total,', + Assembly.objects.filter(conference=conf, state__in=Assembly.PUBLIC_STATES).count(), + 'accepted', + ) + + print( + '- events:', + Event.objects.filter(conference=conf).count(), + 'total,', + Event.objects.filter(conference=conf, is_public=True).count(), + 'visible', + ) print('- rooms:', Room.objects.filter(conference=conf).count(), 'total') for rt in Room.BACKEND_ROOMTYPES: diff --git a/src/core/management/commands/suggestion_tick.py b/src/core/management/commands/suggestion_tick.py index 87336d54d..534ef0dbc 100644 --- a/src/core/management/commands/suggestion_tick.py +++ b/src/core/management/commands/suggestion_tick.py @@ -1,10 +1,10 @@ +import time + from django.core.management.base import BaseCommand -from django.db import transaction, connection +from django.db import connection, transaction from core.models import Assembly, Conference -import time - class Command(BaseCommand): def handle(self, *args, **options): @@ -12,30 +12,36 @@ class Command(BaseCommand): start_time = time.time() with transaction.atomic(), connection.cursor() as cursor: - cursor.execute(''' + cursor.execute( + """ SELECT e.id, lk.likes FROM core_event AS e INNER JOIN core_eventlikecount AS lk ON e.id = lk.event1_id AND e.id=lk.event2_id WHERE e.conference_id = %s AND is_public - ''', [conf.pk]) + """, + [conf.pk], + ) for row in cursor.fetchall(): if row[1] == 0: - cursor.execute("UPDATE core_eventlikecount SET like_ratio=0 WHERE event1_id=%s", [row[0]]) + cursor.execute('UPDATE core_eventlikecount SET like_ratio=0 WHERE event1_id=%s', [row[0]]) else: - cursor.execute("UPDATE core_eventlikecount SET like_ratio=likes*1000/%s WHERE event1_id=%s", [row[1], row[0]]) + cursor.execute('UPDATE core_eventlikecount SET like_ratio=likes*1000/%s WHERE event1_id=%s', [row[1], row[0]]) ts = time.time() - start_time - print("suggestion_tick (events) run took %f Seconds for Conference %s." % (ts, conf)) + print(f'suggestion_tick (events) run took {ts:f} Seconds for Conference {conf}.') start_time = time.time() with transaction.atomic(), connection.cursor() as cursor: - cursor.execute(f''' + cursor.execute( + f""" SELECT a.id, lk.likes FROM core_assembly AS a INNER JOIN core_assemblylikecount AS lk ON a.id = lk.assembly1_id AND a.id=lk.assembly2_id WHERE a.conference_id = %s AND state IN ({','.join(['%s'] * len(Assembly.PUBLIC_STATES))}) - ''', [conf.pk, *Assembly.PUBLIC_STATES]) + """, + [conf.pk, *Assembly.PUBLIC_STATES], + ) for row in cursor.fetchall(): if row[1] == 0: - cursor.execute("UPDATE core_assemblylikecount SET like_ratio=0 WHERE assembly1_id=%s", [row[0]]) + cursor.execute('UPDATE core_assemblylikecount SET like_ratio=0 WHERE assembly1_id=%s', [row[0]]) else: - cursor.execute("UPDATE core_assemblylikecount SET like_ratio=likes*1000/%s WHERE assembly1_id=%s", [row[1], row[0]]) + cursor.execute('UPDATE core_assemblylikecount SET like_ratio=likes*1000/%s WHERE assembly1_id=%s', [row[1], row[0]]) ts = time.time() - start_time - print("suggestion_tick (events) run took %f Seconds for Conference %s." % (ts, conf)) + print(f'suggestion_tick (events) run took {ts:f} Seconds for Conference {conf}.') diff --git a/src/core/markdown.py b/src/core/markdown.py index bd2e27b93..9381fb40f 100644 --- a/src/core/markdown.py +++ b/src/core/markdown.py @@ -1,20 +1,21 @@ +import html +import re from typing import Optional -from urllib.parse import urlparse, quote +from urllib.parse import quote, urlparse import bleach -import html -import re import mistletoe -from mistletoe.html_renderer import HTMLRenderer from mistletoe.block_token import BlockToken -from mistletoe.span_token import Link, AutoLink, SpanToken, tokenize_inner -from modeltranslation.fields import build_localized_fieldname -from modeltranslation.settings import AVAILABLE_LANGUAGES +from mistletoe.html_renderer import HTMLRenderer +from mistletoe.span_token import AutoLink, Link, SpanToken, tokenize_inner + from django.conf import settings from django.db.models import Model +from django.urls import NoReverseMatch, reverse from django.utils.safestring import mark_safe from django.utils.text import slugify -from django.urls import reverse, NoReverseMatch +from modeltranslation.fields import build_localized_fieldname +from modeltranslation.settings import AVAILABLE_LANGUAGES from .models import conference from .utils import scheme_and_netloc_from_url, url_in_allowlist @@ -70,7 +71,7 @@ class AlertBlock(BlockToken): self.alert_type = match[0] self.alert_caption = match[1].strip()[1:-1] if match[1] else None - super().__init__("".join(match[2]), tokenize_inner) + super().__init__(''.join(match[2]), tokenize_inner) @classmethod def start(cls, line): @@ -80,7 +81,7 @@ class AlertBlock(BlockToken): def read(cls, lines): first_line = cls.pattern.match(next(lines)) line_buffer = [] - while (next_line := lines.peek()) is not None and next_line.startswith(" "): + while (next_line := lines.peek()) is not None and next_line.startswith(' '): line_buffer.append(next(lines)) return first_line.group(1), first_line.group(2), line_buffer @@ -96,17 +97,17 @@ class MyHtmlRenderer(HTMLRenderer): @staticmethod def render_html_span(token): - if token.content.startswith("<!--"): + if token.content.startswith('<!--'): # HTML comments can be rendered as-is return token.content return html.escape(token.content) def render_alert_block(self, token: AlertBlock) -> str: - result = f"<div class=\"mx-1 my-3 alert alert-{token.alert_type}\">" + result = f'<div class="mx-1 my-3 alert alert-{token.alert_type}">' if token.alert_caption: - result += f"<p class=\"fw-bold\">{token.alert_caption}</p>" + result += f'<p class="fw-bold">{token.alert_caption}</p>' result += self.render_inner(token) - result += "</div>\n" + result += '</div>\n' return result def __init__(self, conf: 'conference.Conference', result: 'RenderResult', derefer_allowlist: bool = True, *extras, **kwargs): @@ -143,7 +144,7 @@ class MyHtmlRenderer(HTMLRenderer): template = '<a href="{target}"{title}{link_class}>{inner}</a>' target = self.escape_url(token.target) if token.title: - title = ' title="{}"'.format(html.escape(token.title)) + title = f' title="{html.escape(token.title)}"' else: title = '' inner = self.render_inner(token) @@ -158,7 +159,7 @@ class MyHtmlRenderer(HTMLRenderer): template = '<a href="{target}"{link_class}>{inner}</a>' if token.mailto: - target = 'mailto:{}'.format(token.target) + target = f'mailto:{token.target}' else: target = self.escape_url(token.target) inner = self.render_inner(token) @@ -219,7 +220,7 @@ class MyHtmlRenderer(HTMLRenderer): class MySanitizedHtmlRenderer(MyHtmlRenderer): @staticmethod def render_html_block(token): - if token.content.startswith("<!--"): + if token.content.startswith('<!--'): # HTML comments can be rendered as-is return token.content return html.escape(token.content) @@ -250,9 +251,9 @@ class RenderResult: self.linked_urls |= other_result.linked_urls -def render_markdown_ex(conf: 'conference.Conference', markup: str, - allow_embedded_html: bool = False, sanitize_html: bool = True, - dont_derefer_allowlist: bool = False) -> RenderResult: +def render_markdown_ex( + conf: 'conference.Conference', markup: str, allow_embedded_html: bool = False, sanitize_html: bool = True, dont_derefer_allowlist: bool = False +) -> RenderResult: # remove HTML tags embedded in markdown, unless requested otherwise renderer = MyHtmlRenderer if allow_embedded_html else MySanitizedHtmlRenderer @@ -263,17 +264,41 @@ def render_markdown_ex(conf: 'conference.Conference', markup: str, if sanitize_html: cleaner = bleach.Cleaner( - tags=[*[ - 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'dl', - 'table', 'thead', 'tbody', 'th', 'td', 'tr', 'hr', 'p', 'pre', - 'img', 'code', 'div', 'span' - ], *list(bleach.sanitizer.ALLOWED_TAGS)], + tags=[ + *[ + 'br', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ul', + 'ol', + 'li', + 'dl', + 'table', + 'thead', + 'tbody', + 'th', + 'td', + 'tr', + 'hr', + 'p', + 'pre', + 'img', + 'code', + 'div', + 'span', + ], + *list(bleach.sanitizer.ALLOWED_TAGS), + ], attributes={ '*': ['class', 'id'], 'img': ['src', 'alt'], **bleach.sanitizer.ALLOWED_ATTRIBUTES, }, - protocols=['http', 'https', 'mailto', 'ftp', 'ftps'] + protocols=['http', 'https', 'mailto', 'ftp', 'ftps'], ) rendered_markup = cleaner.clean(rendered_markup) @@ -281,39 +306,45 @@ def render_markdown_ex(conf: 'conference.Conference', markup: str, return result -def render_markdown(conf: 'conference.Conference', markup: str, - allow_embedded_html: bool = False, sanitize_html: bool = True, - dont_derefer_allowlist: bool = False) -> str: - render_result = render_markdown_ex(conf, markup, - allow_embedded_html=allow_embedded_html, sanitize_html=sanitize_html, - dont_derefer_allowlist=dont_derefer_allowlist) +def render_markdown( + conf: 'conference.Conference', markup: str, allow_embedded_html: bool = False, sanitize_html: bool = True, dont_derefer_allowlist: bool = False +) -> str: + render_result = render_markdown_ex( + conf, markup, allow_embedded_html=allow_embedded_html, sanitize_html=sanitize_html, dont_derefer_allowlist=dont_derefer_allowlist + ) return render_result.document def store_relationships(conf, link_source, render_result: RenderResult): from django.contrib.contenttypes.models import ContentType + from .models import MarkdownMeta from .models.markdown import MetaTypes src_type = ContentType.objects.get_for_model(link_source) src_object_id = link_source.pk - MarkdownMeta.objects.filter(src_type=src_type, src_object_id=src_object_id) \ - .exclude(dst_type=MetaTypes.USER_SLUG, dst_object__in=render_result.linked_user_slugs) \ - .exclude(dst_type=MetaTypes.PAGE_SLUG, dst_object__in=render_result.linked_page_slugs) + MarkdownMeta.objects.filter(src_type=src_type, src_object_id=src_object_id).exclude( + dst_type=MetaTypes.USER_SLUG, dst_object__in=render_result.linked_user_slugs + ).exclude(dst_type=MetaTypes.PAGE_SLUG, dst_object__in=render_result.linked_page_slugs) MarkdownMeta.objects.bulk_create( - [MarkdownMeta(src_type=src_type, src_object_id=src_object_id, dst_type=MetaTypes.USER_SLUG, dst_object=linked_user_slug) - for linked_user_slug in render_result.linked_user_slugs] + - [MarkdownMeta(src_type=src_type, src_object_id=src_object_id, dst_type=MetaTypes.PAGE_SLUG, dst_object=linked_page_slug) - for linked_page_slug in render_result.linked_page_slugs], - ignore_conflicts=True + [ + MarkdownMeta(src_type=src_type, src_object_id=src_object_id, dst_type=MetaTypes.USER_SLUG, dst_object=linked_user_slug) + for linked_user_slug in render_result.linked_user_slugs + ] + + [ + MarkdownMeta(src_type=src_type, src_object_id=src_object_id, dst_type=MetaTypes.PAGE_SLUG, dst_object=linked_page_slug) + for linked_page_slug in render_result.linked_page_slugs + ], + ignore_conflicts=True, ) def refresh_linking_markdown(conf: 'conference.Conference', link_target): from django.contrib.contenttypes.models import ContentType - from .models import ConferenceMember, StaticPage, Event, Room, Assembly, BulletinBoardEntry, MarkdownMeta + + from .models import Assembly, BulletinBoardEntry, ConferenceMember, Event, MarkdownMeta, Room, StaticPage from .models.markdown import MetaTypes if isinstance(link_target, StaticPage): @@ -323,7 +354,7 @@ def refresh_linking_markdown(conf: 'conference.Conference', link_target): dst_type = MetaTypes.USER_SLUG dst_object = link_target.user.slug else: - raise Exception("Unsupported link_target type %r" % (type(link_target))) + raise Exception('Unsupported link_target type %r' % (type(link_target))) relateds_by_type = {} for src_type_id, src_object_id in MarkdownMeta.objects.filter(dst_type=dst_type, dst_object=dst_object).values_list('src_type_id', 'src_object_id'): @@ -333,7 +364,9 @@ def refresh_linking_markdown(conf: 'conference.Conference', link_target): static_page.body_html = render_markdown(conf, static_page.body) static_page.save() - for conference_member in ConferenceMember.objects.filter(conference=conf, uuid__in=relateds_by_type.get(ContentType.objects.get_for_model(ConferenceMember).pk, [])): # noqa: E501 + for conference_member in ConferenceMember.objects.filter( + conference=conf, uuid__in=relateds_by_type.get(ContentType.objects.get_for_model(ConferenceMember).pk, []) + ): # noqa: E501 conference_member.save(update_fields=['description']) for event in Event.objects.filter(conference=conf, pk__in=relateds_by_type.get(ContentType.objects.get_for_model(Event).pk, [])): @@ -345,15 +378,15 @@ def refresh_linking_markdown(conf: 'conference.Conference', link_target): for assembly in Assembly.objects.filter(conference=conf, pk__in=relateds_by_type.get(ContentType.objects.get_for_model(Assembly).pk, [])): assembly.save(update_fields=['description']) - for assembly in BulletinBoardEntry.objects.filter(conference=conf, pk__in=relateds_by_type.get(ContentType.objects.get_for_model(BulletinBoardEntry).pk, [])): # noqa: E501 + for assembly in BulletinBoardEntry.objects.filter( + conference=conf, pk__in=relateds_by_type.get(ContentType.objects.get_for_model(BulletinBoardEntry).pk, []) + ): # noqa: E501 assembly.save(update_fields=['description']) -def compile_translated_markdown_fields(obj: Model, - conf: 'conference.Conference', - field_name: str, - dst_obj: Optional[Model] = None, - dst_field_name: Optional[str] = None) -> RenderResult: +def compile_translated_markdown_fields( + obj: Model, conf: 'conference.Conference', field_name: str, dst_obj: Optional[Model] = None, dst_field_name: Optional[str] = None +) -> RenderResult: if dst_obj is None: dst_obj = obj if dst_field_name is None: diff --git a/src/core/middleware.py b/src/core/middleware.py index cd0eabb6b..f7a8ff02d 100644 --- a/src/core/middleware.py +++ b/src/core/middleware.py @@ -5,7 +5,6 @@ from django.conf import settings from django.contrib import messages from django.utils import timezone - logger = logging.getLogger(__name__) @@ -36,11 +35,11 @@ class SetRemoteAddrMiddleware: try: real_ip = request.META[lookup_header] except KeyError: - logger.debug("Could not load client ip from %r in request from %r. Misconfigured Proxy?", lookup_header, request.META['REMOTE_ADDR']) + logger.debug('Could not load client ip from %r in request from %r. Misconfigured Proxy?', lookup_header, request.META['REMOTE_ADDR']) else: # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. The # client's IP will be the first one. - real_ip = real_ip.split(",")[0].strip() + real_ip = real_ip.split(',')[0].strip() m = V6_WITH_V4_PREFIX_REGEX.match(real_ip) if m: real_ip = m.group(1) diff --git a/src/core/migrations/0114_prepare_primary_keys_badge_badgetoken.py b/src/core/migrations/0114_prepare_primary_keys_badge_badgetoken.py index 4242b2dfb..e0b2c80c1 100644 --- a/src/core/migrations/0114_prepare_primary_keys_badge_badgetoken.py +++ b/src/core/migrations/0114_prepare_primary_keys_badge_badgetoken.py @@ -26,6 +26,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='badge', name='id', - field=models.CharField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID', max_length=36), + field=models.CharField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID', max_length=36), ), ] diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py index d5622d7ed..f9086f16d 100644 --- a/src/core/models/__init__.py +++ b/src/core/models/__init__.py @@ -1,43 +1,70 @@ from .assemblies import Assembly, AssemblyLikeCount, AssemblyLink, AssemblyLogEntry, AssemblyMember from .badges import Badge, BadgeCategory, BadgeToken, BadgeTokenTimeConstraint, UserBadge -from .conference import Conference, ConferenceExportCache, ConferenceMember, ConferenceNavigationItem, ConferenceTrack -from .dereferrer import UserDereferrerAllowlist, DereferrerStats from .board import BulletinBoardEntry +from .conference import Conference, ConferenceExportCache, ConferenceMember, ConferenceNavigationItem, ConferenceTrack +from .dereferrer import DereferrerStats, UserDereferrerAllowlist from .events import Event, EventAttachment, EventLikeCount, EventParticipant -from .pages import StaticPage, StaticPageRevision, StaticPageNamespace -from .markdown import MarkdownMeta from .map import MapFloor, MapPOI +from .markdown import MarkdownMeta from .messages import DirectMessage from .metanavi import MetaNavItem +from .pages import StaticPage, StaticPageNamespace, StaticPageRevision from .rooms import Room, RoomLink from .schedules import ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping from .shared import BackendMixin from .sso import Application from .tags import ConferenceTag, TagItem from .ticket import ConferenceMemberTicket -from .users import UserCommunicationChannel, UserContact, PlatformUser +from .users import PlatformUser, UserCommunicationChannel, UserContact from .voucher import Voucher, VoucherEntry from .workadventure import WorkadventureSession, WorkadventureTexture __all__ = [ 'Application', - 'Assembly', 'AssemblyLikeCount', 'AssemblyLink', 'AssemblyLogEntry', 'AssemblyMember', - 'BackendMixin', 'Badge', 'BadgeCategory', 'BadgeToken', 'BadgeTokenTimeConstraint', 'BulletinBoardEntry', - 'Conference', 'ConferenceExportCache', - 'ConferenceMember', 'ConferenceMemberTicket', + 'Assembly', + 'AssemblyLikeCount', + 'AssemblyLink', + 'AssemblyLogEntry', + 'AssemblyMember', + 'BackendMixin', + 'Badge', + 'BadgeCategory', + 'BadgeToken', + 'BadgeTokenTimeConstraint', + 'BulletinBoardEntry', + 'Conference', + 'ConferenceExportCache', + 'ConferenceMember', + 'ConferenceMemberTicket', 'ConferenceNavigationItem', - 'ConferenceTag', 'ConferenceTrack', - 'DereferrerStats', 'DirectMessage', - 'Event', 'EventAttachment', 'EventLikeCount', 'EventParticipant', - 'MapFloor', 'MapPOI', + 'ConferenceTag', + 'ConferenceTrack', + 'DereferrerStats', + 'DirectMessage', + 'Event', + 'EventAttachment', + 'EventLikeCount', + 'EventParticipant', + 'MapFloor', + 'MapPOI', 'MetaNavItem', 'PlatformUser', - 'Room', 'RoomLink', - 'ScheduleSource', 'ScheduleSourceImport', 'ScheduleSourceMapping', - 'StaticPage', 'StaticPageRevision', 'StaticPageNamespace', + 'Room', + 'RoomLink', + 'ScheduleSource', + 'ScheduleSourceImport', + 'ScheduleSourceMapping', + 'StaticPage', + 'StaticPageRevision', + 'StaticPageNamespace', 'MarkdownMeta', 'TagItem', - 'UserContact', 'UserCommunicationChannel', 'UserBadge', 'UserDereferrerAllowlist', - 'Voucher', 'VoucherEntry', - 'WorkadventureSession', 'WorkadventureTexture' + 'UserContact', + 'UserCommunicationChannel', + 'UserBadge', + 'UserDereferrerAllowlist', + 'Voucher', + 'VoucherEntry', + 'WorkadventureSession', + 'WorkadventureTexture', ] diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index 0476d8866..54872d949 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -12,10 +12,12 @@ from django.db import models from django.db.models import Q from django.template.loader import render_to_string from django.urls import reverse -from django.utils.html import escape as html_escape, format_html from django.utils.functional import cached_property +from django.utils.html import escape as html_escape +from django.utils.html import format_html from django.utils.safestring import mark_safe -from django.utils.translation import gettext, gettext_lazy as _ +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ from core.validators import FileSizeValidator, ImageDimensionValidator @@ -74,8 +76,11 @@ class AssemblyManager(models.Manager): return qs def conference_accessible(self, conference: Conference): - return self.get_queryset().filter(conference=conference) \ + return ( + self.get_queryset() + .filter(conference=conference) .filter(Q(state_assembly__in=self.model.PUBLIC_STATES) | Q(state_channel__in=self.model.PUBLIC_STATES)) + ) def manageable_by_user(self, user: PlatformUser, conference: Conference, staff_can_manage=True): assert user is not None @@ -108,7 +113,7 @@ class AssemblyManager(models.Manager): return qs -def get_banner_file_name(instance: "Assembly", filename: str): +def get_banner_file_name(instance: 'Assembly', filename: str): return str(Path(str(instance.id)).joinpath('banner', filename)) @@ -141,18 +146,10 @@ class Assembly(TaggedItemMixin, models.Model): id = models.UUIDField(default=uuid4, primary_key=True, editable=False) conference = ConferenceReference(related_name='assemblies') - slug = models.SlugField( - help_text=_('Assembly__slug__help'), - verbose_name=_('Assembly__slug')) - name = models.CharField( - max_length=200, - help_text=_('Assembly__name__help'), - verbose_name=_('Assembly__name')) - - is_official = models.BooleanField( - default=False, - help_text=_('Assembly__is_official__help'), - verbose_name=_('Assembly__is_official')) + slug = models.SlugField(help_text=_('Assembly__slug__help'), verbose_name=_('Assembly__slug')) + name = models.CharField(max_length=200, help_text=_('Assembly__name__help'), verbose_name=_('Assembly__name')) + + is_official = models.BooleanField(default=False, help_text=_('Assembly__is_official__help'), verbose_name=_('Assembly__is_official')) banner_image_height = models.PositiveIntegerField(blank=True, null=True) banner_image_width = models.PositiveIntegerField(blank=True, null=True) @@ -184,89 +181,71 @@ class Assembly(TaggedItemMixin, models.Model): ], ) - description = models.TextField( - blank=True, null=True, - help_text=_('Assembly__description__help'), - verbose_name=_('Assembly__description')) + description = models.TextField(blank=True, null=True, help_text=_('Assembly__description__help'), verbose_name=_('Assembly__description')) description_html = models.TextField(blank=True, null=True) registration_details = models.TextField( - blank=True, null=True, - help_text=_('Assembly__registration_details__help'), - verbose_name=_('Assembly__registration_details')) + blank=True, null=True, help_text=_('Assembly__registration_details__help'), verbose_name=_('Assembly__registration_details') + ) - registration_data = models.JSONField( - blank=True, null=True, - help_text=_('Assembly__registration_data__help'), - verbose_name=_('Assembly__registration_data')) + registration_data = models.JSONField(blank=True, null=True, help_text=_('Assembly__registration_data__help'), verbose_name=_('Assembly__registration_data')) state_assembly = models.CharField( - max_length=20, default=State.NONE, choices=State.choices, - verbose_name=_('Assembly__state_assembly'), - help_text=_('Assembly__state_assembly__help')) + max_length=20, default=State.NONE, choices=State.choices, verbose_name=_('Assembly__state_assembly'), help_text=_('Assembly__state_assembly__help') + ) state_channel = models.CharField( - max_length=20, default=State.NONE, choices=State.choices, - verbose_name=_('Assembly__state_channel'), - help_text=_('Assembly__state_channel__help')) + max_length=20, default=State.NONE, choices=State.choices, verbose_name=_('Assembly__state_channel'), help_text=_('Assembly__state_channel__help') + ) hierarchy = models.CharField( - max_length=20, default=Hierarchy.REGULAR, choices=Hierarchy.choices, - help_text=_('Assembly__hierarchy__help'), - verbose_name=_('Assembly__hierarchy')) + max_length=20, default=Hierarchy.REGULAR, choices=Hierarchy.choices, help_text=_('Assembly__hierarchy__help'), verbose_name=_('Assembly__hierarchy') + ) parent = models.ForeignKey( - 'self', blank=True, null=True, related_name='+', on_delete=models.PROTECT, - verbose_name=_('Assembly__parent'), - help_text=_('Assembly__parent__help')) + 'self', blank=True, null=True, related_name='+', on_delete=models.PROTECT, verbose_name=_('Assembly__parent'), help_text=_('Assembly__parent__help') + ) technical_user = models.ForeignKey( PlatformUser, - blank=True, null=True, - related_name='+', on_delete=models.PROTECT, + blank=True, + null=True, + related_name='+', + on_delete=models.PROTECT, verbose_name=_('Assembly__technical_user'), - help_text=_('Assembly__technical_user__help')) - - is_virtual = models.BooleanField( - blank=True, null=True, - verbose_name=_('Assembly__is_virtual'), - help_text=_('Assembly__is_virtual__help')) - is_physical = models.BooleanField( - blank=True, null=True, - verbose_name=_('Assembly__is_physical'), - help_text=_('Assembly__is_physical__help')) - is_remote = models.BooleanField( - blank=True, null=True, - verbose_name=_('Assembly__is_remote'), - help_text=_('Assembly__is_remote__help')) - - assembly_location = models.TextField( - blank=True, null=True, - verbose_name=_('Assembly__location'), - help_text=_('Assembly__location__help')) - assembly_link = models.URLField( - blank=True, null=True, - verbose_name=_('Assembly__link'), - help_text=_('Assembly__link__help')) + help_text=_('Assembly__technical_user__help'), + ) + + is_virtual = models.BooleanField(blank=True, null=True, verbose_name=_('Assembly__is_virtual'), help_text=_('Assembly__is_virtual__help')) + is_physical = models.BooleanField(blank=True, null=True, verbose_name=_('Assembly__is_physical'), help_text=_('Assembly__is_physical__help')) + is_remote = models.BooleanField(blank=True, null=True, verbose_name=_('Assembly__is_remote'), help_text=_('Assembly__is_remote__help')) + + assembly_location = models.TextField(blank=True, null=True, verbose_name=_('Assembly__location'), help_text=_('Assembly__location__help')) + assembly_link = models.URLField(blank=True, null=True, verbose_name=_('Assembly__link'), help_text=_('Assembly__link__help')) public_contact = models.EmailField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Assembly__public_contact__help'), verbose_name=_('Assembly__public_contact'), ) location_floor = models.ForeignKey( MapFloor, - blank=True, null=True, - related_name='assemblies', on_delete=models.PROTECT, + blank=True, + null=True, + related_name='assemblies', + on_delete=models.PROTECT, help_text=_('Assembly__location_floor__help'), verbose_name=_('Assembly__location_floor'), ) location_point = gis_models.PointField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Assembly__location_point__help'), verbose_name=_('Assembly__location_point'), ) location_boundaries = gis_models.MultiPolygonField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Assembly__location_boundaries__help'), verbose_name=_('Assembly__location_boundaries'), ) @@ -277,21 +256,20 @@ class Assembly(TaggedItemMixin, models.Model): verbose_name=_('Assembly__created'), ) last_update_assembly = models.DateTimeField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Assembly__last_update_assembly__help'), verbose_name=_('Assembly__last_update_assembly'), ) last_update_staff = models.DateTimeField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Assembly__last_update_staff__help'), verbose_name=_('Assembly__last_update_staff'), ) favorited_by = models.ManyToManyField( - PlatformUser, - related_name='favorite_assemblies', - help_text=_('Assembly__favorited_by__help'), - verbose_name=_('Assembly__favorited_by') + PlatformUser, related_name='favorite_assemblies', help_text=_('Assembly__favorited_by__help'), verbose_name=_('Assembly__favorited_by') ) PUBLIC_STATES = [ @@ -433,9 +411,11 @@ class Assembly(TaggedItemMixin, models.Model): @property def basedata_readonly(self): - return \ - self.state_assembly not in [Assembly.State.NONE, Assembly.State.PLANNED, Assembly.State.REGISTERED] or \ - self.state_channel not in [Assembly.State.NONE, Assembly.State.PLANNED, Assembly.State.REGISTERED] + return self.state_assembly not in [Assembly.State.NONE, Assembly.State.PLANNED, Assembly.State.REGISTERED] or self.state_channel not in [ + Assembly.State.NONE, + Assembly.State.PLANNED, + Assembly.State.REGISTERED, + ] @cached_property def sorted_tags(self): @@ -522,6 +502,7 @@ class Assembly(TaggedItemMixin, models.Model): def get_voucher_count(self, with_always_public: bool = True) -> Optional[int]: from .voucher import Voucher + entries = Voucher.objects.for_assembly(self, include_public_ones=False).count() # if we shall not pay respect to Vouchers available to everyone, just return the number here @@ -718,10 +699,7 @@ class AssemblyMember(models.Model): verbose_name=_('AssemblyMember__is_technical_contact'), ) - show_public = models.BooleanField( - default=True, - help_text=_('AssemblyMember__show_public__help'), - verbose_name=_('AssemblyMember__show_public')) + show_public = models.BooleanField(default=True, help_text=_('AssemblyMember__show_public__help'), verbose_name=_('AssemblyMember__show_public')) @property def roles(self): @@ -756,10 +734,9 @@ class AssemblyMember(models.Model): class AssemblyLikeCount(models.Model): class Meta: - indexes = [ - models.Index(fields=['assembly1', 'like_ratio']) - ] + indexes = [models.Index(fields=['assembly1', 'like_ratio'])] unique_together = [('assembly1', 'assembly2')] + assembly1 = models.ForeignKey(Assembly, related_name='suggestions', on_delete=models.CASCADE) assembly2 = models.ForeignKey(Assembly, related_name='+', on_delete=models.CASCADE) likes = models.IntegerField() @@ -783,35 +760,45 @@ class AssemblyLogEntry(models.Model): """Logged activity by an assembly member.""" assembly = models.ForeignKey( - Assembly, related_name='logentries', on_delete=models.CASCADE, - help_text=_('AssemblyLogEntry__assembly__help'), verbose_name=_('AssemblyLogEntry__assembly'), + Assembly, + related_name='logentries', + on_delete=models.CASCADE, + help_text=_('AssemblyLogEntry__assembly__help'), + verbose_name=_('AssemblyLogEntry__assembly'), ) user = models.ForeignKey( PlatformUser, - blank=True, null=True, on_delete=models.SET_NULL, - help_text=_('AssemblyLogEntry__user__help'), verbose_name=_('AssemblyLogEntry__user'), + blank=True, + null=True, + on_delete=models.SET_NULL, + help_text=_('AssemblyLogEntry__user__help'), + verbose_name=_('AssemblyLogEntry__user'), ) timestamp = models.DateTimeField( - auto_now_add=True, editable=False, - help_text=_('AssemblyLogEntry__timestamp__help'), verbose_name=_('AssemblyLogEntry__timestamp'), + auto_now_add=True, + editable=False, + help_text=_('AssemblyLogEntry__timestamp__help'), + verbose_name=_('AssemblyLogEntry__timestamp'), ) kind = models.CharField( - max_length=20, choices=Kind.choices, - help_text=_('AssemblyLogEntry__kind__help'), verbose_name=_('AssemblyLogEntry__kind'), + max_length=20, + choices=Kind.choices, + help_text=_('AssemblyLogEntry__kind__help'), + verbose_name=_('AssemblyLogEntry__kind'), ) changes = models.JSONField( - blank=True, null=True, - help_text=_('AssemblyLogEntry__changes__help'), verbose_name=_('AssemblyLogEntry__changes'), + blank=True, + null=True, + help_text=_('AssemblyLogEntry__changes__help'), + verbose_name=_('AssemblyLogEntry__changes'), ) comment = models.TextField( - blank=True, null=True, - help_text=_('AssemblyLogEntry__comment__help'), verbose_name=_('AssemblyLogEntry__comment'), + blank=True, + null=True, + help_text=_('AssemblyLogEntry__comment__help'), + verbose_name=_('AssemblyLogEntry__comment'), ) def __str__(self): - return ( - f'{self.timestamp} [{self.kind}]' + - (f' @{self.user.username}' if self.user else '') + - (f' "{self.comment}"' if self.comment else '') - ) + return f'{self.timestamp} [{self.kind}]' + (f' @{self.user.username}' if self.user else '') + (f' "{self.comment}"' if self.comment else '') diff --git a/src/core/models/badges.py b/src/core/models/badges.py index 0943a2ef6..0dc080346 100644 --- a/src/core/models/badges.py +++ b/src/core/models/badges.py @@ -4,6 +4,8 @@ from pathlib import Path from string import ascii_lowercase, digits from uuid import uuid4 +from segno import make as segno_make + from django.conf import settings from django.contrib.postgres.fields import DateTimeRangeField from django.core.exceptions import ValidationError @@ -14,7 +16,6 @@ from django.db import models from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from segno import make as segno_make from core.markdown import compile_translated_markdown_fields, store_relationships from core.validators import ImageDimensionValidator diff --git a/src/core/models/board.py b/src/core/models/board.py index bdf8cd79d..f65169149 100644 --- a/src/core/models/board.py +++ b/src/core/models/board.py @@ -1,8 +1,8 @@ -from django.db import models from django.conf import settings +from django.db import models -from core.markdown import compile_translated_markdown_fields, store_relationships from core.fields import ConferenceReference +from core.markdown import compile_translated_markdown_fields, store_relationships class BulletinBoardEntry(models.Model): diff --git a/src/core/models/conference.py b/src/core/models/conference.py index 4ae3fa0ea..7a28185e1 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -2,10 +2,11 @@ import logging import typing import xml.etree.ElementTree as ET from collections import OrderedDict +from datetime import datetime, time, timedelta from json import JSONEncoder - from uuid import uuid4 -from datetime import datetime, time, timedelta + +from timezone_field import TimeZoneField from django.conf import settings from django.contrib.auth.models import Group @@ -14,21 +15,20 @@ from django.core.exceptions import ValidationError from django.core.validators import URLValidator, validate_slug from django.db import models from django.db.models import Max -from django.http import HttpRequest, HttpResponseNotModified, HttpResponse -from django.urls import reverse, NoReverseMatch +from django.http import HttpRequest, HttpResponse, HttpResponseNotModified +from django.urls import NoReverseMatch, reverse from django.utils import timezone from django.utils.functional import cached_property from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.timezone import now -from django.utils.translation import get_language, gettext_lazy as _ -from timezone_field import TimeZoneField +from django.utils.translation import get_language +from django.utils.translation import gettext_lazy as _ from ..fields import ConferenceReference from ..utils import render_markdown_as_text from .users import PlatformUser - logger = logging.getLogger(__name__) @@ -46,59 +46,41 @@ class ConferenceMember(models.Model): active_angel = models.BooleanField(default=False) roles = pg_fields.ArrayField(models.CharField(max_length=50), blank=True, null=True) is_staff = models.BooleanField(default=False) - has_ticket = models.BooleanField( - default=False, - help_text=_('ConferenceMember--has_ticket--help'), - verbose_name=_('ConferenceMember--has_ticket')) + has_ticket = models.BooleanField(default=False, help_text=_('ConferenceMember--has_ticket--help'), verbose_name=_('ConferenceMember--has_ticket')) permission_groups = models.ManyToManyField(Group, blank=True, related_name='+') static_page_groups = pg_fields.ArrayField(models.CharField(max_length=50), blank=True, null=True) - description = models.TextField( - blank=True, null=True, - help_text=_('PlatformUser__description__help'), - verbose_name=_('PlatformUser__description')) + description = models.TextField(blank=True, null=True, help_text=_('PlatformUser__description__help'), verbose_name=_('PlatformUser__description')) description_html = models.TextField(blank=True, null=True) class Meta: permissions = [ ('assembly_team', _('ConferenceMember__permission-assembly_team')), # See all assemblies, not only the accepted ones. - - ('channel_team', _("ConferenceMember__permission-channel_team")), + ('channel_team', _('ConferenceMember__permission-channel_team')), # Channelteam, Assemblyteam for Channel-Type Assemblies (Assemblies that provide their own content) - ('static_pages', _('ConferenceMember__permission-static_pages')), # Access to static pages, can be further limited by configuring static_page_groups. - ('map_edit', _('ConferenceMember__permission-map_edit')), # modification of the map, i.e. POIs - ('platformusers', 'Orga: Users List'), ('rename_platformuser', 'Orga: Rename User'), - ('block_platformuser', _('ConferenceMember__permission-block_platformuser')), # This is the right to block a misbehaving user. - ('change_conferencemember__active_angel', _('ConferenceMember__permission-change_conferencemember__active_angel')), # Permission to set the user's is-currently-on-duty flag. - ('view_platformuser__guardian', _('ConferenceMember__permission-view_platformuser__guardian')), # See the guardian(s) of a user. - ('voucher_admin', _('ConferenceMember__permission-voucher_admin')), # configure conference's vouchers - ('scheduleadmin', _('ConferenceMember__permission-scheduleadmin')), # manage/support ScheduleSupports globally, i.e. without access to an individual assembly - ('workadventure_admin', _('ConferenceMember__permission-workadventure_admin')), # manage/support all WA rooms and backend info ] - constraints = [ - models.constraints.UniqueConstraint(fields=['user', 'conference'], name='conferencemember_user_conference_unique') - ] + constraints = [models.constraints.UniqueConstraint(fields=['user', 'conference'], name='conferencemember_user_conference_unique')] def get_permission_groups_permissions(self): """Returns only the permission codenames which are given via the associated permission_groups of this ConferenceMember instance.""" @@ -193,88 +175,59 @@ class Conference(models.Model): objects = ConferenceManager() id = models.UUIDField(default=uuid4, primary_key=True, editable=False) - slug = models.SlugField( - unique=True, - help_text=_('Conference__slug__help'), - verbose_name=_('Conference__slug')) - name = models.CharField( - max_length=200, - help_text=_('Conference__name__help'), - verbose_name=_('Conference__name')) + slug = models.SlugField(unique=True, help_text=_('Conference__slug__help'), verbose_name=_('Conference__slug')) + name = models.CharField(max_length=200, help_text=_('Conference__name__help'), verbose_name=_('Conference__name')) - is_public = models.BooleanField( - default=False, - help_text=_('Conference__is_public__help'), - verbose_name=_('Conference__is_public')) + is_public = models.BooleanField(default=False, help_text=_('Conference__is_public__help'), verbose_name=_('Conference__is_public')) registration_deadline = models.DateTimeField( - blank=True, null=True, - help_text=_('Conference__registration_deadline__help'), - verbose_name=_('Conference__registration_deadline')) - - start = models.DateTimeField( - blank=True, null=True, - help_text=_('Conference__start__help'), - verbose_name=_('Conference__start')) - end = models.DateTimeField( - blank=True, null=True, - help_text=_('Conference__end__help'), - verbose_name=_('Conference__end')) + blank=True, null=True, help_text=_('Conference__registration_deadline__help'), verbose_name=_('Conference__registration_deadline') + ) + + start = models.DateTimeField(blank=True, null=True, help_text=_('Conference__start__help'), verbose_name=_('Conference__start')) + end = models.DateTimeField(blank=True, null=True, help_text=_('Conference__end__help'), verbose_name=_('Conference__end')) timezone = TimeZoneField( - choices_display="WITH_GMT_OFFSET", - default="UTC", - help_text=_('Conference__timezone__help'), - verbose_name=_('Conference__timezone')) + choices_display='WITH_GMT_OFFSET', default='UTC', help_text=_('Conference__timezone__help'), verbose_name=_('Conference__timezone') + ) global_notification = models.TextField( - default='', blank=True, - help_text=_('Conference__global_notification__help'), - verbose_name=_('Conference__global_notification')) + default='', blank=True, help_text=_('Conference__global_notification__help'), verbose_name=_('Conference__global_notification') + ) logo = models.TextField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Conference__logo__help'), verbose_name=_('Conference__logo'), ) """SVG logo""" - send_pn_disabled = models.BooleanField( - default=False, - help_text=_('Conference__send_pn_disabled__help'), - verbose_name=_('Conference__send_pn_disabled')) - board_disabled = models.BooleanField( - default=False, - help_text=_('Conference__board_disabled__help'), - verbose_name=_('Conference__board_disabled')) - require_login = models.BooleanField( - default=False, - help_text=_('Conference--require_login--help'), - verbose_name=_('Conference--require_login')) - require_ticket = models.BooleanField( - default=False, - help_text=_('Conference--require_ticket--help'), - verbose_name=_('Conference--require_ticket')) + send_pn_disabled = models.BooleanField(default=False, help_text=_('Conference__send_pn_disabled__help'), verbose_name=_('Conference__send_pn_disabled')) + board_disabled = models.BooleanField(default=False, help_text=_('Conference__board_disabled__help'), verbose_name=_('Conference__board_disabled')) + require_login = models.BooleanField(default=False, help_text=_('Conference--require_login--help'), verbose_name=_('Conference--require_login')) + require_ticket = models.BooleanField(default=False, help_text=_('Conference--require_ticket--help'), verbose_name=_('Conference--require_ticket')) - support_clusters = models.BooleanField( - default=True, - help_text=_('Conference__support_clusters__help'), - verbose_name=_('Conference__support_clusters')) + support_clusters = models.BooleanField(default=True, help_text=_('Conference__support_clusters__help'), verbose_name=_('Conference__support_clusters')) additional_fields_schema = models.JSONField( - blank=True, null=True, encoder=PrettyJSONEncoder, + blank=True, + null=True, + encoder=PrettyJSONEncoder, help_text=_('Conference__additional_fields_schema__help'), - verbose_name=_('Conference__additional_fields_schema')) + verbose_name=_('Conference__additional_fields_schema'), + ) disclaimers = models.JSONField( - blank=True, null=True, encoder=PrettyJSONEncoder, - help_text=_('Conference__disclaimers__help'), - verbose_name=_('Conference__disclaimers')) + blank=True, null=True, encoder=PrettyJSONEncoder, help_text=_('Conference__disclaimers__help'), verbose_name=_('Conference__disclaimers') + ) self_organized_sessions_assembly = models.ForeignKey( 'Assembly', - blank=True, null=True, + blank=True, + null=True, on_delete=models.PROTECT, related_name='+', help_text=_('Conference__self_organized_sessions_assembly__help'), - verbose_name=_('Conference__self_organized_sessions_assembly')) + verbose_name=_('Conference__self_organized_sessions_assembly'), + ) """ Benutzer, die eine Self-Organized-Session (SOS) anlegen wollen, können dies entweder in ihrer Assembly tun, in denen sie "Member" sind oder aber in dieser hier. @@ -305,13 +258,16 @@ class Conference(models.Model): ) mail_footer = models.TextField( - blank=True, null=True, + blank=True, + null=True, help_text=_('Conference__mail_footer__help'), verbose_name=_('Conference__mail_footer'), ) map_config = models.JSONField( - blank=True, null=True, encoder=PrettyJSONEncoder, + blank=True, + null=True, + encoder=PrettyJSONEncoder, help_text=_('Conference__map_config__help'), verbose_name=_('Conference__map_config'), ) @@ -330,11 +286,11 @@ class Conference(models.Model): parsed = ET.fromstring(self.logo) assert parsed.tag in ['svg', '{http://www.w3.org/2000/svg}svg'], 'Not a SVG document.' - assert 'width' not in parsed.keys() and 'height' not in parsed.keys(), 'SVG has width and/or height attribute.' + assert 'width' not in parsed and 'height' not in parsed, 'SVG has width and/or height attribute.' xml_start = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' if self.logo.startswith(xml_start): - self.logo = self.logo[len(xml_start):].lstrip() + self.logo = self.logo[len(xml_start) :].lstrip() except ET.ParseError: errors['logo'] = 'SVG (XML) parse error' @@ -466,27 +422,33 @@ class Conference(models.Model): # iterate over the categories (parent entries) for parent in self.nav_items.filter(parent=None, is_visible=True).order_by('index'): # filter children for this parent entry (they're already sorted) and resolve the URL, if necessary - this_children = [{ - 'label': c.label, - 'title': c.title, - 'icon': c.icon, - 'url': c.resolve_url(), - } for c in all_children if c.parent_id == parent.id] + this_children = [ + { + 'label': c.label, + 'title': c.title, + 'icon': c.icon, + 'url': c.resolve_url(), + } + for c in all_children + if c.parent_id == parent.id + ] # append parent entry, reformatted as dict - result.append({ - 'label': parent.label, - 'title': parent.title, - 'icon': parent.icon, - 'children': this_children, - }) + result.append( + { + 'label': parent.label, + 'title': parent.title, + 'icon': parent.icon, + 'children': this_children, + } + ) return result @cached_property def logo_html(self): if not self.logo: - with settings.BASE_DIR.joinpath("hub", "logos", "default.svg").open() as default_logo: + with settings.BASE_DIR.joinpath('hub', 'logos', 'default.svg').open() as default_logo: default_logo_content = default_logo.read() assert default_logo_content.startswith('<svg'), 'Expected default logo to start with SVG tag.' return mark_safe(default_logo_content) @@ -506,6 +468,7 @@ class Conference(models.Model): return None from ..markdown import render_markdown + return format_html( '<div id="footer" style="margin-top: 1em; border-top: 2px solid #999; color: #999; font-size: 80%;">{0}</div>', render_markdown(conf=self, markup=self.mail_footer, dont_derefer_allowlist=True), @@ -558,25 +521,19 @@ class ConferenceTrack(models.Model): objects = ConferenceTrackManager() conference = ConferenceReference(related_name='tracks') - slug = models.SlugField( - help_text=_('ConferenceTrack__slug__help'), - verbose_name=_('ConferenceTrack__slug')) - name = models.CharField( - max_length=200, - help_text=_('ConferenceTrack__name__help'), - verbose_name=_('ConferenceTrack__name')) - is_public = models.BooleanField( - default=True, - help_text=_('ConferenceTrack__is_public__help'), - verbose_name=_('ConferenceTrack__is_public')) + slug = models.SlugField(help_text=_('ConferenceTrack__slug__help'), verbose_name=_('ConferenceTrack__slug')) + name = models.CharField(max_length=200, help_text=_('ConferenceTrack__name__help'), verbose_name=_('ConferenceTrack__name')) + is_public = models.BooleanField(default=True, help_text=_('ConferenceTrack__is_public__help'), verbose_name=_('ConferenceTrack__is_public')) banner_image_height = models.PositiveIntegerField(blank=True, null=True) banner_image_width = models.PositiveIntegerField(blank=True, null=True) banner_image = models.ImageField( - blank=True, null=True, + blank=True, + null=True, height_field='banner_image_height', width_field='banner_image_width', help_text=_('ConferenceTrack__banner_image__help'), - verbose_name=_('ConferenceTrack__banner_image')) + verbose_name=_('ConferenceTrack__banner_image'), + ) last_update = models.DateTimeField(auto_now=True) @@ -610,7 +567,7 @@ class ConferenceExportCache(models.Model): last_generated = models.DateTimeField(blank=True, null=True) data = models.TextField(blank=True, null=True) - class Meta(object): + class Meta: unique_together = [ ('conference', 'ident'), ] @@ -632,24 +589,27 @@ class ConferenceExportCache(models.Model): last_room = Room.objects.filter(conference=self.conference).aggregate(Max('last_update'))['last_update__max'] # if one of those is greater than our last generation, we need to update - if (last_track is not None and last_track > self.last_generated) or \ - (last_event is not None and last_event > self.last_generated) or \ - (last_room is not None and last_room > self.last_generated): + if ( + (last_track is not None and last_track > self.last_generated) + or (last_event is not None and last_event > self.last_generated) + or (last_room is not None and last_room > self.last_generated) + ): return True elif self.type == self.Type.MAP: from .assemblies import Assembly + last_assemblies = Assembly.objects.filter(conference=self.conference).aggregate(Max('last_update_staff'))['last_update_staff__max'] if last_assemblies is not None and last_assemblies > self.last_generated: return True elif self.type == self.Type.ASSEMBLIES: from .assemblies import Assembly + last_updates = Assembly.objects.filter(conference=self.conference).aggregate(Max('last_update_assembly'), Max('last_update_staff')) last_assemblies = last_updates['last_update_assembly__max'] last_staff = last_updates['last_update_staff__max'] - if (last_assemblies is not None and last_assemblies > self.last_generated) or \ - (last_staff is not None and last_staff > self.last_generated): + if (last_assemblies is not None and last_assemblies > self.last_generated) or (last_staff is not None and last_staff > self.last_generated): return True else: @@ -668,7 +628,9 @@ class ConferenceExportCache(models.Model): @classmethod def _get_or_create_entry(cls, conference: Conference, type: Type, ident: str, result_func: typing.Callable[..., bytes]): cache_entry, _ = cls.objects.get_or_create( - conference=conference, type=type, ident=ident, + conference=conference, + type=type, + ident=ident, defaults={'needs_regeneration': True}, ) if cache_entry.is_dirty or settings.DEBUG: @@ -685,9 +647,7 @@ class ConferenceExportCache(models.Model): return content.decode('utf-8') if as_text else content @classmethod - def handle_http_request(cls, request: HttpRequest, - conference: Conference, type: Type, ident: str, - content_type, result_func): + def handle_http_request(cls, request: HttpRequest, conference: Conference, type: Type, ident: str, content_type, result_func): cache_entry = cls._get_or_create_entry(conference, type, ident, result_func) headers = { @@ -760,36 +720,48 @@ class ConferenceNavigationItem(models.Model): conference = ConferenceReference(related_name='nav_items') parent = models.ForeignKey( - 'self', related_name='children', on_delete=models.CASCADE, - blank=True, null=True, - help_text=_('ConferenceNavigationItem__parent__help'), verbose_name=_('ConferenceNavigationItem__parent'), + 'self', + related_name='children', + on_delete=models.CASCADE, + blank=True, + null=True, + help_text=_('ConferenceNavigationItem__parent__help'), + verbose_name=_('ConferenceNavigationItem__parent'), ) is_visible = models.BooleanField( default=True, - help_text=_('ConferenceNavigationItem__is_visible__help'), verbose_name=_('ConferenceNavigationItem__is_visible'), + help_text=_('ConferenceNavigationItem__is_visible__help'), + verbose_name=_('ConferenceNavigationItem__is_visible'), ) index = models.IntegerField( - help_text=_('ConferenceNavigationItem__index__help'), verbose_name=_('ConferenceNavigationItem__index'), + help_text=_('ConferenceNavigationItem__index__help'), + verbose_name=_('ConferenceNavigationItem__index'), ) icon = models.CharField( max_length=50, - blank=True, null=True, - help_text=_('ConferenceNavigationItem__icon__help'), verbose_name=_('ConferenceNavigationItem__icon'), + blank=True, + null=True, + help_text=_('ConferenceNavigationItem__icon__help'), + verbose_name=_('ConferenceNavigationItem__icon'), ) label = models.CharField( max_length=50, - help_text=_('ConferenceNavigationItem__label__help'), verbose_name=_('ConferenceNavigationItem__label'), + help_text=_('ConferenceNavigationItem__label__help'), + verbose_name=_('ConferenceNavigationItem__label'), ) title = models.CharField( max_length=200, - help_text=_('ConferenceNavigationItem__title__help'), verbose_name=_('ConferenceNavigationItem__title'), + help_text=_('ConferenceNavigationItem__title__help'), + verbose_name=_('ConferenceNavigationItem__title'), ) url = models.CharField( max_length=255, - blank=True, validators=[validate_conferencenavigationitem_url], - help_text=_('ConferenceNavigationItem__url__help'), verbose_name=_('ConferenceNavigationItem__url'), + blank=True, + validators=[validate_conferencenavigationitem_url], + help_text=_('ConferenceNavigationItem__url__help'), + verbose_name=_('ConferenceNavigationItem__url'), ) def resolve_url(self): diff --git a/src/core/models/dereferrer.py b/src/core/models/dereferrer.py index a5f01ad1b..eb9d80e93 100644 --- a/src/core/models/dereferrer.py +++ b/src/core/models/dereferrer.py @@ -1,4 +1,5 @@ from django.db import models + from core.models.users import PlatformUser diff --git a/src/core/models/events.py b/src/core/models/events.py index daf213f33..279a3674a 100644 --- a/src/core/models/events.py +++ b/src/core/models/events.py @@ -5,6 +5,8 @@ from re import compile from typing import Any from uuid import UUID, uuid4 +from ordered_set import OrderedSet + from django.conf import settings from django.core.exceptions import NON_FIELD_ERRORS, ValidationError from django.db import models @@ -16,7 +18,6 @@ from django.utils.functional import cached_property from django.utils.text import slugify from django.utils.timezone import is_aware, make_aware from django.utils.translation import gettext_lazy as _ -from ordered_set import OrderedSet from ..fields import ConferenceReference from ..markdown import compile_translated_markdown_fields, store_relationships @@ -82,12 +83,15 @@ class EventManager(models.Manager): return qs.filter(is_public=True) def conference_accessible(self, conference: Conference): - return self.get_queryset().filter(conference=conference, is_public=True) \ + return ( + self.get_queryset() + .filter(conference=conference, is_public=True) .filter( Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES) | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES) | Q(kind=Event.Kind.SELF_ORGANIZED) ) + ) def manageable_by_user(self, user: PlatformUser, conference: Conference): assert user is not None @@ -112,8 +116,9 @@ class EventManager(models.Manager): pass # for everybody else, only show public events - manageable_assemblies_qs = Assembly.objects.manageable_by_user(conference=conference, user=user) \ - .filter(members__member=user, members__can_manage_assembly=True) + manageable_assemblies_qs = Assembly.objects.manageable_by_user(conference=conference, user=user).filter( + members__member=user, members__can_manage_assembly=True + ) return qs.filter(Q(kind=Event.Kind.SELF_ORGANIZED, owner=user) | Q(assembly_id__in=manageable_assemblies_qs.values_list('id', flat=True))) @@ -129,90 +134,58 @@ class Event(TaggedItemMixin, BackendMixin, models.Model): class Kind(models.TextChoices): OFFICIAL = 'official', _('Event__kind-official') # offizielles Event ASSEMBLY = 'assembly', _('Event__kind-assembly') # von einer Assembly kuratiert - SELF_ORGANIZED = 'sos', _('Event__kind-sos') # hat Einzelperson angelegt + SELF_ORGANIZED = 'sos', _('Event__kind-sos') # hat Einzelperson angelegt objects = EventManager() id = models.UUIDField(default=uuid4, primary_key=True, editable=False) conference = ConferenceReference(related_name='events') # Unique together with conference - slug = models.SlugField( - max_length=150, - help_text=_('Event__slug__help'), - verbose_name=_('Event__slug')) - kind = models.CharField( - max_length=10, - choices=Kind.choices, - default=Kind.ASSEMBLY, - help_text=_('Event__kind__help'), - verbose_name=_('Event__kind')) - - name = models.CharField( - max_length=200) + slug = models.SlugField(max_length=150, help_text=_('Event__slug__help'), verbose_name=_('Event__slug')) + kind = models.CharField(max_length=10, choices=Kind.choices, default=Kind.ASSEMBLY, help_text=_('Event__kind__help'), verbose_name=_('Event__kind')) + + name = models.CharField(max_length=200) track = models.ForeignKey(ConferenceTrack, blank=True, null=True, on_delete=models.PROTECT) assembly = models.ForeignKey(Assembly, related_name='events', on_delete=models.PROTECT, blank=True, null=True) room = models.ForeignKey(Room, blank=True, null=True, related_name='events', on_delete=models.PROTECT) - language = models.CharField( - max_length=50, blank=True, null=True, - help_text=_('Event__language__help'), - verbose_name=_('Event__language')) - abstract = models.TextField( - blank=True, - help_text=_('Event__abstract__help'), - verbose_name=_('Event__abstract')) - description = models.TextField( - blank=True, - help_text=_('Event__description__help'), - verbose_name=_('Event__description')) + language = models.CharField(max_length=50, blank=True, null=True, help_text=_('Event__language__help'), verbose_name=_('Event__language')) + abstract = models.TextField(blank=True, help_text=_('Event__abstract__help'), verbose_name=_('Event__abstract')) + description = models.TextField(blank=True, help_text=_('Event__description__help'), verbose_name=_('Event__description')) description_html = models.TextField(blank=True) banner_image_height = models.PositiveIntegerField(blank=True, null=True) banner_image_width = models.PositiveIntegerField(blank=True, null=True) banner_image = models.ImageField( - blank=True, null=True, + blank=True, + null=True, height_field='banner_image_height', width_field='banner_image_width', help_text=_('Event__banner_image__help'), - verbose_name=_('Event__banner_image')) - - is_public = models.BooleanField( - default=False, - help_text=_('Event__is_public__help'), - verbose_name=_('Event__is_public')) - schedule_start = models.DateTimeField( - blank=True, null=True, - help_text=_('Event__schedule_start__help'), - verbose_name=_('Event__schedule_start')) - schedule_duration = EventDurationField( - blank=True, null=True, - help_text=_('Event__schedule_duration__help'), - verbose_name=_('Event__schedule_duration')) - schedule_end = models.DateTimeField( - blank=True, null=True, - help_text=_('Event__schedule_end__help'), - verbose_name=_('Event__schedule_end')) + verbose_name=_('Event__banner_image'), + ) + + is_public = models.BooleanField(default=False, help_text=_('Event__is_public__help'), verbose_name=_('Event__is_public')) + schedule_start = models.DateTimeField(blank=True, null=True, help_text=_('Event__schedule_start__help'), verbose_name=_('Event__schedule_start')) + schedule_duration = EventDurationField(blank=True, null=True, help_text=_('Event__schedule_duration__help'), verbose_name=_('Event__schedule_duration')) + schedule_end = models.DateTimeField(blank=True, null=True, help_text=_('Event__schedule_end__help'), verbose_name=_('Event__schedule_end')) additional_data = models.JSONField(blank=True, null=True) favorited_by = models.ManyToManyField( - PlatformUser, - related_name='favorite_events', - help_text=_('Event__favorited_by__help'), - verbose_name=_('Event__favorited_by') + PlatformUser, related_name='favorite_events', help_text=_('Event__favorited_by__help'), verbose_name=_('Event__favorited_by') ) in_personal_calendar = models.ManyToManyField( - PlatformUser, - related_name='calendar_events', - help_text=_('Event__in_personal_calendar__help'), - verbose_name=_('Event__in_personal_calendar') + PlatformUser, related_name='calendar_events', help_text=_('Event__in_personal_calendar__help'), verbose_name=_('Event__in_personal_calendar') ) owner = models.ForeignKey( settings.AUTH_USER_MODEL, - blank=True, null=True, + blank=True, + null=True, related_name='owned_events', on_delete=models.PROTECT, help_text=_('Event__owner__help'), - verbose_name=_('Event__owner')) + verbose_name=_('Event__owner'), + ) """ Ersteller/Eigentümer des Events, dies ist insb. für Self-Organized-Sessions relevant. """ @@ -286,9 +259,17 @@ class Event(TaggedItemMixin, BackendMixin, models.Model): return self.name @classmethod - def from_dict(cls, data: dict, conference: Conference, assembly: Assembly = None, existing=None, pop_used_keys: bool = False, - allow_kind: bool = False, allow_track: bool = False, - room_lookup=None): + def from_dict( + cls, + data: dict, + conference: Conference, + assembly: Assembly = None, + existing=None, + pop_used_keys: bool = False, + allow_kind: bool = False, + allow_track: bool = False, + room_lookup=None, + ): """ Loads an Event instance from the given dictionary. An existing event can be provided which's data is overwritten (in parts). @@ -312,11 +293,11 @@ class Event(TaggedItemMixin, BackendMixin, models.Model): if existing: obj = existing if assembly: - assert obj.assembly == assembly, 'Existing event\'s assembly does not match given one.' + assert obj.assembly == assembly, "Existing event's assembly does not match given one." if conference: - assert obj.conference == conference, 'Existing event\'s conference does not match given one.' + assert obj.conference == conference, "Existing event's conference does not match given one." if given_uuid is not None: - assert obj.pk == given_uuid, f'expected existing event\'s id {obj.pk} to match the given uuid {given_uuid}' + assert obj.pk == given_uuid, f"expected existing event's id {obj.pk} to match the given uuid {given_uuid}" else: obj = cls(assembly=assembly, conference=conference) if given_uuid is not None: @@ -324,11 +305,14 @@ class Event(TaggedItemMixin, BackendMixin, models.Model): assert obj.conference_id is not None direct_fields = [ - 'slug', 'name', - 'abstract', 'description', + 'slug', + 'name', + 'abstract', + 'description', 'language', 'is_public', - 'schedule_start', 'schedule_end', + 'schedule_start', + 'schedule_end', ] bool_fields = ['is_public'] dt_fields = ['schedule_start', 'schedule_end'] @@ -444,7 +428,7 @@ class Event(TaggedItemMixin, BackendMixin, models.Model): # When setting to public we must have a duration if (test_for_publication or self.is_public) and not self.schedule_duration: publication_errors['schedule_duration'] = errors['schedule_duration'] = ValidationError( - _('Event__schedule_duration__non_empty %(event_type)s') % {'event_type': event_type}, code='empty' + _('Event__schedule_duration__non_empty %(event_type)s') % {'event_type': event_type}, code='empty' ) if self.schedule_duration is not None: # duration must not be zero @@ -516,7 +500,7 @@ class Event(TaggedItemMixin, BackendMixin, models.Model): def _EventAttachment_upload_path(instance, filename): # file will be uploaded to MEDIA_ROOT/user_<id>/<filename> - return 'eventattachment/{0}/{1}'.format(instance.event.id, instance.id) + return f'eventattachment/{instance.event.id}/{instance.id}' class EventAttachment(models.Model): @@ -553,27 +537,18 @@ class EventParticipant(models.Model): participant = models.ForeignKey(PlatformUser, on_delete=models.CASCADE, related_name='events') role = models.CharField( - max_length=20, choices=Role.choices, default=Role.REGULAR, - help_text=_('EventParticipant__role__help'), - verbose_name=_('EventParticipant__role')) - - is_accepted = models.BooleanField( - default=False, - help_text=_('EventParticipant__is_accepted__help'), - verbose_name=_('EventParticipant__is_accepted')) - is_public = models.BooleanField( - default=False, - help_text=_('EventParticipant__is_public__help'), - verbose_name=_('EventParticipant__is_public')) + max_length=20, choices=Role.choices, default=Role.REGULAR, help_text=_('EventParticipant__role__help'), verbose_name=_('EventParticipant__role') + ) + + is_accepted = models.BooleanField(default=False, help_text=_('EventParticipant__is_accepted__help'), verbose_name=_('EventParticipant__is_accepted')) + is_public = models.BooleanField(default=False, help_text=_('EventParticipant__is_public__help'), verbose_name=_('EventParticipant__is_public')) public_description = models.TextField( - blank=True, null=True, - help_text=_('EventParticipant__public_description__help'), - verbose_name=_('EventParticipant__public_description')) + blank=True, null=True, help_text=_('EventParticipant__public_description__help'), verbose_name=_('EventParticipant__public_description') + ) personal_comment = models.TextField( - blank=True, null=True, - help_text=_('EventParticipant__personal_comment__help'), - verbose_name=_('EventParticipant__personal_comment')) + blank=True, null=True, help_text=_('EventParticipant__personal_comment__help'), verbose_name=_('EventParticipant__personal_comment') + ) def clean(self, *args, **kwargs): # verify that the participant is a member of the event's conference @@ -591,10 +566,9 @@ class EventParticipant(models.Model): class EventLikeCount(models.Model): class Meta: - indexes = [ - models.Index(fields=['event1', 'like_ratio']) - ] + indexes = [models.Index(fields=['event1', 'like_ratio'])] unique_together = [('event1', 'event2')] + event1 = models.ForeignKey(Event, related_name='suggestions', on_delete=models.CASCADE) event2 = models.ForeignKey(Event, related_name='+', on_delete=models.CASCADE) likes = models.IntegerField() diff --git a/src/core/models/map.py b/src/core/models/map.py index 8200d4d96..aa991af12 100644 --- a/src/core/models/map.py +++ b/src/core/models/map.py @@ -10,7 +10,6 @@ from django.utils.translation import gettext_lazy as _ from ..fields import ConferenceReference from ..markdown import compile_translated_markdown_fields, store_relationships - logger = logging.getLogger(__name__) @@ -42,37 +41,27 @@ class MapPOI(models.Model): id = models.UUIDField(default=uuid4, primary_key=True, editable=False, serialize=False) conference = ConferenceReference(related_name='pois') - visible = models.BooleanField( - default=True, - help_text=_('MapPOI__visible__help'), - verbose_name=_('MapPOI__visible')) + visible = models.BooleanField(default=True, help_text=_('MapPOI__visible__help'), verbose_name=_('MapPOI__visible')) - name = models.CharField( - max_length=200, - unique=True, - help_text=_('MapPOI__name__help'), - verbose_name=_('MapPOI__name')) - description = models.TextField( - blank=True, null=True, - help_text=_('MapPOI__description__help'), - verbose_name=_('MapPOI__description')) + name = models.CharField(max_length=200, unique=True, help_text=_('MapPOI__name__help'), verbose_name=_('MapPOI__name')) + description = models.TextField(blank=True, null=True, help_text=_('MapPOI__description__help'), verbose_name=_('MapPOI__description')) description_html = models.TextField(blank=True) - is_official = models.BooleanField( - default=False, - help_text=_('MapPOI__is_official__help'), - verbose_name=_('MapPOI__is_official')) + is_official = models.BooleanField(default=False, help_text=_('MapPOI__is_official__help'), verbose_name=_('MapPOI__is_official')) location_floor = models.ForeignKey( MapFloor, - blank=True, null=True, - related_name='pois', on_delete=models.PROTECT, + blank=True, + null=True, + related_name='pois', + on_delete=models.PROTECT, help_text=_('MapPOI__location_floor__help'), verbose_name=_('MapPOI__location_floor'), ) location_point = gis_models.PointField( - blank=True, null=True, + blank=True, + null=True, help_text=_('MapPOI__location_point__help'), verbose_name=_('MapPOI__location_point'), ) diff --git a/src/core/models/messages.py b/src/core/models/messages.py index ed79b3cbf..08315dce1 100644 --- a/src/core/models/messages.py +++ b/src/core/models/messages.py @@ -1,8 +1,9 @@ from uuid import uuid4 + from django.conf import settings from django.db import models from django.template.loader import render_to_string -from django.urls import reverse, NoReverseMatch +from django.urls import NoReverseMatch, reverse from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ @@ -18,11 +19,13 @@ class DirectMessage(models.Model): sender = models.ForeignKey( settings.AUTH_USER_MODEL, - blank=True, null=True, + blank=True, + null=True, on_delete=models.CASCADE, related_name='sent_messages', help_text=_('DirectMessage__sender__help'), - verbose_name=_('DirectMessage__sender')) + verbose_name=_('DirectMessage__sender'), + ) """The sender of this message or None if it is a system message (in which case the conference field is being used).""" recipient = models.ForeignKey( @@ -30,65 +33,46 @@ class DirectMessage(models.Model): on_delete=models.CASCADE, related_name='received_messages', help_text=_('DirectMessage__recipient__help'), - verbose_name=_('DirectMessage__recipient')) + verbose_name=_('DirectMessage__recipient'), + ) """The intended recipient of this message.""" - timestamp = models.DateTimeField( - auto_now_add=True, - help_text=_('DirectMessage__timestamp__help'), - verbose_name=_('DirectMessage__timestamp')) + timestamp = models.DateTimeField(auto_now_add=True, help_text=_('DirectMessage__timestamp__help'), verbose_name=_('DirectMessage__timestamp')) """Timestamp when the message was sent.""" autodelete_after = models.DateTimeField( - blank=True, null=True, - help_text=_('DirectMessage__autodelete_after__help'), - verbose_name=_('DirectMessage__autodelete_after')) + blank=True, null=True, help_text=_('DirectMessage__autodelete_after__help'), verbose_name=_('DirectMessage__autodelete_after') + ) """Automatic purging deletes this message after this timestamp.""" in_reply_to = models.ForeignKey( - 'self', - blank=True, null=True, on_delete=models.SET_NULL, - help_text=_('DirectMessage__in_reply_to__help'), - verbose_name=_('DirectMessage__in_reply_to')) + 'self', blank=True, null=True, on_delete=models.SET_NULL, help_text=_('DirectMessage__in_reply_to__help'), verbose_name=_('DirectMessage__in_reply_to') + ) """Original message to which this is a reply.""" - subject = models.CharField( - max_length=200, - help_text=_('DirectMessage__subject__help'), - verbose_name=_('DirectMessage__subject')) + subject = models.CharField(max_length=200, help_text=_('DirectMessage__subject__help'), verbose_name=_('DirectMessage__subject')) """Subject of the message, might include 'Re:' markers.""" - body = models.TextField( - help_text=_('DirectMessage__body__help'), - verbose_name=_('DirectMessage__body')) + body = models.TextField(help_text=_('DirectMessage__body__help'), verbose_name=_('DirectMessage__body')) """Message with (limited) Markdown support.""" deleted_by_sender = models.BooleanField( - default=False, - help_text=_('DirectMessage__deleted_by_sender__help'), - verbose_name=_('DirectMessage__deleted_by_sender')) + default=False, help_text=_('DirectMessage__deleted_by_sender__help'), verbose_name=_('DirectMessage__deleted_by_sender') + ) deleted_by_recipient = models.BooleanField( - default=False, - help_text=_('DirectMessage__deleted_by_recipient__help'), - verbose_name=_('DirectMessage__deleted_by_recipient')) - - was_read = models.BooleanField( - default=False, - help_text=_('DirectMessage__was_read__help'), - verbose_name=_('DirectMessage__was_read')) + default=False, help_text=_('DirectMessage__deleted_by_recipient__help'), verbose_name=_('DirectMessage__deleted_by_recipient') + ) + + was_read = models.BooleanField(default=False, help_text=_('DirectMessage__was_read__help'), verbose_name=_('DirectMessage__was_read')) """Signals whether the recipient has read the message, this is *not* shown to the sender!""" - has_responded = models.BooleanField( - default=False, - help_text=_('DirectMessage__has_responded__help'), - verbose_name=_('DirectMessage__has_responded')) + has_responded = models.BooleanField(default=False, help_text=_('DirectMessage__has_responded__help'), verbose_name=_('DirectMessage__has_responded')) """Signals whether the recipient has replied to the message. (speed optimization to circumvent EXISTS query)""" flagged_for_abuse = models.BooleanField( - default=False, - help_text=_('DirectMessage__flagged_for_abuse__help'), - verbose_name=_('DirectMessage__flagged_for_abuse')) + default=False, help_text=_('DirectMessage__flagged_for_abuse__help'), verbose_name=_('DirectMessage__flagged_for_abuse') + ) """Messages by abusive users will be hidden, at least if the messages have not been read yet.""" @property diff --git a/src/core/models/pages.py b/src/core/models/pages.py index 61f48a8ea..69f26f368 100644 --- a/src/core/models/pages.py +++ b/src/core/models/pages.py @@ -1,7 +1,9 @@ import logging import re -from typing import Dict, Optional, Tuple import uuid +from typing import Dict, Optional, Tuple + +from urllib3.util import Url, parse_url from django.conf import settings from django.contrib.postgres import indexes as pg_indexes @@ -14,14 +16,13 @@ from django.utils import timezone from django.utils.functional import cached_property from django.utils.html import strip_tags from django.utils.translation import gettext_lazy as _ -from urllib3.util import parse_url, Url from core.markdown import render_markdown_ex, store_relationships from ..fields import ConferenceReference +from ..utils import GitRepo from .conference import Conference from .users import PlatformUser -from ..utils import GitRepo class StaticPageNamespace(models.Model): @@ -45,13 +46,15 @@ class StaticPageNamespace(models.Model): ) upstream_url = models.URLField( - blank=True, null=True, + blank=True, + null=True, help_text=_('StaticPageNamespace__upstream_url__help'), verbose_name=_('StaticPageNamespace__upstream_url'), ) upstream_image_base_url = models.URLField( - blank=True, null=True, + blank=True, + null=True, help_text=_('StaticPageNamespace__upstream_image_base_url__help'), verbose_name=_('StaticPageNamespace__upstream_image_base_url'), ) @@ -68,7 +71,7 @@ class StaticPageNamespace(models.Model): metadata_raw, content = markup[4:].split('---\n', maxsplit=1) metadata = {} for line in metadata_raw.splitlines(): - k, v = line.split(":", maxsplit=1) + k, v = line.split(':', maxsplit=1) metadata[k.strip()] = v.strip() else: metadata = {} @@ -85,7 +88,7 @@ class StaticPageNamespace(models.Model): RE_MARKDOWN_IMAGE = re.compile(r'!\[(.+?)\]\((.+?)\)') with GitRepo(url) as repo: - docs = repo.get_documents(glob="*.md") + docs = repo.get_documents(glob='*.md') if image_base_url := self.upstream_image_base_url: image_base_url_url = parse_url(image_base_url) @@ -97,10 +100,10 @@ class StaticPageNamespace(models.Model): # seems to be a valid URL => don't change anything return match.string - if image_url.startswith("./"): + if image_url.startswith('./'): image_url = image_url[2:] - if not image_url.startswith("/"): + if not image_url.startswith('/'): new_image_url = image_base_url + image_url else: new_image_url = Url( @@ -111,7 +114,7 @@ class StaticPageNamespace(models.Model): path=image_url, ) - return f"" + return f'' for doc_name, doc in docs.items(): docs[doc_name] = RE_MARKDOWN_IMAGE.sub(derive_image_url, doc) @@ -120,7 +123,7 @@ class StaticPageNamespace(models.Model): obsolete_pages = set(existing_pages.keys()) for doc_filename, document_raw in docs.items(): - page_slug = self.prefix.lower() + doc_filename.rstrip(".md") + page_slug = self.prefix.lower() + doc_filename.rstrip('.md') page = existing_pages.get(page_slug) # type: StaticPage try: @@ -149,7 +152,7 @@ class StaticPageNamespace(models.Model): # create new page page = StaticPage.objects.create(conference=self.conference, slug=page_slug) page_rev = page.revisions.create( - title=doc_metadata.get('title') or page_slug[len(self.prefix):].capitalize(), + title=doc_metadata.get('title') or page_slug[len(self.prefix) :].capitalize(), body=document, is_draft=False, author=None, @@ -165,10 +168,10 @@ class StaticPageNamespace(models.Model): updated = StaticPage.objects.filter(conference=self.conference).bulk_update( objs=(existing_pages[page_slug] for page_slug in obsolete_pages), - fields={"privacy": StaticPage.Privacy.PERM}, + fields={'privacy': StaticPage.Privacy.PERM}, ) if updated > 0: - logging.info('set %s wiki page(s) "%s" non-public (removed upstream): %s', updated, page_slug, ";".join(obsolete_pages)) + logging.info('set %s wiki page(s) "%s" non-public (removed upstream): %s', updated, page_slug, ';'.join(obsolete_pages)) class StaticPageManager(models.Manager): @@ -248,21 +251,18 @@ class StaticPageManager(models.Manager): def conference_accessible(self, conference: Conference, language: str): # fetch conference's pages with a public revision and a language set to the given one or no language set - return self.get_queryset() \ - .filter(conference=conference, public_revision__gt=0) \ - .filter(Q(language=language) | Q(language=None)) + return self.get_queryset().filter(conference=conference, public_revision__gt=0).filter(Q(language=language) | Q(language=None)) class StaticPage(models.Model): - class Protection(models.TextChoices): - NONE = 'none', _("StaticPage__protection-none") - PERM = 'perm', _("StaticPage__protection-perm") + NONE = 'none', _('StaticPage__protection-none') + PERM = 'perm', _('StaticPage__protection-perm') class Privacy(models.TextChoices): - NONE = 'none', _("StaticPage__privacy-none") - CONFERENCE = 'conf', _("StaticPage__privacy-conf") - PERM = 'perm', _("StaticPage__privacy-perm") + NONE = 'none', _('StaticPage__privacy-none') + CONFERENCE = 'conf', _('StaticPage__privacy-conf') + PERM = 'perm', _('StaticPage__privacy-perm') id = models.UUIDField(default=uuid.uuid4, primary_key=True) @@ -270,12 +270,14 @@ class StaticPage(models.Model): slug = models.SlugField() language = models.CharField(max_length=20, blank=False, null=True, help_text=_('StaticPage__language__help'), verbose_name=_('StaticPage__language')) protection = models.CharField( - max_length=20, default=Protection.NONE, choices=Protection.choices, - help_text=_('StaticPage__protection__help'), verbose_name=_('StaticPage__protection') + max_length=20, + default=Protection.NONE, + choices=Protection.choices, + help_text=_('StaticPage__protection__help'), + verbose_name=_('StaticPage__protection'), ) privacy = models.CharField( - max_length=20, default=Privacy.NONE, choices=Privacy.choices, - help_text=_('StaticPage__privacy__help'), verbose_name=_('StaticPage__privacy') + max_length=20, default=Privacy.NONE, choices=Privacy.choices, help_text=_('StaticPage__privacy__help'), verbose_name=_('StaticPage__privacy') ) remove_html = models.BooleanField( @@ -289,22 +291,10 @@ class StaticPage(models.Model): verbose_name=_('StaticPage__sanitize_html'), ) - public_revision = models.PositiveIntegerField( - default=0, - help_text=_('StaticPage__public_revision__help'), - verbose_name=_('StaticPage__public_revision')) - title = models.CharField( - max_length=200, - help_text=_('StaticPage__title__help'), - verbose_name=_('StaticPage__title')) - body = models.TextField( - default='', - help_text=_('StaticPage__body__help'), - verbose_name=_('StaticPage__body')) - body_html = models.TextField( - blank=True, null=True, editable=False, - help_text=_('StaticPage__body_html__help'), - verbose_name=_('StaticPage__body_html')) + public_revision = models.PositiveIntegerField(default=0, help_text=_('StaticPage__public_revision__help'), verbose_name=_('StaticPage__public_revision')) + title = models.CharField(max_length=200, help_text=_('StaticPage__title__help'), verbose_name=_('StaticPage__title')) + body = models.TextField(default='', help_text=_('StaticPage__body__help'), verbose_name=_('StaticPage__body')) + body_html = models.TextField(blank=True, null=True, editable=False, help_text=_('StaticPage__body_html__help'), verbose_name=_('StaticPage__body_html')) """HTML of the current public revision""" search_content = models.TextField(blank=True, null=True, editable=False) """content used for searching (internal use only)""" @@ -315,7 +305,14 @@ class StaticPage(models.Model): class Meta: constraints = [ - models.constraints.UniqueConstraint(fields=['conference', 'language', 'slug',], name='unique_conferencestaticpage_slug'), + models.constraints.UniqueConstraint( + fields=[ + 'conference', + 'language', + 'slug', + ], + name='unique_conferencestaticpage_slug', + ), ] indexes = [ pg_indexes.GinIndex(fields=['search_vector'], name='staticpage_searchvector'), @@ -325,7 +322,7 @@ class StaticPage(models.Model): verbose_name_plural = _('StaticPages') def __str__(self): - return '%s[%s]' % (self.title, self.language) + return f'{self.title}[{self.language}]' @cached_property def newest_revision(self): @@ -353,13 +350,8 @@ class StaticPageRevision(models.Model): page = models.ForeignKey(StaticPage, related_name='revisions', on_delete=models.CASCADE) revision = models.AutoField(primary_key=True) - title = models.CharField( - max_length=200, - help_text=_('StaticPageRevision__title__help'), - verbose_name=_('StaticPageRevision__title')) - body = models.TextField( - help_text=_('StaticPageRevision__body__help'), - verbose_name=_('StaticPageRevision__body')) + title = models.CharField(max_length=200, help_text=_('StaticPageRevision__title__help'), verbose_name=_('StaticPageRevision__title')) + body = models.TextField(help_text=_('StaticPageRevision__body__help'), verbose_name=_('StaticPageRevision__body')) # `is_draft` wird momentan nicht mehr wirklich verwendet. # Um das wieder sinnvoll zu nutzen sollte eine Möglichkeit geschaffen zu werden, drafts @@ -367,20 +359,17 @@ class StaticPageRevision(models.Model): # werden. Und dann eine Möglichkeit einen draft zu veröffentlichen. # das Veröffentlichen sollte jedoch eine neue Revision erzeugen, # so dass Revisionen, die als `is_draft` markiert sind, dauerhaft `is_draft` bleiben. - is_draft = models.BooleanField( - default=False, - help_text=_('StaticPageRevision__is_draft__help'), - verbose_name=_('StaticPageRevision__is_draft')) + is_draft = models.BooleanField(default=False, help_text=_('StaticPageRevision__is_draft__help'), verbose_name=_('StaticPageRevision__is_draft')) author = models.ForeignKey( settings.AUTH_USER_MODEL, - blank=True, null=True, on_delete=models.SET_NULL, + blank=True, + null=True, + on_delete=models.SET_NULL, help_text=_('StaticPageRevision__author__help'), - verbose_name=_('StaticPageRevision__author')) - timestamp = models.DateTimeField( - auto_now_add=True, - help_text=_('StaticPageRevision__timestamp__help'), - verbose_name=_('StaticPageRevision__timestamp')) + verbose_name=_('StaticPageRevision__author'), + ) + timestamp = models.DateTimeField(auto_now_add=True, help_text=_('StaticPageRevision__timestamp__help'), verbose_name=_('StaticPageRevision__timestamp')) class Meta: constraints = [ @@ -402,9 +391,7 @@ class StaticPageRevision(models.Model): self.page.title = self.title self.page.body = self.body render_result = render_markdown_ex( - self.page.conference, self.body, - allow_embedded_html=not self.page.remove_html, - sanitize_html=self.page.sanitize_html + self.page.conference, self.body, allow_embedded_html=not self.page.remove_html, sanitize_html=self.page.sanitize_html ) self.page.body_html = render_result.document store_relationships(self.page.conference, self.page, render_result) diff --git a/src/core/models/rooms.py b/src/core/models/rooms.py index f7fc80da6..9bba968c9 100644 --- a/src/core/models/rooms.py +++ b/src/core/models/rooms.py @@ -7,13 +7,13 @@ from django.db.models import Q from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ -from .shared import BackendMixin from ..fields import ConferenceReference +from ..markdown import compile_translated_markdown_fields, store_relationships from ..models.conference import Conference, ConferenceMember from ..models.users import PlatformUser from ..utils import str2bool -from ..markdown import compile_translated_markdown_fields, store_relationships from .assemblies import Assembly +from .shared import BackendMixin class RoomManager(models.Manager): @@ -44,8 +44,11 @@ class RoomManager(models.Manager): return qs.filter(blocked=False) def conference_accessible(self, conference: Conference): - return self.get_queryset().filter(conference=conference, blocked=False) \ + return ( + self.get_queryset() + .filter(conference=conference, blocked=False) .filter(Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES) | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES)) + ) class Room(BackendMixin, models.Model): @@ -84,17 +87,14 @@ class Room(BackendMixin, models.Model): slug = models.SlugField(unique=True) - description = models.TextField( - blank=True, null=True, - help_text=_('Room__description__help'), - verbose_name=_('Room__description')) + description = models.TextField(blank=True, null=True, help_text=_('Room__description__help'), verbose_name=_('Room__description')) """Description of the room/project.""" description_html = models.TextField(blank=True, null=True) is_public_fahrplan = models.BooleanField(default=False, help_text=_('Room__is_public_fahrplan__help'), verbose_name=_('Room__is_public_fahrplan')) - is_official = models.BooleanField(default=False, help_text=_("Room__is_official__help"), verbose_name=_("Room__is_official")) - official_room_order = models.IntegerField(default=0, help_text=_("Room__official_room_order__help"), verbose_name=_("Room__official_room_order")) + is_official = models.BooleanField(default=False, help_text=_('Room__is_official__help'), verbose_name=_('Room__is_official')) + official_room_order = models.IntegerField(default=0, help_text=_('Room__official_room_order__help'), verbose_name=_('Room__official_room_order')) room_type = models.CharField(max_length=20, choices=RoomType.choices, help_text=_('Room__type__help'), verbose_name=_('Room__type')) """Style of the room.""" @@ -121,21 +121,20 @@ class Room(BackendMixin, models.Model): """Arbitrary data necessary for room operation in the backend - used by the actual integration component (not for users!!!).""" backend_status = models.CharField( - max_length=20, blank=True, + max_length=20, + blank=True, choices=BackendMixin.BackendStatus.choices, default=BackendMixin.BackendStatus.NONE, help_text=_('Room__backend_status__help'), - verbose_name=_('Room__backend_status')) + verbose_name=_('Room__backend_status'), + ) """The backend's status information.""" capacity = models.PositiveIntegerField(blank=True, null=True, help_text=_('Room__capacity__help'), verbose_name=_('Room__capacity')) """A room's capacity, i.e. maximum of participants.""" reserve_capacity = models.PositiveIntegerField( - default=20, - help_text=_('Room__reserve_capacity__help'), - verbose_name=_('Room__reserve_capacity'), - editable=False + default=20, help_text=_('Room__reserve_capacity__help'), verbose_name=_('Room__reserve_capacity'), editable=False ) """Number of slots the deployment infrastructure should keep available for this room""" @@ -209,8 +208,9 @@ class Room(BackendMixin, models.Model): return super().save(*args, update_fields=update_fields, **kwargs) @classmethod - def from_dict(cls, data: dict, conference: Conference, assembly: Assembly = None, existing=None, pop_used_keys: bool = False, - allow_backend_roomtypes: bool = False): + def from_dict( + cls, data: dict, conference: Conference, assembly: Assembly = None, existing=None, pop_used_keys: bool = False, allow_backend_roomtypes: bool = False + ): """ Loads an Room instance from the given dictionary. An existing event can be provided which's data is overwritten (in parts). @@ -231,10 +231,10 @@ class Room(BackendMixin, models.Model): if existing: obj = existing if assembly: - assert obj.assembly == assembly, 'Existing room\'s assembly does not match given one.' - assert obj.conference == assembly.conference, 'Existing room\'s conference does not match the given assembly\'s one.' + assert obj.assembly == assembly, "Existing room's assembly does not match given one." + assert obj.conference == assembly.conference, "Existing room's conference does not match the given assembly's one." if conference: - assert obj.conference == conference, 'Existing room\'s conference does not match given one.' + assert obj.conference == conference, "Existing room's conference does not match given one." else: obj = cls(assembly=assembly, conference=conference) assert obj.conference_id is not None @@ -291,21 +291,19 @@ class RoomLink(models.Model): room = models.ForeignKey(Room, related_name='links', on_delete=models.CASCADE) name = models.CharField(max_length=200) - link_type = models.CharField( - max_length=20, choices=LinkType.choices, - help_text=_('RoomLink__type__help'), - verbose_name=_('RoomLink__type')) + link_type = models.CharField(max_length=20, choices=LinkType.choices, help_text=_('RoomLink__type__help'), verbose_name=_('RoomLink__type')) link = models.CharField(max_length=255) conference_internal = models.BooleanField( - default=False, - help_text=_('RoomLink__conference_internal__help'), - verbose_name=_('RoomLink__conference_internal')) + default=False, help_text=_('RoomLink__conference_internal__help'), verbose_name=_('RoomLink__conference_internal') + ) send_token = models.CharField( - max_length=20, choices=Token.choices, + max_length=20, + choices=Token.choices, default=Token.NONE, help_text=_('RoomLink__conference_internal__help'), - verbose_name=_('RoomLink__conference_internal')) + verbose_name=_('RoomLink__conference_internal'), + ) def clean(self): if self.link_type in RoomLink.URL_LINKTYPES: diff --git a/src/core/models/schedules.py b/src/core/models/schedules.py index 16e39ba21..47d404ed7 100644 --- a/src/core/models/schedules.py +++ b/src/core/models/schedules.py @@ -1,19 +1,18 @@ -from datetime import timedelta import logging -from uuid import uuid4, UUID +from datetime import timedelta +from uuid import UUID, uuid4 from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from .assemblies import Assembly -from .events import Event, EventAttachment, EventParticipant -from .rooms import Room from ..fields import ConferenceReference from ..schedules import ScheduleTypeManager from ..utils import mask_url, str2bool - +from .assemblies import Assembly +from .events import Event, EventAttachment, EventParticipant +from .rooms import Room logger = logging.getLogger(__name__) @@ -33,7 +32,8 @@ class ScheduleSource(models.Model): assembly = models.ForeignKey(Assembly, blank=True, null=True, related_name='schedule_sources', on_delete=models.CASCADE) import_type = models.CharField( - blank=False, max_length=20, + blank=False, + max_length=20, help_text=_('ScheduleSource__import_type__help'), verbose_name=_('ScheduleSource__import_type'), ) @@ -60,7 +60,8 @@ class ScheduleSource(models.Model): verbose_name=_('ScheduleSource__import_timeout'), ) last_import = models.DateTimeField( - blank=True, null=True, + blank=True, + null=True, help_text=_('ScheduleSource__last_import__help'), verbose_name=_('ScheduleSource__last_import'), ) @@ -199,12 +200,14 @@ class ScheduleSource(models.Model): item.delete() # log us skipping the entry - activity.append({ - 'action': 'skipped', - 'type': item_type, - 'source_id': item_source_id, - 'local_id': None, - }) + activity.append( + { + 'action': 'skipped', + 'type': item_type, + 'source_id': item_source_id, + 'local_id': None, + } + ) # if we have the item locally but shall skip it, handle it as 'handled' anyway # so that it is not picked up again as e.g. 'missing_events' @@ -240,13 +243,15 @@ class ScheduleSource(models.Model): except Exception as err: # log the error items[item_source_id] = None - activity.append({ - 'action': 'error', - 'type': item_type, - 'source_id': item_source_id, - 'local_id': None, - 'message': str(err), - }) + activity.append( + { + 'action': 'error', + 'type': item_type, + 'source_id': item_source_id, + 'local_id': None, + 'message': str(err), + } + ) # ... and delete the incomplete (wrong) mapping mapping.delete() @@ -255,12 +260,14 @@ class ScheduleSource(models.Model): else: # store new item's data items[item_source_id] = mapping.local_object - activity.append({ - 'action': 'added', - 'type': item_type, - 'source_id': item_source_id, - 'local_id': str(mapping.local_id), - }) + activity.append( + { + 'action': 'added', + 'type': item_type, + 'source_id': item_source_id, + 'local_id': str(mapping.local_id), + } + ) return 'added' # note that we've seen the existing room in the imported data @@ -273,12 +280,14 @@ class ScheduleSource(models.Model): if item_delete: mapping.local_object.delete() items[item_source_id] = None - activity.append({ - 'action': 'deleted', - 'type': item_type, - 'source_id': item_source_id, - 'local_id': str(mapping.local_id), - }) + activity.append( + { + 'action': 'deleted', + 'type': item_type, + 'source_id': item_source_id, + 'local_id': str(mapping.local_id), + } + ) return 'deleted' # update data on existing room @@ -297,12 +306,14 @@ class ScheduleSource(models.Model): mapping.local_object.save() items[item_source_id] = mapping.local_object - activity.append({ - 'action': 'seen', # TODO: set to 'changed' if data was changed (returned by .from_dict()?) - 'type': item_type, - 'source_id': item_source_id, - 'local_id': str(mapping.local_id), - }) + activity.append( + { + 'action': 'seen', # TODO: set to 'changed' if data was changed (returned by .from_dict()?) + 'type': item_type, + 'source_id': item_source_id, + 'local_id': str(mapping.local_id), + } + ) return 'seen' def load_data(self, data): @@ -324,18 +335,22 @@ class ScheduleSource(models.Model): if self.assembly: expected_rooms = list(self.assembly.rooms.values_list('id', flat=True)) else: - expected_rooms = list(self.mappings.filter( - mapping_type=ScheduleSourceMapping.MappingType.ROOM, - ).values_list('local_id', flat=True)) - expected_events = list(self.mappings.filter( - mapping_type=ScheduleSourceMapping.MappingType.EVENT, - ).values_list('local_id', flat=True)) + expected_rooms = list( + self.mappings.filter( + mapping_type=ScheduleSourceMapping.MappingType.ROOM, + ).values_list('local_id', flat=True) + ) + expected_events = list( + self.mappings.filter( + mapping_type=ScheduleSourceMapping.MappingType.EVENT, + ).values_list('local_id', flat=True) + ) # first, load the rooms (as they're needed for events) for r_id, r in data['rooms'].items(): try: if replace_conference_slug_prefix and r.get('slug', '').startswith(replace_conference_slug_prefix): - r['slug'] = self.conference.slug + r['slug'][len(replace_conference_slug_prefix):] + r['slug'] = self.conference.slug + r['slug'][len(replace_conference_slug_prefix) :] action = self._load_dataitem( activity=activity, @@ -356,19 +371,21 @@ class ScheduleSource(models.Model): rooms[r_id] = r_mapping.local_object except Exception as err: - activity.append({ - 'action': 'error', - 'type': 'room', - 'source_id': r_id, - 'local_id': None, - 'message': str(err), - }) + activity.append( + { + 'action': 'error', + 'type': 'room', + 'source_id': r_id, + 'local_id': None, + 'message': str(err), + } + ) # then load events for e_id, e in data['events'].items(): try: if replace_conference_slug_prefix and e.get('slug', '').startswith(replace_conference_slug_prefix): - e['slug'] = self.conference.slug + e['slug'][len(replace_conference_slug_prefix):] + e['slug'] = self.conference.slug + e['slug'][len(replace_conference_slug_prefix) :] self._load_dataitem( activity=activity, @@ -381,25 +398,29 @@ class ScheduleSource(models.Model): 'allow_kind': self.assembly.is_official if self.assembly else False, # TODO: lookup assembly's room if not given 'allow_track': False, # TODO 'room_lookup': lambda r_source_id: rooms.get(r_source_id), - } + }, ) except Exception as err: - activity.append({ - 'action': 'error', - 'type': 'event', - 'source_id': e_id, - 'local_id': e.get('uuid', None), - 'message': str(err), - }) + activity.append( + { + 'action': 'error', + 'type': 'event', + 'source_id': e_id, + 'local_id': e.get('uuid', None), + 'message': str(err), + } + ) # flag the non-loaded rooms as 'missing' for room_id in expected_rooms: - activity.append({ - 'action': 'missing', - 'type': 'room', - 'source_id': None, - 'local_id': str(room_id), - }) + activity.append( + { + 'action': 'missing', + 'type': 'room', + 'source_id': None, + 'local_id': str(room_id), + } + ) # flag the non-loaded events as 'missing' for event_id in expected_events: @@ -474,9 +495,8 @@ class ScheduleSourceMapping(models.Model): if room.assembly_id is not None: if self.schedule_source.assembly_id is not None and room.assembly_id != self.schedule_source.assembly_id: raise LocalObjectAccessViolation('Assembly of Room does not match.') - else: - if self.schedule_source.assembly_id is not None and room.conference_id != self.schedule_source.assembly.conference_id: - raise LocalObjectAccessViolation('Conference of Room does not match.') + elif self.schedule_source.assembly_id is not None and room.conference_id != self.schedule_source.assembly.conference_id: + raise LocalObjectAccessViolation('Conference of Room does not match.') return room if self.mapping_type == self.MappingType.EVENT: @@ -625,15 +645,18 @@ class ScheduleSourceImport(models.Model): # create list of unique errors for summary msgs = list({x['message'].split('\n')[0] for x in errors}) - stats = ', '.join( - (t + '=' + str(sum(1 for x in activity if x['action'] == t))) for t in [ - 'added', 'changed', 'seen', 'deleted', 'missing', 'error', 'skipped' - ] - ) + ' \n' + ' \n'.join(msgs) + stats = ( + ', '.join( + (t + '=' + str(sum(1 for x in activity if x['action'] == t))) + for t in ['added', 'changed', 'seen', 'deleted', 'missing', 'error', 'skipped'] + ) + + ' \n' + + ' \n'.join(msgs) + ) self.summary = ('DONE: ' + stats)[:200] if len(errors) > len(activity) / 2: - raise Exception("Too many errors, aborting import: " + stats) + raise Exception('Too many errors, aborting import: ' + stats) self.save(update_fields=['data', 'summary']) diff --git a/src/core/models/shared.py b/src/core/models/shared.py index 370c97745..77d69925d 100644 --- a/src/core/models/shared.py +++ b/src/core/models/shared.py @@ -64,21 +64,20 @@ class BackendMixin(models.Model): """Arbitrary data necessary for room operation in the backend - used by the actual integration component (not for users!!!).""" backend_status = models.CharField( - max_length=20, blank=True, + max_length=20, + blank=True, choices=BackendStatus.choices, default=BackendStatus.NONE, help_text=_('Room__backend_status__help'), - verbose_name=_('Room__backend_status')) + verbose_name=_('Room__backend_status'), + ) """The backend's status information.""" capacity = models.PositiveIntegerField(blank=True, null=True, help_text=_('Room__capacity__help'), verbose_name=_('Room__capacity')) """A room's capacity, i.e. maximum of participants.""" reserve_capacity = models.PositiveIntegerField( - default=20, - help_text=_('Room__reserve_capacity__help'), - verbose_name=_('Room__reserve_capacity'), - editable=False + default=20, help_text=_('Room__reserve_capacity__help'), verbose_name=_('Room__reserve_capacity'), editable=False ) """Number of slots the deployment infrastructure should keep available for this room""" diff --git a/src/core/models/sso.py b/src/core/models/sso.py index efa9d5ceb..816e4abcd 100644 --- a/src/core/models/sso.py +++ b/src/core/models/sso.py @@ -1,6 +1,7 @@ +from oauth2_provider.models import AbstractApplication + from django.db import models from django.utils.translation import gettext_lazy as _ -from oauth2_provider.models import AbstractApplication from .assemblies import Assembly @@ -8,6 +9,5 @@ from .assemblies import Assembly class Application(AbstractApplication): assembly = models.ForeignKey(Assembly, related_name='applications', blank=True, null=True, on_delete=models.CASCADE) internal_description = models.TextField( - blank=True, null=True, - help_text=_('Application__internal_description__help'), - verbose_name=_('Application__internal_description')) + blank=True, null=True, help_text=_('Application__internal_description__help'), verbose_name=_('Application__internal_description') + ) diff --git a/src/core/models/tags.py b/src/core/models/tags.py index 15015992d..b16550330 100644 --- a/src/core/models/tags.py +++ b/src/core/models/tags.py @@ -1,3 +1,5 @@ +import contextlib + from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -26,11 +28,9 @@ class ConferenceTag(models.Model): is_public = models.BooleanField(default=False, help_text=_('ConferenceTag__is_public__help'), verbose_name=_('ConferenceTag__is_public')) value_type = models.CharField( - max_length=20, choices=Type.choices, default=Type.SIMPLE, - help_text=_('ConferenceTag__value_type__help'), verbose_name=_('ConferenceTag__value_type')) - description = models.TextField( - blank=True, null=True, - help_text=_('ConferenceTag__description__help'), verbose_name=_('ConferenceTag__description')) + max_length=20, choices=Type.choices, default=Type.SIMPLE, help_text=_('ConferenceTag__value_type__help'), verbose_name=_('ConferenceTag__value_type') + ) + description = models.TextField(blank=True, null=True, help_text=_('ConferenceTag__description__help'), verbose_name=_('ConferenceTag__description')) def __str__(self): return self.slug @@ -38,9 +38,7 @@ class ConferenceTag(models.Model): class TagItem(models.Model): class Meta: - constraints = [ - models.UniqueConstraint(fields=['target_type', 'target_id', 'tag'], name='unique_target_tag') - ] + constraints = [models.UniqueConstraint(fields=['target_type', 'target_id', 'tag'], name='unique_target_tag')] tag = models.ForeignKey(ConferenceTag, related_name='+', on_delete=models.CASCADE) @@ -91,7 +89,7 @@ class TagItem(models.Model): return if self.tag.value_type == ConferenceTag.Type.BOOL: - if isinstance(new_value, bool) or isinstance(new_value, int): + if isinstance(new_value, (bool, int)): self._value_int = 0 if new_value else 1 return @@ -119,20 +117,20 @@ class TagItem(models.Model): elif self.tag.value_type == ConferenceTag.Type.INT: if not isinstance(self.value, int): - raise ValidationError('Tag\'s value must be integer.') + raise ValidationError("Tag's value must be integer.") elif self.tag.value_type == ConferenceTag.Type.BOOL: if self.value not in [True, False]: - raise ValidationError('Tag\'s value must be a boolean.') + raise ValidationError("Tag's value must be a boolean.") def __str__(self): if self.tag.value_type == ConferenceTag.Type.SIMPLE: return self.tag.slug - return '{}={}'.format(self.tag.slug, self.value) + return f'{self.tag.slug}={self.value}' -class TaggedItemMixin(object): +class TaggedItemMixin: @property def tags(self): qs = TagItem.objects.filter(target_type=ContentType.objects.get_for_model(type(self)), target_id=self.id, tag__is_public=True).select_related('tag') @@ -164,10 +162,8 @@ class TaggedItemMixin(object): def remove_tag(self, tag, value=None): if not isinstance(tag, ConferenceTag): - try: + with contextlib.suppress(ConferenceTag.DoesNotExist): tag = ConferenceTag.objects.get(conference=self.conference, slug=tag, is_public=True) - except ConferenceTag.DoesNotExist: - pass # TODO: check user's permission TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(type(self)), target_id=self.id).delete() diff --git a/src/core/models/ticket.py b/src/core/models/ticket.py index f0a7c60a7..e8804d537 100644 --- a/src/core/models/ticket.py +++ b/src/core/models/ticket.py @@ -1,16 +1,16 @@ import logging +import jwt + from django.conf import settings from django.db import models from django.db.utils import IntegrityError from django.utils.translation import gettext_lazy as _ -import jwt from ..fields import ConferenceReference from .conference import Conference from .users import PlatformUser - logger = logging.getLogger(__name__) diff --git a/src/core/models/users.py b/src/core/models/users.py index 41f979c34..f410861e5 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -3,7 +3,8 @@ import re from typing import Any from uuid import uuid4 -from django.db.models import Q +from timezone_field import TimeZoneField + from django.contrib.auth.models import AbstractUser from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -12,11 +13,11 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.mail import send_mail from django.core.validators import validate_email from django.db import models +from django.db.models import Q from django.utils.functional import cached_property +from django.utils.text import slugify from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from django.utils.text import slugify -from timezone_field import TimeZoneField from ..fields import ConferenceReference @@ -29,157 +30,103 @@ class PlatformUser(AbstractUser): BOT = ('bot', _('PlatformUser__type-bot')) class Theme(models.TextChoices): - DARK = ('dark', _("PlatformUser__theme-dark")) - LIGHT = ('light', _("PlatformUser__theme-light")) + DARK = ('dark', _('PlatformUser__theme-dark')) + LIGHT = ('light', _('PlatformUser__theme-light')) INTERACTIVE_TYPES = [Type.HUMAN, Type.BOT] """User types which can be interactive (and can thus have an avatar and/or modified by e.g. the Engelsystem).""" user_type = models.CharField( - max_length=10, choices=Type.choices, default=Type.HUMAN, - help_text=_('PlatformUser__type__help'), - verbose_name=_('PlatformUser__type')) + max_length=10, choices=Type.choices, default=Type.HUMAN, help_text=_('PlatformUser__type__help'), verbose_name=_('PlatformUser__type') + ) uuid = models.UUIDField(default=uuid4, unique=True) slug = models.SlugField(blank=False, unique=True) # legal stuff accepted_speakersagreement = models.BooleanField( - null=True, - help_text=_('PlatformUser__accepted_speakersagreement__help'), - verbose_name=_('PlatformUser__accepted_speakersagreement')) + null=True, help_text=_('PlatformUser__accepted_speakersagreement__help'), verbose_name=_('PlatformUser__accepted_speakersagreement') + ) # self portrayal - show_name = models.BooleanField( - null=True, - help_text=_('PlatformUser__show_name__help'), - verbose_name=_('PlatformUser__show_name')) - avatar_url = models.URLField( - blank=True, null=True, - help_text=_('PlatformUser__avatar_url__help'), - verbose_name=_('PlatformUser__avatar_url')) - avatar_config = models.JSONField( - blank=True, null=True, - help_text=_('PlatformUser__avatar_config__help'), - verbose_name=_('PlatformUser__avatar_config')) - pronouns = models.CharField( - max_length=50, blank=True, default='', - help_text=_('PlatformUser__pronouns__help'), - verbose_name=_('PlatformUser__pronouns')) - - status = models.CharField( - max_length=50, blank=True, null=True, - help_text=_('PlatformUser__status__help'), - verbose_name=_('PlatformUser__status')) - status_public = models.BooleanField( - null=True, - help_text=_('PlatformUser__status_public__help'), - verbose_name=_('PlatformUser__status_public')) + show_name = models.BooleanField(null=True, help_text=_('PlatformUser__show_name__help'), verbose_name=_('PlatformUser__show_name')) + avatar_url = models.URLField(blank=True, null=True, help_text=_('PlatformUser__avatar_url__help'), verbose_name=_('PlatformUser__avatar_url')) + avatar_config = models.JSONField(blank=True, null=True, help_text=_('PlatformUser__avatar_config__help'), verbose_name=_('PlatformUser__avatar_config')) + pronouns = models.CharField(max_length=50, blank=True, default='', help_text=_('PlatformUser__pronouns__help'), verbose_name=_('PlatformUser__pronouns')) + + status = models.CharField(max_length=50, blank=True, null=True, help_text=_('PlatformUser__status__help'), verbose_name=_('PlatformUser__status')) + status_public = models.BooleanField(null=True, help_text=_('PlatformUser__status_public__help'), verbose_name=_('PlatformUser__status_public')) timezone = TimeZoneField( help_text=_('PlatformUser__timezone__help'), verbose_name=_('PlatformUser__timezone'), default='Europe/Berlin', - choices_display="WITH_GMT_OFFSET", + choices_display='WITH_GMT_OFFSET', ) # Accessibility Options - no_animations = models.BooleanField( - default=False, - help_text=_('PlatformUser__no_animations__help'), - verbose_name=_('PlatformUser__no_animations')) - colorblind = models.BooleanField( - default=False, - help_text=_('PlatformUser__colorblind__help'), - verbose_name=_('PlatformUser__colorblind')) - high_contrast = models.BooleanField( - default=False, - help_text=_('PlatformUser__high_contrast__help'), - verbose_name=_('PlatformUser__high_contrast')) + no_animations = models.BooleanField(default=False, help_text=_('PlatformUser__no_animations__help'), verbose_name=_('PlatformUser__no_animations')) + colorblind = models.BooleanField(default=False, help_text=_('PlatformUser__colorblind__help'), verbose_name=_('PlatformUser__colorblind')) + high_contrast = models.BooleanField(default=False, help_text=_('PlatformUser__high_contrast__help'), verbose_name=_('PlatformUser__high_contrast')) tag_ignorelist = pg_fields.ArrayField( - models.SlugField( - ), blank=True, default=list, - help_text=_('PlatformUser__tag_ignorelist__help'), - verbose_name=_('PlatformUser__tag_ignorelist')) + models.SlugField(), blank=True, default=list, help_text=_('PlatformUser__tag_ignorelist__help'), verbose_name=_('PlatformUser__tag_ignorelist') + ) theme = models.CharField( - max_length=50, choices=Theme.choices, default=Theme.DARK, - verbose_name=_("PlatformUser__theme"), - help_text=_("PlatformUser__theme__help")) + max_length=50, choices=Theme.choices, default=Theme.DARK, verbose_name=_('PlatformUser__theme'), help_text=_('PlatformUser__theme__help') + ) # Disturbance Settings - receive_dms = models.BooleanField( - default=True, - help_text=_('PlatformUser__receive_dms__help'), - verbose_name=_('PlatformUser__receive_dms')) + receive_dms = models.BooleanField(default=True, help_text=_('PlatformUser__receive_dms__help'), verbose_name=_('PlatformUser__receive_dms')) receive_dm_images = models.BooleanField( - default=False, - help_text=_('PlatformUser__receive_dm_images__help'), - verbose_name=_('PlatformUser__receive_dm_images')) - receive_audio = models.BooleanField( - default=True, - help_text=_('PlatformUser__receive_audio__help'), - verbose_name=_('PlatformUser__receive_audio')) - receive_video = models.BooleanField( - default=True, - help_text=_('PlatformUser__receive_video__help'), - verbose_name=_('PlatformUser__receive_video')) + default=False, help_text=_('PlatformUser__receive_dm_images__help'), verbose_name=_('PlatformUser__receive_dm_images') + ) + receive_audio = models.BooleanField(default=True, help_text=_('PlatformUser__receive_audio__help'), verbose_name=_('PlatformUser__receive_audio')) + receive_video = models.BooleanField(default=True, help_text=_('PlatformUser__receive_video__help'), verbose_name=_('PlatformUser__receive_video')) # Administrative - shadow_banned = models.BooleanField( - default=False, - help_text=_('PlatformUser__shadow_banned__help'), - verbose_name=_('PlatformUser__shadow_banned')) + shadow_banned = models.BooleanField(default=False, help_text=_('PlatformUser__shadow_banned__help'), verbose_name=_('PlatformUser__shadow_banned')) admin_notification = models.TextField( - default='', blank=True, - help_text=_('PlatformUser__admin_notification__help'), - verbose_name=_('PlatformUser__admin_notification')) + default='', blank=True, help_text=_('PlatformUser__admin_notification__help'), verbose_name=_('PlatformUser__admin_notification') + ) audio_muted = models.BooleanField(default=False) audio_volume = models.FloatField(blank=True, null=True) # privacy settings autoaccept_contacts = models.BooleanField( - default=False, - help_text=_('PlatformUser__autoaccept_contacts__help'), - verbose_name=_('PlatformUser__autoaccept_contacts')) + default=False, help_text=_('PlatformUser__autoaccept_contacts__help'), verbose_name=_('PlatformUser__autoaccept_contacts') + ) - is_searchable = models.BooleanField( - default=False, - help_text=_('PlatformUser__is_searchable__help'), - verbose_name=_('PlatformUser__is_searchable')) + is_searchable = models.BooleanField(default=False, help_text=_('PlatformUser__is_searchable__help'), verbose_name=_('PlatformUser__is_searchable')) # menu settings - menu = models.JSONField( - blank=True, default=dict, - help_text=_('PlatformUser__menu__help'), - verbose_name=_('PlatformUser__menu')) + menu = models.JSONField(blank=True, default=dict, help_text=_('PlatformUser__menu__help'), verbose_name=_('PlatformUser__menu')) # workadventure preferences wa_block_external_content = models.BooleanField( default=False, - help_text=_("PlatformUser__wa_block_external_content__help"), - verbose_name=_("PlatformUser__wa_block_external_content"), + help_text=_('PlatformUser__wa_block_external_content__help'), + verbose_name=_('PlatformUser__wa_block_external_content'), ) wa_block_ambient_sounds = models.BooleanField( default=False, - help_text=_("PlatformUser__wa_block_ambient_sounds__help"), - verbose_name=_("PlatformUser__wa_block_ambient_sounds"), + help_text=_('PlatformUser__wa_block_ambient_sounds__help'), + verbose_name=_('PlatformUser__wa_block_ambient_sounds'), ) wa_ignore_follow_requests = models.BooleanField( default=False, - help_text=_("PlatformUser__wa_ignore_follow_requests__help"), - verbose_name=_("PlatformUser__wa_ignore_follow_requests"), + help_text=_('PlatformUser__wa_ignore_follow_requests__help'), + verbose_name=_('PlatformUser__wa_ignore_follow_requests'), ) wa_force_website_trigger = models.BooleanField( default=False, - help_text=_("PlatformUser__wa_force_website_trigger__help"), - verbose_name=_("PlatformUser__wa_force_website_trigger"), + help_text=_('PlatformUser__wa_force_website_trigger__help'), + verbose_name=_('PlatformUser__wa_force_website_trigger'), ) # security settings allow_reset_non_primary = models.BooleanField( - default=False, - help_text=_('PlatformUser__allow_reset_non_primary__help'), - verbose_name=_('PlatformUser__allow_reset_non_primary')) + default=False, help_text=_('PlatformUser__allow_reset_non_primary__help'), verbose_name=_('PlatformUser__allow_reset_non_primary') + ) @classmethod def get_user_flags(cls, user): @@ -194,7 +141,7 @@ class PlatformUser(AbstractUser): @classmethod def get_anonymous_user(cls): - class AnonUser(object): + class AnonUser: def __init__(self): for x in cls._meta.fields: setattr(self, x.name, x.default if x.default != models.fields.NOT_PROVIDED else None) @@ -206,7 +153,7 @@ class PlatformUser(AbstractUser): is_authenticated = False def save(self, *args, **kwargs): - raise Exception() + raise Exception return AnonUser() @@ -231,8 +178,9 @@ class PlatformUser(AbstractUser): return info.get('character_layers', []) def set_character_layers(self, value): + # TODO: Replace both asserts with proper checks assert isinstance(value, list), 'character layers must be an array' - # assert all(isinstance(x, int) for x in value), 'character layers must be an array of integers' + # assert all(isinstance(x, int) for x in value), 'character layers must be an array of integers' # noqa: ERA001 info = self.avatar_config or {} info['character_layers'] = value @@ -281,8 +229,11 @@ class PlatformUser(AbstractUser): if (avatar_custom := update_data.get('custom', _UNSET)) != _UNSET: if avatar_custom is not None: - assert isinstance(avatar_custom, list) and len(avatar_custom) == CUSTOM_LENGTH and \ - all(isinstance(x, int) and MIN_CUSTOM_INDEX <= x <= MAX_CUSTOM_INDEX for x in avatar_custom) + assert ( + isinstance(avatar_custom, list) + and len(avatar_custom) == CUSTOM_LENGTH + and all(isinstance(x, int) and MIN_CUSTOM_INDEX <= x <= MAX_CUSTOM_INDEX for x in avatar_custom) + ) info['custom'] = avatar_custom @@ -318,9 +269,7 @@ class PlatformUser(AbstractUser): @property def guardians(self): - return self.contacts.filter(is_guardian=True) \ - .annotate(pk=models.F('contact__pk'), username=models.F('contact__username')) \ - .values('pk', 'username') + return self.contacts.filter(is_guardian=True).annotate(pk=models.F('contact__pk'), username=models.F('contact__username')).values('pk', 'username') def get_staticpage_groups(self, conference): """Fetches the ConferenceMember's static_page_groups.""" @@ -401,39 +350,28 @@ class PlatformUser(AbstractUser): ).exists() def get_verified_mail_addresses(self): - return list(self.communication_channels.filter( - channel=UserCommunicationChannel.Channel.MAIL, - is_verified=True, - ).values_list('address', flat=True)) + return list( + self.communication_channels.filter( + channel=UserCommunicationChannel.Channel.MAIL, + is_verified=True, + ).values_list('address', flat=True) + ) class UserContact(models.Model): user = models.ForeignKey(PlatformUser, related_name='contacts', on_delete=models.CASCADE) contact = models.ForeignKey(PlatformUser, related_name='+', on_delete=models.CASCADE) - pending = models.BooleanField( - default=True, - help_text=_('UserContact__pending__help'), - verbose_name=_('UserContact__pending')) + pending = models.BooleanField(default=True, help_text=_('UserContact__pending__help'), verbose_name=_('UserContact__pending')) - share_status = models.BooleanField( - null=True, - help_text=_('UserContact__share_status__help'), - verbose_name=_('UserContact__share_status')) + share_status = models.BooleanField(null=True, help_text=_('UserContact__share_status__help'), verbose_name=_('UserContact__share_status')) - receive_dms = models.BooleanField( - default=True, - help_text=_('UserContact__receive_dms__help'), - verbose_name=_('UserContact__receive_dms')) + receive_dms = models.BooleanField(default=True, help_text=_('UserContact__receive_dms__help'), verbose_name=_('UserContact__receive_dms')) receive_dm_images = models.BooleanField( - default=False, - help_text=_('UserContact__receive_dm_images__help'), - verbose_name=_('UserContact__receive_dm_images')) + default=False, help_text=_('UserContact__receive_dm_images__help'), verbose_name=_('UserContact__receive_dm_images') + ) - is_guardian = models.BooleanField( - default=False, - help_text=_('UserContact__is_guardian__help'), - verbose_name=_('UserContact__is_guardian')) + is_guardian = models.BooleanField(default=False, help_text=_('UserContact__is_guardian__help'), verbose_name=_('UserContact__is_guardian')) @cached_property def reverse_contact(self): @@ -454,9 +392,7 @@ _RE_VANITY = re.compile(r'^(00|\+)49[- ]?(180|[789]00)[- ]?[A-Za-z0-9 -]+$') class UserCommunicationChannel(models.Model): class Meta: - constraints = [ - models.UniqueConstraint(fields=['user', 'channel', 'is_primary'], condition=Q(is_primary=True), name='unique_channel_primary') - ] + constraints = [models.UniqueConstraint(fields=['user', 'channel', 'is_primary'], condition=Q(is_primary=True), name='unique_channel_primary')] class Channel(models.TextChoices): MAIL = 'mail', _('UserCommunicationChannel__channel-mail') @@ -469,31 +405,23 @@ class UserCommunicationChannel(models.Model): user = models.ForeignKey(PlatformUser, related_name='communication_channels', on_delete=models.CASCADE) channel = models.CharField( - max_length=20, choices=Channel.choices, - help_text=_('UserCommunicationChannel__channel__help'), - verbose_name=_('UserCommunicationChannel__channel')) - address = models.CharField( - max_length=255, - help_text=_('UserCommunicationChannel__address__help'), - verbose_name=_('UserCommunicationChannel__address')) + max_length=20, choices=Channel.choices, help_text=_('UserCommunicationChannel__channel__help'), verbose_name=_('UserCommunicationChannel__channel') + ) + address = models.CharField(max_length=255, help_text=_('UserCommunicationChannel__address__help'), verbose_name=_('UserCommunicationChannel__address')) is_verified = models.BooleanField( - default=False, - help_text=_('UserCommunicationChannel__is_verified__help'), - verbose_name=_('UserCommunicationChannel__is_verified')) + default=False, help_text=_('UserCommunicationChannel__is_verified__help'), verbose_name=_('UserCommunicationChannel__is_verified') + ) is_primary = models.BooleanField( - default=False, - help_text=_('UserCommunicationChannel__is_primary__help'), - verbose_name=_('UserCommunicationChannel__is_primary')) + default=False, help_text=_('UserCommunicationChannel__is_primary__help'), verbose_name=_('UserCommunicationChannel__is_primary') + ) use_for_notifications = models.BooleanField( - default=False, - help_text=_('UserCommunicationChannel__use_for_notifications__help'), - verbose_name=_('UserCommunicationChannel__use_for_notifications')) + default=False, help_text=_('UserCommunicationChannel__use_for_notifications__help'), verbose_name=_('UserCommunicationChannel__use_for_notifications') + ) show_public = models.BooleanField( - default=False, - help_text=_('UserCommunicationChannel__show_public__help'), - verbose_name=_('UserCommunicationChannel__show_public')) + default=False, help_text=_('UserCommunicationChannel__show_public__help'), verbose_name=_('UserCommunicationChannel__show_public') + ) _logger = logging.getLogger(__name__) @@ -584,7 +512,7 @@ class UserCommunicationChannel(models.Model): ) return False - raise NotImplementedError(f'Don\'t know how to send to a channel of type {self.channel}') + raise NotImplementedError(f"Don't know how to send to a channel of type {self.channel}") class UserTimelineEntry(models.Model): @@ -593,22 +521,13 @@ class UserTimelineEntry(models.Model): user = models.ForeignKey(PlatformUser, related_name='timeline', on_delete=models.CASCADE) conference = ConferenceReference(related_name='+') - timestamp = models.DateTimeField( - default=now, - help_text=_('UserTimelineEntry__timestamp__help'), - verbose_name=_('UserTimelineEntry__timestamp')) + timestamp = models.DateTimeField(default=now, help_text=_('UserTimelineEntry__timestamp__help'), verbose_name=_('UserTimelineEntry__timestamp')) target_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) target_id = models.UUIDField() target = GenericForeignKey('target_type', 'target_id') - is_bookmark = models.BooleanField( - default=False, - help_text=_('UserTimelineEntry__is_bookmark__help'), - verbose_name=_('UserTimelineEntry__is_bookmark')) + is_bookmark = models.BooleanField(default=False, help_text=_('UserTimelineEntry__is_bookmark__help'), verbose_name=_('UserTimelineEntry__is_bookmark')) """Indicates a timeline entry 'saved for later'.""" - comment = models.TextField( - blank=True, null=True, - help_text=_('UserTimelineEntry__comment__help'), - verbose_name=_('UserTimelineEntry__comment')) + comment = models.TextField(blank=True, null=True, help_text=_('UserTimelineEntry__comment__help'), verbose_name=_('UserTimelineEntry__comment')) diff --git a/src/core/models/voucher.py b/src/core/models/voucher.py index 1f1ae163a..835d04b33 100644 --- a/src/core/models/voucher.py +++ b/src/core/models/voucher.py @@ -85,27 +85,15 @@ class Voucher(models.Model): id = models.UUIDField(default=uuid4, primary_key=True, editable=False) conference = ConferenceReference(related_name='vouchers') - name = models.CharField( - max_length=200, - help_text=_('Voucher__name__help'), - verbose_name=_('Voucher__name')) - - enabled = models.BooleanField( - default=False, - help_text=_('Voucher__enabled__help'), - verbose_name=_('Voucher__enabled')) + name = models.CharField(max_length=200, help_text=_('Voucher__name__help'), verbose_name=_('Voucher__name')) + + enabled = models.BooleanField(default=False, help_text=_('Voucher__enabled__help'), verbose_name=_('Voucher__enabled')) """Mark the voucher as being available. This is intended to be used as a 'draft' marker.""" - show_always = models.BooleanField( - default=False, - help_text=_('Voucher__show_always__help'), - verbose_name=_('Voucher__show_always')) + show_always = models.BooleanField(default=False, help_text=_('Voucher__show_always__help'), verbose_name=_('Voucher__show_always')) """Show the voucher in any case for a target, even if it hasn't been assigned yet.""" - hide_from_staff = models.BooleanField( - default=False, - help_text=_('Voucher__hide_from_staff__help'), - verbose_name=_('Voucher__hide_from_staff')) + hide_from_staff = models.BooleanField(default=False, help_text=_('Voucher__hide_from_staff__help'), verbose_name=_('Voucher__hide_from_staff')) """Hide the values even from staff (unless they are Voucher Admin).""" assignment = models.CharField( @@ -114,27 +102,16 @@ class Voucher(models.Model): choices=Assignment.choices, default=Assignment.MANUAL, help_text=_('Voucher__assignment__help'), - verbose_name=_('Voucher__assignment')) + verbose_name=_('Voucher__assignment'), + ) data = models.CharField( - max_length=20, - blank=False, - choices=Data.choices, - default=Data.TEXT, - help_text=_('Voucher__data__help'), - verbose_name=_('Voucher__data')) + max_length=20, blank=False, choices=Data.choices, default=Data.TEXT, help_text=_('Voucher__data__help'), verbose_name=_('Voucher__data') + ) - target = models.CharField( - max_length=20, - blank=False, - choices=Target.choices, - help_text=_('Voucher__target__help'), - verbose_name=_('Voucher__target')) + target = models.CharField(max_length=20, blank=False, choices=Target.choices, help_text=_('Voucher__target__help'), verbose_name=_('Voucher__target')) - description = models.TextField( - blank=True, null=True, - help_text=_('Voucher__description__help'), - verbose_name=_('Voucher__description')) + description = models.TextField(blank=True, null=True, help_text=_('Voucher__description__help'), verbose_name=_('Voucher__description')) objects = VoucherManager() logger = logging.getLogger(__name__) @@ -201,8 +178,9 @@ class Voucher(models.Model): for assembly in missing_assignments: # skip non-public assembly/channel if auto-assignment is for public ones only if self.assignment == self.Assignment.ON_PUBLIC: - if (self.target == self.Target.ASSEMBLY and not assembly.is_public_assembly) or \ - (self.target == self.Target.CHANNEL and not assembly.is_public_channel): + if (self.target == self.Target.ASSEMBLY and not assembly.is_public_assembly) or ( + self.target == self.Target.CHANNEL and not assembly.is_public_channel + ): continue # abort (but warn) if no more available entries are available @@ -222,23 +200,11 @@ class Voucher(models.Model): class VoucherEntry(models.Model): voucher = models.ForeignKey(Voucher, on_delete=models.CASCADE, related_name='entries') - content = models.TextField( - blank=True, null=True, - help_text=_('VoucherEntry__content__help'), - verbose_name=_('VoucherEntry__content')) - - created = models.DateTimeField( - auto_now_add=True, - help_text=_('VoucherEntry__created__help'), - verbose_name=_('VoucherEntry__created')) - expires = models.DateTimeField( - blank=True, null=True, - help_text=_('VoucherEntry__expires__help'), - verbose_name=_('VoucherEntry__expires')) - assigned = models.DateTimeField( - blank=True, null=True, - help_text=_('VoucherEntry__assigned__help'), - verbose_name=_('VoucherEntry__assigned')) + content = models.TextField(blank=True, null=True, help_text=_('VoucherEntry__content__help'), verbose_name=_('VoucherEntry__content')) + + created = models.DateTimeField(auto_now_add=True, help_text=_('VoucherEntry__created__help'), verbose_name=_('VoucherEntry__created')) + expires = models.DateTimeField(blank=True, null=True, help_text=_('VoucherEntry__expires__help'), verbose_name=_('VoucherEntry__expires')) + assigned = models.DateTimeField(blank=True, null=True, help_text=_('VoucherEntry__assigned__help'), verbose_name=_('VoucherEntry__assigned')) assigned_assembly = models.ForeignKey(Assembly, on_delete=models.SET_NULL, related_name='vouchers', blank=True, null=True) assigned_user = models.ForeignKey(PlatformUser, on_delete=models.SET_NULL, related_name='vouchers', blank=True, null=True) diff --git a/src/core/models/workadventure.py b/src/core/models/workadventure.py index f50e1b52a..5c9fc1d7a 100644 --- a/src/core/models/workadventure.py +++ b/src/core/models/workadventure.py @@ -1,5 +1,6 @@ from datetime import timedelta from uuid import uuid4 + from django.conf import settings from django.contrib.auth.models import Group from django.contrib.postgres.fields import ArrayField @@ -9,11 +10,11 @@ from django.db.models import Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from ..fields import ConferenceReference +from ..utils import generate_token from .assemblies import Assembly from .conference import Conference, ConferenceMember, PlatformUser from .rooms import Room -from ..fields import ConferenceReference -from ..utils import generate_token class WorkadventureSession(models.Model): @@ -30,13 +31,15 @@ class WorkadventureSession(models.Model): ) token = models.CharField( - blank=True, null=True, + blank=True, + null=True, max_length=100, help_text=_('WorkadventureSession__token__help'), verbose_name=_('WorkadventureSession__token'), ) token_expiry = models.DateTimeField( - blank=True, null=True, + blank=True, + null=True, help_text=_('WorkadventureSession__token_expiry__help'), verbose_name=_('WorkadventureSession__token_expiry'), ) @@ -44,7 +47,8 @@ class WorkadventureSession(models.Model): room = models.ForeignKey( Room, on_delete=models.SET_NULL, - blank=True, null=True, + blank=True, + null=True, related_name='+', help_text=_('WorkadventureSession__room__help'), verbose_name=_('WorkadventureSession__room'), @@ -57,7 +61,8 @@ class WorkadventureSession(models.Model): default=list, ) additional_data = models.JSONField( - blank=True, null=True, + blank=True, + null=True, help_text=_('WorkadventureSession__additional_data__help'), verbose_name=_('WorkadventureSession__additional_data'), ) @@ -107,10 +112,9 @@ class WorkadventureSession(models.Model): user_tags.add('admin') if is_angel: user_tags.add('angel') - for slug in Assembly.objects \ - .associated_to_user(self.user, self.conference) \ - .filter(state_assembly__in=Assembly.PUBLIC_STATES) \ - .values_list('slug', flat=True): + for slug in ( + Assembly.objects.associated_to_user(self.user, self.conference).filter(state_assembly__in=Assembly.PUBLIC_STATES).values_list('slug', flat=True) + ): user_tags.add('assembly_' + slug) result = { @@ -143,9 +147,9 @@ class WorkadventureSession(models.Model): # TODO: fehlt hier nicht ein "banned" flag? was ist mit den restlichen Flags? # TODO: expand textures and character layers # TODO: bei check-user tauchen folgende Zusatzfelder auf: - # mapUrlStart: string; + # mapUrlStart: string; # noqa: ERA001 # tags: string[]; - # policy_type: number; + # policy_type: number; # noqa: ERA001 return result def update_userdata(self, data): diff --git a/src/core/schedules/__init__.py b/src/core/schedules/__init__.py index f47a72e9b..4f08b004b 100644 --- a/src/core/schedules/__init__.py +++ b/src/core/schedules/__init__.py @@ -1,6 +1,5 @@ from .base import BaseScheduleSupport, ScheduleTypeManager - __all__ = [ 'BaseScheduleSupport', 'ScheduleTypeManager', diff --git a/src/core/schedules/base.py b/src/core/schedules/base.py index eb2751279..6d8cd6ce0 100644 --- a/src/core/schedules/base.py +++ b/src/core/schedules/base.py @@ -1,6 +1,6 @@ +import logging from abc import ABCMeta, abstractmethod from datetime import timedelta -import logging from django.conf import settings from django.utils import timezone @@ -8,9 +8,7 @@ from django.utils.module_loading import import_string def filter_additional_data(data: dict) -> dict: - return {k: v for k, v in data.items() if (v and k not in [ - 'guid', 'room', 'start', 'date', 'duration', 'title', 'abstract', 'description', 'language' - ])} + return {k: v for k, v in data.items() if (v and k not in ['guid', 'room', 'start', 'date', 'duration', 'title', 'abstract', 'description', 'language'])} def schedule_time_to_timedelta(s: str) -> timedelta: @@ -135,7 +133,7 @@ class BaseScheduleSupport(metaclass=ABCMeta): raise NotImplementedError('push(data) was not overriden for ScheduleSupport') -class _ScheduleTypeManager(object): +class _ScheduleTypeManager: def __init__(self): self.types = {} self._initialize_from_settings() diff --git a/src/core/schedules/schedulejson.py b/src/core/schedules/schedulejson.py index d75d9127d..72c6e1584 100644 --- a/src/core/schedules/schedulejson.py +++ b/src/core/schedules/schedulejson.py @@ -1,5 +1,6 @@ -from collections import OrderedDict import json +from collections import OrderedDict + import requests from requests_file import FileAdapter @@ -37,32 +38,32 @@ class ScheduleJSONSupport(BaseScheduleSupport): schedule = ScheduleJSON.from_url(self.remote_url) return { - "rooms": { - r['name']: r for r in schedule.rooms() - }, - "events": { + 'rooms': {r['name']: r for r in schedule.rooms()}, + 'events': { e.get('id'): { - "guid": e.get('guid'), - "slug": e.get('slug').split(f"{e.get('id')}-")[1][0:150].strip('-') or e.get('slug')[0:150].strip('-'), - "name": e.get('title'), - "language": e.get('language'), - "abstract": e.get('abstract') or '', - "description": e.get('description') or '', - "track": e.get('track'), - "room": e.get('room'), - "schedule_start": e.get('date'), - "schedule_duration": str(schedule_time_to_timedelta(e.get('duration'))), - "is_public": True, - "additional_data": filter_additional_data(e) - } for e in schedule.events() - } + 'guid': e.get('guid'), + 'slug': e.get('slug').split(f"{e.get('id')}-")[1][0:150].strip('-') or e.get('slug')[0:150].strip('-'), + 'name': e.get('title'), + 'language': e.get('language'), + 'abstract': e.get('abstract') or '', + 'description': e.get('description') or '', + 'track': e.get('track'), + 'room': e.get('room'), + 'schedule_start': e.get('date'), + 'schedule_duration': str(schedule_time_to_timedelta(e.get('duration'))), + 'is_public': True, + 'additional_data': filter_additional_data(e), + } + for e in schedule.events() + }, } class ScheduleJSON: - ''' + """ Schedule from JSON document - ''' + """ + _schedule = None def __init__(self, json): @@ -73,7 +74,7 @@ class ScheduleJSON: r = s.get(url) if r.ok is False: - raise Exception('Request failed, HTTP {0}.'.format(r.status_code)) + raise Exception(f'Request failed, HTTP {r.status_code}.') # maintain order from input file schedule = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(r.text) @@ -104,13 +105,12 @@ class ScheduleJSON: for day in self.days(): for roomname in day.get('rooms'): rooms.add(roomname) - return [{"name": name} for name in rooms] + return [{'name': name} for name in rooms] def events(self): for day in self.days(): for room in day.get('rooms'): - for event in day.get('rooms')[room]: - yield event + yield from day.get('rooms')[room] def __str__(self): return json.dumps(self._schedule, indent=2) diff --git a/src/core/schedules/schedulexml.py b/src/core/schedules/schedulexml.py index b38afac3b..679adfa6f 100644 --- a/src/core/schedules/schedulexml.py +++ b/src/core/schedules/schedulexml.py @@ -1,12 +1,13 @@ import collections import datetime -from django.utils.dateparse import parse_datetime +from xml.etree import ElementTree + import requests from requests_file import FileAdapter -from xml.etree import ElementTree -from .base import BaseScheduleSupport, filter_additional_data, schedule_time_to_timedelta +from django.utils.dateparse import parse_datetime +from .base import BaseScheduleSupport, filter_additional_data, schedule_time_to_timedelta s = requests.Session() s.mount('file://', FileAdapter()) @@ -40,34 +41,34 @@ class ScheduleXMLSupport(BaseScheduleSupport): schedule = ScheduleXML.from_url(self.remote_url) return { - "rooms": { - r['name']: r for r in schedule.rooms() - }, - "events": { + 'rooms': {r['name']: r for r in schedule.rooms()}, + 'events': { e.get('id'): { - "guid": e.get('guid'), - "slug": e.get('slug').split(f"{e.get('id')}-")[1][0:150].strip('-') or e.get('slug')[0:150].strip('-'), - "name": e.get('title'), - "language": e.get('language'), - "abstract": e.get('abstract') or '', - "description": e.get('description') or '', - "track": e.get('track'), - "room": e.get('room'), - "schedule_start": e.get('date'), - "schedule_duration": str(schedule_time_to_timedelta(e.get('duration'))), - "is_public": True, - "additional_data": filter_additional_data(e) - } for e in schedule.events() - } + 'guid': e.get('guid'), + 'slug': e.get('slug').split(f"{e.get('id')}-")[1][0:150].strip('-') or e.get('slug')[0:150].strip('-'), + 'name': e.get('title'), + 'language': e.get('language'), + 'abstract': e.get('abstract') or '', + 'description': e.get('description') or '', + 'track': e.get('track'), + 'room': e.get('room'), + 'schedule_start': e.get('date'), + 'schedule_duration': str(schedule_time_to_timedelta(e.get('duration'))), + 'is_public': True, + 'additional_data': filter_additional_data(e), + } + for e in schedule.events() + }, } class ScheduleXML: - ''' + """ Schedule from XML document using etree, with inspirations from - https://github.com/pretalx/pretalx-downstream/blob/master/pretalx_downstream/tasks.py#L67 - https://github.com/Zverik/schedule-convert/blob/master/schedule_convert/importers/frab_xml.py#L55 - ''' + """ + _schedule: ElementTree = None tz = None @@ -79,7 +80,7 @@ class ScheduleXML: r = s.get(url) if r.ok is False: - raise Exception('Request failed, HTTP {0}.'.format(r.status_code)) + raise Exception(f'Request failed, HTTP {r.status_code}.') schedule = ElementTree.fromstring(r.text) return ScheduleXML(tree=schedule) diff --git a/src/core/search.py b/src/core/search.py index 25062358d..baf83f2c1 100644 --- a/src/core/search.py +++ b/src/core/search.py @@ -10,8 +10,9 @@ from .models.tags import ConferenceTag from .models.users import PlatformUser -def search(user: PlatformUser, conference: Conference, search_term: str, max_per_category: int = 10) \ - -> Iterator[Union[ConferenceTag, ConferenceTrack, Assembly, Event]]: +def search( + user: PlatformUser, conference: Conference, search_term: str, max_per_category: int = 10 +) -> Iterator[Union[ConferenceTag, ConferenceTrack, Assembly, Event]]: """ Search assemblies, events, pages and tags for the search_term(s). Matches on the name are ranked higher than those only in the description. @@ -47,8 +48,7 @@ def search(user: PlatformUser, conference: Conference, search_term: str, max_per if len(exclude_terms) > 0: for term in exclude_terms: tags = tags.exclude(slug__icontains=term) - for tag in tags[:max_per_category]: - yield tag + yield from tags[:max_per_category] # matching tracks are good, too tracks = conference.tracks.filter(is_public=True) @@ -56,13 +56,11 @@ def search(user: PlatformUser, conference: Conference, search_term: str, max_per tracks = tracks.filter(name__icontains=term) for term in exclude_terms: tracks = tracks.exclude(name__icontains=term) - for track in tracks[:max_per_category]: - yield track + yield from tracks[:max_per_category] # search static pages pages = conference.pages.filter(public_revision__gte=0) - for page in pages.filter(search_vector=search_q): - yield page + yield from pages.filter(search_vector=search_q) # tracking which assemblies we've already seen so we don't return them twice assemblies_seen = [] @@ -83,11 +81,9 @@ def search(user: PlatformUser, conference: Conference, search_term: str, max_per # description match on Assembly if len(assemblies) < max_per_category: description_matches = Assembly.objects.conference_accessible(conference=conference).exclude(pk__in=assemblies_seen) - for assembly in apply_terms_description(description_matches)[:max_per_category - len(assemblies)]: - yield assembly + yield from apply_terms_description(description_matches)[: max_per_category - len(assemblies)] # description match on Event if len(events) < max_per_category: description_matches = Event.objects.conference_accessible(conference=conference).exclude(pk__in=events_seen) - for event in apply_terms_description(description_matches)[:max_per_category - len(events)]: - yield event + yield from apply_terms_description(description_matches)[: max_per_category - len(events)] diff --git a/src/core/sso.py b/src/core/sso.py index 36b8d1a3d..1a70b5a9e 100644 --- a/src/core/sso.py +++ b/src/core/sso.py @@ -1,20 +1,20 @@ -from functools import lru_cache import logging +from functools import lru_cache + +import jwt +from oauth2_provider.scopes import BaseScopes from django.conf import settings from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ -import jwt -from oauth2_provider.scopes import BaseScopes from .models.conference import Conference from .models.users import PlatformUser - logger = logging.getLogger(__name__) -class SingleSignOn(object): +class SingleSignOn: def __init__(self): self._secret = settings.SSO_SECRET settings.SSO_SECRET = 'removed' diff --git a/src/core/tests/__init__.py b/src/core/tests/__init__.py index 851874ec7..da0a30278 100644 --- a/src/core/tests/__init__.py +++ b/src/core/tests/__init__.py @@ -4,14 +4,14 @@ from .bigbluebutton import * # noqa: F401, F403 from .conference import * # noqa: F401, F403 from .events import * # noqa: F401, F403 from .exportcache import * # noqa: F401, F403 +from .markdown import * # noqa: F401, F403 +from .schedules import * # noqa: F401, F403 from .search import * # noqa: F401, F403 -from .users import * # noqa: F401, F403 from .tags import * # noqa: F401, F403 from .tickets import * # noqa: F401, F403 -from .markdown import * # noqa: F401, F403 -from .schedules import * # noqa: F401, F403 +from .users import * # noqa: F401, F403 from .utils import * # noqa: F401, F403 from .validators import * # noqa: F401, F403 from .workadventure import * # noqa: F401, F403 -__all__ = '*' +__all__ = ('*',) # noqa: F405 diff --git a/src/core/tests/assemblies.py b/src/core/tests/assemblies.py index dfc6ebbac..faff811ac 100644 --- a/src/core/tests/assemblies.py +++ b/src/core/tests/assemblies.py @@ -14,30 +14,40 @@ class AssembliesTests(TestCase): self.user_mgmt = PlatformUser(username='manager') self.user_mgmt.save() self.user_mgmt.communication_channels.create( - channel=UserCommunicationChannel.Channel.MAIL, address='manager@unittest.local', - is_verified=True, use_for_notifications=False, + channel=UserCommunicationChannel.Channel.MAIL, + address='manager@unittest.local', + is_verified=True, + use_for_notifications=False, ) self.user_mgmt.communication_channels.create( - channel=UserCommunicationChannel.Channel.MAIL, address='notifications@unittest.local', - is_verified=True, use_for_notifications=True, + channel=UserCommunicationChannel.Channel.MAIL, + address='notifications@unittest.local', + is_verified=True, + use_for_notifications=True, ) self.user_mgmt2 = PlatformUser(username='manager2') self.user_mgmt2.save() self.user_mgmt2.communication_channels.create( - channel=UserCommunicationChannel.Channel.MAIL, address='notifications2@unittest.local', - is_verified=True, use_for_notifications=True, + channel=UserCommunicationChannel.Channel.MAIL, + address='notifications2@unittest.local', + is_verified=True, + use_for_notifications=True, ) self.user_participant = PlatformUser(username='participant') self.user_participant.save() self.user_participant.communication_channels.create( - channel=UserCommunicationChannel.Channel.MAIL, address='participant@unittest.local', - is_verified=True, use_for_notifications=False, + channel=UserCommunicationChannel.Channel.MAIL, + address='participant@unittest.local', + is_verified=True, + use_for_notifications=False, ) self.user_visitor = PlatformUser(username='visitor') self.user_visitor.save() self.user_visitor.communication_channels.create( - channel=UserCommunicationChannel.Channel.MAIL, address='visitor@unittest.local', - is_verified=True, use_for_notifications=False, + channel=UserCommunicationChannel.Channel.MAIL, + address='visitor@unittest.local', + is_verified=True, + use_for_notifications=False, ) ConferenceMember(conference=self.conference, user=self.user_mgmt).save() ConferenceMember(conference=self.conference, user=self.user_mgmt2).save() @@ -78,8 +88,7 @@ class AssembliesTests(TestCase): def test_mail_htmlwithlinks(self): self.assembly1.send_mail_to_managers( subject='Hello World', - message='Please check [the blog](https://events.ccc.de/) and do stuff.\n' - 'And don\'t forget to visit [events.ccc.de](https://events.ccc.de/)!', + message='Please check [the blog](https://events.ccc.de/) and do stuff.\n' "And don't forget to visit [events.ccc.de](https://events.ccc.de/)!", ) self.assertEqual(len(mail.outbox), 2) diff --git a/src/core/tests/badges.py b/src/core/tests/badges.py index 1abb9572f..7eb3f9ce1 100644 --- a/src/core/tests/badges.py +++ b/src/core/tests/badges.py @@ -2,9 +2,9 @@ from django.core.exceptions import ValidationError from django.test import TestCase from ..models.assemblies import Assembly +from ..models.badges import Badge, UserBadge from ..models.conference import Conference, ConferenceMember from ..models.users import PlatformUser -from ..models.badges import Badge, UserBadge class BadgeTests(TestCase): diff --git a/src/core/tests/bigbluebutton.py b/src/core/tests/bigbluebutton.py index fd5e5dec2..4cb0f65c1 100644 --- a/src/core/tests/bigbluebutton.py +++ b/src/core/tests/bigbluebutton.py @@ -1,15 +1,15 @@ -from django.test import TestCase from unittest.mock import Mock, patch +from django.test import TestCase + from core.integrations import BigBlueButtonIntegration, IntegrationError from core.models import Assembly, Conference, PlatformUser, Room - BigBlueButton = BigBlueButtonIntegration('http://localhost/', 'asdf', 'https://localhost/end_meeting', None) # from https://github.com/Grollicus/unittest_patterns/blob/master/unittest_patterns/__init__.py -class Pattern(object): +class Pattern: def __req__(self, lhs): return self.__eq__(lhs) @@ -18,7 +18,7 @@ class Pattern(object): # from https://github.com/Grollicus/unittest_patterns/blob/master/unittest_patterns/__init__.py class Any(Pattern): - """ Equals everything """ + """Equals everything""" def __eq__(self, rhs): return True @@ -31,8 +31,7 @@ class BigBlueButtonTest(TestCase): self.assembly = Assembly(name='TestAssembly', slug='asmbly', conference=self.conf) self.assembly.save() self.room = Room( - conference=self.conf, assembly=self.assembly, name='Room 1', - room_type=Room.RoomType.BIGBLUEBUTTON, backend_status=Room.BackendStatus.NEW + conference=self.conf, assembly=self.assembly, name='Room 1', room_type=Room.RoomType.BIGBLUEBUTTON, backend_status=Room.BackendStatus.NEW ) self.room.save() @@ -47,23 +46,23 @@ class BigBlueButtonTest(TestCase): get_mock.return_value.status_code = 200 get_mock.return_value.text = ( - '<response>' - '<returncode>SUCCESS</returncode>' - '<meetingID>Test</meetingID>' - '<internalMeetingID>640ab2bae07bedc4c163f679a746f7ab7fb5d1fa-1531155809613</internalMeetingID>' - '<parentMeetingID>bbb-none</parentMeetingID>' - '<attendeePW>ap</attendeePW>' - '<moderatorPW>mp</moderatorPW>' - '<createTime>1531155809613</createTime>' - '<voiceBridge>70757</voiceBridge>' - '<dialNumber>613-555-1234</dialNumber>' - '<createDate>Mon Jul 09 17:03:29 UTC 2018</createDate>' - '<hasUserJoined>false</hasUserJoined>' - '<duration>0</duration>' - '<hasBeenForciblyEnded>false</hasBeenForciblyEnded>' - '<messageKey>duplicateWarning</messageKey>' - '<message>This conference was already in existence and may currently be in progress.</message>' - '</response>' + '<response>' + '<returncode>SUCCESS</returncode>' + '<meetingID>Test</meetingID>' + '<internalMeetingID>640ab2bae07bedc4c163f679a746f7ab7fb5d1fa-1531155809613</internalMeetingID>' + '<parentMeetingID>bbb-none</parentMeetingID>' + '<attendeePW>ap</attendeePW>' + '<moderatorPW>mp</moderatorPW>' + '<createTime>1531155809613</createTime>' + '<voiceBridge>70757</voiceBridge>' + '<dialNumber>613-555-1234</dialNumber>' + '<createDate>Mon Jul 09 17:03:29 UTC 2018</createDate>' + '<hasUserJoined>false</hasUserJoined>' + '<duration>0</duration>' + '<hasBeenForciblyEnded>false</hasBeenForciblyEnded>' + '<messageKey>duplicateWarning</messageKey>' + '<message>This conference was already in existence and may currently be in progress.</message>' + '</response>' ) self.room.backend_status = Room.BackendStatus.NEW self.room.save() @@ -72,12 +71,15 @@ class BigBlueButtonTest(TestCase): get_mock.assert_called_once() self.assertEqual(self.room.backend_status, Room.BackendStatus.ACTIVE) self.assertEqual(self.room.backend_link, 'Test') - self.assertEqual(self.room.backend_data, { - 'attendeePW': 'ap', - 'moderatorPW': 'mp', - 'createTime': '1531155809613', - 'close_secret': Any(), - }) + self.assertEqual( + self.room.backend_data, + { + 'attendeePW': 'ap', + 'moderatorPW': 'mp', + 'createTime': '1531155809613', + 'close_secret': Any(), + }, + ) def test_remove_room(self): with patch.object(BigBlueButton._session, 'get') as get_mock: @@ -202,35 +204,39 @@ class BigBlueButtonTest(TestCase): with patch.object(BigBlueButton._session, 'get') as get_mock, self.assertLogs('core.integrations.bigbluebutton'): get_mock.side_effect = [ - Mock(status_code=200, text=( - '<response>' - '<returncode>FAILED</returncode>' - '<messageKey>notFound</messageKey>' - '<message>We could not find a meeting with that meeting ID</message>' - '</response>' - )), - Mock(status_code=200, text=( - '<response>' - '<returncode>SUCCESS</returncode>' - '<meetingID>Test</meetingID>' - '<internalMeetingID>640ab2bae07bedc4c163f679a746f7ab7fb5d1fa-1531155809613</internalMeetingID>' - '<parentMeetingID>bbb-none</parentMeetingID>' - '<attendeePW>ap</attendeePW>' - '<moderatorPW>mp</moderatorPW>' - '<createTime>1531155809613</createTime>' - '<voiceBridge>70757</voiceBridge>' - '<dialNumber>613-555-1234</dialNumber>' - '<createDate>Mon Jul 09 17:03:29 UTC 2018</createDate>' - '<hasUserJoined>false</hasUserJoined>' - '<duration>0</duration>' - '<hasBeenForciblyEnded>false</hasBeenForciblyEnded>' - '<messageKey>duplicateWarning</messageKey>' - '<message>This conference was already in existence and may currently be in progress.</message>' - '</response>' - )), - Mock(status_code=302, headers={ - 'Location': 'https://yourserver.com/client/BigBlueButton.html?sessionToken=ai1wqj8wb6s7rnk0' - }), + Mock( + status_code=200, + text=( + '<response>' + '<returncode>FAILED</returncode>' + '<messageKey>notFound</messageKey>' + '<message>We could not find a meeting with that meeting ID</message>' + '</response>' + ), + ), + Mock( + status_code=200, + text=( + '<response>' + '<returncode>SUCCESS</returncode>' + '<meetingID>Test</meetingID>' + '<internalMeetingID>640ab2bae07bedc4c163f679a746f7ab7fb5d1fa-1531155809613</internalMeetingID>' + '<parentMeetingID>bbb-none</parentMeetingID>' + '<attendeePW>ap</attendeePW>' + '<moderatorPW>mp</moderatorPW>' + '<createTime>1531155809613</createTime>' + '<voiceBridge>70757</voiceBridge>' + '<dialNumber>613-555-1234</dialNumber>' + '<createDate>Mon Jul 09 17:03:29 UTC 2018</createDate>' + '<hasUserJoined>false</hasUserJoined>' + '<duration>0</duration>' + '<hasBeenForciblyEnded>false</hasBeenForciblyEnded>' + '<messageKey>duplicateWarning</messageKey>' + '<message>This conference was already in existence and may currently be in progress.</message>' + '</response>' + ), + ), + Mock(status_code=302, headers={'Location': 'https://yourserver.com/client/BigBlueButton.html?sessionToken=ai1wqj8wb6s7rnk0'}), ] url = BigBlueButton.join_room(self.room, user) self.assertEqual(url, 'https://yourserver.com/client/BigBlueButton.html?sessionToken=ai1wqj8wb6s7rnk0') diff --git a/src/core/tests/events.py b/src/core/tests/events.py index d5efcc079..771032316 100644 --- a/src/core/tests/events.py +++ b/src/core/tests/events.py @@ -74,7 +74,7 @@ class EventTests(TestCase): with self.assertRaises(ValidationError) as cm: event = Event(**event_data) event.full_clean() - self.assertTrue("assembly" in cm.exception.error_dict) + self.assertTrue('assembly' in cm.exception.error_dict) def test_error_negative_duration(self): event_data = self.valid_event.copy() @@ -82,7 +82,7 @@ class EventTests(TestCase): with self.assertRaises(ValidationError) as cm: event = Event(**event_data) event.full_clean() - self.assertTrue("schedule_duration" in cm.exception.error_dict) + self.assertTrue('schedule_duration' in cm.exception.error_dict) def test_error_zero_duration(self): event_data = self.valid_event.copy() @@ -90,7 +90,7 @@ class EventTests(TestCase): with self.assertRaises(ValidationError) as cm: event = Event(**event_data) event.full_clean() - self.assertTrue("schedule_duration" in cm.exception.error_dict) + self.assertTrue('schedule_duration' in cm.exception.error_dict) def test_error_end_after_conference(self): event_data = self.valid_event.copy() @@ -98,7 +98,7 @@ class EventTests(TestCase): with self.assertRaises(ValidationError) as cm: event = Event(**event_data) event.full_clean() - self.assertTrue("schedule_duration" in cm.exception.error_dict) + self.assertTrue('schedule_duration' in cm.exception.error_dict) def test_error_start_before_conference(self): event_data = self.valid_event.copy() @@ -106,7 +106,7 @@ class EventTests(TestCase): with self.assertRaises(ValidationError) as cm: event = Event(**event_data) event.full_clean() - self.assertTrue("schedule_start" in cm.exception.error_dict) + self.assertTrue('schedule_start' in cm.exception.error_dict) def test_no_sos_assembly(self): event_data = self.valid_event.copy() diff --git a/src/core/tests/exportcache.py b/src/core/tests/exportcache.py index 9ea5bdb06..16f87c1ef 100644 --- a/src/core/tests/exportcache.py +++ b/src/core/tests/exportcache.py @@ -1,9 +1,9 @@ from datetime import timedelta +from django.test import TestCase, override_settings from django.utils import timezone from django.utils.datetime_safe import datetime from django.utils.timezone import now -from django.test import TestCase, override_settings from ..models.assemblies import Assembly from ..models.conference import Conference, ConferenceExportCache diff --git a/src/core/tests/markdown.py b/src/core/tests/markdown.py index e313cb67d..90e781cbc 100644 --- a/src/core/tests/markdown.py +++ b/src/core/tests/markdown.py @@ -17,7 +17,6 @@ TEST_CONF_ID = uuid.uuid4() @override_settings(ALLOWED_HOSTS=['hub.test'], PLAINUI_CONFERENCE=TEST_CONF_ID) class MarkdownTest(TestCase): def test_url_class(self): - conf = Conference(name='foo', id=TEST_CONF_ID) conf.save() diff --git a/src/core/tests/schedules.py b/src/core/tests/schedules.py index ec618de47..be0ffc95a 100644 --- a/src/core/tests/schedules.py +++ b/src/core/tests/schedules.py @@ -1,5 +1,5 @@ -from datetime import datetime, timedelta, timezone import json +from datetime import datetime, timedelta, timezone from pathlib import Path from django.test import TestCase, override_settings @@ -7,10 +7,9 @@ from django.test import TestCase, override_settings from ..models import Event from ..models.assemblies import Assembly from ..models.conference import Conference, ConferenceMember -from ..models.users import PlatformUser from ..models.schedules import ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping -from ..schedules.base import BaseScheduleSupport, ScheduleTypeManager, \ - filter_additional_data, schedule_time_to_timedelta +from ..models.users import PlatformUser +from ..schedules.base import BaseScheduleSupport, ScheduleTypeManager, filter_additional_data, schedule_time_to_timedelta from ..schedules.schedulejson import ScheduleJSONSupport from ..schedules.schedulexml import ScheduleXMLSupport @@ -25,7 +24,7 @@ class FileBasedScheduleSupport(BaseScheduleSupport): def filename(self): url = str(self.schedule_source.import_url) assert url.startswith('file://') - fn = url[len('file://'):] + fn = url[len('file://') :] base_path = Path(__file__).parent fn = base_path.joinpath(fn).resolve() @@ -118,10 +117,10 @@ class ScheduleTests(TestCase): self.assertEqual(2, src.mappings.count()) r1_m = src.mappings.get(mapping_type=ScheduleSourceMapping.MappingType.ROOM, source_id='eins') - self.assertEqual(r1_m.local_id, r1.id, 'room\'s mapping\'s local_id doesn\'t match room id') + self.assertEqual(r1_m.local_id, r1.id, "room's mapping's local_id doesn't match room id") e1_m = src.mappings.get(mapping_type=ScheduleSourceMapping.MappingType.EVENT, source_id='cej9dwoi') - self.assertEqual(e1_m.local_id, e1.id, 'event\'s mapping\'s local_id doesn\'t match event id') + self.assertEqual(e1_m.local_id, e1.id, "event's mapping's local_id doesn't match event id") # check other attributes self.assertEqual(e1.room, r1) @@ -259,7 +258,7 @@ class ScheduleTests(TestCase): assembly=self.assembly, conference=self.conference, import_type=ScheduleXMLSupport.identifier, - import_url=f"file://{Path(__file__).parent.joinpath('import_data', 'schedule-2021.xml')}" + import_url=f"file://{Path(__file__).parent.joinpath('import_data', 'schedule-2021.xml')}", ) self.assertFalse(src.has_running_import) @@ -305,7 +304,7 @@ class ScheduleTests(TestCase): assembly=self.assembly, conference=self.conference, import_type=ScheduleJSONSupport.identifier, - import_url=f"file://{Path(__file__).parent.joinpath('import_data', 'schedule-2020.json')}" + import_url=f"file://{Path(__file__).parent.joinpath('import_data', 'schedule-2020.json')}", ) self.check_json(src, rooms=1, events=1, room_name='Yellow Room') @@ -315,7 +314,7 @@ class ScheduleTests(TestCase): assembly=self.assembly, conference=self.conference, import_type=ScheduleJSONSupport.identifier, - import_url=f"file://{Path(__file__).parent.joinpath('import_data', 'schedule-2021.json')}" + import_url=f"file://{Path(__file__).parent.joinpath('import_data', 'schedule-2021.json')}", ) self.check_json(src, rooms=2, events=1, room_name='Gray Room') diff --git a/src/core/tests/search.py b/src/core/tests/search.py index ec7c0152a..1d9f4dadb 100644 --- a/src/core/tests/search.py +++ b/src/core/tests/search.py @@ -22,17 +22,17 @@ class SearchTests(TestCase): self.cocktail_page.save() self.cocktail_page_rev1 = self.cocktail_page.revisions.create( title=self.cocktail_page.title, - body='''Der **Pangalaktische Donnergurgler** ist der angeblich stärkste Drink im Universum. + body="""Der **Pangalaktische Donnergurgler** ist der angeblich stärkste Drink im Universum. Die Wirkung eines Pangalaktischen Donnergurglers ist in etwa so, „als ob man mit einem Goldbarren, -der in Zitronenscheiben gehüllt ist, das Gehirn aus dem Kopf gedroschen bekommt“.''', +der in Zitronenscheiben gehüllt ist, das Gehirn aus dem Kopf gedroschen bekommt“.""", author=page_user, ) self.cocktail_page_rev1.save() self.cocktail_page_rev2 = self.cocktail_page.revisions.create( title=self.cocktail_page.title, - body='''Der **Pangalaktische Donnergurgler** ist der angeblich stärkste Drink der Galaxis. + body="""Der **Pangalaktische Donnergurgler** ist der angeblich stärkste Drink der Galaxis. Die Wirkung eines Pangalaktischen Donnergurglers ist in etwa so, „als ob man mit einem Goldbarren, -der in Zitronenscheiben gehüllt ist, das Gehirn aus dem Kopf gedroschen bekommt“.''', +der in Zitronenscheiben gehüllt ist, das Gehirn aus dem Kopf gedroschen bekommt“.""", author=page_user, ) self.cocktail_page_rev2.save() diff --git a/src/core/tests/tags.py b/src/core/tests/tags.py index a11aaf427..14d4d7d3d 100644 --- a/src/core/tests/tags.py +++ b/src/core/tests/tags.py @@ -119,13 +119,13 @@ class TagItemTests(TestCase): if value is None: return if value_type == ConferenceTag.Type.SIMPLE: - self.fail("Simple tag item did not return None as value") + self.fail('Simple tag item did not return None as value') if value_type == ConferenceTag.Type.STRING: - self.assertTrue(isinstance(value, str), "String tag item did not return None or a string as value") + self.assertTrue(isinstance(value, str), 'String tag item did not return None or a string as value') if value_type == ConferenceTag.Type.INT: - self.assertTrue(isinstance(value, int), "Int tag item did not return None or an int as value") + self.assertTrue(isinstance(value, int), 'Int tag item did not return None or an int as value') if value_type == ConferenceTag.Type.BOOL: - self.assertTrue(isinstance(value, bool), "Bool tag item did not return None or a bool as value") + self.assertTrue(isinstance(value, bool), 'Bool tag item did not return None or a bool as value') def test_get_value_check_type_uninitialized(self): for tag_item in self.tag_items.values(): @@ -135,7 +135,7 @@ class TagItemTests(TestCase): for tag_item in self.tag_items.values(): value_type = tag_item.tag.value_type if value_type == ConferenceTag.Type.STRING: - tag_item.value = "Test" + tag_item.value = 'Test' if value_type == ConferenceTag.Type.INT: tag_item.value = 42 if value_type == ConferenceTag.Type.BOOL: @@ -147,7 +147,7 @@ class TagItemTests(TestCase): value_type = tag_item.tag.value_type value = None if value_type == ConferenceTag.Type.STRING: - value = "Test" + value = 'Test' if value_type == ConferenceTag.Type.INT: value = 42 if value_type == ConferenceTag.Type.BOOL: @@ -156,32 +156,32 @@ class TagItemTests(TestCase): self.assertEqual(tag_item.value, value) def test_value_check_boolean_coerce(self): - tag = self.tag_items["guter Geschmack"] + tag = self.tag_items['guter Geschmack'] for v in [False, 0, 'n', 'no', 'nein', 'false', 'off']: tag.value = True self.assertTrue(tag.value) tag.value = v - self.assertFalse(tag.value, "Value {} did not yield false".format(v)) + self.assertFalse(tag.value, f'Value {v} did not yield false') for v in [True, -2, -1, 1, 2, 3, 'y', 'j', 'yes', 'ja', 'true', 'on']: tag.value = False self.assertFalse(tag.value) tag.value = v - self.assertTrue(tag.value, "Value {} did not yield true".format(v)) + self.assertTrue(tag.value, f'Value {v} did not yield true') def test_value_set_check_simple_with_wrong_argument(self): - tag = self.tag_items["foo"] + tag = self.tag_items['foo'] with self.assertRaises(ValueError): tag.value = self.user with self.assertRaises(ValueError): - tag.value = "Test" + tag.value = 'Test' with self.assertRaises(ValueError): tag.value = 42 with self.assertRaises(ValueError): tag.value = True def test_value_set_check_string_with_wrong_argument(self): - tag = self.tag_items["Autor des besten Gedichts"] + tag = self.tag_items['Autor des besten Gedichts'] with self.assertRaises(ValueError): tag.value = None with self.assertRaises(ValueError): @@ -192,26 +192,26 @@ class TagItemTests(TestCase): tag.value = True def test_value_set_check_int_with_wrong_argument(self): - tag = self.tag_items["michelinstars"] + tag = self.tag_items['michelinstars'] with self.assertRaises(ValueError): tag.value = None with self.assertRaises(ValueError): tag.value = self.user with self.assertRaises(ValueError): - tag.value = "Test" + tag.value = 'Test' # Python coerces bool to int by itself def test_value_set_check_bool_with_wrong_argument(self): - tag = self.tag_items["guter Geschmack"] + tag = self.tag_items['guter Geschmack'] with self.assertRaises(ValueError): tag.value = None with self.assertRaises(ValueError): tag.value = self.user with self.assertRaises(ValueError): - tag.value = "Test" + tag.value = 'Test' with self.assertRaises(ValueError): - tag.value = "foo" + tag.value = 'foo' with self.assertRaises(ValueError): - tag.value = "" + tag.value = '' with self.assertRaises(ValueError): - tag.value = "bar" + tag.value = 'bar' diff --git a/src/core/tests/tickets.py b/src/core/tests/tickets.py index c108a39f6..1a7dfd5b1 100644 --- a/src/core/tests/tickets.py +++ b/src/core/tests/tickets.py @@ -1,12 +1,13 @@ -from datetime import datetime, timedelta, UTC -from django.test import TestCase, override_settings +from datetime import UTC, datetime, timedelta + import jwt +from django.test import TestCase, override_settings + from ..models.conference import Conference from ..models.ticket import ConferenceMemberTicket, TicketValidationError from ..models.users import PlatformUser - _GOOD_SECRET = 'F00Preti%' _BAD_SECRET = 'DämlicherFlanders' @@ -37,7 +38,7 @@ class PretixTests(TestCase): self.assertEqual(ticket.user_id, self.foo_user.id) # trying the same token again must fail - with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', "WARNING"): + with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', 'WARNING'): ConferenceMemberTicket.redeem_pretix_ticket(conference=self.conference, user=self.foo_user, token=token) def test_two_per_user(self): @@ -46,7 +47,7 @@ class PretixTests(TestCase): # validating a second token must fail (even if the token itself is valid) token2 = jwt.encode({**self.pretix_jwt_basepayload, 'uid': 'jklö5678'}, _GOOD_SECRET) - with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', "WARNING"): + with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', 'WARNING'): ConferenceMemberTicket.redeem_pretix_ticket(conference=self.conference, user=self.foo_user, token=token2) # verify that the second token works on another user @@ -57,16 +58,16 @@ class PretixTests(TestCase): def test_invalid_signature(self): token = jwt.encode({**self.pretix_jwt_basepayload, 'uid': 'asdf1234'}, _BAD_SECRET) - with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', "WARNING"): + with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', 'WARNING'): ConferenceMemberTicket.redeem_pretix_ticket(conference=self.conference, user=self.foo_user, token=token) def test_invalid_fields(self): # misspell 'uid' token = jwt.encode({**self.pretix_jwt_basepayload, 'userid': 'asdf1234'}, _GOOD_SECRET) - with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', "WARNING"): + with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', 'WARNING'): ConferenceMemberTicket.redeem_pretix_ticket(conference=self.conference, user=self.foo_user, token=token) # unmatching 'aud' (audience, conference slug) token = jwt.encode({**self.pretix_jwt_basepayload, 'aud': 'FNORD', 'userid': 'asdf1234'}, _GOOD_SECRET) - with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', "WARNING"): + with self.assertRaises(TicketValidationError), self.assertLogs('core.models.ticket', 'WARNING'): ConferenceMemberTicket.redeem_pretix_ticket(conference=self.conference, user=self.foo_user, token=token) diff --git a/src/core/tests/users.py b/src/core/tests/users.py index 8956403c9..7d6eb7650 100644 --- a/src/core/tests/users.py +++ b/src/core/tests/users.py @@ -14,7 +14,7 @@ class UserCommunicationChannelStaticTests(TestCase): self.fail(f'{g} should be accepted as {what} but raised an exception: {err}') for b in bad: - with self.assertRaises(ValidationError, msg=f'{b} shouldn\'t be accepted as {what}'): + with self.assertRaises(ValidationError, msg=f"{b} shouldn't be accepted as {what}"): func(b) def test_mail_validation(self): @@ -37,7 +37,7 @@ class UserCommunicationChannelStaticTests(TestCase): '#matrix:example.org', '!4rguxf:matrix.org', 'https://matrix.to/#/%23somewhere%3Aexample.org', - 'https://matrix.to/#/!somewhere%3Aexample.org' + 'https://matrix.to/#/!somewhere%3Aexample.org', ] bad_uris = [ '', diff --git a/src/core/tests/utils.py b/src/core/tests/utils.py index 89e7b1719..1a7ff7a53 100644 --- a/src/core/tests/utils.py +++ b/src/core/tests/utils.py @@ -2,7 +2,7 @@ from datetime import timedelta from django.test import TestCase -from ..utils import mask_url, str2timedelta, scheme_and_netloc_from_url, GitRepo +from ..utils import GitRepo, mask_url, scheme_and_netloc_from_url, str2timedelta class UtilsTests(TestCase): @@ -59,10 +59,10 @@ class GitRepoOfflineTests(TestCase): self.assertEqual(repo.repo_hash, 'develop') def test_url_params(self): - repo_test_baseurl = "https://git.cccv.de/hub/wiki-import-test.git" + repo_test_baseurl = 'https://git.cccv.de/hub/wiki-import-test.git' - repo = GitRepo(f"https://{repo_test_baseurl}") - self.assertEqual(f"https://{repo_test_baseurl}", repo.repo_url) + repo = GitRepo(f'https://{repo_test_baseurl}') + self.assertEqual(f'https://{repo_test_baseurl}', repo.repo_url) self.assertIsNone(repo.repo_branch) self.assertIsNone(repo.repo_path) self.assertIsNone(repo.repo_hash) diff --git a/src/core/tests/validators.py b/src/core/tests/validators.py index 567324956..cd3f4c795 100644 --- a/src/core/tests/validators.py +++ b/src/core/tests/validators.py @@ -24,9 +24,7 @@ class ValidateFileSizeTests(TestCase): class ValidateImageSizeTests(TestCase): def test_does_not_raise_when_image_meets_requirements(self): mock_image = Mock(width=10, height=10) - validator = ImageDimensionValidator( - min_size=(10, 10), max_size=(10, 10), square=True - ) + validator = ImageDimensionValidator(min_size=(10, 10), max_size=(10, 10), square=True) self.assertEqual(mock_image, validator(mock_image)) diff --git a/src/core/tests/workadventure.py b/src/core/tests/workadventure.py index 7b0940c5b..2e91edea3 100644 --- a/src/core/tests/workadventure.py +++ b/src/core/tests/workadventure.py @@ -58,15 +58,19 @@ class WorkadventureSessionTests(TestCase): self.user.refresh_from_db() self.assertFalse(self.user.receive_audio) - session.update_userdata({'wa_userconfig': { - 'silentMode': False, - 'disableCamera': False, - 'noAnimations': True, - 'blockExternalContent': True, - 'blockAmbientSounds': True, - 'ignoreFollowRequests': True, - 'forceWebsiteTrigger': True, - }}) + session.update_userdata( + { + 'wa_userconfig': { + 'silentMode': False, + 'disableCamera': False, + 'noAnimations': True, + 'blockExternalContent': True, + 'blockAmbientSounds': True, + 'ignoreFollowRequests': True, + 'forceWebsiteTrigger': True, + } + } + ) self.user.refresh_from_db() self.assertTrue(self.user.receive_audio) self.assertTrue(self.user.receive_video) diff --git a/src/core/tokens.py b/src/core/tokens.py index 6ee5a5155..635f82d2d 100644 --- a/src/core/tokens.py +++ b/src/core/tokens.py @@ -1,17 +1,16 @@ from django.contrib.auth.tokens import PasswordResetTokenGenerator - # adapted from https://simpleisbetterthancomplex.com/tutorial/2017/02/18/how-to-create-user-sign-up-view.html class AccountActivationTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): - return (str(user.pk) + str(timestamp) + str(user.profile.mail_verified)) + return str(user.pk) + str(timestamp) + str(user.profile.mail_verified) class ChannelActivationTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, channel, timestamp): - return (str(channel.user.pk) + str(timestamp) + str(channel.channel) + str(channel.address)) + return str(channel.user.pk) + str(timestamp) + str(channel.channel) + str(channel.address) account_activation_token = AccountActivationTokenGenerator() diff --git a/src/core/translation.py b/src/core/translation.py index 8aef618ba..85d7992c1 100644 --- a/src/core/translation.py +++ b/src/core/translation.py @@ -20,7 +20,10 @@ from core.models import ( @register(Assembly) class AssemblyTranslationOptions(TranslationOptions): - fields = ('description', 'description_html', ) + fields = ( + 'description', + 'description_html', + ) @register(Badge) @@ -58,39 +61,58 @@ admin.site.register(BadgeCategory, TranslatedBadgeCategoryAdmin) @register(BulletinBoardEntry) class BulletinBoardEntryTranslationOptions(TranslationOptions): - fields = ('text', 'text_html', ) + fields = ( + 'text', + 'text_html', + ) @register(ConferenceNavigationItem) class ConferenceNavigationItemTranslationOptions(TranslationOptions): - fields = ('label', 'title', ) + fields = ( + 'label', + 'title', + ) @register(Event) class EventTranslationOptions(TranslationOptions): - fields = ('description', 'description_html', ) + fields = ( + 'description', + 'description_html', + ) @register(ConferenceMember) class ConferenceMemberTranslationOptions(TranslationOptions): - fields = ('description', 'description_html', ) + fields = ( + 'description', + 'description_html', + ) @register(Room) class RoomTranslationOptions(TranslationOptions): - fields = ('description', 'description_html', ) + fields = ( + 'description', + 'description_html', + ) @register(MapFloor) class MapFloorTranslationOptions(TranslationOptions): - fields = ('name', ) + fields = ('name',) @register(MapPOI) class MapPOITranslationOptions(TranslationOptions): - fields = ('name', 'description', 'description_html', ) + fields = ( + 'name', + 'description', + 'description_html', + ) @register(MetaNavItem) class MetaNavItemTranslationOptions(TranslationOptions): - fields = ('title', ) + fields = ('title',) diff --git a/src/core/utils.py b/src/core/utils.py index ab233151e..82ef8ba43 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -50,7 +50,7 @@ def render_markdown_as_text(markup: str): """ text_only = strip_tags(markup) link_urls = {} - for (link_text, link_url) in RE_MARKDOWN_LINKS.findall(markup): + for link_text, link_url in RE_MARKDOWN_LINKS.findall(markup): if link_url not in link_urls: link_urls[link_url] = len(link_urls) + 1 idx = link_urls[link_url] @@ -73,7 +73,7 @@ def generate_token(length: int = 50, char_set: str = ''.join([ascii_letters, dig def int_to_custom_string(number: int, alphabet: str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): if alphabet is None or len(alphabet) < 2: - raise ValueError("Alphabet must consist of at least two characters.") + raise ValueError('Alphabet must consist of at least two characters.') base = len(alphabet) result = [] @@ -156,7 +156,7 @@ class GitCheckoutError(Exception): pass -class GitRepo(object): +class GitRepo: working_copy_path: Path def __init__(self, repo_url): @@ -178,27 +178,25 @@ class GitRepo(object): # note down URL without custom optional parts self.repo_url = parsed._replace(query=None, fragment=None).geturl() if '$' in self.repo_url: - raise ValueError('Env variables not allowed in GitRepo\'s repo_url.') + raise ValueError("Env variables not allowed in GitRepo's repo_url.") def __enter__(self): if self.working_copy_path is not None: - raise NotImplementedError("Accessing GitRepo twice is not supported!") + raise NotImplementedError('Accessing GitRepo twice is not supported!') self.working_copy_path = Path(tempfile.mkdtemp()) # assemble git-clone call cmd = ['git', 'clone', '-c', 'core.symlinks=false'] if not self.repo_hash: cmd.extend( - ['--depth', '1',] + [ + '--depth', + '1', + ] ) if self.repo_branch: - cmd.extend( - ['--branch', self.repo_branch] - ) - cmd.extend([ - self.repo_url, - str(self.working_copy_path) - ]) + cmd.extend(['--branch', self.repo_branch]) + cmd.extend([self.repo_url, str(self.working_copy_path)]) # this fails with an exception on non-zero exitcode logger.info(' '.join(cmd)) @@ -223,11 +221,11 @@ class GitRepo(object): def __exit__(self, exc_type, exc_val, exc_tb): if not shutil.rmtree.avoids_symlink_attacks: - raise NotImplementedError("Your OS does not support avoiding symlink attacks.") + raise NotImplementedError('Your OS does not support avoiding symlink attacks.') try: shutil.rmtree(self.working_copy_path) except Exception: - logger.exception("Failed to remove temporary files.") + logger.exception('Failed to remove temporary files.') @cached_property def base_path(self) -> Path: @@ -240,11 +238,11 @@ class GitRepo(object): base_path = (base_path / self.repo_path).absolute() # sanity check that we are not being tricked into some path traversal if not base_path.is_relative_to(self.working_copy_path): - raise ValueError(f"Provided path would result in a directory traversal: {self.repo_path}") + raise ValueError(f'Provided path would result in a directory traversal: {self.repo_path}') return base_path - def get_documents(self, glob: str = "*.md", encoding: str = 'utf-8') -> Dict[str, str]: + def get_documents(self, glob: str = '*.md', encoding: str = 'utf-8') -> Dict[str, str]: """ Read all documents matching the configured glob as text. :param glob: a filter to use when searching the documents to read, defaults to Markdown file extension @@ -260,7 +258,7 @@ class GitRepo(object): # read and store files matching the glob try: documents[file.name] = file.read_text(encoding=encoding) - except (IOError, UnicodeDecodeError): + except (OSError, UnicodeDecodeError): documents[file.name] = None logger.exception('Failed to read document "%s".', file.name) @@ -278,7 +276,7 @@ class GitRepo(object): # sanity check that nobody is trying to do any shenanigans if not file_path.is_relative_to(self.working_copy_path): - raise ValueError(f"Provided file path would result in a directory traversal: {filename}") + raise ValueError(f'Provided file path would result in a directory traversal: {filename}') # read the file (or raise if the file does not exist) return file_path.read_bytes() diff --git a/src/core/validators.py b/src/core/validators.py index 3fc396b20..d7750f148 100644 --- a/src/core/validators.py +++ b/src/core/validators.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ @deconstructible -class FileSizeValidator(object): +class FileSizeValidator: """ A validator class that will check a file size against the given limit @@ -28,9 +28,9 @@ class FileSizeValidator(object): def __call__(self, file): if file.size > self.max_size * 1024 * 1024: raise ValidationError( - _("Validation__error_file_size_MB_exceeded %(max_size)d"), - code="max_file_size", - params={"max_size": self.max_size}, + _('Validation__error_file_size_MB_exceeded %(max_size)d'), + code='max_file_size', + params={'max_size': self.max_size}, ) else: return file @@ -40,7 +40,7 @@ class FileSizeValidator(object): @deconstructible -class ImageDimensionValidator(object): +class ImageDimensionValidator: """ A validator class that will check a image dimension against the given limits @@ -52,13 +52,7 @@ class ImageDimensionValidator(object): max_size: tuple[int | None, int | None] square: bool - def __init__( - self, - *, - min_size: tuple[int | None, int | None] = (None, None), - max_size: tuple[int | None, int | None] = (None, None), - square: bool = False - ): + def __init__(self, *, min_size: tuple[int | None, int | None] = (None, None), max_size: tuple[int | None, int | None] = (None, None), square: bool = False): """Return a model field validator for ImageFields that raises an ValidationError if the image has incorrect dimensions @@ -76,55 +70,51 @@ class ImageDimensionValidator(object): if self.min_size[0] is not None and image.width < self.min_size[0]: errors.append( ValidationError( - _("Validation__error_image_width_low %(size)d"), - code="min_width", - params={"size": self.min_size[0]}, + _('Validation__error_image_width_low %(size)d'), + code='min_width', + params={'size': self.min_size[0]}, ) ) if self.min_size[1] is not None and image.height < self.min_size[1]: errors.append( ValidationError( - _("Validation__error_image_height_low %(size)d"), - code="min_height", - params={"size": self.min_size[1]}, + _('Validation__error_image_height_low %(size)d'), + code='min_height', + params={'size': self.min_size[1]}, ) ) if self.max_size[0] is not None and image.width > self.max_size[0]: errors.append( ValidationError( - _("Validation__error_image_width_high %(size)d"), - code="max_width", - params={"size": self.max_size[0]}, + _('Validation__error_image_width_high %(size)d'), + code='max_width', + params={'size': self.max_size[0]}, ) ) if self.max_size[1] is not None and image.height > self.max_size[1]: errors.append( ValidationError( - _("Validation__error_image_height_high %(size)d"), - code="max_height", - params={"size": self.max_size[1]}, + _('Validation__error_image_height_high %(size)d'), + code='max_height', + params={'size': self.max_size[1]}, ) ) if errors: errors.append( ValidationError( - _( - "Validation__image_dimensions " - "%(min_width)s %(min_height)s " - "%(max_width)s %(max_height)s" - ), - code="image_dimensions", + _('Validation__image_dimensions ' '%(min_width)s %(min_height)s ' '%(max_width)s %(max_height)s'), + code='image_dimensions', params={ - "min_width": self.min_size[0], - "min_height": self.min_size[1], - "max_width": self.max_size[0], - "max_height": self.max_size[1], + 'min_width': self.min_size[0], + 'min_height': self.min_size[1], + 'max_width': self.max_size[0], + 'max_height': self.max_size[1], }, ) ) if self.square and image.width != image.height: - errors.append(ValidationError(_("Validation__error_image_square"))) + errors.append(ValidationError(_('Validation__error_image_square'))) if errors: raise ValidationError(errors) else: diff --git a/src/core/views/auth.py b/src/core/views/auth.py index 8d9e469e5..427f923ad 100644 --- a/src/core/views/auth.py +++ b/src/core/views/auth.py @@ -1,8 +1,9 @@ - import logging from smtplib import SMTPException from typing import Any +from django_ratelimit.decorators import ratelimit + from django.contrib import messages from django.contrib.auth.views import LoginView, PasswordResetConfirmView, PasswordResetView from django.core.exceptions import NON_FIELD_ERRORS, ImproperlyConfigured, ValidationError @@ -16,7 +17,6 @@ from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import FormView, View -from django_ratelimit.decorators import ratelimit from core.forms import LoginForm, PasswordResetForm, RegistrationForm from core.models import PlatformUser, UserCommunicationChannel diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py index a9053c10b..683623572 100644 --- a/src/hub/settings/base.py +++ b/src/hub/settings/base.py @@ -10,15 +10,15 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.1/ref/settings/ """ -from email.utils import getaddresses import json import os -from pathlib import Path import re +from email.utils import getaddresses +from pathlib import Path -from django.utils.translation import gettext_lazy as _ import environ +from django.utils.translation import gettext_lazy as _ SCRIPT_NAME = os.getenv('SCRIPT_NAME', '') @@ -38,28 +38,22 @@ env = environ.FileAwareEnv( CLIENT_IP_HEADER=(str, None), DISABLE_RATELIMIT=(bool, False), BADGE_RATE_LIMIT=(str, '180/h'), - SENTRY_ENDPOINT=(str, None), SENTRY_TRACES_SAMPLE_RATE=(float, 0.05), # create a trace for this percentage of all requests SENTRY_PROFILES_SAMPLE_RATE=(float, 0.50), # do a profiling for this percentage of _traced_ requests - SSO_SECRET=(str, None), SSO_SECRET_GENERATE=(bool, False), PRETIX_ISSUER=(str, 'tickets.events.ccc.de'), PRETIX_SECRET=(str, None), - METRICS_SERVER_IPS=(list, ['*']), TIMEZONE=(str, 'Europe/Berlin'), - BIGBLUEBUTTON=(bool, False), BIGBLUEBUTTON_API_URL=(str, None), BIGBLUEBUTTON_API_TOKEN=(str, None), BIGBLUEBUTTON_END_MEETING_CALLBACK=(str, None), BIGBLUEBUTTON_INITIAL_PRESENTATION_URL=(str, None), - HANGAR=(bool, False), HANGAR_URL=(str, None), - WORKADVENTURE=(bool, False), WORKADVENTURE_URL_SCHEME_GENERAL=(str, 'http://visit.at.localhost:8080/'), WORKADVENTURE_URL_SCHEME_ASSEMBLY=( @@ -84,7 +78,6 @@ env = environ.FileAwareEnv( WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL=(str, None), WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION=(str, None), WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY=(bool, True), - ADMIN_BASE_URL=(str, '/c3admin'), API_BASE_URL=(str, '/api'), BACKOFFICE_BASE_URL=(str, '/backoffice'), @@ -94,22 +87,17 @@ env = environ.FileAwareEnv( PLAINUI_DEREFERER_ALLOWLIST=(list, []), PLAINUI_DEREFERER_COUNTLIST=(list, []), PLAINUI_THEME_SUPPORT=(bool, False), - SHIBBOLEET_WA_ROOM_ID=('str', None), - ASSEMBLY_BANNER_FILE_SIZE_LIMIT=(int, 2), ASSEMBLY_BANNER_WIDTH_MINIMUM=(int, 100), ASSEMBLY_BANNER_HEIGHT_MINIMUM=(int, 100), ASSEMBLY_BANNER_WIDTH_MAXIMUM=(int, 2048), ASSEMBLY_BANNER_HEIGHT_MAXIMUM=(int, 1024), - BADGE_IMAGE_WIDTH_MINIMUM=(int, 256), BADGE_IMAGE_HEIGHT_MINIMUM=(int, 256), BADGE_IMAGE_WIDTH_MAXIMUM=(int, 512), BADGE_IMAGE_HEIGHT_MAXIMUM=(int, 512), - API_USERS=(list, []), - DISABLE_REQUEST_LOGGING=(bool, False), ) @@ -147,9 +135,7 @@ BASE_DIR = Path(__file__).resolve().parents[2] if env('DJANGO_ENV_FILE'): DJANGO_ENV_FILE_PATH = Path(env('DJANGO_ENV_FILE')) - assert ( - DJANGO_ENV_FILE_PATH.exists() - ), f'DJANGO_ENV_FILE path {DJANGO_ENV_FILE_PATH} does not exist!' + assert DJANGO_ENV_FILE_PATH.exists(), f'DJANGO_ENV_FILE path {DJANGO_ENV_FILE_PATH} does not exist!' environ.Env.read_env(DJANGO_ENV_FILE_PATH) # prepare SECRET_KEY but must be overridden, i.e. in default.py @@ -200,7 +186,8 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'core.middleware.TimezoneMiddleware', - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', # TODO drüber nachdenken ob wir die brauchen (ist default an in Django) + # TODO drüber nachdenken ob wir die brauchen (ist default an in Django) + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', # noqa: ERA001 ] ROOT_URLCONF = 'hub.urls' @@ -271,9 +258,7 @@ SESSION_COOKIE_HTTPONLY = True # session cookie is unavailable to JavaScript (d SESSION_COOKIE_SAMESITE = 'Lax' # set SameSite=Lax (default) SESSION_COOKIE_PATH = env('COOKIE_PATH') or '/' # use configured path, SESSION_NAME or default '/' SESSION_COOKIE_SECURE = True # mark session cookie as https-only -SESSION_SAVE_EVERY_REQUEST = ( - False # no need to update a session on each request (default) -) +SESSION_SAVE_EVERY_REQUEST = False # no need to update a session on each request (default) # CSRF Cookie configuration CSRF_COOKIE_NAME = env('CSRF_COOKIE_NAME', default=SESSION_COOKIE_NAME.replace('_SESSION', '_CSRF') if '_SESSION' in SESSION_COOKIE_NAME else 'HUB_CSRF') @@ -291,7 +276,9 @@ OAUTH2_PROVIDER = { # https://docs.djangoproject.com/en/3.1/topics/i18n/ LANGUAGE_CODE = 'en' -LANGUAGE_COOKIE_NAME = env('LANGUAGE_COOKIE_NAME', default=SESSION_COOKIE_NAME.replace('_SESSION', '_LANG') if '_SESSION' in SESSION_COOKIE_NAME else 'HUB_LANG') # noqa:E501 +LANGUAGE_COOKIE_NAME = env( + 'LANGUAGE_COOKIE_NAME', default=SESSION_COOKIE_NAME.replace('_SESSION', '_LANG') if '_SESSION' in SESSION_COOKIE_NAME else 'HUB_LANG' +) # noqa:E501 LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH LANGUAGE_COOKIE_SECURE = SESSION_COOKIE_SECURE @@ -299,8 +286,8 @@ TIME_ZONE = env('TIMEZONE') USE_I18N = True USE_TZ = True LANGUAGES = [ - ('de', _("German")), - ('en', _("English")), + ('de', _('German')), + ('en', _('English')), ] MODELTRANSLATION_FALLBACK_LANGUAGES = ['en', 'de'] @@ -316,13 +303,13 @@ STATIC_ROOT = BASE_DIR / 'static.dist' # setup storage, we assume the default STORAGES = { - "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage", + 'staticfiles': { + 'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage', }, } -if env.bool("USE_PLAIN_STATICFILES", False): - STORAGES["staticfiles"]["BACKEND"] = "django.contrib.staticfiles.storage.StaticFilesStorage" +if env.bool('USE_PLAIN_STATICFILES', False): + STORAGES['staticfiles']['BACKEND'] = 'django.contrib.staticfiles.storage.StaticFilesStorage' storage_type = env('STORAGE_TYPE').lower() if storage_type == 's3': @@ -330,7 +317,7 @@ if storage_type == 's3': 'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage', 'OPTIONS': { 'file_overwrite': False, - } + }, } AWS_STORAGE_BUCKET_NAME = env.str('S3_BUCKET', 'static') @@ -351,17 +338,18 @@ elif storage_type == 'sftp': SFTP_STORAGE_INTERACTIVE = False SFTP_STORAGE_PARAMS = env.dict('SFTP_PARAMS') -elif storage_type == 'local' or storage_type == '': +elif storage_type in ('local', ''): if storage_type == '': import warnings + warnings.warn('No STORAGE_TYPE selected, defaulting to "local". Verify if that is what you want!', RuntimeWarning) STORAGES['default'] = { - "BACKEND": "django.core.files.storage.FileSystemStorage", + 'BACKEND': 'django.core.files.storage.FileSystemStorage', 'OPTIONS': { 'base_url': env('MEDIA_URL'), 'location': env.path('MEDIA_PATH', BASE_DIR / 'media'), - } + }, } else: @@ -412,11 +400,11 @@ FORBIDDEN_ASSEMBLY_SLUGS = ['admin', 'visit', 'maps', 'api', 'pusher'] # Logging configuration LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "handlers": { - "console": { - "class": "logging.StreamHandler", + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', }, 'null': { 'class': 'logging.NullHandler', @@ -428,9 +416,9 @@ LOGGING = { 'propagate': False, }, }, - "root": { - "handlers": ["console"], - "level": env('LOG_LEVEL'), + 'root': { + 'handlers': ['console'], + 'level': env('LOG_LEVEL'), }, } @@ -481,7 +469,7 @@ PRETIX_SECRET_KEY = env('PRETIX_SECRET') # the JWT shared secret with Pretix # Restrict the access to /metrics/ to certain IP addresses (e.g. allowe the Prometheus/Grafana server only). # You can add one or multiple IP addresse by setting the `METRICS_SERVER_IPS` env var. -# e.g. METRICS_SERVER_IPS="127.0.0.1,80.147.140.51" +# e.g. METRICS_SERVER_IPS="127.0.0.1,80.147.140.51" # noqa: ERA001 # The default is to allow every IP address (METRICS_SERVER_IPS="*"). METRICS_SERVER_IPS = env('METRICS_SERVER_IPS') @@ -506,14 +494,8 @@ SCHEDULES_SUPPORT_FILE_PROTOCOL = False INTEGRATIONS_BBB = env('BIGBLUEBUTTON') BIGBLUEBUTTON_API_URL = env('BIGBLUEBUTTON_API_URL') BIGBLUEBUTTON_API_TOKEN = env('BIGBLUEBUTTON_API_TOKEN') -BIGBLUEBUTTON_END_MEETING_CALLBACK = env( - 'BIGBLUEBUTTON_END_MEETING_CALLBACK' -) # url bbb will call to notify us of ending meetings -# BIGBLUEBUTTON_END_MEETING_CALLBACK = 'https://rc3.world/api/bbb_meeting_end' -BIGBLUEBUTTON_INITIAL_PRESENTATION_URL = env( - 'BIGBLUEBUTTON_INITIAL_PRESENTATION_URL' -) # url to tell bbb to use as initial presentation -# BIGBLUEBUTTON_INITIAL_PRESENTATION_URL = 'https://rc3.world/static/plainui/bbb-background.jpg' +BIGBLUEBUTTON_END_MEETING_CALLBACK = env('BIGBLUEBUTTON_END_MEETING_CALLBACK') # url bbb will call to notify us of ending meetings +BIGBLUEBUTTON_INITIAL_PRESENTATION_URL = env('BIGBLUEBUTTON_INITIAL_PRESENTATION_URL') # url to tell bbb to use as initial presentation # Hangar INTEGRATIONS_HANGAR = env('HANGAR') @@ -530,40 +512,26 @@ WORKADVENTURE_TERMINATE_OLD_SESSIONS_ON_REGISTER = False # URL (and optional Authorization-Header value) for pushing maps to exneuland, accepts "%CONFSLUG% variable WORKADVENTURE_BACKEND_MAP_PUSH_URL = env('WORKADVENTURE_BACKEND_MAP_PUSH_URL') -WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION = env( - 'WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION' -) +WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION = env('WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION') # URL (and optional Authorization-Header value) for pushing userinfo to exneuland, accepts "%CONFSLUG% and %USERID% variables WORKADVENTURE_BACKEND_USERINFO_PUSH_URL = env('WORKADVENTURE_BACKEND_USERINFO_PUSH_URL') -WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION = env( - 'WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION' -) +WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION = env('WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION') # URL (and optional Authorization-Header value) for requesting mapservice sync, accepts "%CONFSLUG% variable WORKADVENTURE_MAPSERVICE_SYNC_URL = env('WORKADVENTURE_MAPSERVICE_SYNC_URL') -WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION = env( - 'WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION' -) +WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION = env('WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION') WORKADVENTURE_MAPSERVICE_SYNC_NOVERIFY = env('WORKADVENTURE_MAPSERVICE_SYNC_NOVERIFY') # URL (and optional Authorization-Header value) for requesting mapservice sync of a single room, accepts "%CONFSLUG% and %ROOMID% variable WORKADVENTURE_MAPSERVICE_SYNCROOM_URL = env('WORKADVENTURE_MAPSERVICE_SYNCROOM_URL') -WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION = env( - 'WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION' -) -WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY = env( - 'WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY' -) +WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION = env('WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION') +WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY = env('WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY') # URL (and optional Authorization-Header value) for setting mapservice's loglevel, accepts "%CONFSLUG% variable WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL = env('WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL') -WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION = env( - 'WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION' -) -WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY = env( - 'WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY' -) +WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION = env('WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION') +WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY = env('WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY') # push maps to backend upon new data from mapservice? WORKADVENTURE_BACKEND_PUSH_ON_MAPSERVICE_DATA = True @@ -614,15 +582,15 @@ WORKADVENTURE_LOBBY_ROOM_SLUG = 'lobby' # flag if newly created pages are localized by default (or not) STATIC_PAGE_LOCALIZED_BY_DEFAULT = env.bool('STATICPAGES_LOCALIZED_BY_DEFAULT', False) -PLAINUI_THEME_SUPPORT = env("PLAINUI_THEME_SUPPORT") +PLAINUI_THEME_SUPPORT = env('PLAINUI_THEME_SUPPORT') -ASSEMBLY_BANNER_FILE_SIZE_LIMIT = env("ASSEMBLY_BANNER_FILE_SIZE_LIMIT") -ASSEMBLY_BANNER_WIDTH_MINIMUM = env("ASSEMBLY_BANNER_WIDTH_MINIMUM") -ASSEMBLY_BANNER_HEIGHT_MINIMUM = env("ASSEMBLY_BANNER_HEIGHT_MINIMUM") -ASSEMBLY_BANNER_WIDTH_MAXIMUM = env("ASSEMBLY_BANNER_WIDTH_MAXIMUM") -ASSEMBLY_BANNER_HEIGHT_MAXIMUM = env("ASSEMBLY_BANNER_HEIGHT_MAXIMUM") +ASSEMBLY_BANNER_FILE_SIZE_LIMIT = env('ASSEMBLY_BANNER_FILE_SIZE_LIMIT') +ASSEMBLY_BANNER_WIDTH_MINIMUM = env('ASSEMBLY_BANNER_WIDTH_MINIMUM') +ASSEMBLY_BANNER_HEIGHT_MINIMUM = env('ASSEMBLY_BANNER_HEIGHT_MINIMUM') +ASSEMBLY_BANNER_WIDTH_MAXIMUM = env('ASSEMBLY_BANNER_WIDTH_MAXIMUM') +ASSEMBLY_BANNER_HEIGHT_MAXIMUM = env('ASSEMBLY_BANNER_HEIGHT_MAXIMUM') -BADGE_IMAGE_WIDTH_MINIMUM = env("BADGE_IMAGE_WIDTH_MINIMUM") -BADGE_IMAGE_HEIGHT_MINIMUM = env("BADGE_IMAGE_HEIGHT_MINIMUM") -BADGE_IMAGE_WIDTH_MAXIMUM = env("BADGE_IMAGE_WIDTH_MAXIMUM") -BADGE_IMAGE_HEIGHT_MAXIMUM = env("BADGE_IMAGE_HEIGHT_MAXIMUM") +BADGE_IMAGE_WIDTH_MINIMUM = env('BADGE_IMAGE_WIDTH_MINIMUM') +BADGE_IMAGE_HEIGHT_MINIMUM = env('BADGE_IMAGE_HEIGHT_MINIMUM') +BADGE_IMAGE_WIDTH_MAXIMUM = env('BADGE_IMAGE_WIDTH_MAXIMUM') +BADGE_IMAGE_HEIGHT_MAXIMUM = env('BADGE_IMAGE_HEIGHT_MAXIMUM') diff --git a/src/hub/settings/build.py b/src/hub/settings/build.py index aedac262f..bda3e5fa7 100644 --- a/src/hub/settings/build.py +++ b/src/hub/settings/build.py @@ -1,6 +1,6 @@ from hub.settings.base import * # noqa: F403 -SECRET_KEY = "BUILD_KEY" +SECRET_KEY = 'BUILD_KEY' # Load apps, so static files can be loaded during build INSTALLED_APPS += ['backoffice', 'django_bootstrap5', 'widget_tweaks'] # noqa: F405 diff --git a/src/hub/settings/default.py b/src/hub/settings/default.py index 84a050b1c..75566aa50 100644 --- a/src/hub/settings/default.py +++ b/src/hub/settings/default.py @@ -17,7 +17,7 @@ default_env = environ.FileAwareEnv( ) -def print_banner(message: str, filler: str = "*"): +def print_banner(message: str, filler: str = '*'): print(filler * 72) print(filler, message) print(filler * 72) @@ -25,8 +25,8 @@ def print_banner(message: str, filler: str = "*"): # configure database using environment variable "DATABASE_URL" DATABASES['default'] = default_env.db(default='postgis://localhost/hub') # noqa: F405 -if DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql', 'django.contrib.gis.db.backends.postgis'] and default_env("HUB_DB_SCHEMA"): # noqa:F405 - DATABASES['default']['OPTIONS'] = { # noqa:F405 +if DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql', 'django.contrib.gis.db.backends.postgis'] and default_env('HUB_DB_SCHEMA'): # noqa:F405 + DATABASES['default']['OPTIONS'] = { # noqa:F405 'options': f'-c search_path={default_env("HUB_DB_SCHEMA")}' } @@ -36,7 +36,7 @@ if (django_host := default_env('DJANGO_HOST')) is not None: ALLOWED_HOSTS.append(django_host) # noqa:F405 # set flags -SERVE_MODULE_OUTPUT = default_env("SERVE_MODULE_OUTPUT") +SERVE_MODULE_OUTPUT = default_env('SERVE_MODULE_OUTPUT') IS_ADMIN = default_env('SERVE_ADMIN') if SERVE_MODULE_OUTPUT and IS_ADMIN: print_banner('Admin module has been selected and will be served', '-') @@ -67,19 +67,19 @@ except ImportError: # read SECRET_KEY from a file not stored in git if DEBUG and SECRET_KEY is None: # noqa: F405 - SECRET_FILE = BASE_DIR.joinpath("hub", ".settings.secret") # noqa: F405 + SECRET_FILE = BASE_DIR.joinpath('hub', '.settings.secret') # noqa: F405 try: with SECRET_FILE.open() as secret_file: SECRET_KEY = secret_file.read().strip() - except IOError: + except OSError: try: SECRET_KEY = gen_secret_key() # noqa: F405 - with SECRET_FILE.open("w") as secret: + with SECRET_FILE.open('w') as secret: secret.write(SECRET_KEY) print('*' * 72) print('*', 'Written SECRET_KEY file:', SECRET_FILE) print('*' * 72) - except IOError: + except OSError: Exception('Please create a %s file with random characters to serve as your Django SECRET_KEY!' % SECRET_FILE) # include API django api @@ -148,23 +148,20 @@ if SENTRY_ENDPOINT is not None: # noqa: F405 sentry_sdk.init( dsn=SENTRY_ENDPOINT, # noqa: F405 integrations=[DjangoIntegration()], - # trace sample rate in percent traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE, # noqa: F405 - # profiling rate (percentage of traces) profiles_sample_rate=SENTRY_PROFILES_SAMPLE_RATE, # noqa: F405 - # do not send user information (django.contrib.auth) - send_default_pii=False + send_default_pii=False, ) if IS_API and SSO_SECRET is None: # noqa: F405 SSO_SECRET = SECRET_KEY[::-1] - if default_env("SSO_WARNING"): + if default_env('SSO_WARNING'): print_banner('Generated a SSO_SECRET from your SECRET_KEY, this is probably not what you want!') -if '*' in METRICS_SERVER_IPS: # noqa: F405 +if '*' in METRICS_SERVER_IPS: # noqa: F405 print_banner('Warning: /metrics/ is accessible from every IP address ("*"). Set METRICS_SERVER_IPS="<prometheus server ip>".') TEST_RUNNER = 'core.tests.runner.PostgresSchemaTestRunner' diff --git a/src/hub/settings/dev.py b/src/hub/settings/dev.py index 258a2a0ba..6948b660a 100644 --- a/src/hub/settings/dev.py +++ b/src/hub/settings/dev.py @@ -2,19 +2,17 @@ import os import environ -dev_env = environ.FileAwareEnv( - SERVE_DEBUGPY=(bool, False) -) - -if dev_env("SERVE_DEBUGPY"): +dev_env = environ.FileAwareEnv(SERVE_DEBUGPY=(bool, False)) +if dev_env('SERVE_DEBUGPY'): # Disable debug warnings for python 3.11 see https://github.com/microsoft/debugpy/issues/861 - os.environ.setdefault('PYDEVD_DISABLE_FILE_VALIDATION', "1") + os.environ.setdefault('PYDEVD_DISABLE_FILE_VALIDATION', '1') try: import debugpy - debugpy.listen(("0.0.0.0", 5678)) - print("debugpy ready for connection at 0.0.0.0:5678") + + debugpy.listen(('0.0.0.0', 5678)) + print('debugpy ready for connection at 0.0.0.0:5678') except ImportError: print('!' * 72) @@ -38,7 +36,7 @@ os.environ.setdefault('SERVE_BACKOFFICE', 'yes') os.environ.setdefault('SERVE_FRONTEND', 'yes') # serve metrics locally only (and silence the warning) -os.environ.setdefault("METRICS_SERVER_IPS", "127.0.0.1") +os.environ.setdefault('METRICS_SERVER_IPS', '127.0.0.1') # store media files locally os.environ.setdefault('STORAGE_TYPE', 'local') diff --git a/src/hub/views.py b/src/hub/views.py index e62038e62..c92cfcc14 100644 --- a/src/hub/views.py +++ b/src/hub/views.py @@ -1,11 +1,12 @@ import json +from django_ratelimit.exceptions import Ratelimited + from django.conf import settings from django.contrib.auth import get_user_model from django.http import HttpResponse, HttpResponseForbidden, HttpResponseServerError, JsonResponse from django.utils.cache import add_never_cache_headers from django.views.decorators.http import require_safe -from django_ratelimit.exceptions import Ratelimited from hub.http import HttpResponseRateLimited @@ -44,7 +45,7 @@ def index(request): def debug_headers(request): param = request.GET if request.method not in ['POST', 'DELETE', 'PUT', 'PATCH'] else request.POST token = param.get('token', '') - if settings.HEADERS_DEBUG_ENDPOINT_TOKEN != token: + if token != settings.HEADERS_DEBUG_ENDPOINT_TOKEN: return HttpResponse('Wrong token.', 'text/plain', status=403) request_headers = dict(request.headers) diff --git a/src/manage.py b/src/manage.py index b0e2fe530..1558aee21 100755 --- a/src/manage.py +++ b/src/manage.py @@ -14,8 +14,8 @@ def main(): except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" + 'available on your PYTHONPATH environment variable? Did you ' + 'forget to activate a virtual environment?' ) from exc execute_from_command_line(sys.argv) diff --git a/src/plainui/forms.py b/src/plainui/forms.py index 8d4a9c34c..5d717a2e7 100644 --- a/src/plainui/forms.py +++ b/src/plainui/forms.py @@ -5,12 +5,22 @@ from django.contrib.auth import forms as auth_forms from django.forms import ValidationError from django.utils.formats import localize from django.utils.timezone import localtime, now -from django.utils.translation import gettext_lazy as _, gettext +from django.utils.translation import gettext +from django.utils.translation import gettext_lazy as _ from core.abuse import REPORT_CATEGORIES, report_content from core.base_forms import TranslatedFieldsForm -from core.models import BadgeToken, BulletinBoardEntry, ConferenceMember, ConferenceMemberTicket, DirectMessage, \ - PlatformUser, StaticPage, UserCommunicationChannel, UserBadge +from core.models import ( + BadgeToken, + BulletinBoardEntry, + ConferenceMember, + ConferenceMemberTicket, + DirectMessage, + PlatformUser, + StaticPage, + UserBadge, + UserCommunicationChannel, +) from core.models.badges import TOKEN_VALIDATOR @@ -18,7 +28,7 @@ class UsernameField(forms.CharField): def to_python(self, value): user = PlatformUser.objects.filter(username=value).first() if user is None: - raise ValidationError(gettext("Unknown User!"), code='invalid') + raise ValidationError(gettext('Unknown User!'), code='invalid') return user @@ -30,15 +40,15 @@ class NewDirectMessageForm(forms.Form): super().__init__(*args, **kwargs) in_reply_to = forms.UUIDField(required=False) - recipient = UsernameField(widget=forms.TextInput(attrs={'placeholder': _("Please enter the recipient name")})) - subject = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _("Please enter a subject")})) + recipient = UsernameField(widget=forms.TextInput(attrs={'placeholder': _('Please enter the recipient name')})) + subject = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _('Please enter a subject')})) body = forms.CharField(widget=forms.Textarea) def clean(self): if self.conf.send_pn_disabled: - raise ValidationError(_("Sending Messages is currently disabled")) + raise ValidationError(_('Sending Messages is currently disabled')) if self.request.limited: - raise ValidationError(_("rate-limited")) + raise ValidationError(_('rate-limited')) class BulletinBoardEntryForm(TranslatedFieldsForm): @@ -52,19 +62,20 @@ class BulletinBoardEntryForm(TranslatedFieldsForm): self.user = user super().__init__(*args, **kwargs) - title = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _("Please enter a title")})) + title = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _('Please enter a title')})) is_public = forms.BooleanField(required=False) def clean(self): if self.conf.board_disabled: - raise ValidationError(_("Bulletin Board is currently disabled")) + raise ValidationError(_('Bulletin Board is currently disabled')) if self.request.limited: - raise ValidationError(_("rate-limited")) + raise ValidationError(_('rate-limited')) class ExampleForm(forms.Form): - """ used in the component gallery """ - text = forms.CharField(min_length=1, help_text='Help Text', widget=forms.TextInput(attrs={'placeholder': _("Placeholder text")})) + """used in the component gallery""" + + text = forms.CharField(min_length=1, help_text='Help Text', widget=forms.TextInput(attrs={'placeholder': _('Placeholder text')})) password = forms.CharField(help_text='Help Text', widget=forms.PasswordInput) checkbox = forms.BooleanField(help_text='Help Text') textarea = forms.CharField(widget=forms.Textarea, help_text='Help Text') @@ -72,40 +83,39 @@ class ExampleForm(forms.Form): def clean(self): super().clean() if 'text' not in self.cleaned_data: - raise ValidationError("Some general error message of the form") + raise ValidationError('Some general error message of the form') class ProfileEditForm(TranslatedFieldsForm): class Meta: model = PlatformUser fields = [ - 'pronouns', 'timezone', + 'pronouns', + 'timezone', ] class ProfileDescriptionEditForm(TranslatedFieldsForm): class Meta: model = ConferenceMember - fields = [ - 'description' - ] + fields = ['description'] class RedeemBadgeForm(forms.Form): - token = forms.CharField(required=True, label="", widget=forms.TextInput(attrs={'placeholder': 'Token'}), validators=[TOKEN_VALIDATOR]) - purpose = forms.CharField(required=True, label="", widget=forms.HiddenInput(), initial="redeem_token") + token = forms.CharField(required=True, label='', widget=forms.TextInput(attrs={'placeholder': 'Token'}), validators=[TOKEN_VALIDATOR]) + purpose = forms.CharField(required=True, label='', widget=forms.HiddenInput(), initial='redeem_token') conference = None def __init__(self, *args, **kwargs) -> None: - self.conference = kwargs.pop("conference", None) + self.conference = kwargs.pop('conference', None) super().__init__(*args, **kwargs) def clean_token(self): - token = self.cleaned_data["token"] + token = self.cleaned_data['token'] try: badge_token = BadgeToken.objects.get(token=token, badge__conference=self.conference) except BadgeToken.DoesNotExist: - raise ValidationError(_('BadgeToken__does_not_exist %(token)s') % {"token": token}) + raise ValidationError(_('BadgeToken__does_not_exist %(token)s') % {'token': token}) return badge_token @@ -117,7 +127,7 @@ class ManageBadgeForm(forms.ModelForm): class InputTokenForm(forms.Form): - jwt = forms.CharField(max_length=None, required=True, label=_("Token")) + jwt = forms.CharField(max_length=None, required=True, label=_('Token')) def __init__(self, conf, user, *args, **kwargs): super().__init__(*args, **kwargs) @@ -126,7 +136,7 @@ class InputTokenForm(forms.Form): def clean_jwt(self): if self.conf.end <= now(): - raise ValidationError(gettext("The Conference is over!")) + raise ValidationError(gettext('The Conference is over!')) try: ConferenceMemberTicket.validate_pretix_ticket(self.conf, self.user, self.cleaned_data['jwt']) return self.cleaned_data['jwt'] @@ -143,9 +153,9 @@ class RedeemTokenAddToUserForm(auth_forms.AuthenticationForm): def clean(self): if self.conf.end <= now(): - raise ValidationError(gettext("The Conference is over!")) + raise ValidationError(gettext('The Conference is over!')) if self.request.limited: - raise ValidationError(_("rate-limited")) + raise ValidationError(_('rate-limited')) return super().clean() @@ -155,16 +165,17 @@ class WorkadventureCompatibleUsernameField(auth_forms.UsernameField): Django on the otherhand has a default of max_length=150 for usernames. (see https://git.cccv.de/hub/hub/-/issues/228) """ + def __init__(self, *args, **kwargs): # overwrite or set the maximum length kwargs['max_length'] = 20 - return super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class RedeemTokenUserCreateForm(auth_forms.UserCreationForm): class Meta: model = PlatformUser - fields = ("username",) + fields = ('username',) # Overwrite the auth_forms.UsernameField with its extended version that # enforces usernames with less than 20 characters. field_classes = {'username': WorkadventureCompatibleUsernameField} @@ -174,20 +185,23 @@ class RedeemTokenUserCreateForm(auth_forms.UserCreationForm): def __init__(self, conf, *args, **kwargs): super().__init__(*args, **kwargs) self.conf = conf - self['username'].help_text = \ - gettext("Required. 20 characters or fewer. Letters, digits and @/./+/-/_ only.") + '<br><b>' \ - + gettext("Your username is visible to others and cannot be changed later.") + '</b><br>' \ - + gettext("The username will also be displayed next to your avatar inside the 2D world.") + self['username'].help_text = ( + gettext('Required. 20 characters or fewer. Letters, digits and @/./+/-/_ only.') + + '<br><b>' + + gettext('Your username is visible to others and cannot be changed later.') + + '</b><br>' + + gettext('The username will also be displayed next to your avatar inside the 2D world.') + ) def clean(self): if self.conf.end <= now(): - raise ValidationError(gettext("The Conference is over!")) + raise ValidationError(gettext('The Conference is over!')) return super().clean() class TokenPasswortResetForm(auth_forms.SetPasswordForm): jwt = forms.CharField(max_length=None, required=True, widget=forms.HiddenInput()) - username = forms.CharField(required=True, label=_("Username")) + username = forms.CharField(required=True, label=_('Username')) def __init__(self, conf, user, *args, **kwargs): super().__init__(user, *args, **kwargs) @@ -205,13 +219,13 @@ class TokenPasswortResetForm(auth_forms.SetPasswordForm): user = PlatformUser.objects.filter(username=self.cleaned_data['username']).first() if user is None: - raise ValidationError(gettext("Unknown User")) + raise ValidationError(gettext('Unknown User')) if not user.is_active: - raise ValidationError(gettext("User is not active!")) + raise ValidationError(gettext('User is not active!')) if user.email or user.communication_channels.filter(channel=UserCommunicationChannel.Channel.MAIL, is_verified=True).exists(): - raise ValidationError(gettext("User can use password reset by email!")) + raise ValidationError(gettext('User can use password reset by email!')) pretix_ident, ticket_data = ConferenceMemberTicket.validate_pretix_ticket(self.conf, self.user, self.cleaned_data['jwt'], validate_only=True) if not ConferenceMemberTicket.objects.filter(conference=self.conf, user=user, ident=pretix_ident).exists(): @@ -226,22 +240,25 @@ class RoomChoiceField(forms.ModelChoiceField): def label_from_instance(self, obj): if obj.pk in self.blocked_rooms: - return markupsafe.Markup('%s [%s]') % (obj, gettext("blocked")) + return markupsafe.Markup('%s [%s]') % (obj, gettext('blocked')) return str(obj) class ReportForm(forms.Form): - kind = forms.ChoiceField(choices=[ - ('url', 'url'), - ('pn', 'pn'), - ('board', 'board'), - ], widget=forms.HiddenInput()) + kind = forms.ChoiceField( + choices=[ + ('url', 'url'), + ('pn', 'pn'), + ('board', 'board'), + ], + widget=forms.HiddenInput(), + ) kind_data = forms.CharField(min_length=1, widget=forms.HiddenInput()) next = forms.CharField(widget=forms.HiddenInput(), required=False) category = forms.ChoiceField(choices=[(k, v[0]) for k, v in REPORT_CATEGORIES.items()], initial='security') - message = forms.CharField(min_length=1, widget=forms.Textarea(), label=_("describe the problem"), required=True) - message2 = forms.CharField(min_length=1, widget=forms.Textarea(), label=_("describe a solution"), required=True) + message = forms.CharField(min_length=1, widget=forms.Textarea(), label=_('describe the problem'), required=True) + message2 = forms.CharField(min_length=1, widget=forms.Textarea(), label=_('describe a solution'), required=True) def __init__(self, *args, request, conf, **kwargs): super().__init__(*args, **kwargs) @@ -250,7 +267,7 @@ class ReportForm(forms.Form): def clean(self): if self.request.limited: - raise ValidationError(_("rate-limited")) + raise ValidationError(_('rate-limited')) return super().clean() def send_report_mail(self, request): @@ -288,7 +305,7 @@ class ReportForm(forms.Form): } else: - raise Exception("Unknown kind") + raise Exception('Unknown kind') report_content( request=request, diff --git a/src/plainui/jinja2.py b/src/plainui/jinja2.py index ae9717c94..8c155ff7e 100644 --- a/src/plainui/jinja2.py +++ b/src/plainui/jinja2.py @@ -1,4 +1,9 @@ from datetime import datetime, timedelta + +from jinja2 import Environment, pass_context +from jinja2.runtime import Context + +from django.contrib.humanize.templatetags.humanize import NaturalTimeFormatter from django.contrib.messages import get_messages from django.templatetags.static import static from django.urls import reverse @@ -7,12 +12,7 @@ from django.utils.formats import localize from django.utils.functional import LazyObject from django.utils.html import json_script from django.utils.timezone import localdate, localtime -from django.utils.translation import gettext, ngettext, get_language -from django.contrib.humanize.templatetags.humanize import NaturalTimeFormatter - -from jinja2 import Environment, pass_context -from jinja2.runtime import Context - +from django.utils.translation import get_language, gettext, ngettext from modeltranslation.fields import build_localized_fieldname from modeltranslation.settings import AVAILABLE_LANGUAGES @@ -122,8 +122,8 @@ def show_vars(ctx, var=_UNSET): var = var.__dict__ ret = '' - for (k, v) in var.items(): - ret += '%r: %r\n' % (k, v) + for k, v in var.items(): + ret += f'{k!r}: {v!r}\n' return ret except (AttributeError, TypeError): @@ -182,29 +182,28 @@ class MyEnvironment(Environment): def environment(**options): - env = MyEnvironment(**{ - 'extensions': ["jinja2.ext.i18n", "jinja2.ext.debug"], - **options - }) - env.globals.update({ - 'browser_type': browser_type, - 'css_scope': css_scope, - 'active_theme': active_theme, - 'get_language': get_language, - 'get_messages': get_messages, - 'hub_absolute': hub_absolute, - 'json_script': json_script, - 'num_of_notifications': num_of_notifications, - 'num_of_unread_messages': num_of_unread_messages, - 'num_of_pending_badges': num_of_pending_badges, - 'static': static, - 'url': url, - 'show_vars': show_vars, - 'unique_id': unique_id, - 'translated_fields_for_field': translated_fields_for_field, - 'field_translation_languages': field_translation_languages, - 'now': timezone.now(), - }) + env = MyEnvironment(**{'extensions': ['jinja2.ext.i18n', 'jinja2.ext.debug'], **options}) + env.globals.update( + { + 'browser_type': browser_type, + 'css_scope': css_scope, + 'active_theme': active_theme, + 'get_language': get_language, + 'get_messages': get_messages, + 'hub_absolute': hub_absolute, + 'json_script': json_script, + 'num_of_notifications': num_of_notifications, + 'num_of_unread_messages': num_of_unread_messages, + 'num_of_pending_badges': num_of_pending_badges, + 'static': static, + 'url': url, + 'show_vars': show_vars, + 'unique_id': unique_id, + 'translated_fields_for_field': translated_fields_for_field, + 'field_translation_languages': field_translation_languages, + 'now': timezone.now(), + } + ) env.filters['strftdelta'] = custom_timedelta env.filters['strftdelta_short'] = custom_timedelta_short env.filters['strftimehm'] = custom_strftimehm diff --git a/src/plainui/management/commands/makemessages.py b/src/plainui/management/commands/makemessages.py index a9f8a891a..80aa8fe8f 100644 --- a/src/plainui/management/commands/makemessages.py +++ b/src/plainui/management/commands/makemessages.py @@ -1,10 +1,10 @@ import re +from io import StringIO +from pathlib import Path from babel.messages.extract import extract from babel.messages.pofile import read_po, write_po from jinja2.ext import babel_extract -from io import StringIO -from pathlib import Path from django.core.management.commands import makemessages diff --git a/src/plainui/tests/test_views.py b/src/plainui/tests/test_views.py index 35add5230..2d0423526 100644 --- a/src/plainui/tests/test_views.py +++ b/src/plainui/tests/test_views.py @@ -57,7 +57,7 @@ from plainui.views import ConferenceRequiredMixin, LandingView # from https://github.com/Grollicus/unittest_patterns/blob/master/unittest_patterns/__init__.py -class Pattern(object): +class Pattern: def __req__(self, lhs): return self.__eq__(lhs) @@ -66,7 +66,7 @@ class Pattern(object): # from https://github.com/Grollicus/unittest_patterns/blob/master/unittest_patterns/__init__.py class Any(Pattern): - """ Equals everything """ + """Equals everything""" def __eq__(self, rhs): return True @@ -200,7 +200,7 @@ class ViewsTestBase(TestCase): def assertSetsMessage(self, request, msg: str, level=messages.SUCCESS): msgs = list(messages.get_messages(request.wsgi_request)) - self.assertEqual(len(msgs), 1, "Expected exactly one Message") + self.assertEqual(len(msgs), 1, 'Expected exactly one Message') self.assertEqual(str(msgs[0].message), msg) self.assertEqual(msgs[0].level, level) @@ -218,20 +218,35 @@ class ViewsTest(ViewsTestBase): tag_hidden.save() event = Event( - conference=self.conf, slug='event1', assembly=assembly, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + slug='event1', + assembly=assembly, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), kind=Event.Kind.OFFICIAL, ) event.save() suggested_event = Event( - conference=self.conf, slug='suggested', assembly=assembly, name='Event1_2', is_public=True, - schedule_start=datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + slug='suggested', + assembly=assembly, + name='Event1_2', + is_public=True, + schedule_start=datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), kind=Event.Kind.OFFICIAL, ) suggested_event.save() suggested_event2 = Event( - conference=self.conf, slug='suggested2', assembly=assembly, name='Event1_3', is_public=True, - schedule_start=datetime(2020, 1, 2, 2, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + slug='suggested2', + assembly=assembly, + name='Event1_3', + is_public=True, + schedule_start=datetime(2020, 1, 2, 2, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), kind=Event.Kind.OFFICIAL, ) suggested_event2.save() @@ -280,13 +295,21 @@ class ViewsTest(ViewsTestBase): tag.save() event1 = Event( - conference=self.conf, assembly=assembly1, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly1, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event1.save() event2 = Event( - conference=self.conf, assembly=assembly1, name='Event1_2', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly1, + name='Event1_2', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() @@ -319,13 +342,21 @@ class ViewsTest(ViewsTestBase): tag_item = TagItem(tag=tag, target_type=ContentType.objects.get_for_model(Assembly), target_id=assembly.pk) tag_item.save() event1 = Event( - conference=self.conf, assembly=assembly, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event1.save() event2 = Event( - conference=self.conf, assembly=assembly, name='Event1_2', is_public=False, - schedule_start=datetime(2020, 1, 2, 0, 0, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event1_2', + is_public=False, + schedule_start=datetime(2020, 1, 2, 0, 0, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() AssemblyLikeCount(assembly1=assembly, assembly2=assembly, likes=10, like_ratio=1).save() @@ -390,23 +421,51 @@ class ViewsTest(ViewsTestBase): room_public.save() event = Event( - conference=self.conf, assembly=assembly, room=room, slug='Event1_1', name='Event1_1', is_public=True, kind=Event.Kind.ASSEMBLY, - schedule_start=datetime(2020, 1, 2, 0, 45, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + room=room, + slug='Event1_1', + name='Event1_1', + is_public=True, + kind=Event.Kind.ASSEMBLY, + schedule_start=datetime(2020, 1, 2, 0, 45, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event.save() event2 = Event( - conference=self.conf, assembly=assembly, room=room, slug='Event1_2', name='Event1_2', is_public=True, kind=Event.Kind.OFFICIAL, - schedule_start=datetime(2020, 1, 2, 0, 50, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + room=room, + slug='Event1_2', + name='Event1_2', + is_public=True, + kind=Event.Kind.OFFICIAL, + schedule_start=datetime(2020, 1, 2, 0, 50, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() event3 = Event( - conference=self.conf, assembly=assembly, room=room_public, slug='Event1_3', name='Event1_3', is_public=True, kind=Event.Kind.OFFICIAL, - schedule_start=datetime(2020, 1, 2, 0, 55, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + room=room_public, + slug='Event1_3', + name='Event1_3', + is_public=True, + kind=Event.Kind.OFFICIAL, + schedule_start=datetime(2020, 1, 2, 0, 55, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event3.save() event4 = Event( - conference=self.conf, assembly=assembly, room=room, slug='Event1_4', name='Event1_4', is_public=False, kind=Event.Kind.OFFICIAL, - schedule_start=datetime(2020, 1, 2, 1, 0, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + room=room, + slug='Event1_4', + name='Event1_4', + is_public=False, + kind=Event.Kind.OFFICIAL, + schedule_start=datetime(2020, 1, 2, 1, 0, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event4.save() @@ -748,9 +807,9 @@ class ViewsTest(ViewsTestBase): self.assertEqual(sp_de.revisions.count(), 1) # Preview - resp = self.client.post(reverse('plainui:static_page_edit', kwargs={'page_slug': sp.slug}), { - 'preview': 'true', 'title': 'New Title Preview', 'body': 'New Body Preview' - }) + resp = self.client.post( + reverse('plainui:static_page_edit', kwargs={'page_slug': sp.slug}), {'preview': 'true', 'title': 'New Title Preview', 'body': 'New Body Preview'} + ) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['page'], sp) self.assertEqual(resp.context_data['page_slug'], sp.slug) @@ -851,8 +910,9 @@ class ViewsTest(ViewsTestBase): self.assertEqual(spr.author, self.user) with override_settings(STATIC_PAGE_LOCALIZED_BY_DEFAULT=True): - resp = self.client.post(reverse('plainui:static_page_edit', kwargs={'page_slug': 'test_new2'}), - {'title': 'Some New Page!', 'body': 'Some New Page!'}) + resp = self.client.post( + reverse('plainui:static_page_edit', kwargs={'page_slug': 'test_new2'}), {'title': 'Some New Page!', 'body': 'Some New Page!'} + ) self.assertRedirects(resp, reverse('plainui:static_page', kwargs={'page_slug': 'test_new2'})) sp_new2 = StaticPage.objects.get(slug='test_new2') @@ -860,19 +920,18 @@ class ViewsTest(ViewsTestBase): self.assertEqual(sp_new2.language, 'en') # page links to nonexistent page, that then gets created, after which the link should be updated to refrect that the link target now exists - resp = self.client.post(reverse('plainui:static_page_edit', kwargs={'page_slug': 'test_linking'}), { - 'title': 'Linking to other Pages test!', - 'body': '[[test_linking_second_page]]' - }) + resp = self.client.post( + reverse('plainui:static_page_edit', kwargs={'page_slug': 'test_linking'}), + {'title': 'Linking to other Pages test!', 'body': '[[test_linking_second_page]]'}, + ) self.assertRedirects(resp, reverse('plainui:static_page', kwargs={'page_slug': 'test_linking'})) self.assertSetsMessage(resp, 'Created Wiki Page', level=messages.SUCCESS) sp_new = StaticPage.objects.get(slug='test_linking') self.assertIn('internal-nonexist', sp_new.body_html) self.assertEqual(MarkdownMeta.objects.filter(src_type=ContentType.objects.get_for_model(StaticPage), src_object_id=sp_new.pk).count(), 1) - resp = self.client.post(reverse('plainui:static_page_edit', kwargs={'page_slug': 'test_linking_second_page'}), { - 'title': 'Linked to test!', - 'body': 'asdf' - }) + resp = self.client.post( + reverse('plainui:static_page_edit', kwargs={'page_slug': 'test_linking_second_page'}), {'title': 'Linked to test!', 'body': 'asdf'} + ) sp_new.refresh_from_db() self.assertNotIn('internal-nonexist', sp_new.body_html) self.assertIn('internal', sp_new.body_html) @@ -990,8 +1049,12 @@ class ViewsTest(ViewsTestBase): assembly = Assembly(conference=self.conf, slug='assembly1', name='asdf', state_assembly=Assembly.State.PLACED) assembly.save() event = Event( - conference=self.conf, assembly=assembly, name='asdf', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='asdf', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event.save() tag = ConferenceTag(conference=self.conf, slug='aSdf', is_public=True) @@ -1005,31 +1068,40 @@ class ViewsTest(ViewsTestBase): revision.set_public() self.assertNeedsLogin(reverse('plainui:search'), data={'q': 'asdf'}, post=True) - resp = self.client.post(reverse('plainui:search'), { - 'q': 'asdf' - }) + resp = self.client.post(reverse('plainui:search'), {'q': 'asdf'}) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['search_query'], 'asdf') - self.assertEqual(sorted(resp.context_data['search_results'], key=lambda r: r['type']), [ - {'type': 'Assembly', 'item': assembly}, - {'type': 'ConferenceTag', 'item': tag}, - {'type': 'ConferenceTrack', 'item': track}, - {'type': 'Event', 'item': event}, - {'type': 'StaticPage', 'item': page}, - ]) + self.assertEqual( + sorted(resp.context_data['search_results'], key=lambda r: r['type']), + [ + {'type': 'Assembly', 'item': assembly}, + {'type': 'ConferenceTag', 'item': tag}, + {'type': 'ConferenceTrack', 'item': track}, + {'type': 'Event', 'item': event}, + {'type': 'StaticPage', 'item': page}, + ], + ) @override_locale('en') def test_ProfileView_get(self): assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state_assembly=Assembly.State.PLACED) assembly.save() event = Event( - conference=self.conf, assembly=assembly, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event.save() event2 = Event( - conference=self.conf, assembly=assembly, name='Event1_2', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event1_2', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() badge = Badge(conference=self.conf, name='badge-name', issuing_assembly=assembly) @@ -1075,18 +1147,25 @@ class ViewsTest(ViewsTestBase): assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state_assembly=Assembly.State.PLACED) assembly.save() event = Event( - conference=self.conf, assembly=assembly, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event.save() self.assertNeedsLogin(reverse('plainui:userprofile'), post=True, check_user=True) - resp = self.client.post(reverse('plainui:userprofile'), { - 'description_de': 'new_description de', - 'description_en': 'new_description en', - 'pronouns': 'they', - 'timezone': 'Europe/Berlin', - }) + resp = self.client.post( + reverse('plainui:userprofile'), + { + 'description_de': 'new_description de', + 'description_en': 'new_description en', + 'pronouns': 'they', + 'timezone': 'Europe/Berlin', + }, + ) self.assertRedirects(resp, reverse('plainui:userprofile')) self.user.refresh_from_db() self.conference_member.refresh_from_db() @@ -1107,29 +1186,38 @@ class ViewsTest(ViewsTestBase): self.assertNeedsLogin(reverse('plainui:modify_theme'), post=True) # setting theme works - resp = self.client.post(reverse('plainui:modify_theme'), { - 'theme': PlatformUser.Theme.LIGHT, - 'next': reverse('plainui:landing'), - }) + resp = self.client.post( + reverse('plainui:modify_theme'), + { + 'theme': PlatformUser.Theme.LIGHT, + 'next': reverse('plainui:landing'), + }, + ) self.assertRedirects(resp, reverse('plainui:landing')) self.user.refresh_from_db() self.assertEqual(self.user.theme, PlatformUser.Theme.LIGHT) self.assertEqual(self.client.session['theme'], PlatformUser.Theme.LIGHT) # invalid destination redirects to index - resp = self.client.post(reverse('plainui:modify_theme'), { - 'theme': PlatformUser.Theme.LIGHT, - 'next': 'https://www.google.de/', - }) + resp = self.client.post( + reverse('plainui:modify_theme'), + { + 'theme': PlatformUser.Theme.LIGHT, + 'next': 'https://www.google.de/', + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.user.refresh_from_db() self.assertEqual(self.user.theme, PlatformUser.Theme.LIGHT) self.assertEqual(self.client.session['theme'], PlatformUser.Theme.LIGHT) # invalid theme does not change anything, next defaults to index - resp = self.client.post(reverse('plainui:modify_theme'), { - 'theme': 'doesnotexist', - }) + resp = self.client.post( + reverse('plainui:modify_theme'), + { + 'theme': 'doesnotexist', + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.user.refresh_from_db() self.assertEqual(self.user.theme, PlatformUser.Theme.LIGHT) @@ -1142,13 +1230,16 @@ class ViewsTest(ViewsTestBase): self.conf.start = datetime(2020, 12, 27, 0, 0, 0, tzinfo=UTC) self.conf.end = datetime(2020, 12, 31, 23, 59, 0, tzinfo=UTC) self.conf.save() - valid_token = jwt.encode({ - 'aud': self.conf.slug, - 'exp': datetime.now(UTC) + timedelta(hours=1), - 'iat': datetime.now(UTC) - timedelta(hours=1), - 'iss': 'tickets.events.ccc.de', - 'uid': 'blblbl123', - }, key='^v^') + valid_token = jwt.encode( + { + 'aud': self.conf.slug, + 'exp': datetime.now(UTC) + timedelta(hours=1), + 'iat': datetime.now(UTC) - timedelta(hours=1), + 'iss': 'tickets.events.ccc.de', + 'uid': 'blblbl123', + }, + key='^v^', + ) invalid_token = ( 'fyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJzbHVnMSIsImV4cCI6MTYwODc2ODMwNywiaWF0IjoxNjA' '4NzYxMTA3LCJpc3MiOiJ0aWNrZXRzLmV2ZW50cy5jY2MuZGUiLCJ1aWQiOiJibGJsYmwxMjMifQ.QglU9dLHbIAOJ2e8hj_HSc6rZBpAcTrPbvY2H7HXnFw' @@ -1211,13 +1302,16 @@ class ViewsTest(ViewsTestBase): self.user.set_password('test') self.user.save() ConferenceMember.objects.all().delete() - valid_token = jwt.encode({ - 'aud': self.conf.slug, - 'exp': datetime.now(UTC) + timedelta(hours=1), - 'iat': datetime.now(UTC) - timedelta(hours=1), - 'iss': 'tickets.events.ccc.de', - 'uid': 'blblbl123', - }, key='^v^') + valid_token = jwt.encode( + { + 'aud': self.conf.slug, + 'exp': datetime.now(UTC) + timedelta(hours=1), + 'iat': datetime.now(UTC) - timedelta(hours=1), + 'iss': 'tickets.events.ccc.de', + 'uid': 'blblbl123', + }, + key='^v^', + ) # get-redirect resp = self.client.get(reverse('plainui:redeem_token_add_to_user')) @@ -1228,11 +1322,7 @@ class ViewsTest(ViewsTestBase): 'fyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJzbHVnMSIsImV4cCI6MTYwODc2ODMwNywiaWF0IjoxNjA' '4NzYxMTA3LCJpc3MiOiJ0aWNrZXRzLmV2ZW50cy5jY2MuZGUiLCJ1aWQiOiJibGJsYmwxMjMifQ.QglU9dLHbIAOJ2e8hj_HSc6rZBpAcTrPbvY2H7HXnFw' ) - resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), { - 'token': invalid_token, - 'username': self.user.username, - 'password': 'test' - }) + resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), {'token': invalid_token, 'username': self.user.username, 'password': 'test'}) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['step'], 'user') self.assertEqual(list(resp.context_data['form_add'].non_field_errors()), ['Could not verify ticket. (Invalid Ticket)']) @@ -1241,11 +1331,7 @@ class ViewsTest(ViewsTestBase): # conference already over self.conf.end = datetime(2020, 12, 28, 0, 0, 0, tzinfo=UTC) self.conf.save() - resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password': 'test' - }) + resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), {'token': valid_token, 'username': self.user.username, 'password': 'test'}) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['step'], 'user') self.assertEqual(list(resp.context_data['form_add'].non_field_errors()), ['The Conference is over!']) @@ -1254,11 +1340,7 @@ class ViewsTest(ViewsTestBase): self.conf.save() # submit valid token and join conference - resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password': 'test' - }) + resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), {'token': valid_token, 'username': self.user.username, 'password': 'test'}) self.assertRedirects(resp, reverse('plainui:index')) self.assertTrue(ConferenceMemberTicket.objects.filter(conference=self.conf, user=self.user, ident='blblbl123').exists()) self.assertTrue(ConferenceMember.objects.filter(conference=self.conf, user=self.user).exists()) @@ -1266,11 +1348,7 @@ class ViewsTest(ViewsTestBase): # user already has a ConferenceMember but no ticket -> add ticket but don't create second ConferenceMember ConferenceMemberTicket.objects.all().delete() self.assertEqual(ConferenceMember.objects.filter(conference=self.conf, user=self.user).count(), 1) - resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password': 'test' - }) + resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), {'token': valid_token, 'username': self.user.username, 'password': 'test'}) self.assertRedirects(resp, reverse('plainui:index')) self.assertTrue(ConferenceMemberTicket.objects.filter(conference=self.conf, user=self.user, ident='blblbl123').exists()) self.assertEqual(ConferenceMember.objects.filter(conference=self.conf, user=self.user).count(), 1) @@ -1280,20 +1358,12 @@ class ViewsTest(ViewsTestBase): for i in range(5): ConferenceMemberTicket.objects.all().delete() ConferenceMember.objects.all().delete() - resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password': 'test' - }) + resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), {'token': valid_token, 'username': self.user.username, 'password': 'test'}) self.assertRedirects(resp, reverse('plainui:index')) ConferenceMemberTicket.objects.all().delete() ConferenceMember.objects.all().delete() - resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password': 'test' - }) + resp = self.client.post(reverse('plainui:redeem_token_add_to_user'), {'token': valid_token, 'username': self.user.username, 'password': 'test'}) self.assertIsInstance(resp, HttpResponseRateLimited) @override_settings(LANGUAGE_CODE='en', PRETIX_SECRET_KEY='^v^', PRETIX_ISSUER='tickets.events.ccc.de', AUTH_PASSWORD_VALIDATORS=[]) @@ -1308,37 +1378,34 @@ class ViewsTest(ViewsTestBase): 'fyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJzbHVnMSIsImV4cCI6MTYwODc2ODMwNywiaWF0IjoxNjA' '4NzYxMTA3LCJpc3MiOiJ0aWNrZXRzLmV2ZW50cy5jY2MuZGUiLCJ1aWQiOiJibGJsYmwxMjMifQ.QglU9dLHbIAOJ2e8hj_HSc6rZBpAcTrPbvY2H7HXnFw' ) - valid_token = jwt.encode({ - 'aud': self.conf.slug, - 'exp': datetime.now(UTC) + timedelta(hours=1), - 'iat': datetime.now(UTC) - timedelta(hours=1), - 'iss': 'tickets.events.ccc.de', - 'uid': 'blblbl123', - }, key='^v^') + valid_token = jwt.encode( + { + 'aud': self.conf.slug, + 'exp': datetime.now(UTC) + timedelta(hours=1), + 'iat': datetime.now(UTC) - timedelta(hours=1), + 'iss': 'tickets.events.ccc.de', + 'uid': 'blblbl123', + }, + key='^v^', + ) # get-redirect resp = self.client.get(reverse('plainui:redeem_token_create_user')) self.assertRedirects(resp, reverse('plainui:redeem_token')) # invalid token - resp = self.client.post(reverse('plainui:redeem_token_create_user'), { - 'token': invalid_token, - 'username': 'testuser2', - 'password1': 'test', - 'password2': 'test' - }) + resp = self.client.post( + reverse('plainui:redeem_token_create_user'), {'token': invalid_token, 'username': 'testuser2', 'password1': 'test', 'password2': 'test'} + ) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['step'], 'user') self.assertTrue(resp.context_data['form_add']) self.assertEqual(list(resp.context_data['form_create'].non_field_errors()), ['Could not verify ticket. (Invalid Ticket)']) # submit valid token but username exists already - resp = self.client.post(reverse('plainui:redeem_token_create_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password1': 'test', - 'password2': 'test' - }) + resp = self.client.post( + reverse('plainui:redeem_token_create_user'), {'token': valid_token, 'username': self.user.username, 'password1': 'test', 'password2': 'test'} + ) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['step'], 'user') self.assertTrue(resp.context_data['form_add']) @@ -1347,12 +1414,9 @@ class ViewsTest(ViewsTestBase): # valid token but conference already over self.conf.end = datetime(2020, 12, 28, 0, 0, 0, tzinfo=UTC) self.conf.save() - resp = self.client.post(reverse('plainui:redeem_token_create_user'), { - 'token': valid_token, - 'username': self.user.username, - 'password1': 'test', - 'password2': 'test' - }) + resp = self.client.post( + reverse('plainui:redeem_token_create_user'), {'token': valid_token, 'username': self.user.username, 'password1': 'test', 'password2': 'test'} + ) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['step'], 'user') self.assertTrue(resp.context_data['form_add']) @@ -1361,12 +1425,9 @@ class ViewsTest(ViewsTestBase): self.conf.save() # submit valid token and join conference - resp = self.client.post(reverse('plainui:redeem_token_create_user'), { - 'token': valid_token, - 'username': 'testuser2', - 'password1': 'test', - 'password2': 'test' - }) + resp = self.client.post( + reverse('plainui:redeem_token_create_user'), {'token': valid_token, 'username': 'testuser2', 'password1': 'test', 'password2': 'test'} + ) self.assertRedirects(resp, reverse('plainui:index')) user = PlatformUser.objects.get(username='testuser2') self.assertTrue(ConferenceMemberTicket.objects.filter(conference=self.conf, user=user, ident='blblbl123').exists()) @@ -1384,13 +1445,16 @@ class ViewsTest(ViewsTestBase): 'fyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJzbHVnMSIsImV4cCI6MTYwODc2ODMwNywiaWF0IjoxNjA' '4NzYxMTA3LCJpc3MiOiJ0aWNrZXRzLmV2ZW50cy5jY2MuZGUiLCJ1aWQiOiJibGJsYmwxMjMifQ.QglU9dLHbIAOJ2e8hj_HSc6rZBpAcTrPbvY2H7HXnFw' ) - valid_token = jwt.encode({ - 'aud': self.conf.slug, - 'exp': datetime.now(UTC) + timedelta(hours=1), - 'iat': datetime.now(UTC) - timedelta(hours=1), - 'iss': 'tickets.events.ccc.de', - 'uid': 'blblbl123', - }, key='^v^') + valid_token = jwt.encode( + { + 'aud': self.conf.slug, + 'exp': datetime.now(UTC) + timedelta(hours=1), + 'iat': datetime.now(UTC) - timedelta(hours=1), + 'iss': 'tickets.events.ccc.de', + 'uid': 'blblbl123', + }, + key='^v^', + ) self.assertNeedsLogin(reverse('plainui:redeem_token_loggedin'), check_conference_member=False, check_user=True) resp = self.client.get(reverse('plainui:redeem_token_loggedin')) @@ -1399,25 +1463,19 @@ class ViewsTest(ViewsTestBase): # invalid token self.conf.end = datetime(2020, 12, 28, 0, 0, 0, tzinfo=UTC) self.conf.save() - resp = self.client.post(reverse('plainui:redeem_token_loggedin'), { - 'token': invalid_token - }) + resp = self.client.post(reverse('plainui:redeem_token_loggedin'), {'token': invalid_token}) self.assertRedirects(resp, reverse('plainui:redeem_token')) self.assertSetsMessage(resp, 'The Conference is over!', level=messages.ERROR) self.conf.end = datetime(2020, 12, 31, 23, 59, 0, tzinfo=UTC) self.conf.save() # valid token but conference over - resp = self.client.post(reverse('plainui:redeem_token_loggedin'), { - 'token': invalid_token - }) + resp = self.client.post(reverse('plainui:redeem_token_loggedin'), {'token': invalid_token}) self.assertRedirects(resp, reverse('plainui:redeem_token')) self.assertSetsMessage(resp, 'Could not verify ticket. (Invalid Ticket)', level=messages.ERROR) # valid token - resp = self.client.post(reverse('plainui:redeem_token_loggedin'), { - 'token': valid_token - }) + resp = self.client.post(reverse('plainui:redeem_token_loggedin'), {'token': valid_token}) self.assertTrue(ConferenceMemberTicket.objects.filter(conference=self.conf, user=self.user, ident='blblbl123').exists()) self.assertTrue(ConferenceMember.objects.filter(conference=self.conf, user=self.user).exists()) self.assertRedirects(resp, reverse('plainui:index')) @@ -1444,42 +1502,54 @@ class ViewsTest(ViewsTestBase): 'fyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJzbHVnMSIsImV4cCI6MTYwODc2ODMwNywiaWF0IjoxNjA' '4NzYxMTA3LCJpc3MiOiJ0aWNrZXRzLmV2ZW50cy5jY2MuZGUiLCJ1aWQiOiJibGJsYmwxMjMifQ.QglU9dLHbIAOJ2e8hj_HSc6rZBpAcTrPbvY2H7HXnFw' ) - resp = self.client.post(reverse('plainui:token_password_reset'), { - 'jwt': invalid_token, - 'username': self.user.username, - 'new_password1': 'new', - 'new_password2': 'new', - }) + resp = self.client.post( + reverse('plainui:token_password_reset'), + { + 'jwt': invalid_token, + 'username': self.user.username, + 'new_password1': 'new', + 'new_password2': 'new', + }, + ) self.assertTrue(resp.context_data['form'].errors) self.user.refresh_from_db() self.assertFalse(self.user.check_password('new')) - resp = self.client.post(reverse('plainui:token_password_reset'), { - 'jwt': valid_token, - 'username': 'doesnotexist', - 'new_password1': 'new', - 'new_password2': 'new', - }) + resp = self.client.post( + reverse('plainui:token_password_reset'), + { + 'jwt': valid_token, + 'username': 'doesnotexist', + 'new_password1': 'new', + 'new_password2': 'new', + }, + ) self.assertTrue(resp.context_data['form'].errors) self.user.refresh_from_db() self.assertFalse(self.user.check_password('new')) - resp = self.client.post(reverse('plainui:token_password_reset'), { - 'jwt': valid_token, - 'username': self.user.username, - 'new_password1': 'new', - 'new_password2': 'new2', - }) + resp = self.client.post( + reverse('plainui:token_password_reset'), + { + 'jwt': valid_token, + 'username': self.user.username, + 'new_password1': 'new', + 'new_password2': 'new2', + }, + ) self.assertTrue(resp.context_data['form'].errors) self.user.refresh_from_db() self.assertFalse(self.user.check_password('new')) - resp = self.client.post(reverse('plainui:token_password_reset'), { - 'jwt': valid_token, - 'username': self.user.username, - 'new_password1': 'new', - 'new_password2': 'new', - }) + resp = self.client.post( + reverse('plainui:token_password_reset'), + { + 'jwt': valid_token, + 'username': self.user.username, + 'new_password1': 'new', + 'new_password2': 'new', + }, + ) self.assertRedirects(resp, reverse('plainui:login')) self.user.refresh_from_db() self.assertTrue(self.user.check_password('new')) @@ -1498,22 +1568,33 @@ class ViewsTest(ViewsTestBase): assembly2 = Assembly(conference=self.conf, slug='assembly2', name='Assembly2', state_assembly=Assembly.State.PLACED) assembly2.save() event1 = Event( - conference=self.conf, assembly=assembly1, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly1, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event1.save() event2 = Event( - conference=self.conf, assembly=assembly1, name='Event1_2', is_public=True, - schedule_start=datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly1, + name='Event1_2', + is_public=True, + schedule_start=datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() self.assertNeedsLogin(reverse('plainui:modify_favorites'), post=True, check_user=True) - resp = self.client.post(reverse('plainui:modify_favorites'), { - 'mode': 'add', - 'type': 'assembly', - 'id': str(assembly1.pk), - }) + resp = self.client.post( + reverse('plainui:modify_favorites'), + { + 'mode': 'add', + 'type': 'assembly', + 'id': str(assembly1.pk), + }, + ) self.assertRedirects(resp, reverse('plainui:userprofile')) self.assertTrue(self.user.favorite_assemblies.filter(pk=assembly1.pk).exists()) self.assertEqual(self.client.session['fav_a'], [str(assembly1.pk)]) @@ -1522,23 +1603,29 @@ class ViewsTest(ViewsTestBase): self.assertEqual(lc.assembly2, assembly1) self.assertEqual(lc.likes, 1) - resp = self.client.post(reverse('plainui:modify_favorites'), { - 'mode': 'add', - 'type': 'assembly', - 'id': str(assembly2.pk), - }) + resp = self.client.post( + reverse('plainui:modify_favorites'), + { + 'mode': 'add', + 'type': 'assembly', + 'id': str(assembly2.pk), + }, + ) self.assertEqual(AssemblyLikeCount.objects.count(), 4) self.assertEqual(AssemblyLikeCount.objects.get(assembly1=assembly1, assembly2=assembly1).likes, 1) self.assertEqual(AssemblyLikeCount.objects.get(assembly1=assembly2, assembly2=assembly2).likes, 1) self.assertEqual(AssemblyLikeCount.objects.get(assembly1=assembly1, assembly2=assembly2).likes, 1) self.assertEqual(AssemblyLikeCount.objects.get(assembly1=assembly2, assembly2=assembly1).likes, 1) - resp = self.client.post(reverse('plainui:modify_favorites'), { - 'mode': 'remove', - 'type': 'assembly', - 'id': str(assembly1.pk), - 'next': reverse('plainui:index'), - }) + resp = self.client.post( + reverse('plainui:modify_favorites'), + { + 'mode': 'remove', + 'type': 'assembly', + 'id': str(assembly1.pk), + 'next': reverse('plainui:index'), + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.assertFalse(self.user.favorite_assemblies.filter(pk=assembly1.pk).exists()) self.assertEqual(self.client.session['fav_a'], [str(assembly2.pk)]) @@ -1547,12 +1634,15 @@ class ViewsTest(ViewsTestBase): self.assertEqual(AssemblyLikeCount.objects.get(assembly1=assembly1, assembly2=assembly2).likes, 0) self.assertEqual(AssemblyLikeCount.objects.get(assembly1=assembly2, assembly2=assembly1).likes, 0) - resp = self.client.post(reverse('plainui:modify_favorites'), { - 'mode': 'add', - 'type': 'event', - 'id': str(event1.pk), - 'next': reverse('plainui:index'), - }) + resp = self.client.post( + reverse('plainui:modify_favorites'), + { + 'mode': 'add', + 'type': 'event', + 'id': str(event1.pk), + 'next': reverse('plainui:index'), + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.assertTrue(self.user.favorite_events.filter(pk=event1.pk).exists()) self.assertEqual(self.client.session['fav_e'], [str(event1.pk)]) @@ -1561,23 +1651,29 @@ class ViewsTest(ViewsTestBase): self.assertEqual(lc.event2, event1) self.assertEqual(lc.likes, 1) - resp = self.client.post(reverse('plainui:modify_favorites'), { - 'mode': 'add', - 'type': 'event', - 'id': str(event2.pk), - 'next': reverse('plainui:index'), - }) + resp = self.client.post( + reverse('plainui:modify_favorites'), + { + 'mode': 'add', + 'type': 'event', + 'id': str(event2.pk), + 'next': reverse('plainui:index'), + }, + ) self.assertEqual(EventLikeCount.objects.count(), 4) self.assertEqual(EventLikeCount.objects.get(event1=event1, event2=event1).likes, 1) self.assertEqual(EventLikeCount.objects.get(event1=event2, event2=event2).likes, 1) self.assertEqual(EventLikeCount.objects.get(event1=event1, event2=event2).likes, 1) self.assertEqual(EventLikeCount.objects.get(event1=event2, event2=event1).likes, 1) - resp = self.client.post(reverse('plainui:modify_favorites'), { - 'mode': 'remove', - 'type': 'event', - 'id': str(event1.pk), - }) + resp = self.client.post( + reverse('plainui:modify_favorites'), + { + 'mode': 'remove', + 'type': 'event', + 'id': str(event1.pk), + }, + ) self.assertRedirects(resp, reverse('plainui:userprofile')) self.assertFalse(self.user.favorite_events.filter(pk=event1.pk).exists()) self.assertEqual(self.client.session['fav_e'], [str(event2.pk)]) @@ -1596,16 +1692,22 @@ class ViewsTest(ViewsTestBase): @override_locale('en') def test_ModifyLanguageView_post(self): self.assertNeedsLogin(reverse('plainui:modify_language'), post=True) - resp = self.client.post(reverse('plainui:modify_language'), { - 'language': 'en', - 'next': reverse('plainui:landing'), - }) + resp = self.client.post( + reverse('plainui:modify_language'), + { + 'language': 'en', + 'next': reverse('plainui:landing'), + }, + ) self.assertRedirects(resp, translate_url(reverse('plainui:landing'), 'en')) self.assertEqual(self.client.cookies['language_cookie'].value, 'en') - resp = self.client.post(reverse('plainui:modify_language'), { - 'language': 'de', - }) + resp = self.client.post( + reverse('plainui:modify_language'), + { + 'language': 'de', + }, + ) self.assertRedirects(resp, translate_url(reverse('plainui:index'), 'de')) self.assertEqual(self.client.cookies['language_cookie'].value, 'de') @@ -1620,25 +1722,35 @@ class ViewsTest(ViewsTestBase): assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state_assembly=Assembly.State.PLACED) assembly.save() event = Event( - conference=self.conf, assembly=assembly, name='Event1_1', is_public=True, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event1_1', + is_public=True, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event.save() self.assertNeedsLogin(reverse('plainui:modify_personal_calendar'), post=True, check_user=True) - resp = self.client.post(reverse('plainui:modify_personal_calendar'), { - 'mode': 'add', - 'id': str(event.pk), - 'next': reverse('plainui:index'), - }) + resp = self.client.post( + reverse('plainui:modify_personal_calendar'), + { + 'mode': 'add', + 'id': str(event.pk), + 'next': reverse('plainui:index'), + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.assertTrue(self.user.calendar_events.filter(pk=event.pk).exists()) self.assertEqual(self.client.session['sch_e'], [str(event.pk)]) - resp = self.client.post(reverse('plainui:modify_personal_calendar'), { - 'mode': 'remove', - 'id': str(event.pk), - }) + resp = self.client.post( + reverse('plainui:modify_personal_calendar'), + { + 'mode': 'remove', + 'id': str(event.pk), + }, + ) self.assertRedirects(resp, reverse('plainui:userprofile')) self.assertFalse(self.user.calendar_events.filter(pk=event.pk).exists()) self.assertEqual(self.client.session['sch_e'], []) @@ -1694,12 +1806,15 @@ class ViewsTest(ViewsTestBase): user2.save() self.assertNeedsLogin(reverse('plainui:personal_message_send'), post=True, check_user=True) - resp = self.client.post(reverse('plainui:personal_message_send'), { - 'in_reply_to': '', - 'recipient': 'testuser2', - 'subject': 'Subj', - 'body': 'Body', - }) + resp = self.client.post( + reverse('plainui:personal_message_send'), + { + 'in_reply_to': '', + 'recipient': 'testuser2', + 'subject': 'Subj', + 'body': 'Body', + }, + ) self.assertRedirects(resp, reverse('plainui:personal_message')) new_dm = DirectMessage.objects.get() self.assertEqual(new_dm.conference, self.conf) @@ -1711,12 +1826,15 @@ class ViewsTest(ViewsTestBase): received_dm = DirectMessage(conference=self.conf, sender=user2, recipient=self.user) received_dm.save() - resp = self.client.post(reverse('plainui:personal_message_send'), { - 'in_reply_to': str(received_dm.pk), - 'recipient': 'testuser2', - 'subject': 'Subj2', - 'body': 'Body2', - }) + resp = self.client.post( + reverse('plainui:personal_message_send'), + { + 'in_reply_to': str(received_dm.pk), + 'recipient': 'testuser2', + 'subject': 'Subj2', + 'body': 'Body2', + }, + ) self.assertRedirects(resp, reverse('plainui:personal_message')) new_dm = DirectMessage.objects.get(in_reply_to=received_dm) self.assertEqual(new_dm.conference, self.conf) @@ -1729,12 +1847,15 @@ class ViewsTest(ViewsTestBase): self.assertTrue(received_dm.has_responded) self.assertSetsMessage(resp, 'Message sent.') - resp = self.client.post(reverse('plainui:personal_message_send'), { - 'in_reply_to': '', - 'recipient': 'Unknown User', - 'subject': '', - 'body': '', - }) + resp = self.client.post( + reverse('plainui:personal_message_send'), + { + 'in_reply_to': '', + 'recipient': 'Unknown User', + 'subject': '', + 'body': '', + }, + ) form = resp.context_data['form'] self.assertEqual(form.errors['recipient'], ['Unknown User!']) self.assertEqual(form.errors['subject'], ['This field is required.']) @@ -1743,12 +1864,15 @@ class ViewsTest(ViewsTestBase): DirectMessage.objects.all().delete() self.user.shadow_banned = True self.user.save() - resp = self.client.post(reverse('plainui:personal_message_send'), { - 'in_reply_to': '', - 'recipient': 'testuser2', - 'subject': 'subject', - 'body': 'body', - }) + resp = self.client.post( + reverse('plainui:personal_message_send'), + { + 'in_reply_to': '', + 'recipient': 'testuser2', + 'subject': 'subject', + 'body': 'body', + }, + ) self.assertRedirects(resp, reverse('plainui:personal_message')) new_dm = DirectMessage.objects.get(subject='subject') self.user.refresh_from_db() @@ -1759,20 +1883,26 @@ class ViewsTest(ViewsTestBase): self.user.save() with override_settings(RATELIMIT_ENABLE=True): for i in range(3): - resp = self.client.post(reverse('plainui:personal_message_send'), { - 'in_reply_to': '', - 'recipient': 'testuser2', - 'subject': 'subject' + str(i), - 'body': 'body', - }) + resp = self.client.post( + reverse('plainui:personal_message_send'), + { + 'in_reply_to': '', + 'recipient': 'testuser2', + 'subject': 'subject' + str(i), + 'body': 'body', + }, + ) self.assertRedirects(resp, reverse('plainui:personal_message')) - resp = self.client.post(reverse('plainui:personal_message_send'), { - 'in_reply_to': '', - 'recipient': 'testuser2', - 'subject': 'subject-ratelimited', - 'body': 'body-ratelimited', - }) + resp = self.client.post( + reverse('plainui:personal_message_send'), + { + 'in_reply_to': '', + 'recipient': 'testuser2', + 'subject': 'subject-ratelimited', + 'body': 'body-ratelimited', + }, + ) self.assertIsInstance(resp, HttpResponseForbidden) @override_locale('en') @@ -1844,11 +1974,11 @@ class ViewsTest(ViewsTestBase): other_user = PlatformUser(username='other_user') other_user.save() - e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title="Title1", text="some text") + e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title='Title1', text='some text') e1.save() - e2 = BulletinBoardEntry(conference=self.conf, owner=self.user, is_public=False, title="Title2", text="other text") + e2 = BulletinBoardEntry(conference=self.conf, owner=self.user, is_public=False, title='Title2', text='other text') e2.save() - e3 = BulletinBoardEntry(conference=self.conf, owner=self.user, title="Title3", text="different text") + e3 = BulletinBoardEntry(conference=self.conf, owner=self.user, title='Title3', text='different text') e3.save() self.assertNeedsLogin(reverse('plainui:board')) @@ -1862,9 +1992,9 @@ class ViewsTest(ViewsTestBase): self.user.shadow_banned = True self.user.save() - e4 = BulletinBoardEntry(conference=self.conf, owner=self.user, title="Title4", text="some text", hidden=True) + e4 = BulletinBoardEntry(conference=self.conf, owner=self.user, title='Title4', text='some text', hidden=True) e4.save() - e5 = BulletinBoardEntry(conference=self.conf, owner=other_user, title="Title5", text="some text", hidden=True) + e5 = BulletinBoardEntry(conference=self.conf, owner=other_user, title='Title5', text='some text', hidden=True) e5.save() resp = self.client.get(reverse('plainui:board')) self.assertEqual(list(resp.context_data['board']), [e4, e3, e1]) @@ -1873,9 +2003,9 @@ class ViewsTest(ViewsTestBase): def test_BoardPrivateView(self): user2 = PlatformUser(username='testuser2') user2.save() - e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title="Title1", text="some text", hidden=True) + e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title='Title1', text='some text', hidden=True) e1.save() - e2 = BulletinBoardEntry(conference=self.conf, owner=self.user, is_public=False, title="Title2", text="other text") + e2 = BulletinBoardEntry(conference=self.conf, owner=self.user, is_public=False, title='Title2', text='other text') e2.save() e_other_owner = BulletinBoardEntry(conference=self.conf, owner=user2) e_other_owner.save() @@ -1888,7 +2018,7 @@ class ViewsTest(ViewsTestBase): @override_locale('en') @patch('modeltranslation.settings.AVAILABLE_LANGUAGES', ['en', 'de']) def test_BoardEntryEditView_get(self): - e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, is_public=False, title="Title1", text_de="some de text", text_en="some en text") + e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, is_public=False, title='Title1', text_de='some de text', text_en='some en text') e1.save() self.assertNeedsLogin(reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)}), check_user=True) @@ -1913,19 +2043,22 @@ class ViewsTest(ViewsTestBase): def test_BoardEntryEditView_post(self): user2 = PlatformUser(username='testuser2') user2.save() - e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title="Title1", text="some text") + e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title='Title1', text='some text') e1.save() e2 = BulletinBoardEntry(conference=self.conf, owner=user2) e2.save() # edit entry with checked is_public self.assertNeedsLogin(reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)}), post=True, check_user=True) - resp = self.client.post(reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)}), { - 'title': 'Edit Title', - 'text_de': 'blblblblbl', - 'text_en': 'blblblbl EN', - 'is_public': 'on', - }) + resp = self.client.post( + reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)}), + { + 'title': 'Edit Title', + 'text_de': 'blblblblbl', + 'text_en': 'blblblbl EN', + 'is_public': 'on', + }, + ) self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)})) e1.refresh_from_db() self.assertEqual(e1.is_public, True) @@ -1936,30 +2069,39 @@ class ViewsTest(ViewsTestBase): self.assertSetsMessage(resp, 'Bulletin Board Entry updated.') # unchecked is_public - resp = self.client.post(reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)}), { - 'title': 'Edit Title', - 'text_de': 'blblblblbl', - 'text_en': 'blblblblbl ENL', - }) + resp = self.client.post( + reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)}), + { + 'title': 'Edit Title', + 'text_de': 'blblblblbl', + 'text_en': 'blblblblbl ENL', + }, + ) self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'id': str(e1.pk)})) e1.refresh_from_db() self.assertEqual(e1.is_public, False) # can't edit other users entries - resp = self.client.post(reverse('plainui:board_entry_edit', kwargs={'id': str(e2.pk)}), { - 'title': 'Edit Title', - 'text_de': 'blblblblbl', - 'text_en': 'blblblblbl e', - }) + resp = self.client.post( + reverse('plainui:board_entry_edit', kwargs={'id': str(e2.pk)}), + { + 'title': 'Edit Title', + 'text_de': 'blblblblbl', + 'text_en': 'blblblblbl e', + }, + ) self.assertEqual(resp.status_code, 404) # create new entry self.assertNeedsLogin(reverse('plainui:board_entry_new'), post=True, check_user=True) - resp = self.client.post(reverse('plainui:board_entry_new'), { - 'title': 'New Entry', - 'text_de': 'some texty text de', - 'text_en': 'some texty text', - }) + resp = self.client.post( + reverse('plainui:board_entry_new'), + { + 'title': 'New Entry', + 'text_de': 'some texty text de', + 'text_en': 'some texty text', + }, + ) new_entry = BulletinBoardEntry.objects.exclude(pk__in=[e1.pk, e2.pk]).get() self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'id': str(new_entry.pk)})) self.assertEqual(new_entry.conference, self.conf) @@ -1974,11 +2116,14 @@ class ViewsTest(ViewsTestBase): BulletinBoardEntry.objects.all().delete() self.user.shadow_banned = True self.user.save() - resp = self.client.post(reverse('plainui:board_entry_new'), { - 'title': 'New Entry', - 'text_de': 'some texty text ed', - 'text_en': 'some texty text ne', - }) + resp = self.client.post( + reverse('plainui:board_entry_new'), + { + 'title': 'New Entry', + 'text_de': 'some texty text ed', + 'text_en': 'some texty text ne', + }, + ) new_entry = BulletinBoardEntry.objects.get() self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'id': str(new_entry.pk)})) self.assertEqual(new_entry.hidden, True) @@ -1988,17 +2133,23 @@ class ViewsTest(ViewsTestBase): self.user.save() with override_settings(RATELIMIT_ENABLE=True): for i in range(3): - resp = self.client.post(reverse('plainui:board_entry_new'), { - 'title': 'New Entry' + str(i), - 'text': 'some texty text', - }) + resp = self.client.post( + reverse('plainui:board_entry_new'), + { + 'title': 'New Entry' + str(i), + 'text': 'some texty text', + }, + ) new_entry = BulletinBoardEntry.objects.get(title='New Entry' + str(i)) self.assertRedirects(resp, reverse('plainui:board_entry_edit', kwargs={'id': str(new_entry.pk)})) - resp = self.client.post(reverse('plainui:board_entry_new'), { + resp = self.client.post( + reverse('plainui:board_entry_new'), + { 'title': 'New Entry-Ratelimited', 'text': 'some texty text-Ratelimited', - }) + }, + ) self.assertIsInstance(resp, HttpResponseRateLimited) @override_locale('en') @@ -2012,7 +2163,7 @@ class ViewsTest(ViewsTestBase): def test_BoardEntryDeleteView_post(self): user2 = PlatformUser(username='testuser2') user2.save() - e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title="Title1", text="some text") + e1 = BulletinBoardEntry(conference=self.conf, owner=self.user, title='Title1', text='some text') e1.save() e2 = BulletinBoardEntry(conference=self.conf, owner=user2) e2.save() @@ -2045,14 +2196,26 @@ class ViewsTest(ViewsTestBase): track = ConferenceTrack(conference=self.conf, is_public=True, name='Track1') track.save() event1 = Event( - conference=self.conf, assembly=assembly, track=track, name='Event1_1', is_public=True, room=room, - schedule_start=datetime(2020, 1, 2, 0, 15, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + track=track, + name='Event1_1', + is_public=True, + room=room, + schedule_start=datetime(2020, 1, 2, 0, 15, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event1.save() EventParticipant(is_public=True, participant=self.user, event=event1, role=EventParticipant.Role.SPEAKER).save() event2 = Event( - conference=self.conf, assembly=assembly, track=track, name='Event1_2', is_public=True, room=room, - schedule_start=datetime(2020, 1, 2, 1, 15, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + track=track, + name='Event1_2', + is_public=True, + room=room, + schedule_start=datetime(2020, 1, 2, 1, 15, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() @@ -2069,23 +2232,31 @@ class ViewsTest(ViewsTestBase): resp = self.client.get(reverse('plainui:fahrplan'), {'mode': 'calendar'}) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['mode'], 'calendar') - self.assertEqual(resp.context_data['events'], { - 'rooms_with_events': [(room, [ - {'type': 'space', 'minutes': 15.0}, - {'type': 'event', 'event': event1, 'minutes': 45.0}, - {'type': 'space', 'minutes': 15.0}, - {'type': 'event', 'event': event2, 'minutes': 45.0} - ])], - 'calendar_start': datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), - 'calendar_end': datetime(2020, 1, 2, 2, 0, 0, tzinfo=UTC), - 'calendar_time_steps': [ - {'newdate': True, 'ts': datetime(2020, 1, 2, 0, 0, tzinfo=UTC)}, - {'newdate': False, 'ts': datetime(2020, 1, 2, 0, 30, tzinfo=UTC)}, - {'newdate': False, 'ts': datetime(2020, 1, 2, 1, 0, tzinfo=UTC)}, - {'newdate': False, 'ts': datetime(2020, 1, 2, 1, 30, tzinfo=UTC)}, - ], - 'calendar_step_minutes': 30, - }) + self.assertEqual( + resp.context_data['events'], + { + 'rooms_with_events': [ + ( + room, + [ + {'type': 'space', 'minutes': 15.0}, + {'type': 'event', 'event': event1, 'minutes': 45.0}, + {'type': 'space', 'minutes': 15.0}, + {'type': 'event', 'event': event2, 'minutes': 45.0}, + ], + ) + ], + 'calendar_start': datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + 'calendar_end': datetime(2020, 1, 2, 2, 0, 0, tzinfo=UTC), + 'calendar_time_steps': [ + {'newdate': True, 'ts': datetime(2020, 1, 2, 0, 0, tzinfo=UTC)}, + {'newdate': False, 'ts': datetime(2020, 1, 2, 0, 30, tzinfo=UTC)}, + {'newdate': False, 'ts': datetime(2020, 1, 2, 1, 0, tzinfo=UTC)}, + {'newdate': False, 'ts': datetime(2020, 1, 2, 1, 30, tzinfo=UTC)}, + ], + 'calendar_step_minutes': 30, + }, + ) # some random requests that test building the filter content resp = self.client.get(reverse('plainui:fahrplan'), {'mode': 'calendar', 'show_day_filters': 'y'}) @@ -2120,20 +2291,35 @@ class ViewsTest(ViewsTestBase): room2 = Room(conference=self.conf, assembly=assembly, name='Some Other Room', is_public_fahrplan=False) room2.save() event = Event( - conference=self.conf, assembly=assembly, name='Event1_1', is_public=True, room=room, - schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + assembly=assembly, + name='Event1_1', + is_public=True, + room=room, + schedule_start=datetime(2020, 1, 2, 0, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), kind=Event.Kind.OFFICIAL, ) event.save() event2 = Event( - conference=self.conf, assembly=assembly, name='Event1_2', is_public=False, room=room, - schedule_start=datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + assembly=assembly, + name='Event1_2', + is_public=False, + room=room, + schedule_start=datetime(2020, 1, 2, 1, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), kind=Event.Kind.OFFICIAL, ) event2.save() event3 = Event( - conference=self.conf, assembly=assembly, name='Event1_3', is_public=True, room=room2, - schedule_start=datetime(2020, 1, 2, 2, 0, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + assembly=assembly, + name='Event1_3', + is_public=True, + room=room2, + schedule_start=datetime(2020, 1, 2, 2, 0, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), kind=Event.Kind.ASSEMBLY, ) event3.save() @@ -2197,23 +2383,43 @@ class ViewsTest(ViewsTestBase): room = Room(conference=self.conf, assembly=assembly, name='Some Room') room.save() event_pre = Event( - conference=self.conf, assembly=assembly, name='Event_pre', is_public=True, room=room, - schedule_start=now - timedelta(hours=1), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event_pre', + is_public=True, + room=room, + schedule_start=now - timedelta(hours=1), + schedule_duration=timedelta(minutes=45), ) event_pre.save() event_still_running = Event( - conference=self.conf, assembly=assembly, name='Event_still_running', is_public=True, room=room, - schedule_start=now - timedelta(minutes=30), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event_still_running', + is_public=True, + room=room, + schedule_start=now - timedelta(minutes=30), + schedule_duration=timedelta(minutes=45), ) event_still_running.save() event_soon = Event( - conference=self.conf, assembly=assembly, name='Event_soon', is_public=True, room=room, - schedule_start=now + timedelta(minutes=15), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event_soon', + is_public=True, + room=room, + schedule_start=now + timedelta(minutes=15), + schedule_duration=timedelta(minutes=45), ) event_soon.save() event_post = Event( - conference=self.conf, assembly=assembly, name='Event_post', is_public=True, room=room, - schedule_start=now + timedelta(minutes=35), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + name='Event_post', + is_public=True, + room=room, + schedule_start=now + timedelta(minutes=35), + schedule_duration=timedelta(minutes=45), ) event_post.save() @@ -2244,19 +2450,38 @@ class ViewsTest(ViewsTestBase): room_link2.save() event = Event( - conference=self.conf, assembly=assembly, room=room1, slug='Event1_1', name='Event1_1', is_public=True, kind=Event.Kind.ASSEMBLY, - schedule_start=datetime(2020, 1, 1, 0, 45, 0, tzinfo=UTC), schedule_duration=timedelta(minutes=45), + conference=self.conf, + assembly=assembly, + room=room1, + slug='Event1_1', + name='Event1_1', + is_public=True, + kind=Event.Kind.ASSEMBLY, + schedule_start=datetime(2020, 1, 1, 0, 45, 0, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), additional_data={'persons': [{'public_name': 'Gottfried Fluffke'}, {'public_name': 'Ferdinand Federviech'}]}, ) event.save() event2 = Event( - conference=self.conf, assembly=assembly, room=room1, slug='Event1_2', name='Event1_2', is_public=True, - schedule_start=datetime(2020, 1, 1, 1, 50, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + room=room1, + slug='Event1_2', + name='Event1_2', + is_public=True, + schedule_start=datetime(2020, 1, 1, 1, 50, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event2.save() event_upcoming = Event( - conference=self.conf, assembly=assembly, room=room2, slug='Event2_1', name='Event2_1', is_public=True, - schedule_start=datetime(2020, 1, 1, 1, 50, 1, tzinfo=UTC), schedule_duration=timedelta(minutes=45) + conference=self.conf, + assembly=assembly, + room=room2, + slug='Event2_1', + name='Event2_1', + is_public=True, + schedule_start=datetime(2020, 1, 1, 1, 50, 1, tzinfo=UTC), + schedule_duration=timedelta(minutes=45), ) event_upcoming.save() @@ -2265,16 +2490,26 @@ class ViewsTest(ViewsTestBase): self.assertNeedsLogin(reverse('plainui:channel_events')) resp = self.client.get(reverse('plainui:channel_events')) self.assertEqual(resp.context_data['conf'], self.conf) - self.assertEqual(resp.context_data['rooms_active'], [{ - 'room': room1, - 'current_event': event, - 'next_event': event2, - }]) - self.assertEqual(resp.context_data['rooms_inactive'], [{ - 'room': room2, - 'current_event': None, - 'next_event': event_upcoming, - }]) + self.assertEqual( + resp.context_data['rooms_active'], + [ + { + 'room': room1, + 'current_event': event, + 'next_event': event2, + } + ], + ) + self.assertEqual( + resp.context_data['rooms_inactive'], + [ + { + 'room': room2, + 'current_event': None, + 'next_event': event_upcoming, + } + ], + ) @override_locale('en') def test_WorldView(self): @@ -2310,7 +2545,8 @@ class ViewsTest(ViewsTestBase): @override_locale('en') def _dereferrer_test(self, my_endpoint): from django.core.signing import loads - my_salt = '%sdereferrer' % (self.user.pk,) + + my_salt = f'{self.user.pk}dereferrer' anonymous_salt = '000000dereferrer' # local links are always allowed @@ -2426,6 +2662,7 @@ class ViewsTest(ViewsTestBase): @override_locale('en') def test_ReportContentView(self): from plainui.forms import REPORT_CATEGORIES + category_id = iter(REPORT_CATEGORIES.keys()).__next__() user2 = PlatformUser(username='testuser2') @@ -2436,132 +2673,154 @@ class ViewsTest(ViewsTestBase): bbe.save() self.assertNeedsLogin(reverse('plainui:report_content'), check_user=True) - resp = self.client.get(reverse('plainui:report_content'), { - 'kind': 'url', 'kind_data': reverse('plainui:index') - }) + resp = self.client.get(reverse('plainui:report_content'), {'kind': 'url', 'kind_data': reverse('plainui:index')}) self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['form']['kind'].value(), 'url') self.assertEqual(resp.context_data['form']['kind_data'].value(), reverse('plainui:index')) - resp = self.client.post(reverse('plainui:report_content'), { - 'kind': 'url', - 'kind_data': reverse('plainui:index'), - 'next': '', - 'category': category_id, - 'message': 'Some Message', - 'message2': 'Some Resolution', - }) + resp = self.client.post( + reverse('plainui:report_content'), + { + 'kind': 'url', + 'kind_data': reverse('plainui:index'), + 'next': '', + 'category': category_id, + 'message': 'Some Message', + 'message2': 'Some Resolution', + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.assertEqual(len(mail.outbox), 1) sent_mail = mail.outbox[0] self.assertEqual(sent_mail.to, [REPORT_CATEGORIES[category_id][2]]) - self.assertEqual(sent_mail.subject, f"New {REPORT_CATEGORIES[category_id][1]} Report") - self.assertEqual(sent_mail.body, ( - f"testuser ({self.user.id!s}) sent a new {REPORT_CATEGORIES[category_id][1]} Report.\n" - f"Profile of the Report Author: {reverse('plainui:user_by_uuid', kwargs={'uuid': str(self.user.uuid)})}\n" - f"\n" - f"Problem description:\n" - f"\n" - f"Some Message\n" - f"\n" - f"Solution description:\n" - f"\n" - f"Some Resolution\n" - f"\n" - f"Problematic content:\n" - f"\n" - f"{reverse('plainui:index')}" - )) + self.assertEqual(sent_mail.subject, f'New {REPORT_CATEGORIES[category_id][1]} Report') + self.assertEqual( + sent_mail.body, + ( + f"testuser ({self.user.id!s}) sent a new {REPORT_CATEGORIES[category_id][1]} Report.\n" + f"Profile of the Report Author: {reverse('plainui:user_by_uuid', kwargs={'uuid': str(self.user.uuid)})}\n" + f"\n" + f"Problem description:\n" + f"\n" + f"Some Message\n" + f"\n" + f"Solution description:\n" + f"\n" + f"Some Resolution\n" + f"\n" + f"Problematic content:\n" + f"\n" + f"{reverse('plainui:index')}" + ), + ) mail.outbox.clear() - resp = self.client.post(reverse('plainui:report_content'), { - 'kind': 'pn', - 'kind_data': str(dm_recv.pk), - 'next': reverse('plainui:personal_message_show', kwargs={'msg_id': str(dm_recv.pk)}), - 'category': category_id, - 'message': 'Some Message', - 'message2': 'Some Resolution', - }) + resp = self.client.post( + reverse('plainui:report_content'), + { + 'kind': 'pn', + 'kind_data': str(dm_recv.pk), + 'next': reverse('plainui:personal_message_show', kwargs={'msg_id': str(dm_recv.pk)}), + 'category': category_id, + 'message': 'Some Message', + 'message2': 'Some Resolution', + }, + ) self.assertRedirects(resp, reverse('plainui:personal_message_show', kwargs={'msg_id': str(dm_recv.pk)})) self.assertEqual(len(mail.outbox), 1) sent_mail = mail.outbox[0] self.assertEqual(sent_mail.to, [REPORT_CATEGORIES[category_id][2]]) - self.assertEqual(sent_mail.subject, f"New {REPORT_CATEGORIES[category_id][1]} Report") - self.assertEqual(sent_mail.body, ( - f'testuser ({self.user.id!s}) sent a new {REPORT_CATEGORIES[category_id][1]} Report.\n' - f"Profile of the Report Author: {reverse('plainui:user_by_uuid', kwargs={'uuid': str(self.user.uuid)})}\n" - f'\n' - f'Problem description:\n' - f'\n' - f'Some Message\n' - f'\n' - f"Solution description:\n" - f"\n" - f"Some Resolution\n" - f"\n" - f'Problematic content:\n' - f'\n' - f'DM by testuser2 ({user2.id!s}) to testuser ({self.user.id!s}) at {localize(localtime(dm_recv.timestamp))} ({dm_recv.pk!s}).\n' - f'Subject: "sj1"\n' - f'Message: "bd1"' - )) + self.assertEqual(sent_mail.subject, f'New {REPORT_CATEGORIES[category_id][1]} Report') + self.assertEqual( + sent_mail.body, + ( + f'testuser ({self.user.id!s}) sent a new {REPORT_CATEGORIES[category_id][1]} Report.\n' + f"Profile of the Report Author: {reverse('plainui:user_by_uuid', kwargs={'uuid': str(self.user.uuid)})}\n" + f'\n' + f'Problem description:\n' + f'\n' + f'Some Message\n' + f'\n' + f"Solution description:\n" + f"\n" + f"Some Resolution\n" + f"\n" + f'Problematic content:\n' + f'\n' + f'DM by testuser2 ({user2.id!s}) to testuser ({self.user.id!s}) at {localize(localtime(dm_recv.timestamp))} ({dm_recv.pk!s}).\n' + f'Subject: "sj1"\n' + f'Message: "bd1"' + ), + ) mail.outbox.clear() - resp = self.client.post(reverse('plainui:report_content'), { - 'kind': 'board', - 'kind_data': str(bbe.pk), - 'next': '', - 'category': category_id, - 'message': 'Other Message', - 'message2': 'Other Resolution', - }) + resp = self.client.post( + reverse('plainui:report_content'), + { + 'kind': 'board', + 'kind_data': str(bbe.pk), + 'next': '', + 'category': category_id, + 'message': 'Other Message', + 'message2': 'Other Resolution', + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.assertEqual(len(mail.outbox), 1) sent_mail = mail.outbox[0] self.assertEqual(sent_mail.to, [REPORT_CATEGORIES[category_id][2]]) - self.assertEqual(sent_mail.subject, f"New {REPORT_CATEGORIES[category_id][1]} Report") - self.assertEqual(sent_mail.body, ( - f'testuser ({self.user.id!s}) sent a new {REPORT_CATEGORIES[category_id][1]} Report.\n' - f"Profile of the Report Author: {reverse('plainui:user_by_uuid', kwargs={'uuid': str(self.user.uuid)})}\n" - f'\n' - f'Problem description:\n' - f'\n' - f'Other Message\n' - f'\n' - f"Solution description:\n" - f"\n" - f"Other Resolution\n" - f"\n" - f'Problematic content:\n' - f'\n' - f'Board Entry by testuser ({self.user.id!s}) at {localize(localtime(dm_recv.timestamp))} ({bbe.pk!s})\n' - f'Title: "board entry title"\n' - f'Text: "board entry text"' - )) + self.assertEqual(sent_mail.subject, f'New {REPORT_CATEGORIES[category_id][1]} Report') + self.assertEqual( + sent_mail.body, + ( + f'testuser ({self.user.id!s}) sent a new {REPORT_CATEGORIES[category_id][1]} Report.\n' + f"Profile of the Report Author: {reverse('plainui:user_by_uuid', kwargs={'uuid': str(self.user.uuid)})}\n" + f'\n' + f'Problem description:\n' + f'\n' + f'Other Message\n' + f'\n' + f"Solution description:\n" + f"\n" + f"Other Resolution\n" + f"\n" + f'Problematic content:\n' + f'\n' + f'Board Entry by testuser ({self.user.id!s}) at {localize(localtime(dm_recv.timestamp))} ({bbe.pk!s})\n' + f'Title: "board entry title"\n' + f'Text: "board entry text"' + ), + ) mail.outbox.clear() # testing the rate limiting: Will block after 1 allowed requests with override_settings(RATELIMIT_ENABLE=True): - resp = self.client.post(reverse('plainui:report_content'), { - 'kind': 'board', - 'kind_data': str(bbe.pk), - 'next': '', - 'category': category_id, - 'message': 'Other Message', - 'message2': 'Other Resolution', - }) + resp = self.client.post( + reverse('plainui:report_content'), + { + 'kind': 'board', + 'kind_data': str(bbe.pk), + 'next': '', + 'category': category_id, + 'message': 'Other Message', + 'message2': 'Other Resolution', + }, + ) self.assertRedirects(resp, reverse('plainui:index')) self.assertEqual(len(mail.outbox), 1) mail.outbox.clear() - resp = self.client.post(reverse('plainui:report_content'), { - 'kind': 'board', - 'kind_data': str(bbe.pk), - 'next': '', - 'category': category_id, - 'message': 'Other Message', - 'message2': 'Other Resolution', - }) + resp = self.client.post( + reverse('plainui:report_content'), + { + 'kind': 'board', + 'kind_data': str(bbe.pk), + 'next': '', + 'category': category_id, + 'message': 'Other Message', + 'message2': 'Other Resolution', + }, + ) self.assertIsInstance(resp, HttpResponseRateLimited) self.assertEqual(len(mail.outbox), 0) @@ -2606,8 +2865,8 @@ class ViewsTest(ViewsTestBase): badge = Badge(conference=self.conf, name='badge-name', issuing_assembly=assembly) badge.save() badge_token = BadgeToken.objects.create(badge=badge) - resp = self.client.get(reverse('plainui:manage_badges'), {"redeem_token": badge_token.token}) - self.assertRedirects(resp, reverse("plainui:manage_badges")) + resp = self.client.get(reverse('plainui:manage_badges'), {'redeem_token': badge_token.token}) + self.assertRedirects(resp, reverse('plainui:manage_badges')) self.assertTrue(UserBadge.objects.filter(user=self.user, badge=badge).exists()) @override_settings(RATELIMIT_ENABLE=False) @@ -2617,8 +2876,8 @@ class ViewsTest(ViewsTestBase): badge = Badge(conference=self.conf, name='badge-name', issuing_assembly=assembly) badge.save() badge_token = BadgeToken.objects.create(badge=badge) - resp = self.client.post(reverse('plainui:manage_badges'), {"token": badge_token.token, 'purpose': 'redeem_token'}) - self.assertRedirects(resp, reverse("plainui:manage_badges")) + resp = self.client.post(reverse('plainui:manage_badges'), {'token': badge_token.token, 'purpose': 'redeem_token'}) + self.assertRedirects(resp, reverse('plainui:manage_badges')) self.assertTrue(UserBadge.objects.filter(user=self.user, badge=badge).exists()) def test_badge_redeem_view_rate_limit_post(self): @@ -2690,7 +2949,6 @@ class ViewsTest(ViewsTestBase): # @override_settings(PRETIX_SECRET_KEY=_GOOD_SECRET) class TestWorkadventurCompattibleUsernameField(TestCase): - def setUp(self): class MyTestForm(forms.Form): username = WorkadventureCompatibleUsernameField(max_length=123) @@ -2709,7 +2967,7 @@ class TestWorkadventurCompattibleUsernameField(TestCase): class TestConferenceDetection(TestCase): def setUp(self): - self.request = RequestFactory().get("/") + self.request = RequestFactory().get('/') self.view = LandingView() def test_no_conference_config(self): @@ -2718,20 +2976,14 @@ class TestConferenceDetection(TestCase): @override_settings(PLAINUI_CONFERENCE=None) def test_valid_conference_config(self): - self.conf = Conference( - id=TEST_CONF_ID, name="conf_asdf", slug="slug1" - ) + self.conf = Conference(id=TEST_CONF_ID, name='conf_asdf', slug='slug1') self.conf.save() self.view.dispatch(self.request) def setUpTwoDatabases(self): - self.conf = Conference( - id=TEST_CONF_ID, name="slug1", slug="slug1" - ) + self.conf = Conference(id=TEST_CONF_ID, name='slug1', slug='slug1') self.conf.save() - self.conf2 = Conference( - id=TEST_CONF_ID_2, name="slug2", slug="slug2" - ) + self.conf2 = Conference(id=TEST_CONF_ID_2, name='slug2', slug='slug2') self.conf2.save() @override_settings(PLAINUI_CONFERENCE=TEST_CONF_ID) @@ -2751,7 +3003,5 @@ class TestConferenceDetection(TestCase): def test_invalid_conference_config_no_setting(self): self.setUpTwoDatabases() - with self.assertRaisesMessage( - ImproperlyConfigured, - expected_message="Multiple Conferences found, set PLAINUI_CONFERENCE in settings!"): + with self.assertRaisesMessage(ImproperlyConfigured, expected_message='Multiple Conferences found, set PLAINUI_CONFERENCE in settings!'): self.view.dispatch(self.request) diff --git a/src/plainui/urls.py b/src/plainui/urls.py index 7061a3ba6..99d091126 100644 --- a/src/plainui/urls.py +++ b/src/plainui/urls.py @@ -18,7 +18,11 @@ urlpatterns = [ path('login', views.LoginView.as_view(), name='login'), path('signup', views.RegistrationView.as_view(), name='signup'), path('signup/done', views.RegistrationDoneView.as_view(), name='signup_done'), - re_path(r'^signup/activate/(?P<uid_b64>[0-9A-Za-z_\-]+)/(?P<channel_id>\d+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,40})/$', views.RegistrationActivationView.as_view(), name='signup_activate'), # noqa: E501 + re_path( + r'^signup/activate/(?P<uid_b64>[0-9A-Za-z_\-]+)/(?P<channel_id>\d+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,40})/$', + views.RegistrationActivationView.as_view(), + name='signup_activate', + ), # noqa: E501 path('password_reset', views.PasswordResetView.as_view(), name='password_reset'), path('password_reset_sent', views.PasswordResetDoneView.as_view(), name='password_reset_done'), path('password_change', views.PasswordChangeView.as_view(), name='password_change'), @@ -33,7 +37,6 @@ urlpatterns = [ path('redeem_token_create', views.RedeemTokenUserCreateView.as_view(), name='redeem_token_create_user'), path('redeem_token_loggedin', views.RedeemTokenLoggedIn.as_view(), name='redeem_token_loggedin'), path('token_password_reset', views.TokenPasswordResetView.as_view(), name='token_password_reset'), - path('', views.LandingView.as_view(), name='landing'), path('board', views.BoardView.as_view(), name='board'), path('my_board', views.BoardPrivateView.as_view(), name='board_private'), @@ -82,7 +85,6 @@ urlpatterns = [ path('user-by-uuid/<uuid:uuid>/', views.UserByUuidView.as_view(), name='user_by_uuid'), path('wa_contact', views.WorkadventureContactPage.as_view(), name='wa_contact'), path('world_vcard/<uuid:wa_session_id>', views.WorkadventureVCard.as_view(), name='wa_vcard'), - path('shibboleet', views.ShibboleetView.as_view(), name='shibboleet'), ] diff --git a/src/plainui/utils.py b/src/plainui/utils.py index 6a01bb4b6..25ee46962 100644 --- a/src/plainui/utils.py +++ b/src/plainui/utils.py @@ -25,19 +25,22 @@ def check_message_content(conf, request, text, kind, kind_data): request.user.shadow_banned = True request.user.save(update_fields=['shadow_banned']) - report_form = ReportForm(conf=conf, data={ - 'kind': kind, - 'kind_data': str(kind_data), - 'category': 'abuse', - 'message': 'Autoban triggered', - 'message2': '.', - }) + report_form = ReportForm( + conf=conf, + data={ + 'kind': kind, + 'kind_data': str(kind_data), + 'category': 'abuse', + 'message': 'Autoban triggered', + 'message2': '.', + }, + ) report_form.is_valid() report_form.send_report_mail(request) return True except Exception: - raise Exception("Boom") # don't allow leaking eg. ValidationErrors that might be handled in a View + raise Exception('Boom') # don't allow leaking eg. ValidationErrors that might be handled in a View class StaticPageDiff(HtmlDiff): @@ -45,16 +48,16 @@ class StaticPageDiff(HtmlDiff): def _format_line(self, side, flag, linenum, text): try: linenum = '%d' % linenum - id = ' id="%s%s"' % (self._prefix[side], linenum) + id = f' id="{self._prefix[side]}{linenum}"' except TypeError: # handle blank lines where linenum is '>' or '' id = '' # replace those things that would get confused with HTML symbols - text = text.replace("&", "&").replace(">", ">").replace("<", "<") + text = text.replace('&', '&').replace('>', '>').replace('<', '<') # make space non-breakable so they don't get compressed or line wrapped text = text.replace(' ', ' ').rstrip() # vvv ---- removed `nowrap="nowrap"` --- vvv - return '<td class="diff_header"%s>%s</td><td class="diff_content">%s</td>' % (id, linenum, text) + return f'<td class="diff_header"{id}>{linenum}</td><td class="diff_content">{text}</td>' diff --git a/src/plainui/views.py b/src/plainui/views.py index 01907b4e2..4c264cb97 100644 --- a/src/plainui/views.py +++ b/src/plainui/views.py @@ -1,56 +1,93 @@ -from datetime import datetime, time, timedelta, UTC import logging import threading - -from typing import ClassVar, List, Optional, Dict, Any -from urllib.parse import urlparse, ParseResult, unquote +from datetime import UTC, datetime, time, timedelta +from typing import Any, ClassVar, Dict, List, Optional +from urllib.parse import ParseResult, unquote, urlparse from uuid import UUID # noqa:F401 as flake8 does not properly support PEP526 yet -.- -from modeltranslation.fields import build_localized_fieldname -from modeltranslation.settings import AVAILABLE_LANGUAGES + +from django_ratelimit.decorators import ratelimit from django.conf import settings from django.contrib import messages from django.contrib.auth import login -from django.contrib.auth import views as auth_views, mixins as auth_mixins +from django.contrib.auth import mixins as auth_mixins +from django.contrib.auth import views as auth_views from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied, ValidationError, ImproperlyConfigured from django.core import signing -from django.core.exceptions import SuspiciousOperation -from django.db import transaction, connection -from django.db.models import Q, F, Prefetch -from django.http import Http404, HttpRequest, HttpResponseRedirect, HttpResponseNotFound, HttpResponse, HttpResponseForbidden -from django.shortcuts import redirect, get_object_or_404 -from django.urls import reverse, reverse_lazy, translate_url, NoReverseMatch +from django.core.exceptions import ImproperlyConfigured, PermissionDenied, SuspiciousOperation, ValidationError +from django.db import connection, transaction +from django.db.models import F, Prefetch, Q +from django.http import Http404, HttpRequest, HttpResponse, HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect +from django.template.response import TemplateResponse +from django.urls import NoReverseMatch, reverse, reverse_lazy, translate_url from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.http import url_has_allowed_host_and_scheme from django.utils.timezone import localtime -from django.utils.translation import gettext, check_for_language, get_language +from django.utils.translation import check_for_language, get_language, gettext +from django.utils.translation import gettext_lazy as _ from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.base import View, TemplateView +from django.views.generic.base import TemplateView, View from django.views.generic.edit import FormView, UpdateView -from django.template.response import TemplateResponse -from django.utils.translation import gettext_lazy as _ -from django_ratelimit.decorators import ratelimit +from modeltranslation.fields import build_localized_fieldname +from modeltranslation.settings import AVAILABLE_LANGUAGES from core import integrations from core.integrations import WorkAdventureIntegration -from core.models import Assembly, AssemblyLikeCount, Badge, Conference, ConferenceMember, ConferenceMemberTicket, ConferenceTag, DereferrerStats, UserContact, \ - ConferenceTrack, DirectMessage, Event, EventAttachment, EventLikeCount, EventParticipant, PlatformUser, Room, RoomLink, StaticPage, StaticPageRevision, \ - TagItem, UserBadge, BadgeToken, UserDereferrerAllowlist, WorkadventureSession, BulletinBoardEntry, MetaNavItem +from core.markdown import refresh_linking_markdown, render_markdown +from core.models import ( + Assembly, + AssemblyLikeCount, + Badge, + BadgeToken, + BulletinBoardEntry, + Conference, + ConferenceMember, + ConferenceMemberTicket, + ConferenceTag, + ConferenceTrack, + DereferrerStats, + DirectMessage, + Event, + EventAttachment, + EventLikeCount, + EventParticipant, + MetaNavItem, + PlatformUser, + Room, + RoomLink, + StaticPage, + StaticPageRevision, + TagItem, + UserBadge, + UserContact, + UserDereferrerAllowlist, + WorkadventureSession, +) +from core.models.badges import TokenInvalid from core.models.ticket import TicketValidationError from core.search import search from core.sso import SSO -from core.markdown import render_markdown, refresh_linking_markdown from core.utils import url_in_allowlist from core.views import BaseLoginView, BasePasswordResetConfirmView, BasePasswordResetView, BaseRegistrationActivationView, BaseRegistrationView -from core.models.badges import TokenInvalid from plainui.utils import StaticPageDiff -from .forms import BulletinBoardEntryForm, ExampleForm, InputTokenForm, NewDirectMessageForm, \ - ProfileEditForm, ProfileDescriptionEditForm, RedeemTokenAddToUserForm, RedeemTokenUserCreateForm, ReportForm, \ - RedeemBadgeForm, StaticPageBodyForm, TokenPasswortResetForm +from .forms import ( + BulletinBoardEntryForm, + ExampleForm, + InputTokenForm, + NewDirectMessageForm, + ProfileDescriptionEditForm, + ProfileEditForm, + RedeemBadgeForm, + RedeemTokenAddToUserForm, + RedeemTokenUserCreateForm, + ReportForm, + StaticPageBodyForm, + TokenPasswortResetForm, +) def _session_refresh_favorite_assemblies(session, user) -> List[str]: @@ -162,27 +199,26 @@ class ConferenceRequiredMixin(auth_mixins.AccessMixin): ConferenceRequiredMixin._conf = conf = conferences.get() self._conf_timeout = now + timedelta(300) except Conference.MultipleObjectsReturned: - raise ImproperlyConfigured("Multiple Conferences found, set PLAINUI_CONFERENCE in settings!") + raise ImproperlyConfigured('Multiple Conferences found, set PLAINUI_CONFERENCE in settings!') except Conference.DoesNotExist: if Conference.objects.exists(): - raise ImproperlyConfigured("PLAINUI_CONFERENCE from settings not found in database!") + raise ImproperlyConfigured('PLAINUI_CONFERENCE from settings not found in database!') else: - raise ImproperlyConfigured("No conference found in database!") + raise ImproperlyConfigured('No conference found in database!') except ValidationError: - raise ImproperlyConfigured("Invalid value for PLAINUI_CONFERENCE in settings!") + raise ImproperlyConfigured('Invalid value for PLAINUI_CONFERENCE in settings!') finally: self._conf_lock.release() self.conf = conf - if (self.require_login and conf.require_login - or conf.require_ticket and self.require_conference_member - or self.require_user) \ - and not request.user.is_authenticated: + if ( + self.require_login and conf.require_login or conf.require_ticket and self.require_conference_member or self.require_user + ) and not request.user.is_authenticated: return self.handle_no_permission() if conf.require_ticket and self.require_conference_member and not self.has_ticket(request): - messages.error(request, gettext("Please activate your Ticket to access this conference!")) + messages.error(request, gettext('Please activate your Ticket to access this conference!')) return redirect(reverse('plainui:redeem_token')) if self._test_cork: @@ -243,13 +279,10 @@ class EventView(ConferenceRequiredMixin, TemplateView): context['running_state'] = running_state context['attachments'] = EventAttachment.objects.filter( - event=event, - visibility__in=[EventAttachment.Visibility.PUBLIC, EventAttachment.Visibility.CONFERENCE] + event=event, visibility__in=[EventAttachment.Visibility.PUBLIC, EventAttachment.Visibility.CONFERENCE] ) - context['report_info'] = { - 'url': reverse('plainui:event', kwargs={'event_slug': event.slug}) - } + context['report_info'] = {'url': reverse('plainui:event', kwargs={'event_slug': event.slug})} context['share_url'] = reverse('plainui:event', kwargs={'event_slug': event.slug}) context['schedule_info'] = {'id': event.id, 'is': str(event.id) in personal_calendar} context['fav_info'] = {'type': 'event', 'id': event.id, 'is': str(event.id) in favorites} @@ -268,9 +301,11 @@ class TagView(ConferenceRequiredMixin, TemplateView): context['tag'] = tag # TODO other types. What should we link here? - context['events'] = Event.objects.conference_accessible(self.conf).filter( - id__in=TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(Event)).values_list('target_id') - ).filter(schedule_start__isnull=False, schedule_end__isnull=False) + context['events'] = ( + Event.objects.conference_accessible(self.conf) + .filter(id__in=TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(Event)).values_list('target_id')) + .filter(schedule_start__isnull=False, schedule_end__isnull=False) + ) context['my_favorite_events'] = _session_get_favorite_events(self.request.session, self.request.user) context['my_scheduled_events'] = _session_get_scheduled_events(self.request.session, self.request.user) @@ -294,34 +329,32 @@ class SosList(ConferenceRequiredMixin, TemplateView): context['is_favorite_events'] = _session_get_favorite_events(self.request.session, self.request.user) context['is_scheduled_events'] = _session_get_scheduled_events(self.request.session, self.request.user) - context['report_info'] = { - 'url': reverse('plainui:sos') - } + context['report_info'] = {'url': reverse('plainui:sos')} context['share_url'] = reverse('plainui:sos') return context class SosJoin(ConferenceRequiredMixin, View): - """ extra view to join BBB rooms from the Workadventure """ + """extra view to join BBB rooms from the Workadventure""" require_user = True def get(self, request, event_slug): event = get_object_or_404(Event.objects.conference_accessible(self.conf), slug=event_slug) if not event.kind == Event.Kind.SELF_ORGANIZED: - raise Http404() + raise Http404 try: join_url = integrations.BigBlueButton.join_room(event, self.request.user, not self.request.user.show_name) if not join_url: - error = gettext("Unspecified error, room is probably full?") + error = gettext('Unspecified error, room is probably full?') else: return HttpResponseRedirect(join_url) except integrations.IntegrationError as e: error = str(e) - messages.error(request, gettext("Joining Failed: %s") % (error,)) + messages.error(request, gettext('Joining Failed: %s') % (error,)) return redirect(reverse('plainui:event', kwargs={'event_slug': event_slug})) @@ -350,15 +383,11 @@ class AssemblyView(ConferenceRequiredMixin, TemplateView): suggestions = assembly.suggestions.select_related('assembly2') suggestions = suggestions.exclude(assembly2=assembly.pk).filter(assembly2__state_assembly__in=Assembly.PUBLIC_STATES).order_by('-like_ratio')[:5] - context['suggested'] = [ - s.assembly2 for s in suggestions - ] + context['suggested'] = [s.assembly2 for s in suggestions] context['spokespeople'] = assembly.members.filter(is_representative=True, show_public=True).select_related('member') - context['report_info'] = { - 'url': reverse('plainui:assembly', kwargs={'assembly_slug': assembly.slug}) - } + context['report_info'] = {'url': reverse('plainui:assembly', kwargs={'assembly_slug': assembly.slug})} context['share_url'] = reverse('plainui:assembly', kwargs={'assembly_slug': assembly.slug}) context['fav_info'] = {'type': 'assembly', 'id': assembly.id, 'is': str(assembly.id) in favorites} @@ -372,16 +401,14 @@ class AssembliesView(ConferenceRequiredMixin, TemplateView): context = super().get_context_data(**kwargs) context['conf'] = self.conf context['events_upcoming'] = _event_filter(self.request.user, self.conf, upcoming=True) - # TODO: reecommended events. TODO: Implement suggestions based on liked assemblies - # context['events_recommended'] = _event_filter(self.request.user, self.conf, user_schedule_only=True)[0:4] + # TODO: recommended events. + # TODO: Implement suggestions based on liked assemblies context['is_favorite_events'] = _session_get_favorite_events(self.request.session, self.request.user) context['is_scheduled_events'] = _session_get_scheduled_events(self.request.session, self.request.user) context['assemblies'] = Assembly.objects.conference_accessible(self.conf).order_by('name') context['my_favorite_assemblies'] = _session_get_favorite_assemblies(self.request.session, self.request.user) - context['report_info'] = { - 'url': reverse('plainui:assemblies') - } + context['report_info'] = {'url': reverse('plainui:assemblies')} context['share_url'] = reverse('plainui:assemblies') return context @@ -402,9 +429,7 @@ class AssembliesAllView(ConferenceRequiredMixin, TemplateView): context['assemblies'] = context['assemblies'].filter(is_official=False) context['my_favorite_assemblies'] = _session_get_favorite_assemblies(self.request.session, self.request.user) - context['report_info'] = { - 'url': reverse('plainui:assemblies_all') - } + context['report_info'] = {'url': reverse('plainui:assemblies_all')} context['share_url'] = reverse('plainui:assemblies_all') return context @@ -419,15 +444,11 @@ class AssembliesEventsView(ConferenceRequiredMixin, TemplateView): context['events_upcoming'] = _event_filter(self.request.user, self.conf, upcoming=True) # not used atm - # assemblies = Assembly.objects.conference_accessible(self.conf) - # context['assemblies'] = assemblies context['events_from_assemblies'] = _event_filter(self.request.user, self.conf, kinds=[Event.Kind.ASSEMBLY], public_fahrplan=False) context['is_favorite_events'] = _session_get_favorite_events(self.request.session, self.request.user) context['is_scheduled_events'] = _session_get_scheduled_events(self.request.session, self.request.user) - context['report_info'] = { - 'url': reverse('plainui:assemblies_events') - } + context['report_info'] = {'url': reverse('plainui:assemblies_events')} context['share_url'] = reverse('plainui:assemblies_events') return context @@ -467,7 +488,7 @@ class StaticPageView(ConferenceRequiredMixin, TemplateView): if not self.request.user.is_authenticated: return self.handle_no_permission() else: - messages.error(self.request, gettext("You need an active Ticket to access this Page!")) + messages.error(self.request, gettext('You need an active Ticket to access this Page!')) return redirect(reverse('plainui:redeem_token')) return super().get(request, page_slug=page_slug, **kwargs) @@ -511,13 +532,11 @@ class StaticPageView(ConferenceRequiredMixin, TemplateView): sanitize_html=static_page.sanitize_html, ) if can_edit: - context["edit_url"] = reverse( - "plainui:static_page_edit", kwargs={"page_slug": page_slug} - ) + ("?rev=" + revision if revision else "") + context['edit_url'] = reverse('plainui:static_page_edit', kwargs={'page_slug': page_slug}) + ('?rev=' + revision if revision else '') else: - context["edit_url"] = None + context['edit_url'] = None except StaticPageRevision.DoesNotExist: - context["revision_not_found"] = True + context['revision_not_found'] = True page_body = static_page.body_html else: page_body = static_page.body_html @@ -552,16 +571,16 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): if not static_page: # the page does not exist and the user does not have the permission to create it - messages.error(request, gettext("You do not have the required permissions to create this page.")) + messages.error(request, gettext('You do not have the required permissions to create this page.')) return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) if static_page.privacy == StaticPage.Privacy.CONFERENCE and not self.has_ticket(request): - messages.error(request, gettext("You do not have the required permissions to edit this page.")) + messages.error(request, gettext('You do not have the required permissions to edit this page.')) # TODO: after redeem, redirect to edit view for page_slug return redirect(reverse('plainui:redeem_token')) if static_page.privacy == StaticPage.Privacy.PERM and not self.request.user.has_conference_staffpermission(self.conf, 'static_pages'): - messages.error(request, gettext("You do not have the required permissions to edit this page.")) + messages.error(request, gettext('You do not have the required permissions to edit this page.')) return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) revision = request.GET.get('rev') @@ -575,25 +594,31 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): page_title = static_page.title not_latest_revision = False - form = StaticPageBodyForm(initial={ - 'title': page_title, - 'body': instance.body, - }) + form = StaticPageBodyForm( + initial={ + 'title': page_title, + 'body': instance.body, + } + ) if not writeable: form.fields['title'].disabled = True form.fields['body'].disabled = True - return TemplateResponse(request, self.template_name, { - 'page': static_page, - 'conf': self.conf, - 'page_slug': page_slug, - 'static_page': static_page, - 'writeable': writeable, - 'not_latest_revision': not_latest_revision, - 'revision': revision, - 'form': form, - }) + return TemplateResponse( + request, + self.template_name, + { + 'page': static_page, + 'conf': self.conf, + 'page_slug': page_slug, + 'static_page': static_page, + 'writeable': writeable, + 'not_latest_revision': not_latest_revision, + 'revision': revision, + 'form': form, + }, + ) @method_decorator(ratelimit(key='ip', rate='5/m', method=ratelimit.UNSAFE)) @method_decorator(ratelimit(key='post:username', rate='5/m', method=ratelimit.UNSAFE)) @@ -623,16 +648,20 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): not_latest_revision = False if not form.is_valid(): - return TemplateResponse(request, self.template_name, { - 'page': static_page, - 'conf': self.conf, - 'page_slug': page_slug, - 'static_page': static_page, - 'writeable': True, # otherwise, `static_page` would be `None` and we'd have raised `PermissionDenied()` earlier - 'not_latest_revision': not_latest_revision, - 'revision': revision, - 'form': form, - }) + return TemplateResponse( + request, + self.template_name, + { + 'page': static_page, + 'conf': self.conf, + 'page_slug': page_slug, + 'static_page': static_page, + 'writeable': True, # otherwise, `static_page` would be `None` and we'd have raised `PermissionDenied` earlier + 'not_latest_revision': not_latest_revision, + 'revision': revision, + 'form': form, + }, + ) title = form.cleaned_data['title'] body = form.cleaned_data['body'] @@ -645,18 +674,22 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): sanitize_html=static_page.sanitize_html, ) - return TemplateResponse(request, self.template_name, { - 'page': static_page, - 'preview_title': title, - 'preview_body': preview_body, - 'conf': self.conf, - 'page_slug': page_slug, - 'static_page': static_page, - 'writeable': True, # otherwise, `static_page` would be `None` and we'd error out early - 'not_latest_revision': not_latest_revision, - 'revision': revision, - 'form': form, - }) + return TemplateResponse( + request, + self.template_name, + { + 'page': static_page, + 'preview_title': title, + 'preview_body': preview_body, + 'conf': self.conf, + 'page_slug': page_slug, + 'static_page': static_page, + 'writeable': True, # otherwise, `static_page` would be `None` and we'd error out early + 'not_latest_revision': not_latest_revision, + 'revision': revision, + 'form': form, + }, + ) if not page_exists: static_page.save() @@ -667,9 +700,9 @@ class StaticPageEditView(ConferenceRequiredMixin, TemplateView): revision.save() if not page_exists: - messages.success(request, gettext("Created Static Page")) + messages.success(request, gettext('Created Static Page')) else: - messages.success(request, gettext("Updated Static Page")) + messages.success(request, gettext('Updated Static Page')) return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) @@ -678,8 +711,7 @@ class StaticPageHistoryView(ConferenceRequiredMixin, TemplateView): require_user = True def get(self, request, page_slug, **kwargs): - self.static_page = StaticPage.objects.conference_accessible(conference=self.conf, language=get_language())\ - .filter(slug=page_slug).first() + self.static_page = StaticPage.objects.conference_accessible(conference=self.conf, language=get_language()).filter(slug=page_slug).first() if not self.static_page: return redirect(reverse('plainui:static_page', kwargs={'page_slug': page_slug})) @@ -730,13 +762,13 @@ class StaticPageDiffView(ConferenceRequiredMixin, TemplateView): rev2_id = self.request.GET.get('rev2') if rev1_id is None or rev2_id is None: - raise SuspiciousOperation("Only one revision given, need two") + raise SuspiciousOperation('Only one revision given, need two') try: rev1 = self.static_page.revisions.get(revision=rev1_id) rev2 = self.static_page.revisions.get(revision=rev2_id) except StaticPageRevision.DoesNotExist: - raise Http404("One of the selected revisions was not found") + raise Http404('One of the selected revisions was not found') differ = StaticPageDiff(tabsize=4) diff = differ.make_table(rev1.body.splitlines(keepends=True), rev2.body.splitlines(keepends=True), context=True, numlines=3) @@ -770,7 +802,7 @@ class StaticPageGlobalHistoryView(ConferenceRequiredMixin, TemplateView): context = super().get_context_data(**kwargs) context['conf'] = self.conf - context['history'] = history[PAGE_SIZE * page: PAGE_SIZE * page + PAGE_SIZE] + context['history'] = history[PAGE_SIZE * page : PAGE_SIZE * page + PAGE_SIZE] return context @@ -791,7 +823,7 @@ class ProfileView(ConferenceRequiredMixin, UpdateView): form1 = super().get_form(form_class) form2_kwargs = super().get_form_kwargs() - if hasattr(self, "object"): + if hasattr(self, 'object'): cm = self.object.conferences.filter(conference=self.conf).first() if cm: form2_kwargs['instance'] = cm @@ -814,11 +846,19 @@ class ProfileView(ConferenceRequiredMixin, UpdateView): context['badges'] = UserBadge.objects.filter(user=user, accepted_by_user=True).select_related('badge') context['amount_badges_not_accepted'] = len(UserBadge.objects.filter(user=user, accepted_by_user=False).select_related('badge')) context['is_favorite_events'] = favorite_events = _session_get_favorite_events(self.request.session, self.request.user) - context['my_favorite_events'] = Event.objects.conference_accessible(conference=self.conf).filter(pk__in=favorite_events).filter(schedule_start__isnull=False, schedule_end__isnull=False) # noqa:E501 + context['my_favorite_events'] = ( + Event.objects.conference_accessible(conference=self.conf) + .filter(pk__in=favorite_events) + .filter(schedule_start__isnull=False, schedule_end__isnull=False) + ) # noqa:E501 context['is_favorite_assemblies'] = favorite_assemblies = _session_get_favorite_assemblies(self.request.session, self.request.user) context['my_favorite_assemblies'] = Assembly.objects.conference_accessible(conference=self.conf).filter(pk__in=favorite_assemblies) context['is_fahrplan_events'] = scheduled_events = _session_get_scheduled_events(self.request.session, self.request.user) - context['my_fahrplan_events'] = Event.objects.conference_accessible(conference=self.conf).filter(pk__in=scheduled_events).filter(schedule_start__isnull=False, schedule_end__isnull=False) # noqa:E501 + context['my_fahrplan_events'] = ( + Event.objects.conference_accessible(conference=self.conf) + .filter(pk__in=scheduled_events) + .filter(schedule_start__isnull=False, schedule_end__isnull=False) + ) # noqa:E501 context['dereferrer_allowlist'] = UserDereferrerAllowlist.objects.filter(user=self.request.user) context['redeem_badge_form'] = RedeemBadgeForm() return context @@ -848,7 +888,7 @@ class ProfileView(ConferenceRequiredMixin, UpdateView): form2.save() self.request.session['theme'] = form1.instance.theme - messages.success(self.request, gettext("Updated Profile")) + messages.success(self.request, gettext('Updated Profile')) try: WorkAdventureIntegration.push_userinfo(conference=self.conf, user=self.request.user) @@ -881,7 +921,7 @@ class ModifyThemeView(ConferenceRequiredMixin, View): return redirect(redirect_to) -@method_decorator(ratelimit(group="redeem_badge", key='user', rate=settings.BADGE_RATE_LIMIT), name='dispatch') +@method_decorator(ratelimit(group='redeem_badge', key='user', rate=settings.BADGE_RATE_LIMIT), name='dispatch') class RedeemBadgeView(ConferenceRequiredMixin, FormView): require_user = True template_name = 'plainui/manage_badges.html' @@ -889,7 +929,7 @@ class RedeemBadgeView(ConferenceRequiredMixin, FormView): external_context = None def __init__(self, **kwargs: Any) -> None: - self.external_context = kwargs.pop("external_context", {}) + self.external_context = kwargs.pop('external_context', {}) super().__init__(**kwargs) def get_form_kwargs(self) -> dict[str, Any]: @@ -913,7 +953,7 @@ class RedeemBadgeView(ConferenceRequiredMixin, FormView): context = super().get_context_data(**kwargs) context['conf'] = self.conf context.update(self.external_context) - context["redeem_badge_form"] = context.pop("form") + context['redeem_badge_form'] = context.pop('form') return context def get_success_url(self): @@ -938,11 +978,11 @@ class ManageBadgeView(ConferenceRequiredMixin, TemplateView): template_name = 'plainui/manage_badges.html' def get_queryset(self): - return UserBadge.objects.filter(user=self.request.user).values_list("badge", flat=True) + return UserBadge.objects.filter(user=self.request.user).values_list('badge', flat=True) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - if hasattr(self, "conf"): + if hasattr(self, 'conf'): context['conf'] = self.conf user = self.request.user @@ -959,7 +999,7 @@ class ManageBadgeView(ConferenceRequiredMixin, TemplateView): return resp def dispatch(self, request, *args, **kwargs): - if hasattr(request, "GET") and request.GET.get('redeem_token') or hasattr(request, "GET") and request.POST.get('purpose', None) == 'redeem_token': + if hasattr(request, 'GET') and request.GET.get('redeem_token') or hasattr(request, 'GET') and request.POST.get('purpose', None) == 'redeem_token': return RedeemBadgeView.as_view(external_context=self.get_context_data())(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs) @@ -972,7 +1012,7 @@ class ManageBadgeView(ConferenceRequiredMixin, TemplateView): userbadge = UserBadge.objects.filter(user=user, badge=badge).first() if not userbadge: - raise Exception("The badge is not found.") + raise Exception('The badge is not found.') if not visibility: # Change acceptanceState if userbadge.accepted_by_user: @@ -1118,7 +1158,7 @@ class RedeemTokenLoggedIn(ConferenceRequiredMixin, View): try: with transaction.atomic(): if self.conf.end <= self.now: - raise TicketValidationError(gettext("The Conference is over!")) + raise TicketValidationError(gettext('The Conference is over!')) ConferenceMemberTicket.redeem_pretix_ticket(self.conf, request.user, token) ConferenceMember.objects.update_or_create(conference=self.conf, user=request.user, defaults={'has_ticket': True}) return HttpResponseRedirect(reverse('plainui:index')) @@ -1151,39 +1191,50 @@ class ModifyFavoritesView(ConferenceRequiredMixin, View): EventLikeCount.objects.filter(event1=fav_id, event2__in=user.favorite_events.exclude(pk=fav_id)).update(likes=F('likes') - 1) EventLikeCount.objects.filter(event2=fav_id, event1__in=user.favorite_events.all()).update(likes=F('likes') - 1) request.user.favorite_events.remove(fav_id) - else: - if request.POST['type'] == 'assembly': - assembly = Assembly.objects.conference_accessible(conference=self.conf).get(pk=fav_id) - if not request.user.favorite_assemblies.filter(pk=fav_id).exists(): - request.user.favorite_assemblies.add(assembly) - with connection.cursor() as cursor: - cursor.execute(""" + elif request.POST['type'] == 'assembly': + assembly = Assembly.objects.conference_accessible(conference=self.conf).get(pk=fav_id) + if not request.user.favorite_assemblies.filter(pk=fav_id).exists(): + request.user.favorite_assemblies.add(assembly) + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO core_assemblylikecount AS lc (assembly1_id, assembly2_id, likes, like_ratio) + SELECT %s, e.assembly_id, 1, 0 FROM core_assembly_favorited_by AS e WHERE e.assembly_id <> %s AND e.platformuser_id = %s + ON CONFLICT (assembly1_id, assembly2_id) DO UPDATE SET likes = lc.likes + 1 + """, + [fav_id, fav_id, user.pk], + ) + + cursor.execute( + """ INSERT INTO core_assemblylikecount AS lc (assembly1_id, assembly2_id, likes, like_ratio) - SELECT %s, e.assembly_id, 1, 0 FROM core_assembly_favorited_by AS e WHERE e.assembly_id <> %s AND e.platformuser_id = %s + SELECT e.assembly_id, %s, 1, 0 FROM core_assembly_favorited_by AS e WHERE e.platformuser_id = %s ON CONFLICT (assembly1_id, assembly2_id) DO UPDATE SET likes = lc.likes + 1 - """, [fav_id, fav_id, user.pk]) - - cursor.execute(""" - INSERT INTO core_assemblylikecount AS lc (assembly1_id, assembly2_id, likes, like_ratio) - SELECT e.assembly_id, %s, 1, 0 FROM core_assembly_favorited_by AS e WHERE e.platformuser_id = %s - ON CONFLICT (assembly1_id, assembly2_id) DO UPDATE SET likes = lc.likes + 1 - """, [fav_id, user.pk]) - elif request.POST['type'] == 'event': - event = Event.objects.conference_accessible(conference=self.conf).get(pk=fav_id) - if not request.user.favorite_events.filter(pk=fav_id).exists(): - request.user.favorite_events.add(event) - with connection.cursor() as cursor: - cursor.execute(""" + """, + [fav_id, user.pk], + ) + elif request.POST['type'] == 'event': + event = Event.objects.conference_accessible(conference=self.conf).get(pk=fav_id) + if not request.user.favorite_events.filter(pk=fav_id).exists(): + request.user.favorite_events.add(event) + with connection.cursor() as cursor: + cursor.execute( + """ + INSERT INTO core_eventlikecount AS lc (event1_id, event2_id, likes, like_ratio) + SELECT %s, e.event_id, 1, 0 FROM core_event_favorited_by AS e WHERE e.event_id <> %s AND e.platformuser_id = %s + ON CONFLICT (event1_id, event2_id) DO UPDATE SET likes = lc.likes + 1 + """, + [fav_id, fav_id, user.pk], + ) + + cursor.execute( + """ INSERT INTO core_eventlikecount AS lc (event1_id, event2_id, likes, like_ratio) - SELECT %s, e.event_id, 1, 0 FROM core_event_favorited_by AS e WHERE e.event_id <> %s AND e.platformuser_id = %s + SELECT e.event_id, %s, 1, 0 FROM core_event_favorited_by AS e WHERE e.platformuser_id = %s ON CONFLICT (event1_id, event2_id) DO UPDATE SET likes = lc.likes + 1 - """, [fav_id, fav_id, user.pk]) - - cursor.execute(""" - INSERT INTO core_eventlikecount AS lc (event1_id, event2_id, likes, like_ratio) - SELECT e.event_id, %s, 1, 0 FROM core_event_favorited_by AS e WHERE e.platformuser_id = %s - ON CONFLICT (event1_id, event2_id) DO UPDATE SET likes = lc.likes + 1 - """, [fav_id, user.pk]) + """, + [fav_id, user.pk], + ) if request.POST['type'] == 'assembly': _session_refresh_favorite_assemblies(self.request.session, self.request.user) @@ -1207,6 +1258,7 @@ class ModifyLanguageView(ConferenceRequiredMixin, View): def post(self, request): from core.templatetags.hub_absolute import hub_absolute + redirect_to = request.POST.get('next') or hub_absolute('plainui:index') url_is_safe = url_has_allowed_host_and_scheme( @@ -1278,7 +1330,7 @@ class PersonalMessageListView(ConferenceRequiredMixin, TemplateView): context['start'] = start context['total'] = dms.count() - context['msgs'] = dms[start:start + PAGE_SIZE] + context['msgs'] = dms[start : start + PAGE_SIZE] return context @@ -1290,10 +1342,7 @@ class PersonalMessageSendView(ConferenceRequiredMixin, FormView): form_class = NewDirectMessageForm def get_context_data(self, **kwargs): - return super().get_context_data( - conf=self.conf, - **kwargs - ) + return super().get_context_data(conf=self.conf, **kwargs) def get_initial(self): initial = super().get_initial() @@ -1328,7 +1377,7 @@ class PersonalMessageSendView(ConferenceRequiredMixin, FormView): if in_reply_to: DirectMessage.objects.filter(id=in_reply_to, recipient=self.request.user).update(has_responded=True) - messages.success(self.request, gettext("Message sent.")) + messages.success(self.request, gettext('Message sent.')) return super().form_valid(form) @@ -1347,10 +1396,7 @@ class PersonalMessageShowView(ConferenceRequiredMixin, TemplateView): DirectMessage.objects.filter(pk=message.pk).update(was_read=True) context['msg_body'] = render_markdown(self.conf, message.body) - context['report_info'] = { - 'kind': 'pn', - 'url': message.id - } + context['report_info'] = {'kind': 'pn', 'url': message.id} return context @@ -1366,7 +1412,7 @@ class PersonalMessageDeleteView(ConferenceRequiredMixin, View): DirectMessage.objects.filter(recipient=request.user, pk=request.POST['id']).update(deleted_by_recipient=True) DirectMessage.objects.filter(pk=request.POST['id'], deleted_by_sender=True, deleted_by_recipient=True).delete() - messages.success(self.request, gettext("Message deleted.")) + messages.success(self.request, gettext('Message deleted.')) return redirect(reverse('plainui:personal_message')) @@ -1381,7 +1427,7 @@ class IndexView(ConferenceRequiredMixin, TemplateView): default_title = 'Page Missing' if not default_body: - url = reverse("plainui:static_page_edit", kwargs={"page_slug": slug}) + url = reverse('plainui:static_page_edit', kwargs={'page_slug': slug}) default_body = 'Please configure the wiki page "{slug}" (or change slug). <a href="{url}">(edit)</a>' static_page = StaticPage(conference=self.conf, title=default_title, body_html=default_body.format(slug=slug, url=url)) @@ -1391,9 +1437,7 @@ class IndexView(ConferenceRequiredMixin, TemplateView): context = super().get_context_data(**kwargs) context['conf'] = self.conf - context['report_info'] = { - 'url': reverse('plainui:index') - } + context['report_info'] = {'url': reverse('plainui:index')} context['share_url'] = reverse('plainui:index') context['start'] = self._fetch_page('start') @@ -1461,13 +1505,10 @@ class PasswordChangeView(ConferenceRequiredMixin, auth_views.PasswordChangeView) return super().dispatch(*args, **kwargs) def get_context_data(self, **kwargs): - return super().get_context_data( - conf=self.conf, - **kwargs - ) + return super().get_context_data(conf=self.conf, **kwargs) def get_success_url(self): - messages.success(self.request, gettext("Password changed successfully.")) + messages.success(self.request, gettext('Password changed successfully.')) return reverse('plainui:userprofile') @@ -1476,9 +1517,7 @@ class PasswordResetView(ConferenceRequiredMixin, BasePasswordResetView): require_login = False success_url = reverse_lazy('plainui:password_reset_done') template_name = 'plainui/registration/password_reset_form.html' - extra_email_context = { - "reset_url": 'plainui:password_reset_confirm' - } + extra_email_context = {'reset_url': 'plainui:password_reset_confirm'} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -1544,12 +1583,12 @@ class TokenPasswordResetView(ConferenceRequiredMixin, FormView): def form_valid(self, form): form.save() - messages.success(self.request, gettext("Passwort successfully reset!")) + messages.success(self.request, gettext('Passwort successfully reset!')) return HttpResponseRedirect(reverse('plainui:login')) class LogoutView(auth_views.LogoutView): - next_page = "plainui:login" + next_page = 'plainui:login' class BoardView(ConferenceRequiredMixin, TemplateView): @@ -1564,22 +1603,17 @@ class BoardView(ConferenceRequiredMixin, TemplateView): start = int(self.request.GET['start']) except (ValueError, KeyError): start = 0 - entries = BulletinBoardEntry.objects \ - .select_related('owner') \ - .filter(conference=self.conf, is_public=True) \ - .order_by('-timestamp') + entries = BulletinBoardEntry.objects.select_related('owner').filter(conference=self.conf, is_public=True).order_by('-timestamp') if self.request.user.is_authenticated and self.request.user.shadow_banned: entries = entries.filter(Q(owner=self.request.user) | Q(hidden=False)) else: entries = entries.filter(hidden=False) - context['board'] = entries[start:start + PAGE_SIZE] + context['board'] = entries[start : start + PAGE_SIZE] context['current_user'] = self.request.user.id if self.request.user.is_authenticated else 0 context['next_url'] = reverse('plainui:board') + '?start=' + str(start + PAGE_SIZE) context['prev_url'] = reverse('plainui:board') + '?start=' + str(start - PAGE_SIZE) if start != 0 else None - context['report_info'] = { - 'url': reverse('plainui:board') - } + context['report_info'] = {'url': reverse('plainui:board')} context['share_url'] = reverse('plainui:board') if start == 0 else reverse('plainui:board') + '?start=' + str(start) return context @@ -1593,9 +1627,7 @@ class BoardPrivateView(ConferenceRequiredMixin, TemplateView): context = super().get_context_data(**kwargs) context['conf'] = self.conf - context['board'] = BulletinBoardEntry.objects \ - .filter(conference=self.conf, owner=self.request.user) \ - .order_by('-timestamp') + context['board'] = BulletinBoardEntry.objects.filter(conference=self.conf, owner=self.request.user).order_by('-timestamp') return context @@ -1609,10 +1641,7 @@ class BoardEntryView(ConferenceRequiredMixin, TemplateView): context['entry'] = get_object_or_404(BulletinBoardEntry, conference=self.conf, pk=self.kwargs['id'], is_public=True) - context['report_info'] = { - 'kind': 'board', - 'url': self.kwargs['id'] - } + context['report_info'] = {'kind': 'board', 'url': self.kwargs['id']} context['share_url'] = reverse('plainui:board_entry', kwargs={'id': self.kwargs['id']}) return context @@ -1626,11 +1655,7 @@ class BoardEntryEditView(ConferenceRequiredMixin, FormView): form_class = BulletinBoardEntryForm def get_context_data(self, **kwargs): - return super().get_context_data( - conf=self.conf, - edit_mode=self.kwargs.get('id', 0) != 0, - **kwargs - ) + return super().get_context_data(conf=self.conf, edit_mode=self.kwargs.get('id', 0) != 0, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -1652,9 +1677,9 @@ class BoardEntryEditView(ConferenceRequiredMixin, FormView): self.board_entry.save() if 'id' in self.kwargs: - messages.success(self.request, gettext("Bulletin Board Entry updated.")) + messages.success(self.request, gettext('Bulletin Board Entry updated.')) else: - messages.success(self.request, gettext("Bulletin Board Entry created.")) + messages.success(self.request, gettext('Bulletin Board Entry created.')) return super().form_valid(form) @@ -1667,7 +1692,7 @@ class BoardEntryDeleteView(ConferenceRequiredMixin, View): def post(self, request, **kwargs): BulletinBoardEntry.objects.filter(conference_id=self.conf, owner=self.request.user, id=request.POST['id']).delete() - messages.success(self.request, gettext("Bulletin Board Entry deleted.")) + messages.success(self.request, gettext('Bulletin Board Entry deleted.')) return redirect(reverse('plainui:board_private')) @@ -1696,12 +1721,12 @@ def _organize_events_for_calendar(conf, events): if not events_by_room: return None - rooms = Room.objects.conference_accessible(conf) \ - .filter(pk__in=events_by_room.keys()) \ + rooms = ( + Room.objects.conference_accessible(conf) + .filter(pk__in=events_by_room.keys()) .order_by('official_room_order', F('capacity').desc(nulls_last=True), 'name') - rooms_with_events = [ - (room, events_by_room[room.id]) for room in rooms - ] + ) + rooms_with_events = [(room, events_by_room[room.id]) for room in rooms] # calendar starts at first scheduled event rounded down to the full hour calendar_start = min( @@ -1711,7 +1736,7 @@ def _organize_events_for_calendar(conf, events): calendar_start = localtime(calendar_start.replace(minute=0, second=0, microsecond=0)) # calendar ends at the end of the last scheduled event rounded up to the full hour - calendar_end = max((room_events[-1]['event'].schedule_end for (_, room_events) in rooms_with_events)) + calendar_end = max(room_events[-1]['event'].schedule_end for (_, room_events) in rooms_with_events) calendar_end = (calendar_end + timedelta(0, 3599)).replace(minute=0, second=0, microsecond=0) calendar_end = localtime(calendar_end) @@ -1736,8 +1761,18 @@ UPCOMING_WINDOW = timedelta(minutes=30) def _event_filter( - user: PlatformUser, conf, day=None, kinds=None, assembly=None, track=None, - room=None, user_schedule_only=False, upcoming=False, calendar_mode=True, public_fahrplan=None): + user: PlatformUser, + conf, + day=None, + kinds=None, + assembly=None, + track=None, + room=None, + user_schedule_only=False, + upcoming=False, + calendar_mode=True, + public_fahrplan=None, +): min_date, max_date = conf.start, conf.end if min_date is None or max_date is None: return Event.objects.none() @@ -1766,20 +1801,11 @@ def _event_filter( events = events.filter(Q(schedule_start__lt=now, schedule_end__gte=now) | Q(schedule_start__gte=now, schedule_start__lt=now + UPCOMING_WINDOW)) if calendar_mode: events = events.filter(room__isnull=False) - res = events.filter( - schedule_duration__isnull=False, - **filters - ).order_by('schedule_start', 'schedule_end') + res = events.filter(schedule_duration__isnull=False, **filters).order_by('schedule_start', 'schedule_end') res = res.annotate(track_name=F('track__name')) speakers = EventParticipant.objects.filter(is_public=True, role=EventParticipant.Role.SPEAKER).order_by('participant__username') speakers = speakers.annotate(speaker_name=F('participant__username')) - return res.prefetch_related( - Prefetch( - 'participants', - queryset=speakers, - to_attr='speakers' - ) - ) + return res.prefetch_related(Prefetch('participants', queryset=speakers, to_attr='speakers')) class FahrplanView(ConferenceRequiredMixin, TemplateView): @@ -1804,7 +1830,7 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): min_date = self.conf.start max_date = self.conf.end if min_date is None or max_date is None: - raise Http404() + raise Http404 n_days = (max_date - min_date).days if (max_date - min_date) != timedelta(n_days): n_days += 1 @@ -1907,7 +1933,8 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): track=track, calendar_mode=mode == 'calendar', public_fahrplan=public_fahrplan, - **self.filter_opts) + **self.filter_opts, + ) if mode == 'calendar': context['mode'] = 'calendar' @@ -1939,20 +1966,19 @@ class PublicFahrplanView(ConferenceRequiredMixin, TemplateView): context['conference_timezone'] = self.conf.timezone events = _event_filter(self.request.user, self.conf, public_fahrplan=True) - # events = _event_filter(self.request.user, self.conf) context['events'] = _organize_events_for_calendar(self.conf, events) return context class AssemblyJoinBBB(ConferenceRequiredMixin, View): - """ extra view to join BBB rooms from the Workadventure """ + """extra view to join BBB rooms from the Workadventure""" require_user = True def get(self, request, assembly_slug, room_slug): room = get_object_or_404(Room.objects.conference_accessible(self.conf), assembly__slug=assembly_slug, slug=room_slug) if not room.room_type == Room.RoomType.BIGBLUEBUTTON: - raise Http404() + raise Http404 try: return HttpResponseRedirect(integrations.BigBlueButton.join_room(room, self.request.user, not self.request.user.show_name)) @@ -1973,14 +1999,14 @@ class RoomView(ConferenceRequiredMixin, TemplateView): if self.request.user.is_authenticated: return HttpResponseRedirect(integrations.BigBlueButton.join_room(self.room, self.request.user, not self.request.user.show_name)) - messages.error(request, gettext("RoomView--loginrequired")) + messages.error(request, gettext('RoomView--loginrequired')) elif self.room.room_type == Room.RoomType.WORKADVENTURE: if self.request.user.is_authenticated: wa_session = WorkadventureSession.create_for_conference_user(self.conf, self.request.user, self.room) token_url = wa_session.get_token_url() return redirect(token_url) - messages.error(request, gettext("RoomView--loginrequired")) + messages.error(request, gettext('RoomView--loginrequired')) except integrations.IntegrationError as e: messages.error(request, str(e)) @@ -2005,9 +2031,7 @@ class RoomView(ConferenceRequiredMixin, TemplateView): context['my_favorite_events'] = _session_get_favorite_events(self.request.session, self.request.user) context['my_scheduled_events'] = _session_get_scheduled_events(self.request.session, self.request.user) - context['report_info'] = { - 'url': reverse('plainui:room', kwargs={'room_slug': self.room.slug}) - } + context['report_info'] = {'url': reverse('plainui:room', kwargs={'room_slug': self.room.slug})} context['share_url'] = reverse('plainui:room', kwargs={'room_slug': self.room.slug}) return context @@ -2021,9 +2045,7 @@ class RoomsView(ConferenceRequiredMixin, TemplateView): context['conf'] = self.conf context['rooms'] = Room.objects.conference_accessible(self.conf) - context['report_info'] = { - 'url': reverse('plainui:rooms') - } + context['report_info'] = {'url': reverse('plainui:rooms')} context['share_url'] = reverse('plainui:rooms') return context @@ -2040,7 +2062,7 @@ class UpcomingView(ConferenceRequiredMixin, TemplateView): events=events, my_favorite_events=_session_get_favorite_events(self.request.session, self.request.user), my_scheduled_events=_session_get_scheduled_events(self.request.session, self.request.user), - **kwargs + **kwargs, ) @@ -2077,33 +2099,29 @@ class ChannelEventsView(ConferenceRequiredMixin, TemplateView): rooms.append(room) room_ids.append(room.pk) - current_events_qs = Event.objects.conference_accessible(self.conf)\ - .filter(room_id__in=room_ids, schedule_start__lte=now, schedule_end__gte=now).order_by('room_id', 'schedule_start').distinct('room_id') + current_events_qs = ( + Event.objects.conference_accessible(self.conf) + .filter(room_id__in=room_ids, schedule_start__lte=now, schedule_end__gte=now) + .order_by('room_id', 'schedule_start') + .distinct('room_id') + ) speakers = EventParticipant.objects.filter(is_public=True, role=EventParticipant.Role.SPEAKER).order_by('participant__username') speakers = speakers.annotate(speaker_name=F('participant__username')) - current_events_qs = current_events_qs.prefetch_related( - Prefetch( - 'participants', - queryset=speakers, - to_attr='speakers' - ) - ) + current_events_qs = current_events_qs.prefetch_related(Prefetch('participants', queryset=speakers, to_attr='speakers')) current_events = {} # type: Dict[UUID, Event] for event in current_events_qs: current_events[event.room_id] = event - next_events_qs = Event.objects.conference_accessible(self.conf)\ - .filter(room_id__in=room_ids, schedule_start__gt=now, schedule_end__isnull=False).order_by('room_id', 'schedule_start').distinct('room_id') + next_events_qs = ( + Event.objects.conference_accessible(self.conf) + .filter(room_id__in=room_ids, schedule_start__gt=now, schedule_end__isnull=False) + .order_by('room_id', 'schedule_start') + .distinct('room_id') + ) speakers = EventParticipant.objects.filter(is_public=True, role=EventParticipant.Role.SPEAKER).order_by('participant__username') speakers = speakers.annotate(speaker_name=F('participant__username')) - next_events_qs = next_events_qs.prefetch_related( - Prefetch( - 'participants', - queryset=speakers, - to_attr='speakers' - ) - ) + next_events_qs = next_events_qs.prefetch_related(Prefetch('participants', queryset=speakers, to_attr='speakers')) next_events = {} # type: Dict[UUID, Event] for event in next_events_qs: @@ -2131,12 +2149,9 @@ class ChannelEventsView(ConferenceRequiredMixin, TemplateView): context['my_scheduled_events'] = _session_get_scheduled_events(self.request.session, self.request.user) events = _event_filter(self.request.user, self.conf, public_fahrplan=True) - # events = _event_filter(self.request.user, self.conf) context['events'] = _organize_events_for_calendar(self.conf, events) - context['report_info'] = { - 'url': reverse('plainui:channel_events') - } + context['report_info'] = {'url': reverse('plainui:channel_events')} context['share_url'] = reverse('plainui:channel_events') return context @@ -2181,7 +2196,7 @@ class WorkadventureEnter(ConferenceRequiredMixin, View): class BaseDereferrerView(ConferenceRequiredMixin, View): def gen_salt(self): if self.request.user.is_authenticated: - return '%sdereferrer' % (self.request.user.pk,) + return f'{self.request.user.pk}dereferrer' else: return '000000dereferrer' @@ -2189,7 +2204,7 @@ class BaseDereferrerView(ConferenceRequiredMixin, View): try: return signing.loads(signed_payload, salt=self.gen_salt()) except signing.BadSignature: - raise PermissionDenied() + raise PermissionDenied def sign_payload(self, payload: str) -> str: return signing.dumps(payload, salt=self.gen_salt()) @@ -2201,11 +2216,12 @@ class BaseDereferrerView(ConferenceRequiredMixin, View): if url_in_allowlist(scheme_and_netloc, settings.DEREFERRER_COUNT_ACCESS): with connection.cursor() as cursor: cursor.execute( - "INSERT INTO " + DereferrerStats._meta.db_table + " AS ds (domain, hits) VALUES (%s, 1) \ - ON CONFLICT (domain) DO UPDATE SET hits = ds.hits + 1", - [scheme_and_netloc] + 'INSERT INTO ' + + DereferrerStats._meta.db_table + + ' AS ds (domain, hits) VALUES (%s, 1) \ + ON CONFLICT (domain) DO UPDATE SET hits = ds.hits + 1', + [scheme_and_netloc], ) - # DereferrerStats.objects.filter(domain=scheme_and_netloc).update(hits=F('hits') + 1) return HttpResponseRedirect(url.geturl()) @@ -2247,7 +2263,7 @@ class DereferrerView(BaseDereferrerView): url = urlparse(destination) # local urls are always allowed - allowlist_match = (not url.scheme and not url.netloc) + allowlist_match = not url.scheme and not url.netloc scheme = url.scheme if url.scheme else request.scheme scheme_and_netloc = scheme + '://' + url.netloc @@ -2271,13 +2287,17 @@ class DereferrerView(BaseDereferrerView): else: # if the url is not allowed, show the dereferrer page signed_url = self.sign_payload(url.geturl()) - return TemplateResponse(request, self.template_name, context={ - 'conf': self.conf, - 'plain_url': url.geturl(), - 'domain': scheme_and_netloc, - 'signed_url': signed_url, - 'can_allow': request.user.is_authenticated, - }) + return TemplateResponse( + request, + self.template_name, + context={ + 'conf': self.conf, + 'plain_url': url.geturl(), + 'domain': scheme_and_netloc, + 'signed_url': signed_url, + 'can_allow': request.user.is_authenticated, + }, + ) class WorkadventureDereferrerView(DereferrerView): @@ -2295,7 +2315,7 @@ class DereferrerRemoveFromAllowlistView(View): return redirect(reverse('plainui:userprofile')) entry.delete() - messages.success(request, gettext("UserDereferrerAllowlist--deleted")) + messages.success(request, gettext('UserDereferrerAllowlist--deleted')) return redirect(reverse('plainui:userprofile')) @@ -2314,11 +2334,7 @@ class ReportContentView(ConferenceRequiredMixin, FormView): conference=self.conf, title='Page Missing', body_html='Please configure the static page "report_content" (or change slug... ).' ) - return super().get_context_data( - conf=self.conf, - page=static_page, - **kwargs - ) + return super().get_context_data(conf=self.conf, page=static_page, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -2338,10 +2354,10 @@ class ReportContentView(ConferenceRequiredMixin, FormView): messages.success( self.request, gettext( - "Thank you for your help to make this plattform safer and better! " - "Please give us some time to find a solution and keep an eye on your Messages, we may contact you." - ) - ) + 'Thank you for your help to make this plattform safer and better! ' + 'Please give us some time to find a solution and keep an eye on your Messages, we may contact you.' + ), + ) redirect_to = form.cleaned_data['next'] url_is_safe = url_has_allowed_host_and_scheme( @@ -2375,20 +2391,26 @@ class UserView(ConferenceRequiredMixin, TemplateView): contact = user.is_authenticated and UserContact.objects.filter(user=display_user, contact=user).first() if contact and not contact.pending: # Show Friend Badges - context['badges'] = UserBadge.objects.filter(user=display_user, accepted_by_user=True) \ - .filter(Q(visibility__in=[UserBadge.Visibility.PUBLIC, UserBadge.Visibility.FRIENDS, UserBadge.Visibility.CLUBFRIENDS]) | - Q(badge__in=user_badges, visibility__in=[UserBadge.Visibility.CLUB, UserBadge.Visibility.CLUBFRIENDS])) \ - .select_related('badge') + context['badges'] = ( + UserBadge.objects.filter(user=display_user, accepted_by_user=True) + .filter( + Q(visibility__in=[UserBadge.Visibility.PUBLIC, UserBadge.Visibility.FRIENDS, UserBadge.Visibility.CLUBFRIENDS]) + | Q(badge__in=user_badges, visibility__in=[UserBadge.Visibility.CLUB, UserBadge.Visibility.CLUBFRIENDS]) + ) + .select_related('badge') + ) else: # Show only public badges - context['badges'] = UserBadge.objects.filter(user=display_user, accepted_by_user=True) \ - .filter(Q(visibility__in=[UserBadge.Visibility.PUBLIC]) | - Q(badge__in=user_badges, visibility__in=[UserBadge.Visibility.CLUB, UserBadge.Visibility.CLUBFRIENDS])) \ - .select_related('badge') + context['badges'] = ( + UserBadge.objects.filter(user=display_user, accepted_by_user=True) + .filter( + Q(visibility__in=[UserBadge.Visibility.PUBLIC]) + | Q(badge__in=user_badges, visibility__in=[UserBadge.Visibility.CLUB, UserBadge.Visibility.CLUBFRIENDS]) + ) + .select_related('badge') + ) - context['report_info'] = { - 'url': reverse('plainui:user', kwargs={'user_slug': display_user.slug}) - } + context['report_info'] = {'url': reverse('plainui:user', kwargs={'user_slug': display_user.slug})} return context @@ -2411,44 +2433,34 @@ class ComponentGalleryView(TemplateView): conf={ 'slug': 'sample', 'name': 'Example', - "id": "confID", - "get_navigation_tree": lambda: [{ - 'label': 'Conference', - 'title': 'Conference', - 'icon': 'hammer', - 'url': '/', - }], - "map_config": {"frontend": { - "start": { - "latitude": 53.56172, - "longitude": 9.98593, - "zoom": 16 - }, - "style": { - "version": 8, - "sources": { - "osm-raster": { - "type": "raster", - "tiles": [ - "https://tile.openstreetmap.org/{z}/{x}/{y}.png" - ], - "tileSize": 256, - "attribution": "© OpenStreetMap contributors / test use only!!!" - } + 'id': 'confID', + 'get_navigation_tree': lambda: [ + { + 'label': 'Conference', + 'title': 'Conference', + 'icon': 'hammer', + 'url': '/', + } + ], + 'map_config': { + 'frontend': { + 'start': {'latitude': 53.56172, 'longitude': 9.98593, 'zoom': 16}, + 'style': { + 'version': 8, + 'sources': { + 'osm-raster': { + 'type': 'raster', + 'tiles': ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], + 'tileSize': 256, + 'attribution': '© OpenStreetMap contributors / test use only!!!', + } + }, + 'layers': [{'id': 'simple-tiles', 'type': 'raster', 'source': 'osm-raster', 'minzoom': 0, 'maxzoom': 19}], }, - "layers": [ - { - "id": "simple-tiles", - "type": "raster", - "source": "osm-raster", - "minzoom": 0, - "maxzoom": 19 - } - ] } - }} + }, }, - demo_map_startpos={"longitude": 9.98641, "latitude": 53.56148}, + demo_map_startpos={'longitude': 9.98641, 'latitude': 53.56148}, csrf_input='xss-safe', hide_header=True, time1=timezone.now(), diff --git a/tests/utils.py b/tests/utils.py index eec7db6e6..988e88631 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -4,7 +4,6 @@ from urllib.parse import urljoin import requests - _DEFAULT_BASE_URL = 'http://hubapp' _DEFAULT_ADMIN_USERNAME = 'admin' _DEFAULT_ADMIN_PASSWORD = 'password' -- GitLab