diff --git a/deployment/docker/check_psql.py b/deployment/docker/check_psql.py index e542e5c8c075e72f633bfa0b06d768df501f5b15..1c2d2e41d8378c916d3817d322ee95af396ef697 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 a07fb65e16c51efa78f8329dc598927ed318a20c..3a1449f286a6d71fb943f9dca0db31f24b59750a 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 5c2c7aae635b8d844852787d8ad6f284d59dec39..431a9b1806bea49f50207cb3b42a9c941af41bec 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 33da86f37b2e6ac05a70756217e51c3c7d1349a0..5c9ed35a26409e65cb09a6d28667d4a239a6a887 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 fa6856eb0155276df04f4294a0f2e6e90bf050fe..15310fb7cff40c88fa716339ce59d40b034c2c0a 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 e6a579db4f2421d51e9da0eec44b1afb06224fbc..8fb7973a17a79486bf8775d3b886df46eb305234 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 9f8ea1f236142d2db6a3ce7e7e5f3135b3efccfe..125f02f8e912ca4224f3353c859fda31acdf91d6 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 c285cbc3e17c5e3c81d4182a18a3338cc59c47df..8ad0861591722f36198f3707725fbe20a450807d 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 29667843bdeb420092648460851659006c93627c..7b30bb3c486f740c1c24a2d32ae29dfaf574332e 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 20e03fe68acf7602ed502fe4518f1eb7b7ddea10..6f56c24324a186e8e0602d75e8b9cc4712c52afe 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 021da5eee95e13f429014e65bce81c2ecf503fc7..92fef7d754435b8bacbd57c7be073c0c93eb7e5b 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 16ce0b4a75061cb6954930d196de4af43546db21..24d394e40092eac450cddb581ba0cb9ea9240206 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 87d0498fb04b15eee719123d57d036cd347f32fb..c329b9b1e637a3b49a679e3c9e0677eed370672b 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 86996c8c6317a1b7ca354ba1d7a7b1569464c9df..a0e94b2e655525270e2aad3572d2b3695d2a0535 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 294ec61ecaae8e9e052bf47d1dd6db19e5bbdb9a..d0b90462b61bc9e963c2251a9ef07f6785131de5 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 d498b70e1fd7ded5b2a86b119707b768a6f3adfa..cf012e6c0504e09c25d1b8c09fb942f41b1e892c 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 a975e597084700164a74015305f3d25323e77f2a..98c5ec518cc2b33574ec45cdc7cc8b15bf999c61 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 2e7260b154cb0b18296171dd815b7e03b72050a2..63ad4f1b5fb958d05dc9f882c90023ab0456a981 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 79876416a5cd1c1bdb3962c1a6ca5d8ce02ec3d1..08e4f15887ab3f238fc4a5f1951897c731f03d1e 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 328a99f1faff63fe87da3bef4b757fdd4b3c2102..22eda299ab1cedea653e08b979c5c8c7d810fd26 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 74a2d7f12be390ce593ce533ec2d1232201afc2c..a4232805bae359e77ccc2f6517142de6adfa5701 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 ff9324649068afa2e01ee93ed576d7f152abcf75..509deb13888d41ad24139ebf3fd72948a9aa15e8 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 84168b88d4c1bb3ffa973db3617eb2b03c58e0b7..491a16fb130268ad07245da052609ce287d35380 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 3f3d06280d9f0b213bed4fa6f592e56bd1361952..4a8722c79610208e124645ee6d594606c5badd1c 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 a265b67c216561bb2a107e07d196b133d24be726..8b99f77aa8560d39e9a31cbe0feb61cdd0bd7f33 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 544c26cf13e0daade2bf902b6aa37a7bc640c70d..b55800b3871d1b73c8d5310de18e46195c3d45ae 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 b2226234f2f27a29eb3c2910a907c5025bcbf558..d2b44ea3f989466ea00bce41763e729ddd9de2d4 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 80c5e849d1f6f861f992de6c89cc073ddb10f08c..47543108cf693887e493baf959592a459d92759e 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 69699b4a4d5d6004a51d6b4040248cdb54d32bf1..5dd37ac3ee989690e8b9b05f518aad3e32c66388 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 9e40b5ee61ca685643b2558ef6beb904cb645992..57110681cb813d2454c51076ebaf9b9c75245c0d 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 062b6c3f5fe8cf5b305d87f7584f15fac5e0f964..16576eca3ffb7f933a8b7793b98ebafe2faee61f 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 f96c7c07ac1181ab68c4918534e4be6c6b8ae339..15e30a999f78720ff89fc769fc7c7c00f2bacb73 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 89c9be319a317f2cdacef66baa52d85628ddcd3e..3169e36fc4c077f41cdc5b8492d9da0800812f01 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 f4740e5db9145613df17fdfcefee3c5b9dc9d114..a931e439e062ffeff96c5936b73b4d8c57fcebfd 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 e6130b06b70003c97e28039171934019c1d912c1..429cdb13ca7bc881ddee3d1fd4163d646cf97387 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 b868f77e08c8a08217ff1a0faa10a8dd1a373e43..5d4ed2f7142ca8b472bac3224e6303e1a2806875 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 94e89525f1795993b606415241d9cc51a971bd4e..1c885754866c0fc1cc90346c26786881e07286a7 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 6e93cd3a8402a767050c3a6a976f8e23d533965c..2d7099ec3cd8acf046ebb6368e24d5d41f7b96bb 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 503704a7f60531219e0e958a570d83f1973dca9f..55affa7ab941bfc833ca380d7dcfb1be007c5a87 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 3c5c283a150535c6759d74f3931b8aac34f319bc..e453fbed6fcd7f351f7658ae792f75c1c6e78f25 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 51a4f4e833b32aad8254499e353df5d90a7962a3..a2121e519b1a21fb22f1f1a2d7230def1223bd4e 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 1d352b968012d6406d57d605997ed90dcc16be2e..bee9d64cf481a98404e49e4a9c8f07574b9b49d5 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 b3cf344ec8733ba6737d5e52491380905c39e661..8a0a693d089362a1d1d1af237ffc1954c8e40725 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 f3c0ce91ad21f50edac0d6d8bc5e9e173593e119..8ac2ed094984a7a9c2048a87e015a74d3108544b 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 788bf322e5a05070c88eebce9132aaa60658e30e..bfdc982f045a5459402a6abdd3795c61c68cfb15 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 c3b37a8fe52a30652e8531b8438ef6e502ee866a..7151e278b49f0aec67b6d7ff9a677a99f8004af3 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 f000fc1d3927aa57bc36510da9d56a3e4137365c..ed92c9481a39d94394d53062929d8b412c1dbeb0 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 c0423c445b466686e6929d220bed1dc6d318efd3..02e6976dd0e972288587ba12394314972fa5e5ab 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 af7afe2fb805f94ea70e05874ecf5993c1e40a54..08b16493d43678792b42221946f9e01cca4d560e 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 c49edc4770c18017d39568682ae092b88c0d0f7c..7a184b693fea3ac51362edf74796b4fe13d6c47c 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 ed9dda4fd777ef7de1f863116e3e57e1ce0ada65..8d0aa72682725c45daafbdd532849c07a122bc3b 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 baa65f135a617406baebba0ffb78853c4bc5ba31..8a13ebe8495ef9f0ddd6bd1bd4a4daee20c58526 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 d92df660c7801d81ed2eb9f57e80bc4198c6bd52..14f4216b41fb9f7e229cd5bf5269e53d96e7e860 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 21a378a78ad4216d920dc7034d020edf3033bcc4..22348202511d24907b5720b188d8399c43638cc2 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 08c8a22a972ce38b0f8a03879d5a5a0c40219dd6..9e5e1e9e87fac9035b655212bb9eeb49b76e4bae 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 bbea55ed3a2a35f70da15f5f47e2955fabbbc050..4c5fe68dc377a513e8ab069ec817ce38899001af 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 bae3f6827c34067cf1fbf86d1cacee7ee59adb90..1f5c325319d74cf1edbc6438c300ddefb1140795 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 c6d4856c1e8d00ff2d60282b24b07bcc13b604b8..ab02f4673572fe5982890df6dacc4588717d7d35 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 ffe772d112946077ada8210b3abaed61cd286619..bbc8f7c907914e85df5fdb7a4beb3d966b63c331 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 dd5a3c3c724df735afda38d247cdf1ad55924157..094bed765356d0f6fae4d68f81e1cc5d9ea671c9 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 361e5a03731faad2cd1f44e49567fd2b052cfbad..4caf3905b00d7898b3c109c8d21f77a356ad0632 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 af4041322d03057260f4a826cda43689e924cf6e..dd92c61ce5e78c4547a964e6b358002e68f48640 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 a718cb65099925134673aa90e6697ab7fcf164d9..201952c69f9bc9d896902cd730de3354327e2c38 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 a1645d065c62615fd60e433a04d0e1b1bab8a1eb..8e7ab72ca3bd17dda15bcee4d15bc135819d8fcd 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 f55d27c4bc84910ee0dc0cdb8ed610a754c888f6..4c99565217eb42e1c7c8677a26cd0f5cc563f248 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 77a0c86e7817a00f0d0c3ba6715c03cfecf2781c..9dce899decaceb6e9c5f28835259587daedcb3a0 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 23e2fffd89051a4cfb9965f47c3b4c971b1e3c46..d151568ea73c1533932960ad882a5a61c92b6014 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 100e10c8f7a6817b06ce47605d7171e14f127d47..a9d4b9cd6f3d77f8e017b53350c1b48cefe5552e 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 a898830b6c5d402031c022441b291f8d05e00948..2d857bf11f74dfb7cd160da73997ccd7eb038c23 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 84f09ee9b924d4dd8cf953aff028212d63ff0b50..08368fe7126796826f7f4a95de37cce8fa0c0acc 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 87336d54dc5fe26ace59c0a47812fb4cba4a3e6c..534ef0dbc1c5244b76b0aa5fcacfd31e32bc7067 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 bd2e27b93677088c47539b5168a7fc0642635a4f..9381fb40fb5747e3bba5659c7b2a0fcee349ab5d 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 cd0eabb6b10ca7b5aa0f9629f59a57cf5b46a784..f7a8ff02d473ab73819f6341e3195dcd67523620 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 4242b2dfba3290b5589309f2665d7e5fe8593171..e0b2c80c16bca3685fdd9f859b8c9f32bf59ce47 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 d5622d7ed9e4e3fc2c6d74835d6e1774a119ef75..f9086f16d9ed0e912d5966d43a13dd967687ec7c 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 0476d886625ba4a1e2bed28d08c7ad0ed4ac0da9..54872d949eb097a803ed9403e90a194c5a2190a1 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 0943a2ef61e98118603da76045e58b0dbfbbdca9..0dc080346091ad9ce7b9e09a3b18ffda0aa37e40 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 bdf8cd79d4f61ef4534c30b17206e1e9616ee83b..f651691499b8c0a3fa27af3f4fb3ca3cedb66f49 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 4ae3fa0ea970f084669d380c72c5ccb77868ae4f..7a28185e1cfed540343180f411509f554c35771f 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 a5f01ad1b9f8caed75304768b221d524c2d84b94..eb9d80e93f9fe2f66bb4271209bbc34b65037900 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 daf213f33776ee42372e8612e397529a34cebc7d..279a3674a91b1547e0664064aa7d4fdad309834c 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 8200d4d9606b1223dbf30ac402d0ca95ce74f67f..aa991af12997e4a00233dc6cd8c6ac14045cc786 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 ed79b3cbf9bdad854d8dfda72f02d4c744adf85e..08315dce1b48ebe5c536ab0b3ad1b01c45d1447f 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 61f48a8ea1406896839cb484ab47bf348a392641..69f26f3687323354bfe6835881248a2889a4e695 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 f7fc80da6cd887fee66a2cd88c7c6b2f0032ba66..9bba968c9553d705cee1509b6590aae397df2af2 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 16e39ba21f0aae9608a2a29a55182322ce93a86c..47d404ed783fa159801c46d5c62d3321a0234c4f 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 370c977459b0d0ee2a043f121f0056f6250222b2..77d69925df55fca9553719454a82412b1a4070de 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 efa9d5ceb56137fa94107240077e483240e3e988..816e4abcd7de7238d2f6d035fc1bdb145f1ccbec 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 15015992dedee70c1a033dd3bd4303194a2251f2..b1655033070b3d49d5f252ebf6efcea382549fc3 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 f0a7c60a74afc97f0131db4e1a5a65c1b0a49583..e8804d537400a27e0d17865ea51c51e2a3c450ea 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 41f979c346ab2c53796984bb1473c8631316313b..f410861e519ae642c02fe9389615f0074b383257 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 1f1ae163a8a9b0e6884e0279bf6471c0cb305ea8..835d04b33f02077adc0797ad99bf99a919d16caf 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 f50e1b52a3b8f111005876f01e30b1df40379e2c..5c9fc1d7aa218fcfadf5cdfa0e4e665e34a303db 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 f47a72e9b68e4b0226b8c2938cc7d3c5bcf82d8d..4f08b004bc8532d47348ea706a40f3dac7265b16 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 eb2751279a31c9c8768e1b16c4b09d3c60b9f4f8..6d8cd6ce0b91217dc7dd7c82f24dc288a937fbfa 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 d75d9127dc40f1b26b64f302786e3310074a551a..72c6e1584a4a0c7f0010062fc6b16ce43e34d21b 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 b38afac3b1424a0aa17d396df5070488e1136e79..679adfa6f98045788e1c66a573dda4a959795be1 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 25062358d6397ba9736f0d7de51e443ce64d8943..baf83f2c1a72da3b955b7d3b618e94f8819563cb 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 36b8d1a3d268c95a1cd5172fb239df5c682648b3..1a70b5a9e87424e3c692f25f0e693ca126c02a30 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 851874ec76180fe2e1164064b4f5df32ad79ac11..da0a3027898f8e5c3acceff01130f82f91680280 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 dfc6ebbacf23a5236a95b613e7e4be9243d4cdfb..faff811ac56c51e361ff85b81ad1faef34b44315 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 1abb9572fe8a0763710a12762aae609068d66022..7eb3f9ce1648992d836494a79050299acdcdafe6 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 fd5e5dec2b38783aa6181ea4336c392ae2320bfd..4cb0f65c1c47ae392cd50c613eb57300c732b491 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 d5efcc079c1cf6bb3f01d9fc24399b1eadadb35c..77103231650839fca1deb840156e0b004a660376 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 9ea5bdb0693d51b752d94454340a5abbcb02955c..16f87c1ef11aa73da34bec95778b6cbf4803f95b 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 e313cb67dbb1f63a1c1cf03da5fb4153fb66ab1f..90e781cbc57b153893bd2bf95be0df2680e2ff25 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 ec618de472de2719bd0034e6a7dfa03bcfe0d67a..be0ffc95ac722c6fa7d90d5daba297e2022889de 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 ec7c0152adab7954fae7f903931dfa3278df69e5..1d9f4dadb8b4fdcb934384517fc0207b4c58c9a6 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 a11aaf4277e0f4b5f7ef26765df1b568ded021df..14d4d7d3d2f4ef1baff7e83e8142052c408e4f9a 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 c108a39f6017a9363f63268c46bdb95c74482c0f..1a7dfd5b1a6338588248286e8818041897de83e1 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 8956403c987ec17eb0a5e76b76d1ff8195670a2f..7d6eb76501b4713080cba57158b2eba1c02cf985 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 89e7b171949ab36de81d482d5e5eaa6612efbc0d..1a7ff7a5353833808dfc718c0e39333343cef4b3 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 5673249566c527636b5b8f74fdb0e2a0a5b7397b..cd3f4c795524730a458d21fd22882b8eb481519f 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 7b0940c5b3a61d43f9766b4adc2e582627ff4fd0..2e91edea33863e754f8144fba6c08d38ed983cc0 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 6ee5a51559273c524f27a902c2257adefce7ea3f..635f82d2d443000b17d949f0f4db3d3887241ab9 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 8aef618ba34f35ac484181d7f0138e8222eb0f33..85d7992c175353e8686baf025c4a8cd88b123885 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 ab233151e43fcdf6b0e1ada174c0ed8e292dc656..82ef8ba432b57a54a5fa81f6be7a1c8aeb57de5b 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 3fc396b20466d8e6f068ec8371236e9323d8b4e1..d7750f148b048c619d768f61796a184d2f1c8628 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 8d9e469e50ad9eb8420a79a9fd91ad844a906e66..427f923adef7ae403f4c21bc1537b9776a9f8163 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 a9053c10bd6faca9c71be1691c0e49e32557106f..6836235722a61d1ab1245fa536a686efbbd5f5af 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 aedac262f68dd34fdf618d5b075ac7f72f6d5bec..bda3e5fa7ebeb64d1d43241c888b3d226e3cbb8a 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 84a050b1c299221b70cf6d6f2ff4cebec07be53b..75566aa502cb9e01a638dacfeadf9ea32bbd08e9 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 258a2a0baf5679c4fa2c173fd71ec69e5f26d7cc..6948b660a599e51143aeeae6def6c3d0e15d4875 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 e62038e622f16d0b51b53428e50450ce1902a8ce..c92cfcc1437c36da50fe2a7613de616b350b964f 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 b0e2fe530924fbefc4be75e185ea34f9f3330d04..1558aee21b8330e3430f36828ced8ba2e6df798a 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 8d4a9c34cec493fd540e424116b602723313a9bd..5d717a2e7c71f0b4fb618bf832257dfd95c6a600 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 ae9717c94a77e790a1a5dd7da4bc98bae8c76205..8c155ff7e7399e3093581890455bb2d55ec53dff 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 a9f8a891aa66763030b416b4237fb3b65f92d50a..80aa8fe8fb42acf76e12dfb89d2f10abac010f38 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 35add5230dbedecf4ce4e9a6b5b235007202c7d1..2d042352651366d44c99559c6baf0f99e521cc75 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 7061a3ba66523c7c4aab66175bb5eb51506f638c..99d0911261c680ab9924e224cbe95828d3fb0cb9 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 6a01bb4b65814b78079636964fd327446509f8d0..25ee469623bf059a7a6b5f0ef34a54d12572f3f4 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 01907b4e201266ec7dd9e0a8be571204933c9834..4c264cb97a1b767604d605ade213226a576c0180 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 eec7db6e6ed349c98703c608fcb5f44988b2af7f..988e8863196903b0c85999f7a6a82de6e51126a6 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'