diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7a212b2e869bd9b4127929231d57ca74c4a3cd22..6cc89c48922c8da92e75d37d1c37d03b0fdd8815 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -69,6 +69,7 @@ build: - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - echo "{\"tag\":\"$CI_COMMIT_TAG\",\"commit\":\"$CI_COMMIT_SHA\",\"branch\":\"$CI_COMMIT_BRANCH\",\"ci\":true}" > src/version.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:ci-$CI_PIPELINE_ID --single-snapshot + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --target nginx --destination $CI_REGISTRY_IMAGE/nginx:ci-$CI_PIPELINE_ID --single-snapshot only: - develop - production @@ -166,6 +167,9 @@ test_image_frontend: publish: stage: publish + needs: + - test_image_api + - test_image_frontend image: name: gcr.io/go-containerregistry/crane:debug entrypoint: [""] @@ -174,6 +178,7 @@ publish: script: - crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - crane tag $CI_REGISTRY_IMAGE:ci-${CI_PIPELINE_ID} $CI_COMMIT_REF_NAME + - crane tag $CI_REGISTRY_IMAGE/nginx:ci-${CI_PIPELINE_ID} $CI_COMMIT_REF_NAME rules: # skip if Deployment Freeze is active - if: $CI_DEPLOY_FREEZE != null diff --git a/Dockerfile b/Dockerfile index 818d64a107810044563db33fb6fed11b550b1dba..26b82448d195dac45c70e4363343a0179c43ffc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-buster +FROM python:3.8-buster as monolith VOLUME /data /media @@ -6,21 +6,22 @@ ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 ENV LC_ALL=C.UTF-8 ENV DJANGO_SETTINGS_MODULE=rc3platform.settings.default +ENV DOCKER_UID=1000 RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ gettext \ locales \ - nginx \ + nginx-light \ sudo \ supervisor && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ dpkg-reconfigure locales && \ - locale-gen C.UTF-8 && \ - /usr/sbin/update-locale LANG=C.UTF-8 && \ - useradd -ms /bin/bash -d /app appuser && \ + locale-gen C.UTF-8 && \ + /usr/sbin/update-locale LANG=C.UTF-8 && \ + useradd -u $DOCKER_UID -ms /bin/bash -d /app appuser && \ echo 'appuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \ echo 'appuser ALL=(ALL) NOPASSWD: /usr/bin/tail -F /var/log/nginx/error.log' >> /etc/sudoers && \ chown appuser: /media @@ -50,3 +51,28 @@ USER appuser EXPOSE 80 ENTRYPOINT ["/usr/local/bin/app"] CMD ["all"] + + +FROM docker.io/bitnami/minideb as base + +ENV DOCKER_UID=1000 +RUN useradd -u $DOCKER_UID -ms /bin/bash -d /app appuser +VOLUME /data /media + + +FROM base as nginx + +EXPOSE 80 443 +STOPSIGNAL SIGQUIT + +RUN install_packages nginx-light + +RUN chown -R appuser:appuser /var/lib/nginx /var/log/nginx + +COPY deployment/docker/nginx-standalone.conf /etc/nginx/nginx.conf +COPY --from=monolith /app/static.dist /app/static.dist + +CMD ["nginx"] + + +FROM monolith as default_image diff --git a/README.md b/README.md index 5be7f61af3430d5c8bac964ceb12f43695a11a5c..1e79d8061be2dca802886cc35fb6fd0328ee0e07 100644 --- a/README.md +++ b/README.md @@ -155,3 +155,20 @@ oder: ## Übersetzungen definineren * in Python: `gettext` wie üblich in Django, siehe [Doku](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#internationalization-in-python-code) * in jinja2-Templates via `_`, bspw. `{{ _("Settings") }}` oder mit Platzhalter `{{ _("Hello, %(username)s", username=user.display_name) }}` + +## Tests +Es gibt Django (Unit-)Tests für die einzelnen Apps. Diese finden sich jeweils im `tests` Ordner der App. +Außerdem gibt es im repository root unter [tests](tests) auch noch einfache Integrationstests. + +### Django Tests ausführen +Um die Tests ausführen zu können muss der Datenbanknutzer das Recht haben neue Datenbaken anzulegen. +Dafür mit `psql postgres` die Datenbankkonsole starten. Dort `ALTER USER rc3app CREATEDB;` ausführen (ggf. `rc3app` durch den gewählten Nutzernamen ersetzen). Am Ende mit `Strg-D` Konsole wieder schließen. + +Manche Tests benötigen, die kompilierten Übersetzungen. Deshalb ist es sinvoll vor der Testausführung `./manage.py compilemessages` auszuführen. + +Um alle Tests auszuführen in das `src` Verzeichnis wechseln und `./manage.py test` ausführen. \ +Um nur die Tests einer App auszführen stattdessen `./manage.py test <app>.tests` ausführen. \ +Hilfreiche Argumente sind `-v 2` um die ausgeführten Tests anzuzeigen und `--failfast` um nach dem ersten Fehler abzubrechen. \ +Für weitere Infos zu dem Befehl ist https://docs.djangoproject.com/en/3.1/ref/django-admin/#django-admin-test hilfreich. + +Um eine Ausführungsgebung zu verwenden die ähnlicher zu der der CI ist, kann `DJANGO_SETTINGS_MODULE=rc3platform.settings.ci ./manage.py test` verwendet werden. diff --git a/deployment/docker/app.sh b/deployment/docker/app.sh index c34d38b9e487aed507690c6154412de4a899d985..0fe5f684db7f0d84dc78fcdff48918ad878755c8 100644 --- a/deployment/docker/app.sh +++ b/deployment/docker/app.sh @@ -29,6 +29,7 @@ if [ "$1" == "webworker" ]; then --max-requests 1200 \ --max-requests-jitter 50 \ --log-level=info \ + --bind=0.0.0.0:8000 \ --bind=unix:/tmp/rc3platform.sock fi diff --git a/deployment/docker/nginx-standalone.conf b/deployment/docker/nginx-standalone.conf new file mode 100644 index 0000000000000000000000000000000000000000..450dbf77151bd49079185399caf48cb05a74e3f6 --- /dev/null +++ b/deployment/docker/nginx-standalone.conf @@ -0,0 +1,77 @@ +user appuser appuser; +worker_processes auto; +daemon off; + +events { + worker_connections 2048; +} + +http { + server_tokens off; + sendfile on; + charset utf-8; + tcp_nopush on; + tcp_nodelay on; + client_max_body_size 50M; + + types_hash_max_size 2048; + server_names_hash_bucket_size 64; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + add_header X-Content-Type-Options nosniff; + + access_log /data/access.log combined; + error_log /data/error.log; + add_header Referrer-Policy same-origin; + + gzip on; + gzip_disable "msie6"; + gzip_types text/plain text/html text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml image/svg+xml; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + + include /etc/nginx/conf.d/*.conf; + + server { + listen 80 backlog=4096 default_server; + listen [::]:80 ipv6only=on default_server; + server_name _; + + return 301 https://$host$request_uri; + } + + server { + listen 443 backlog=4096 ssl http2; + listen [::]:443 ipv6only=on ssl http2; + server_name _; + index index.html; + root /var/www; + + ssl_certificate /data/fullchain.pem; + ssl_certificate_key /data/privkey.pem; + + location /static/ { + alias /app/static.dist/; + access_log off; + expires 365d; + add_header Cache-Control "public"; + } + + location /media/ { + alias /media/; + autoindex off; + access_log off; + expires 365d; + add_header Cache-Control "public"; + } + + location / { + proxy_pass http://hub:8000/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + } + } +} diff --git a/deployment/docker/nginx.conf b/deployment/docker/nginx.conf index ec4223f8a8efc900e04acfd9e65ad172073a54a7..8cfe255e6aafa4047846b0e48d8427fb19162e2a 100644 --- a/deployment/docker/nginx.conf +++ b/deployment/docker/nginx.conf @@ -1,6 +1,5 @@ -user www-data www-data; -worker_processes 1; -pid /var/run/nginx.pid; +user appuser appuser; +worker_processes auto; daemon off; events { diff --git a/src/api/permissions.py b/src/api/permissions.py index 5daddb88dc3542d793f106d56ee2d0c4dd64a66a..1859a68e036db313e3b6644de4734256648f36e2 100644 --- a/src/api/permissions.py +++ b/src/api/permissions.py @@ -1,3 +1,5 @@ +from django.conf import settings + from rest_framework import permissions from core.models.assemblies import Assembly, AssemblyMember @@ -35,6 +37,18 @@ class IsConferenceService(permissions.BasePermission): return False +class IsApiUserOrReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + if request.method in permissions.SAFE_METHODS: + return True + + if settings.API_USERS is None: + return False + + assert isinstance(settings.API_USERS, list) + return request.user.is_authenticated and request.user.username in settings.API_USERS + + class IsReadOnly(permissions.BasePermission): def has_permission(self, request, view): return request.method in permissions.SAFE_METHODS diff --git a/src/api/schedule.py b/src/api/schedule.py index 0bca9c43e1c286cbc5d1c68609a1bd70a006a3c6..f1acb9e14ba60abaa918d9c30af4f69c39fa3337 100644 --- a/src/api/schedule.py +++ b/src/api/schedule.py @@ -1,5 +1,6 @@ import json import pytz +import re from collections import OrderedDict from uuid import UUID @@ -32,22 +33,24 @@ class ScheduleEncoder(json.JSONEncoder): tz = None def encode_event(self, event, tz=None): - start = event.schedule_start.astimezone(tz or self.tz) - duration = event.schedule_duration.seconds + start = event.schedule_start.astimezone(tz or self.tz) if event.schedule_start is not None else None + duration = event.schedule_duration.seconds if event.schedule_start is not None else None + additional_data = event.additional_data or {} return OrderedDict({ **event_template, - **(event.additional_data or {}), + 'id': additional_data.get('id') or int(re.sub('[^0-9]+', '', str(event.id))[0:6]), + 'description': event.description, # TODO: if the description also exists in additional_data it is overwritten due to concatination with abstract + 'slug': event.slug, + **additional_data, 'guid': event.id, - 'date': start.isoformat(), - 'start': start.strftime('%H:%M'), - 'duration': '%d:%02d' % (duration/3600, duration % 3600/60), - 'room': event.room.name, - # 'slug': None, # TODO + 'date': start.isoformat() if start is not None else None, + 'start': start.strftime('%H:%M') if start is not None else None, + 'duration': ('%d:%02d' % (duration / 3600, duration % 3600 / 60)) if duration is not None else None, + '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, - 'description': event.description, # TODO: duplicates abstract... }) def encode_day(self, obj): diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py index 63b4d921c6f34af9b38bc3ffc46446c07af9df8d..82daa5fb6d222d49d90eab1eb749750a57920fe0 100644 --- a/src/api/tests/__init__.py +++ b/src/api/tests/__init__.py @@ -1,4 +1,6 @@ +from .bbb import * # noqa: F401, F403 from .engelsystem import * # noqa: F401, F403 +from .schedule import * # noqa: F401, F403 from .workadventure import * # noqa: F401, F403 __all__ = '*' diff --git a/src/api/tests/bbb.py b/src/api/tests/bbb.py new file mode 100644 index 0000000000000000000000000000000000000000..c285cbc3e17c5e3c81d4182a18a3338cc59c47df --- /dev/null +++ b/src/api/tests/bbb.py @@ -0,0 +1,33 @@ +from uuid import uuid4 +from django.test import TestCase +from django.urls import reverse + +from core.models import Assembly, Conference, Room + + +class BBBTest(TestCase): + def test_MeetingEnded(self): + conf = Conference(slug='conf', name='TestConf') + conf.save() + 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'} + ) + room.save() + + 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', + }) + room.refresh_from_db() + self.assertEqual(room.backend_status, Room.BackendStatus.INACTIVE) diff --git a/src/api/tests/schedule.py b/src/api/tests/schedule.py new file mode 100644 index 0000000000000000000000000000000000000000..6bd544d494d6a948103ce8529df925e249d1f406 --- /dev/null +++ b/src/api/tests/schedule.py @@ -0,0 +1,100 @@ +import json + +from django.test import TestCase +from django.urls import reverse +from rest_framework.authtoken.models import Token + +from core.models import Assembly, Conference, Event, PlatformUser, Room + + +class ScheduleTest(TestCase): + def setUp(self): + self.conf = Conference(slug='conf', name='TestConf', is_public=True) + self.conf.save() + self.conf.tracks.create(name='Community').save() + self.assembly = Assembly(name='TestAssembly', slug='asmbly', conference=self.conf) + self.assembly.save() + self.room = Room(conference=self.conf, assembly=self.assembly, name='Foo Room', room_type=Room.RoomType.STAGE) + self.room.save() + + self.user = PlatformUser(username='bernd', is_active=True) + self.user.save() + + self.token = Token(user=self.user) + self.token.save() + + def test_push_existing_event(self): + event = Event(conference=self.conf, assembly=self.assembly, name='Example Event') + 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 = reverse('api:event-schedule', kwargs={'conference': self.conf.slug, 'pk': event.pk}) + + 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}') + + self.assertEqual(201, resp.status_code, f'Unexpected result from POST: {resp.content}') + + event.refresh_from_db() + self.assertTrue('rC3' in event.name, f'Expected "rC3" in event name "{event.name}".') + + def test_push_new_event(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": "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": [] + } + + 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}): + 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}') + + self.assertTrue(Event.objects.filter(pk=update['guid']).exists()) + event = Event.objects.get(pk=update['guid']) + self.assertTrue('rC3' in event.name, f'Expected "rC3" in event name "{event.name}".') diff --git a/src/api/urls.py b/src/api/urls.py index 2d9880ae6e4284a5122f28c8aff0ea9a5c8facc8..e13c6fde833c1ec788ae220f8af2ed58755401b9 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -3,7 +3,7 @@ from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.authtoken import views as authtoken_views from .views import api_root -from .views import assemblies, conferencemember, conferences, events, rooms, users, schedule +from .views import assemblies, bbb, conferencemember, conferences, events, rooms, users, schedule app_name = 'api' @@ -39,6 +39,9 @@ urlpatterns = [ # integration with other components path('c/<slug:conference>/is_angel/<str:username>', conferencemember.AngelView.as_view(), name='user-angel'), path('wa/<slug:conference>', conferencemember.WorkadventureView.as_view(), name='user-wa'), + + # BBB meeting ended callback + path('bbb_meeting_end', bbb.MeetingEnded.as_view(), name='bbb_meeting_end'), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/src/api/views/bbb.py b/src/api/views/bbb.py new file mode 100644 index 0000000000000000000000000000000000000000..79876416a5cd1c1bdb3962c1a6ca5d8ce02ec3d1 --- /dev/null +++ b/src/api/views/bbb.py @@ -0,0 +1,17 @@ +from django.shortcuts import get_object_or_404 +from django.http import HttpResponse +from django.views import View + +from core.models import Room + + +class MeetingEnded(View): + def get(self, request): + meeting_id = request.GET['meetingID'] + close_secret = request.GET['close_secret'] + + room = get_object_or_404(Room.objects.filter(room_type=Room.RoomType.BIGBLUEBUTTON), backend_link=meeting_id) + if room.backend_data.get('close_secret') == close_secret: + room.backend_status = Room.BackendStatus.INACTIVE + room.save(update_fields=['backend_status']) + return HttpResponse() diff --git a/src/api/views/rooms.py b/src/api/views/rooms.py index cdfd050c5e3184ec1c43cb16cfaddba3b2e70ceb..b3b3b83301a83b596c22eba1619a093814f734eb 100644 --- a/src/api/views/rooms.py +++ b/src/api/views/rooms.py @@ -1,7 +1,7 @@ import logging from django.http import Http404 -from rest_framework import generics, permissions +from rest_framework import generics from rest_framework.response import Response from rest_framework.views import APIView @@ -18,7 +18,6 @@ logger = logging.getLogger(__name__) class ConferenceRoomList(ConferenceSlugMixin, generics.ListAPIView): serializer_class = RoomSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly] def get_queryset(self, **kwargs): return Room.objects.conference_accessible(conference=self.conference).order_by('name') @@ -26,7 +25,6 @@ class ConferenceRoomList(ConferenceSlugMixin, generics.ListAPIView): class ConferenceRoomDetail(ConferenceSlugMixin, generics.RetrieveAPIView): serializer_class = RoomSerializer - permission_classes = [permissions.IsAuthenticatedOrReadOnly] def get_object(self, **kwargs): room_id = self.request.resolver_match.kwargs['pk'] diff --git a/src/api/views/schedule.py b/src/api/views/schedule.py index ce1ed6cf8fa5644840ec122e48509eeb50ea697e..e9249d4e4001796431cd94477a7b1a747bdb944e 100644 --- a/src/api/views/schedule.py +++ b/src/api/views/schedule.py @@ -1,14 +1,27 @@ -import pytz -from rest_framework.response import Response +from datetime import timedelta +import logging + 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 +import pytz from core.models.events import Event +from core.models.rooms import Room +from core.models.conference import ConferenceTrack + +from ..permissions import IsApiUserOrReadOnly from ..schedule import Schedule, ScheduleEncoder from .mixins import ConferenceSlugMixin +logger = logging.getLogger(__name__) + + def schedule_response(schedule, kwargs): if kwargs.get('format') == 'json': return JsonResponse(schedule, encoder=ScheduleEncoder, safe=False) @@ -52,10 +65,77 @@ class RoomSchedule(ConferenceSlugMixin, View): return schedule_response(schedule, kwargs) -class EventSchedule(ConferenceSlugMixin, View): - def get(self, *args, **kwargs): +def schedulexml_time_to_timedelta(s): + if ':' in s: + hours, minutes = s.split(':') + else: + hours, minutes = 0, s + + timedelta(hours=int(hours), minutes=int(minutes)) + + +class EventSchedule(ConferenceSlugMixin, APIView): + authentication_classes = [authentication.TokenAuthentication] + permission_classes = [IsApiUserOrReadOnly] + + def get(self, request, conference, pk, format=None, **kwargs): tz = pytz.timezone(self.conference.timezone) event = Event.objects \ .accessible_by_user(conference=self.conference, user=self.request.user) \ - .get(pk=kwargs.get('pk')) - return Response(ScheduleEncoder.encode_event(event, tz)) + .get(pk=pk) + return Response(ScheduleEncoder().encode_event(event, tz)) + + def post(self, request, conference, pk, format=None, **kwargs): + event = request.data + if len(event) == 0: + return Response({'error': 'No data.'}, status=400) + + try: + obj = Event.objects.get(conference=self.conference, pk=pk) + except Event.DoesNotExist: + obj = Event(conference=self.conference, pk=pk) + logger.warning('Event schedule POST: id %s did not exist yet, creating.', pk) + + try: + if 'guid' in event: + if event['guid'] != str(obj.pk): + logger.warning('Attempted update of event %s with guid "%s".', obj.pk, event["guid"]) + return JsonResponse({'error': 'GUID mismatch.'}) + + if 'slug' in event: + obj.slug = event['slug'] + + obj.room = Room.objects.get(conference=self.conference, name__iexact=event['room']) + obj.assembly = obj.room.assembly + obj.kind = 'assembly' if not obj.room.assembly.is_official else 'official' + obj.is_public = True + + if 'title' in event: + obj.name = event['title'] + + if 'language' in event: + obj.language = event['language'] + + obj.description = str(event['abstract']) + "\n\n" + str(event['description']) + + obj.schedule_start = parse_datetime(event['date']) + obj.schedule_duration = schedulexml_time_to_timedelta(event['duration']) + + obj.track = ConferenceTrack.objects.get(conference=self.conference, name__iexact=event['track']) + + obj.additional_data = filter_additional_data(event) + + except Room.DoesNotExist: + return Response({'error': 'Room {} does not exist'.format(event['room'])}, status=400) + + except ConferenceTrack.DoesNotExist: + return Response({'error': 'Track {} does not exist'.format(event['track'])}, status=400) + + obj.save() + logger.info('Event %s updated via POST by %s', obj, request.user) + + return HttpResponse(status=201) + + +def filter_additional_data(data): + return {k: v for k, v in data.items() if k not in ['guid', 'slug', 'room', 'start', 'date', 'duration', 'track']} diff --git a/src/backoffice/forms.py b/src/backoffice/forms.py index ade95ab8be3cb76d4f88d2b8885e394dfc6eade1..e4f3d7c6355c56d56a4f0778bdde827fa989ebd3 100644 --- a/src/backoffice/forms.py +++ b/src/backoffice/forms.py @@ -32,6 +32,7 @@ class ProfileForm(forms.ModelForm): 'show_name', 'description', 'status', 'status_public', + 'time_zone', 'no_animations', 'colorblind', 'high_contrast', 'receive_dms', 'receive_dm_images', 'autoaccept_contacts', @@ -337,3 +338,7 @@ class StaticPageForm(forms.Form): if self.cleaned_data['is_draft'] and self.cleaned_data['publish']: raise ValidationError(_('StaticPage__cannot_publish_draft')) return self.cleaned_data['publish'] + + +class AssignBadgeForm(forms.Form): + nickname = forms.CharField() diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index 324caab396f77aae0bd6a09e122932a877da09e6..888f387e9d1a970cbf0a3f4a99c497412615a55b 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -96,13 +96,22 @@ msgstr "speichern" msgid "Badge" msgstr "Badge" +msgid "Badge__award-title" +msgstr "Badge zuweisen" + +msgid "Badge__award-explanation" +msgstr "Diesen Badge direkt einem Benutzer zuweisen." + +msgid "Badge__award-btn" +msgstr "Badge zuweisen" + msgid "Badge-renew-explanation" msgstr "Das Badge kann Nutzern mittels ein sogenanntes Redeem-Token bereitgestellt werden welches mittels einen geheimen Tokens bei der API angefragt werden kann. Durch Klick auf 'Token erneuern' kann dieses geheime Token erneuert werden." msgid "Badge-renew" msgstr "Token erneuern" -msgid "Badge-assign-explanation" +msgid "Badge-assign-api-explanation" msgstr "Alternativ kann das Badge auch über einen API-Aufruf einem Nutzer zugeordnet werden. Dieser erhält eine Benachrichtigung, das Badge wird jedoch nicht automatisch sichtbar angezeigt (wie es bei Nutzung des Redeem-Tokens der Fall wäre)." msgid "Badge__remove" @@ -642,6 +651,12 @@ msgstr "Für die OAuth2-Applikation wurde ein \"Client Secret\" generiert, diese msgid "Assembly__authentication__newtoken" msgstr "Ein neues Token für \"{assembly}\" wurde erstellt, an der API zu verwenden mit \"Authorization: Token {token}\"." +msgid "Badge__awarded-to-user" +msgstr "Badge zugewiesen an: " + +msgid "404__User Not Found: " +msgstr "404 Benutzer nicht gefunden: " + msgid "removed" msgstr "entfernt" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index b5e41fe51cfda6d6f16b264339ca1810fb4b3fd2..8ab10ac7f18dfa5e02fcbe0169d839cdcbce4502 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -96,13 +96,22 @@ msgstr "save" msgid "Badge" msgstr "badge" +msgid "Badge__award-title" +msgstr "Assign Badge" + +msgid "Badge__award-explanation" +msgstr "You can assign this badge directly to a user by entering his name." + +msgid "Badge__award-btn" +msgstr "Assign" + msgid "Badge-renew-explanation" msgstr "The badge can be issued to users by requesting a 'redeem token' at the API with a secret token. If you forgot the secret token for this badge you can get a new one by clicking 'renew token'." msgid "Badge-renew" msgstr "renew token" -msgid "Badge-assign-explanation" +msgid "Badge-assign-api-explanation" msgstr "Alternatively, badges can be awarded to users via an API call. The user receives a notification but the badge will not be shown automatically (as it is done when the user uses the redeem token above)." msgid "Badge__remove" @@ -643,6 +652,12 @@ msgstr "For the OAuth2 application a new \"client secret\" has been generated. I msgid "Assembly__authentication__newtoken" msgstr "A new token for \"{assembly}\" has been generated, use it on the API with \"Authorization: Token {token}\"." +msgid "Badge__awarded-to-user" +msgstr "Badge zugewiesen an: " + +msgid "404__User Not Found: " +msgstr "404 User Not Found: " + msgid "removed" msgstr "removed" diff --git a/src/backoffice/templates/backoffice/assembly_badge.html b/src/backoffice/templates/backoffice/assembly_badge.html index 35f92502e555c3547198c2bf850538003bce45ba..496f4d410493b331ed8b681dddeb406d4be37107 100644 --- a/src/backoffice/templates/backoffice/assembly_badge.html +++ b/src/backoffice/templates/backoffice/assembly_badge.html @@ -1,5 +1,6 @@ {% extends 'backoffice/base.html' %} {% load bootstrap4 %} +{% load widget_tweaks %} {% load i18n %} {% block title %} @@ -46,6 +47,25 @@ </div> </div> + +<div class="row mb-3"> + <div class="col-md-12"> + <div class="card border-secondary"> + <div class="card-header bg-default"> + {% trans 'Badge__award-title' %} + </div> + <div class="card-body"> + <p>{% trans 'Badge__award-explanation' %}</p> + <form class="form-inline" action="{% url 'backoffice:assembly-badge-award' assembly=badge.issuing_assembly_id pk=badge.pk %}" method="POST">{% csrf_token %} + <label class="sr-only" for="nickname">Name</label> + {% render_field assign_form.nickname class+="form-control mb-2 mr-sm-2" placeholder="Nickname" %} + <button type="submit" class="btn btn-secondary mb-2">{% trans 'Badge__award-btn' %}</button> + </form> + </div> + </div> + </div> +</div> + <div class="row mb-3"> <div class="col-md-12"> <div class="card border-secondary"> @@ -53,12 +73,12 @@ <div class="card-body"> <p>{% trans 'Badge-renew-explanation' %}</p> <p><button type="submit" class="btn btn-secondary">{% trans 'Badge-renew' %}</button></p> - <p>{% trans 'Badge-assign-explanation' %}</p> + <p>{% trans 'Badge-assign-api-explanation' %}</p> </div> - </div> </form> </div> </div> +</div> {% if can_manage %} <div class="row mb-3"> diff --git a/src/backoffice/templates/backoffice/assembly_detail.html b/src/backoffice/templates/backoffice/assembly_detail.html index c78809a72eb371d8e6195d6c3eae292150f911db..74fa805da38f8cf1568c09e6c1a7105f695fddcd 100644 --- a/src/backoffice/templates/backoffice/assembly_detail.html +++ b/src/backoffice/templates/backoffice/assembly_detail.html @@ -23,7 +23,7 @@ {% endif %} <div id="description"> - {{ assembly.description_html|default:"¯\_(ツ)_/¯" }} + {{ assembly.description_html|safe|default:"¯\_(ツ)_/¯" }} </div> {% if assembly.public_contact != None and assembly.public_contact != '' %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 7942101673647df1723e2f1be8c2ccdb3af07294..749620ccd09b90fedc7707102083eb6aa7c6a5c9 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -53,6 +53,7 @@ urlpatterns = [ path('assembly/<uuid:assembly>/badge/<int:pk>', assemblies.BadgeView.as_view(), name='assembly-badge'), path('assembly/<uuid:assembly>/badge/<int:pk>/renew_token', assemblies.RenewBadgeView.as_view(), name='assembly-badge-renew'), path('assembly/<uuid:assembly>/badge/<int:pk>/remove', assemblies.RemoveBadgeView.as_view(), name='assembly-badge-remove'), + path('assembly/<uuid:assembly>/badge/<int:pk>/award', assemblies.AwardBadgeView.as_view(), name='assembly-badge-award'), 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'), diff --git a/src/backoffice/views/assemblies.py b/src/backoffice/views/assemblies.py index 42e8eceff4ae048ac1aa6e441e3d77a1c0ad7c7d..278026ce64f13298cf48002c8aa5394a500be24f 100644 --- a/src/backoffice/views/assemblies.py +++ b/src/backoffice/views/assemblies.py @@ -28,7 +28,9 @@ from ..forms import \ AssemblyCreateForm, AssemblyCreateRoomGenericForm, AssemblyCreateRoomBigBlueButtonForm, AssemblyCreateRoomWorkAdventureForm, AssemblyCreateRoomHangarForm, \ AssemblyEditForm, \ CreateAssemblyRoomLinkForm, \ - EditAssemblyRoomForm, EditAssemblyRoomWorkAdventureForm + EditAssemblyRoomForm, EditAssemblyRoomWorkAdventureForm, \ + AssignBadgeForm + from .mixins import ConferenceMixin, AssemblyMixin @@ -503,6 +505,32 @@ class BadgesView(AssemblyMixin, ListView): return Badge.objects.filter(conference=self.conference, issuing_assembly=self.assembly) +class AwardBadgeView(AssemblyMixin, FormView): + form_class = AssignBadgeForm + assembly_url_param = 'assembly' + assembly_management = True + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + ctx['assign_form'] = AssignBadgeForm() + return ctx + + def form_valid(self, form, **kwargs): + pk = self.kwargs['pk'] + data = form.cleaned_data + try: + user = PlatformUser.objects.only('id', 'username').get(is_active=True, username=data['nickname']) + badge = get_object_or_404(Badge, pk=pk) + badge.award_to_user(user) + messages.success(self.request, _('Badge__awarded-to-user') + str(user)) + logger.info(f'assigned badge { pk } to { user } by { self.request.user }') + + return redirect('backoffice:assembly-badge', assembly=self.assembly.pk, pk=self.kwargs['pk']) + except PlatformUser.DoesNotExist: + messages.error(self.request, _('404__User Not Found: ') + data['nickname']) + return redirect('backoffice:assembly-badge', assembly=self.assembly.pk, pk=self.kwargs['pk']) + + class CreateBadgeView(AssemblyMixin, CreateView): model = Badge fields = ['name', 'is_achievement', 'image'] @@ -569,6 +597,11 @@ class BadgeView(AssemblyMixin, UpdateView): assembly_url_param = 'assembly' + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + ctx['assign_form'] = AssignBadgeForm() + return ctx + def get_queryset(self, *args, **kwargs): return Badge.objects.filter(conference=self.conference, issuing_assembly=self.assembly) diff --git a/src/core/admin.py b/src/core/admin.py index 8dbe821a63c0e6b39f0388e3d0fab28060af4603..670b8fd99bcf77c81e6c55f983ecae3c413630a8 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -72,7 +72,7 @@ class PlatformUserAdmin(UserAdmin): list_filter = ['user_type', 'is_active', 'is_staff'] fieldsets = ( - (None, {'fields': ('username', 'password', 'user_type')}), + (None, {'fields': ('username', 'password', 'user_type', 'time_zone')}), ('Personal info', {'fields': ('first_name', 'last_name', 'email')}), ('Legal stuff', {'fields': ('accepted_speakersagreement',)}), ('Self Portrayal', {'fields': (('pronouns', 'show_name'), ('status', 'status_public'), 'description', ('avatar_url', 'avatar_config'))}), @@ -337,7 +337,7 @@ class RoomAdmin(admin.ModelAdmin): list_filter = ['conference', 'room_type', 'backend_status', 'blocked'] search_fields = ['assembly__name', 'name'] inlines = [RoomLinkInline] - readonly_fields = ['id', 'conference', 'occupants'] + readonly_fields = ['id', 'conference', 'occupants', 'reserve_capacity'] ordering = ('-conference__id', F('assembly__is_official').desc(nulls_last=True), 'assembly__name', F('capacity').desc(nulls_last=True), 'name') fieldsets = ( diff --git a/src/core/fields.py b/src/core/fields.py index 4335fca9dda1eba850c0a9d1c7876ad8c4485082..b5cf6aebfafad87a5dfe173be369ea9ca96f4c73 100644 --- a/src/core/fields.py +++ b/src/core/fields.py @@ -1,6 +1,10 @@ +import pytz + from django.db import models from django.utils.translation import gettext_lazy as _ +from . import form_fields + class ConferenceReference(models.ForeignKey): def __init__(self, **kwargs): @@ -33,3 +37,18 @@ class FskField(models.CharField): if kwargs is not None: options.update(kwargs) super().__init__(**options) + + +class TimeZoneField(models.CharField): + default_choices = [(tz, tz) for tz in pytz.common_timezones] + + def __init__(self, **kwargs): + options = { + 'max_length': 63, + } + if kwargs is not None: + options.update(kwargs) + super().__init__(**options) + + def formfield(self, form_class=None, choices_form_class=None, **kwargs): + return super().formfield(form_class=form_fields.TimeZoneField, choices=self.default_choices, **kwargs) diff --git a/src/core/fixtures/anhalter.json b/src/core/fixtures/anhalter.json index 2d1840f5cf031ad8fee2864762e77a99d15312e7..4b2e4914e16b37045f4948a51633d2353280de81 100644 --- a/src/core/fixtures/anhalter.json +++ b/src/core/fixtures/anhalter.json @@ -74,6 +74,28 @@ "model": "core.conferencetag", "pk": 3 }, + { + "fields": { + "conference": "017c0749-a2ea-4f86-92cd-e60b4508dd98", + "slug": "guter Geschmack", + "value_type": "boolean", + "is_public": true, + "description": null + }, + "model": "core.conferencetag", + "pk": 4 + }, + { + "fields": { + "conference": "017c0749-a2ea-4f86-92cd-e60b4508dd98", + "slug": "Autor des besten Gedichts", + "value_type": "string", + "is_public": true, + "description": null + }, + "model": "core.conferencetag", + "pk": 5 + }, { "fields": { "conference": "017c0749-a2ea-4f86-92cd-e60b4508dd98", @@ -164,4 +186,4 @@ "model": "core.event", "pk": "35e10dfc-11c2-475b-b682-da1ddd291770" } -] \ No newline at end of file +] diff --git a/src/core/form_fields.py b/src/core/form_fields.py new file mode 100644 index 0000000000000000000000000000000000000000..2f12fe63ded86d9e67d11c38fbfe30f96f9800ec --- /dev/null +++ b/src/core/form_fields.py @@ -0,0 +1,15 @@ +import pytz +from django import forms +from django.utils.translation import gettext as _ + + +class TimeZoneField(forms.ChoiceField): + def __init__(self, max_length=None, **kwargs): + super().__init__(**kwargs) + + def validate(self, value): + try: + pytz.timezone(value) + except pytz.UnknownTimeZoneError: + raise forms.ValidationError(_("Invalid Timezone")) from None + return super().validate(value) diff --git a/src/core/integrations/__init__.py b/src/core/integrations/__init__.py index ead3150ea37c17dc19aaf3ed688b6af170ff6fcd..8ded4750e683cc544adbdef7582db2ef484f20e8 100644 --- a/src/core/integrations/__init__.py +++ b/src/core/integrations/__init__.py @@ -5,9 +5,9 @@ from .error import IntegrationError from .workadventure import WorkAdventureIntegration if settings.BIGBLUEBUTTON_API_URL is not None: - BigBlueButton = BigBlueButtonIntegration(settings.BIGBLUEBUTTON_API_URL, settings.BIGBLUEBUTTON_API_TOKEN) + BigBlueButton = BigBlueButtonIntegration(settings.BIGBLUEBUTTON_API_URL, settings.BIGBLUEBUTTON_API_TOKEN, settings.BIGBLUEBUTTON_END_MEETING_CALLBACK) else: - BigBlueButton = None + BigBlueButton = None # type: BigBlueButtonIntegration if settings.HANGAR_URL is not None: Hangar = HangarIntegration(settings.HANGAR_URL) diff --git a/src/core/integrations/bigbluebutton.py b/src/core/integrations/bigbluebutton.py index 9f1b938b2c96ba10b784c3323f7db4024435f515..b7b4ed5ce66d6f9d06f564080da0e8665eb48f91 100644 --- a/src/core/integrations/bigbluebutton.py +++ b/src/core/integrations/bigbluebutton.py @@ -1,12 +1,39 @@ +from hashlib import sha1 +import logging +from random import SystemRandom import requests +import string +from typing import Dict, Union +from urllib.parse import urlencode, urljoin, quote +from uuid import uuid4 +from xml.etree import ElementTree as ET -from core.models.assemblies import Assembly +from django.utils.translation import gettext as _ + +from core.models.assemblies import Assembly, AssemblyMember from core.models.rooms import Room from core.models.users import PlatformUser from .error import IntegrationError +logger = logging.getLogger(__name__) +PASSWORD_CHARS = string.ascii_letters + string.digits + + +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' + elif isinstance(v, int): + v = str(v) + else: + assert isinstance(v, str), f'{v!r} is no string!' + res[k] = v + return res + + class BigBlueButtonIntegration(object): """ This class talks with a BigBlueButton server's API. @@ -15,14 +42,45 @@ class BigBlueButtonIntegration(object): https://docs.bigbluebutton.org/dev/api.html """ - def __init__(self, api_url, api_token): + def __init__(self, api_url, api_token, end_meeting_callback): self._api_url = api_url self._api_token = api_token + self._end_meeting_callback = end_meeting_callback self._session = requests.session() + def _send_request(self, resource: str, params: Dict[str, str] = {}, raw=False): + encoded_params = urlencode(_params_to_str(params)) + hash_input = resource + encoded_params + self._api_token + hash_input = hash_input.encode('utf-8') + checksum = sha1(hash_input).hexdigest() + + if encoded_params: + params_str = encoded_params + '&checksum=' + checksum + else: + params_str = 'checksum=' + checksum + request_url = urljoin(self._api_url, resource) + + resp = self._session.get(request_url, params=params_str, allow_redirects=False) + if raw: + 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")) + + try: + resp = ET.fromstring(resp.text) + except UnicodeDecodeError: + logger.exception("Response decoding failed") + raise IntegrationError(_("Invalid Response")) + + return resp + def is_available(self): - # TODO: do a proper check based an API response - return False + if not self._api_token: + return False + + return True def can_create_for_assembly(self, assembly: Assembly): assert assembly is not None @@ -31,26 +89,178 @@ class BigBlueButtonIntegration(object): return True def create_room(self, room: Room): - assert room is not None and room.room_type == Room.RoomType.BIGBLUEBUTTON and room.backend_status == Room.BackendStatus.NEW + assert room is not None and room.room_type == Room.RoomType.BIGBLUEBUTTON + + if room.backend_status in {Room.BackendStatus.ACTIVE, Room.BackendStatus.FULL}: + # room was already created, don't need to create it twice + return if not self.is_available(): raise IntegrationError('Currently not available.') - raise NotImplementedError() + room.backend_status = Room.BackendStatus.SETUP + room.save(update_fields=['backend_status']) + + room_id = str(uuid4()) + join_pw = ''.join(SystemRandom().choices(PASSWORD_CHARS, k=32)) + mod_pw = ''.join(SystemRandom().choices(PASSWORD_CHARS, k=32)) + close_secret = ''.join(SystemRandom().choices(PASSWORD_CHARS, k=32)) + + if self._end_meeting_callback: + end_meeting_callback = f'{self._end_meeting_callback}?close_secret={quote(close_secret)}' + else: + end_meeting_callback = '' + + params = { + 'name': room.name, + 'meetingID': room_id, + 'attendeePW': join_pw, + 'moderatorPW': mod_pw, + # 'maxParticipants': 100, + 'record': False, + 'muteOnStart': False, + 'allowModsToUnmuteUsers': False, + 'meta_endCallbackUrl': end_meeting_callback, + } + try: + result = self._send_request('create', params) + except Exception: + room.backend_status = Room.BackendStatus.ERROR + room.save(update_fields=['backend_status']) + raise + + retcode = result.find('returncode') + if retcode is not None and retcode.text != 'SUCCESS': + room.backend_status = Room.BackendStatus.ERROR + room.save(update_fields=['backend_status']) + 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")) + + room.backend_link = result.find('meetingID').text + room.backend_status = Room.BackendStatus.ACTIVE + room.backend_data = { + 'attendeePW': result.find('attendeePW').text, + 'moderatorPW': result.find('moderatorPW').text, + 'createTime': result.find('createTime').text, + 'close_secret': close_secret, + } + room.occupants = 0 + room.capacity = None + + if room.backend_link != room_id: + logger.warning('Requested meetingID %s but got %s', room_id, room.backend_link) + if room.backend_data['attendeePW'] != join_pw: + logger.warning('Requested attendeePW %s but got %s', join_pw, room.backend_data['attendeePW']) + if room.backend_data['moderatorPW'] != mod_pw: + logger.warning('Requested moderatorPW %s but got %s', mod_pw, room.backend_data['moderatorPW']) + + room.save(update_fields=['backend_data', 'backend_status', 'backend_link', 'occupants', 'capacity']) + return result def remove_room(self, room: Room): assert room is not None and room.room_type == Room.RoomType.BIGBLUEBUTTON - raise NotImplementedError() + if room.backend_status not in {Room.BackendStatus.ACTIVE, Room.BackendStatus.FULL, Room.BackendStatus.INACTIVE}: + return - def room_status(self, room: Room): + mod_pw = room.backend_data.get('moderatorPW') + params = { + 'meetingID': room.backend_link, + 'password': mod_pw, + } + result = self._send_request('end', params) + + retcode = result.find('returncode') + if retcode is not None and retcode.text != 'SUCCESS': + room.backend_status = Room.BackendStatus.ERROR + room.save(update_fields=['backend_status']) + 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")) + + room.backend_status = Room.BackendStatus.INACTIVE + room.save(update_fields=['backend_status']) + return result + + def _room_status(self, room: Room, commit=True): assert room is not None and room.room_type == Room.RoomType.BIGBLUEBUTTON - raise NotImplementedError() + if room.backend_status not in {Room.BackendStatus.ACTIVE, Room.BackendStatus.FULL, Room.BackendStatus.INACTIVE}: + return False - def join_room(self, room: Room, user: PlatformUser, anonymous: bool = False): + resp = self._send_request('getMeetingInfo', {'meetingID': room.backend_link}) + 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': + 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")) + + try: + room.capacity = int(resp.find('maxUsers').text) + except (ValueError, AttributeError): + room.capacity = None + try: + room.occupants = int(resp.find('participantCount').text) + except (ValueError, AttributeError): + room.occupants = None + + if commit: + room.save(update_fields=['backend_status', 'capacity', 'occupants']) + + def room_status(self, room: Room): + return self._room_status(room, True) + + def _join_room(self, room: Room, user: PlatformUser, anonymous: bool = False, retrying: bool = False): assert room is not None and room.room_type == Room.RoomType.BIGBLUEBUTTON assert user is not None - raise IntegrationError("Not Implemented Yet") - raise NotImplementedError() + create_room = False + if room.backend_status in {Room.BackendStatus.NEW, Room.BackendStatus.ERROR}: + create_room = True + else: + self._room_status(room, False) + if room.backend_status == Room.BackendStatus.FULL: + room.save(update_fields=['backend_status', 'capacity', 'occupants']) + return None + if room.backend_status == Room.BackendStatus.INACTIVE: + create_room = True + + if create_room: + self.create_room(room) + + if room.backend_status != Room.BackendStatus.ACTIVE: + return None # can't join a nonexistent room + + conf = room.conference + is_moderator = conf.users.filter(user=user, is_staff=True).exists() + + if not is_moderator and room.assembly_id: + if AssemblyMember.objects.filter(assembly_id=room.assembly_id, member=user).exclude(role=AssemblyMember.Role.BLOCKED).exists(): + is_moderator = True + + if is_moderator: + join_pw = room.backend_data.get('moderatorPW') + else: + join_pw = room.backend_data.get('attendeePW') + + params = { + 'fullName': user.username, + 'meetingID': room.backend_link, + 'password': join_pw, + 'userID': str(user.id), + 'createTime': room.backend_data.get('createTime'), + 'redirect': True, + } + result = self._send_request('join', params, raw=True) + return result.headers['Location'] + + def join_room(self, room: Room, user: PlatformUser, anonymous: bool = False): + return self._join_room(room, user, anonymous, False) diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po index 78b9106efc54dc898c8b56a7e7e928489fcb34a3..2ba3b3a1edabb4f68915231c26f8c4d831f7e54e 100644 --- a/src/core/locale/de/LC_MESSAGES/django.po +++ b/src/core/locale/de/LC_MESSAGES/django.po @@ -60,6 +60,9 @@ msgstr "Angabe zur Eignung anhand des Publikum-Alters (nach deutschem Recht)" msgid "FSK" msgstr "Alterseinstufung" +msgid "Invalid Timezone" +msgstr "Fehlerhafte Zeitzone" + msgid "Assembly" msgstr "Assembly" @@ -228,6 +231,9 @@ msgstr "Favorisiert von" msgid "Assembly__technical_user__must_be_assembly" msgstr "Der technische Benutzer muss vom Typ 'Assembly' sein." +msgid "Assembly__slug__is_forbidden" +msgstr "Dieser Kurzname ist verboten!" + msgid "AssemblyLink__type-related" msgstr "themenverwandt" @@ -1029,6 +1035,12 @@ msgstr "den eigenen Status für Jedermann sichtbar machen" msgid "PlatformUser__status_public" msgstr "öffentlicher Status" +msgid "PlatformUser__timezone__help" +msgstr "Zeitzone, in der Uhrzeiten angezeigt werden" + +msgid "PlatformUser__timezone" +msgstr "Zeitzone" + msgid "PlatformUser__no_animations__help" msgstr "alle Animationen ausschalten, so wenig Gewackel wie möglich" diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po index 0f74240110b388ea065e3f68ff28aa3c5d180db4..d415042cbe8c753fec3705d942da123539d03f66 100644 --- a/src/core/locale/en/LC_MESSAGES/django.po +++ b/src/core/locale/en/LC_MESSAGES/django.po @@ -60,6 +60,9 @@ msgstr "content is suitable for people of this age (based on Germany's law)" msgid "FSK" msgstr "age restriction" +msgid "Invalid Timezone" +msgstr "Invalid Timezone" + msgid "Assembly" msgstr "Assembly" @@ -228,6 +231,9 @@ msgstr "favorited by these users" msgid "Assembly__technical_user__must_be_assembly" msgstr "The technical user must be of type 'assembly'." +msgid "Assembly__slug__is_forbidden" +msgstr "this short name is forbidden" + msgid "AssemblyLink__type-related" msgstr "related (similar topic)" @@ -1029,6 +1035,12 @@ msgstr "show status to everyone" msgid "PlatformUser__status_public" msgstr "public status" +msgid "PlatformUser__timezone__help" +msgstr "timezone to use when displaying times" + +msgid "PlatformUser__timezone" +msgstr "timezone" + msgid "PlatformUser__no_animations__help" msgstr "do not show animations, try to skip anything wobbling/moving/whatever" diff --git a/src/core/management/commands/bbb_integration_revisit.py b/src/core/management/commands/bbb_integration_revisit.py new file mode 100644 index 0000000000000000000000000000000000000000..af4041322d03057260f4a826cda43689e924cf6e --- /dev/null +++ b/src/core/management/commands/bbb_integration_revisit.py @@ -0,0 +1,45 @@ +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 + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + '-a', '--all', action='store_true', + help='Revisit all Rooms, not just failing ones', + ) + + def handle(self, *args, **options): + revisit_active = options['all'] + conf = Conference.objects.current_conference + + start = time.time() + n_fail = 0 + n_healthy = 0 + + for room in Room.objects.filter(conference=conf, room_type=Room.RoomType.BIGBLUEBUTTON): + if room.backend_status in {Room.BackendStatus.NEW, Room.BackendStatus.ERROR, Room.BackendStatus.SETUP}: + n_fail += 1 + try: + with transaction.atomic(): + BigBlueButton.create_room(room) + room.save() + except IntegrationError as e: + 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 + try: + with transaction.atomic(): + BigBlueButton.room_status(room) + room.save() + except IntegrationError as e: + 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/middleware.py b/src/core/middleware.py new file mode 100644 index 0000000000000000000000000000000000000000..7d03464d86ffe7d6ee3558ebd11185ff9b3ea25b --- /dev/null +++ b/src/core/middleware.py @@ -0,0 +1,15 @@ +import pytz + +from django.utils import timezone + + +class TimezoneMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated: + timezone.activate(pytz.timezone(request.user.time_zone)) + else: + timezone.deactivate() + return self.get_response(request) diff --git a/src/core/migrations/0047_longer_event_slugs.py b/src/core/migrations/0047_longer_event_slugs.py new file mode 100644 index 0000000000000000000000000000000000000000..4b150c019dfc590d0605358608aa40d78f16a798 --- /dev/null +++ b/src/core/migrations/0047_longer_event_slugs.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.4 on 2020-12-25 18:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0046_additional_fields_for_apis'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='slug', + field=models.SlugField(help_text='Event__slug__help', max_length=150, verbose_name='Event__slug'), + ), + ] diff --git a/src/core/migrations/0048_Platformuser_time_zone.py b/src/core/migrations/0048_Platformuser_time_zone.py new file mode 100644 index 0000000000000000000000000000000000000000..9c72d324f440c6c36621fe8e56232b8c128950c5 --- /dev/null +++ b/src/core/migrations/0048_Platformuser_time_zone.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.4 on 2020-12-25 20:51 + +import core.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0047_longer_event_slugs'), + ] + + operations = [ + migrations.AddField( + model_name='platformuser', + name='time_zone', + field=core.fields.TimeZoneField(default='Europe/Berlin', help_text='PlatformUser__timezone__help', max_length=63, verbose_name='PlatformUser__timezone'), + ), + ] diff --git a/src/core/migrations/0049_Event_repair_start.py b/src/core/migrations/0049_Event_repair_start.py new file mode 100644 index 0000000000000000000000000000000000000000..b96c89c6c8282c035cca2f1180e07cc6bfec55d1 --- /dev/null +++ b/src/core/migrations/0049_Event_repair_start.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.4 on 2020-12-25 18:43 + +from datetime import timedelta + +from django.db import migrations + + +def move_events_by(apps, delta): + Event = apps.get_model('core', 'Event') + for evt in Event.objects.all(): + if evt.schedule_start: + evt.schedule_start = evt.schedule_start + delta + if evt.schedule_end: + evt.schedule_end = evt.schedule_end + delta + evt.save() + + +def do(apps, schema_editor): + move_events_by(apps, timedelta(hours=-1)) + + +def undo(apps, schema_editor): + move_events_by(apps, timedelta(hours=1)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0048_Platformuser_time_zone'), + ] + + operations = [ + migrations.RunPython(do, undo, elidable=True) + ] diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index a7f4466bbc34955cb4a2f206a2b8d0dd3709b7cd..fcbf599818e3f3ba1c7902f88884e55c3effa898 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -2,6 +2,7 @@ import logging import re from uuid import uuid4 +from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.utils.html import escape as html_escape, format_html @@ -248,6 +249,9 @@ class Assembly(TaggedItemMixin, models.Model): if self.technical_user.user_type != PlatformUser.Type.ASSEMBLY: raise ValidationError({'technical_user': _('Assembly__technical_user__must_be_assembly')}) + if self.slug in settings.FORBIDDEN_ASSEMBLY_SLUGS: + raise ValidationError({'slug': _('Assembly__slug__is_forbidden')}) + @cached_property def conference_slug(self): return self.conference.slug @@ -341,6 +345,10 @@ class Assembly(TaggedItemMixin, models.Model): else: self.description_html = render_markdown(self.description) + if update_fields is None or 'slug' in update_fields: + if self.slug in settings.FORBIDDEN_ASSEMBLY_SLUGS: + raise Exception("Won't save assembly with forbidden slug!") + return super().save(*args, update_fields=update_fields, **kwargs) diff --git a/src/core/models/events.py b/src/core/models/events.py index 611a095d0d4659fc2b5d994433d5c87e3ee9fc0b..19b03e20b6409d27621893457745b59011abdced 100644 --- a/src/core/models/events.py +++ b/src/core/models/events.py @@ -4,6 +4,7 @@ from uuid import uuid4 from django.conf import settings from django.core.exceptions import ValidationError from django.db import models +from django.utils import timezone from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -93,6 +94,7 @@ class Event(TaggedItemMixin, models.Model): id = models.UUIDField(default=uuid4, primary_key=True, editable=False) conference = ConferenceReference(related_name='events') slug = models.SlugField( + max_length=150, help_text=_('Event__slug__help'), verbose_name=_('Event__slug')) kind = models.CharField( @@ -173,6 +175,19 @@ class Event(TaggedItemMixin, models.Model): """Returns a list of all public speakers of this event.""" return self.participants.objects.filter(role=EventParticipant.Role.SPEAKER, is_accepted=True, is_public=True).select_related('participant') + @property + def starts_in(self): + """ + Returns a timedelta between now and the scheduled start is in the future. + Returns false otherwise + """ + if self.schedule_start is not None: + now = timezone.now() + if now > self.schedule_start: + return False + return self.schedule_start - now + return False + def __str__(self): return self.name diff --git a/src/core/models/pages.py b/src/core/models/pages.py index f9116042cb4d51c59fefa140ef1b08e3b993aaaa..60730c3b47773331b6c76a81c6d31e99ac290839 100644 --- a/src/core/models/pages.py +++ b/src/core/models/pages.py @@ -26,6 +26,10 @@ class StaticPageManager(models.Manager): if user.is_superuser or user.is_staff: return qs + # content team can access non-public pages + if user.has_conference_staffpermission(self.conference, 'static_pages'): + return qs + # return only pages which are marked public return qs.filter(public_revision__gt=0) diff --git a/src/core/models/tags.py b/src/core/models/tags.py index 96368d7c2a7779c9384a83f48edab681b3c7bbab..c2d7272009ea5c91966e103302845d2cbaddf0b7 100644 --- a/src/core/models/tags.py +++ b/src/core/models/tags.py @@ -66,7 +66,7 @@ class TagItem(models.Model): return self._value_int == 0 @value.setter - def _set_value(self, new_value): + def value(self, new_value): self.set_value(new_value) def set_value(self, new_value): diff --git a/src/core/models/users.py b/src/core/models/users.py index 9eb0a264031edf9f6bae80ff329ffef11baf3b6a..3b23ae1200ebe6998afd47a458780158617f2550 100644 --- a/src/core/models/users.py +++ b/src/core/models/users.py @@ -14,7 +14,7 @@ from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from ..fields import ConferenceReference +from ..fields import ConferenceReference, TimeZoneField from ..utils import render_markdown @@ -71,6 +71,11 @@ class PlatformUser(AbstractUser): null=True, help_text=_('PlatformUser__status_public__help'), verbose_name=_('PlatformUser__status_public')) + time_zone = TimeZoneField( + help_text=_('PlatformUser__timezone__help'), + verbose_name=_('PlatformUser__timezone'), + default='Europe/Berlin', + ) # Accessibility Options no_animations = models.BooleanField( diff --git a/src/core/tests/__init__.py b/src/core/tests/__init__.py index 0a2ae62164cf7f461b6416bb226e135606caef91..312ffef6bb1fe94d89b47874bca3de4ddd828669 100644 --- a/src/core/tests/__init__.py +++ b/src/core/tests/__init__.py @@ -1,4 +1,5 @@ from .badges import * # noqa: F401, F403 +from .bigbluebutton import * # noqa: F401, F403 from .events import * # noqa: F401, F403 from .search import * # noqa: F401, F403 from .users import * # noqa: F401, F403 diff --git a/src/core/tests/bigbluebutton.py b/src/core/tests/bigbluebutton.py new file mode 100644 index 0000000000000000000000000000000000000000..0b1e384a28ec3de3f3f32f88fd29e9a19f117444 --- /dev/null +++ b/src/core/tests/bigbluebutton.py @@ -0,0 +1,240 @@ +from django.test import TestCase +from unittest.mock import Mock, patch + +from core.integrations import BigBlueButtonIntegration, IntegrationError +from core.models import Assembly, Conference, PlatformUser, Room + + +BigBlueButton = BigBlueButtonIntegration('http://localhost/', 'asdf', 'https://localhost/end_meeting') + + +# from https://github.com/Grollicus/unittest_patterns/blob/master/unittest_patterns/__init__.py +class Pattern(object): + def __req__(self, lhs): + return self.__eq__(lhs) + + __hash__ = None + + +# from https://github.com/Grollicus/unittest_patterns/blob/master/unittest_patterns/__init__.py +class Any(Pattern): + """ Equals everything """ + + def __eq__(self, rhs): + return True + + +class BigBlueButtonTest(TestCase): + def setUp(self): + self.conf = Conference(slug='conf', name='TestConf') + self.conf.save() + 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 + ) + self.room.save() + + def test_create_room(self): + with patch.object(BigBlueButton._session, 'get') as get_mock: + get_mock.return_value.status_code = 404 + with self.assertRaises(IntegrationError), self.assertLogs('core.integrations.bigbluebutton'): + BigBlueButton.create_room(self.room) + self.assertEqual(self.room.backend_status, Room.BackendStatus.ERROR) + get_mock.assert_called_once() + get_mock.reset_mock() + + 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>' + ) + self.room.backend_status = Room.BackendStatus.NEW + self.room.save() + with self.assertLogs('core.integrations.bigbluebutton'): + BigBlueButton.create_room(self.room) + 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(), + }) + + def test_remove_room(self): + with patch.object(BigBlueButton._session, 'get') as get_mock: + get_mock.return_value.status_code = 404 + self.room.backend_status = Room.BackendStatus.ACTIVE + self.room.backend_link = 'Test' + self.room.backend_data = { + 'attendeePW': 'ap', + 'moderatorPW': 'mp', + 'createTime': '1531155809613', + } + self.room.save() + with self.assertRaises(IntegrationError), self.assertLogs('core.integrations.bigbluebutton'): + BigBlueButton.remove_room(self.room) + self.assertEqual(self.room.backend_status, Room.BackendStatus.ACTIVE) + get_mock.assert_called_once() + get_mock.reset_mock() + + get_mock.return_value.status_code = 200 + get_mock.return_value.text = ( + '<response>' + '<returncode>SUCCESS</returncode>' + '<messageKey>sentEndMeetingRequest</messageKey>' + '<message>' + 'A request to end the meeting was sent. Please wait a few seconds, and then use the getMeetingInfo or' + ' isMeetingRunning API calls to verify that it was ended' + '</message>' + '</response>' + ) + self.room.backend_status = Room.BackendStatus.ACTIVE + self.room.save() + BigBlueButton.remove_room(self.room) + get_mock.assert_called_once() + self.assertEqual(self.room.backend_status, Room.BackendStatus.INACTIVE) + + def test_room_status(self): + self.room.backend_status = Room.BackendStatus.NEW + self.room.backend_link = 'Demo Meeting' + self.room.backend_data = { + 'attendeePW': 'ap', + 'moderatorPW': 'mp', + 'createTime': '1531155809613', + } + self.room.capacity = None + self.room.occupants = None + self.room.save() + + with patch.object(BigBlueButton._session, 'get') as get_mock: + BigBlueButton.room_status(self.room) + get_mock.assert_not_called() + + self.room.backend_status = Room.BackendStatus.ACTIVE + get_mock.return_value.status_code = 200 + get_mock.return_value.text = ( + '<response>' + '<returncode>SUCCESS</returncode>' + '<meetingName>Demo Meeting</meetingName>' + '<meetingID>Demo Meeting</meetingID>' + '<internalMeetingID>183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1531155809613</internalMeetingID>' + '<createTime>1531155809613</createTime>' + '<createDate>Tue Jul 10 16:36:25 UTC 2018</createDate>' + '<voiceBridge>70066</voiceBridge>' + '<dialNumber>613-555-1234</dialNumber>' + '<attendeePW>ap</attendeePW>' + '<moderatorPW>mp</moderatorPW>' + '<running>true</running>' + '<duration>0</duration>' + '<hasUserJoined>true</hasUserJoined>' + '<recording>false</recording>' + '<hasBeenForciblyEnded>false</hasBeenForciblyEnded>' + '<startTime>1531240585239</startTime>' + '<endTime>0</endTime>' + '<participantCount>2</participantCount>' + '<listenerCount>1</listenerCount>' + '<voiceParticipantCount>1</voiceParticipantCount>' + '<videoCount>1</videoCount>' + '<maxUsers>20</maxUsers>' + '<moderatorCount>1</moderatorCount>' + '<attendees>' + '<attendee>' + '<userID>w_2wzzszfaptsp</userID>' + '<fullName>stu</fullName>' + '<role>VIEWER</role>' + '<isPresenter>false</isPresenter>' + '<isListeningOnly>true</isListeningOnly>' + '<hasJoinedVoice>false</hasJoinedVoice>' + '<hasVideo>false</hasVideo>' + '<clientType>FLASH</clientType>' + '</attendee>' + '</attendees>' + '<metadata />' + '<isBreakout>false</isBreakout>' + '</response>' + ) + BigBlueButton.room_status(self.room) + self.assertEqual(self.room.occupants, 2) + self.assertEqual(self.room.capacity, 20) + get_mock.reset_mock() + + get_mock.return_value.status_code = 200 + get_mock.return_value.text = ( + '<response>' + '<returncode>FAILED</returncode>' + '<messageKey>notFound</messageKey>' + '<message>We could not find a meeting with that meeting ID</message>' + '</response>' + ) + BigBlueButton.room_status(self.room) + self.assertEqual(self.room.occupants, None) + self.assertEqual(self.room.capacity, None) + self.assertEqual(self.room.backend_status, Room.BackendStatus.INACTIVE) + + def test_join_room(self): + user = PlatformUser(username='asdf') + user.save() + + self.room.backend_status = Room.BackendStatus.ACTIVE + self.room.backend_link = 'Test' + self.room.backend_data = { + 'attendeePW': 'ap', + 'moderatorPW': 'mp', + 'createTime': '1531155809613', + } + self.room.save() + + 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' + }), + ] + url = BigBlueButton.join_room(self.room, user) + self.assertEqual(url, 'https://yourserver.com/client/BigBlueButton.html?sessionToken=ai1wqj8wb6s7rnk0') + self.assertEqual(self.room.backend_status, Room.BackendStatus.ACTIVE) diff --git a/src/core/tests/markdown.py b/src/core/tests/markdown.py index 5e1d4a836d5e1bd3578ca37c74e665c9d9960b3d..fc2b3f8074251f6597b26d86ed1e1b3a27d0956a 100644 --- a/src/core/tests/markdown.py +++ b/src/core/tests/markdown.py @@ -1,6 +1,7 @@ from django.test import TestCase from ..utils import render_markdown +from urllib.parse import quote class MarkdownTest(TestCase): @@ -22,9 +23,15 @@ class MarkdownTest(TestCase): ] for test_url, should_be_local in tests: with self.subTest(test_url): - class_ = 'internal' if should_be_local else 'external' - self.assertEqual(render_markdown(f'[.]({test_url})'), f'<p><a class="{class_}" href="{test_url}">.</a></p>') - self.assertEqual(render_markdown(f'[.][1]\n\n[1]: {test_url}'), f'<p><a class="{class_}" href="{test_url}">.</a></p>') + if should_be_local: + class_ = 'internal' + href = test_url + else: + href = '/rc3/dereferrer/' + quote(test_url) + class_ = 'external' + + self.assertEqual(render_markdown(f'[.]({test_url})'), f'<p><a class="{class_}" href="{href}">.</a></p>') + self.assertEqual(render_markdown(f'[.][1]\n\n[1]: {test_url}'), f'<p><a class="{class_}" href="{href}">.</a></p>') forbidden = [ 'data:text/html;charset=utf-8;base64,PGh0bWw+PGhlYWQ+PHRpdGxlPnRlc3Q8L3RpdGxlPjwvaGVhZD48Ym9keT48aDE+VGVzdDwvaDE+PC9ib2R5PjwvaHRtbD4=', diff --git a/src/core/tests/tags.py b/src/core/tests/tags.py index 30f196a3239ddf2bf9c0393af87b65a7447451d4..a11aaf4277e0f4b5f7ef26765df1b568ded021df 100644 --- a/src/core/tests/tags.py +++ b/src/core/tests/tags.py @@ -1,10 +1,11 @@ from django.contrib.auth.models import AnonymousUser +from django.contrib.contenttypes.models import ContentType from django.test import TestCase from ..models.assemblies import Assembly from ..models.conference import Conference from ..models.events import Event -from ..models.tags import ConferenceTag, TaggedItemMixin +from ..models.tags import ConferenceTag, TaggedItemMixin, TagItem class TaggingTests(TestCase): @@ -90,3 +91,127 @@ class TaggingTests(TestCase): def testAssemblyTagging(self): assembly = Assembly.objects.filter(conference=self.conference).first() self._testTagManagement(assembly) + + +class TagItemTests(TestCase): + fixtures = ['anhalter.json'] + + def setUp(self): + self.conference = Conference.objects.get(slug='vogc') + self.user = AnonymousUser() + self.event = Event.objects.filter(conference=self.conference).first() + self.tag_items = self._createTagItems() + + def _createTagItem(self, tag_slug: str) -> TagItem: + tag = ConferenceTag.objects.get(conference=self.conference, slug=tag_slug) + return TagItem(tag=tag, target_type=ContentType.objects.get_for_model(type(self.event)), target_id=self.event.id) + + def _createTagItems(self): + result = {} + for tag_slug in ['foo', 'bar', 'michelinstars', 'guter Geschmack', 'Autor des besten Gedichts']: + result[tag_slug] = self._createTagItem(tag_slug) + + return result + + def _validate_value_type(self, tag_item: TagItem): + value_type = tag_item.tag.value_type + value = tag_item.value + if value is None: + return + if value_type == ConferenceTag.Type.SIMPLE: + 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") + if value_type == ConferenceTag.Type.INT: + 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") + + def test_get_value_check_type_uninitialized(self): + for tag_item in self.tag_items.values(): + self._validate_value_type(tag_item) + + def test_get_value_check_type_after_set(self): + 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" + if value_type == ConferenceTag.Type.INT: + tag_item.value = 42 + if value_type == ConferenceTag.Type.BOOL: + tag_item.value = True + self._validate_value_type(tag_item) + + def test_value_check_get_after_set_intended(self): + for tag_item in self.tag_items.values(): + value_type = tag_item.tag.value_type + value = None + if value_type == ConferenceTag.Type.STRING: + value = "Test" + if value_type == ConferenceTag.Type.INT: + value = 42 + if value_type == ConferenceTag.Type.BOOL: + value = True + tag_item.value = value + self.assertEqual(tag_item.value, value) + + def test_value_check_boolean_coerce(self): + 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)) + + 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)) + + def test_value_set_check_simple_with_wrong_argument(self): + tag = self.tag_items["foo"] + with self.assertRaises(ValueError): + tag.value = self.user + with self.assertRaises(ValueError): + 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"] + with self.assertRaises(ValueError): + tag.value = None + with self.assertRaises(ValueError): + tag.value = self.user + with self.assertRaises(ValueError): + tag.value = 42 + with self.assertRaises(ValueError): + tag.value = True + + def test_value_set_check_int_with_wrong_argument(self): + 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" + # Python coerces bool to int by itself + + def test_value_set_check_bool_with_wrong_argument(self): + 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" + with self.assertRaises(ValueError): + tag.value = "foo" + with self.assertRaises(ValueError): + tag.value = "" + with self.assertRaises(ValueError): + tag.value = "bar" diff --git a/src/core/utils.py b/src/core/utils.py index d2621b93f12947bda69b20a8063571e87bef5a0c..98310093fc80fa25739536337c5dc53e47a62f64 100644 --- a/src/core/utils.py +++ b/src/core/utils.py @@ -5,7 +5,7 @@ import markdown from markdown.extensions import Extension as MarkdownExtension from markdown.inlinepatterns import LinkInlineProcessor, LINK_RE, ReferenceInlineProcessor, REFERENCE_RE import random -from urllib.parse import urlparse +from urllib.parse import urlparse, quote MARKDOWN_EXTENSIONS = [ @@ -20,8 +20,15 @@ TOKEN_CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' def _mark_link_tag(el): url = urlparse(el.get('href')) is_local_domain = url.netloc == 'rc3.world' - class_ = 'external' if (url.scheme or url.netloc) and not is_local_domain else 'internal' - el.set('class', class_) + is_external = (url.scheme or url.netloc) and not is_local_domain + if is_external: + if url.scheme and url.scheme in {'http', 'https', 'ftp', 'ftps'}: + href = el.get('href') + dereferrer_href = '/rc3/dereferrer/' + quote(href) + el.set('href', dereferrer_href) + el.set('class', 'external') + else: + el.set('class', 'internal') class CustomLinkInlineProcessor(LinkInlineProcessor): diff --git a/src/plainui/forms.py b/src/plainui/forms.py index c3b529451fe038f1b498086df0e8fe1d43e799ea..2d1834452e9f28d40e1282fe4e3f0a2aa45b339a 100644 --- a/src/plainui/forms.py +++ b/src/plainui/forms.py @@ -5,6 +5,7 @@ from django.contrib.sites.shortcuts import get_current_site from django.contrib.auth import forms as auth_forms from django.core.mail import send_mail from django.forms import ValidationError +from django.db.models import Q from django.template import loader from django.urls import reverse from django.utils.formats import localize @@ -52,7 +53,7 @@ class ExampleForm(forms.Form): class ProfileEditForm(forms.ModelForm): class Meta: model = PlatformUser - fields = ['pronouns', 'description', 'high_contrast', 'receive_audio', 'receive_video'] + fields = ['pronouns', 'description', 'time_zone', 'high_contrast', 'receive_audio', 'receive_video'] class InputTokenForm(forms.Form): @@ -99,20 +100,23 @@ class RoomChoiceField(forms.ModelChoiceField): class SelfOrganizedSessionForm(forms.Form): + """ + TODO: Will explode if the related event has no start or end date yet. + """ name = forms.CharField(max_length=200, min_length=1, strip=True, widget=forms.TextInput(attrs={'placeholder': _("Please enter a name")})) - room = RoomChoiceField(queryset=Room.objects.none()) + room = RoomChoiceField(queryset=Room.objects.none(), help_text=_("BBB or Workshop Rooms only.")) description = forms.CharField(widget=forms.Textarea) language = forms.CharField(max_length=50) is_public = forms.BooleanField(required=False) - schedule_start = forms.DateTimeField(required=True) - schedule_duration = forms.DurationField(required=True) + schedule_start = forms.DateTimeField(required=True, help_text=_("ISO 8601 format, e.g. '2020-12-27 13:37'")) + schedule_duration = forms.DurationField(required=True, help_text=_("'HH:MM' or even 'DD HH:MM' e.g. 3hours ='03:00'")) def __init__(self, conf, assembly, event=None, *args, **kwargs): super().__init__(*args, **kwargs) self.conf = conf self.assembly = assembly self.event = event - self.fields['room'].queryset = assembly.rooms.filter(room_type=Room.RoomType.BIGBLUEBUTTON) + self.fields['room'].queryset = assembly.rooms.filter(Q(room_type=Room.RoomType.BIGBLUEBUTTON) | Q(room_type=Room.RoomType.WORKSHOP)) def _check_schedule_end(self): if 'schedule_start' in self.cleaned_data and 'schedule_duration' in self.cleaned_data: @@ -132,7 +136,7 @@ class SelfOrganizedSessionForm(forms.Form): if self.event is not None: blocking_events = blocking_events.exclude(pk=self.event.pk) if blocking_events.exists(): - self.add_error('room', ValidationError(gettext("Room is not free!"), code='invalid')) + self.add_error('room', ValidationError(gettext("Room is not free, choose other time or room"), code='invalid')) def clean_schedule_start(self): if self.cleaned_data['schedule_start'] < self.conf.start: diff --git a/src/plainui/jinja2.py b/src/plainui/jinja2.py index d69d5f3c046eb89d160e740cd92b9392705b100c..7470c509f996be3834214c8a01e7fcad85c2ea9e 100644 --- a/src/plainui/jinja2.py +++ b/src/plainui/jinja2.py @@ -6,8 +6,9 @@ from django.utils.formats import localize from django.utils.functional import LazyObject from django.utils.timezone import localdate, localtime from django.utils.translation import ugettext, ungettext, get_language +from django.contrib.humanize.templatetags.humanize import NaturalTimeFormatter -from jinja2 import contextfilter, contextfunction, Environment, Markup +from jinja2 import contextfunction, Environment def url(name, *args, current_app=None, **kwargs): @@ -30,23 +31,22 @@ def num_of_unread_messages(request): return user.received_messages.filter(was_read=False).count() -@contextfilter -def custom_strftime(ctx, date): +def custom_timedelta(tdelta): + return NaturalTimeFormatter.string_for(tdelta) + + +def custom_strftime(date): if not isinstance(date, datetime): return '' - return Markup('<script>document.write(new Date(%d).toLocaleString())</script>' % (date.timestamp() * 1000,)) + \ - Markup('<noscript>') + localize(localtime(date)) + Markup('</noscript>') + return localize(localtime(date)) -@contextfilter -def custom_strfdate(ctx, date): +def custom_strfdate(date): if not isinstance(date, datetime): return '' - ret = Markup('<script>document.write(new Date(%d).toLocaleDateString())</script><noscript>' % (date.timestamp() * 1000,)) - ret += localize(localdate(date)) + Markup('</noscript>') - return ret + return localize(localdate(date)) # set up an internal represenative for an unset variable as parameter for show_vars() @@ -104,6 +104,7 @@ def environment(**options): 'url': url, 'show_vars': show_vars, }) + env.filters['strftdelta'] = custom_timedelta env.filters['strftime'] = custom_strftime env.filters['strfdate'] = custom_strfdate env.install_gettext_callables(ugettext, ungettext, newstyle=True) diff --git a/src/plainui/jinja2/plainui/assemblies.html b/src/plainui/jinja2/plainui/assemblies.html index 24804daf9caab92da0a7c18d91457b8de5b4e862..cf90119f6b94bf6d6c2ab61809f40b921a655c87 100644 --- a/src/plainui/jinja2/plainui/assemblies.html +++ b/src/plainui/jinja2/plainui/assemblies.html @@ -41,7 +41,7 @@ <h2>{{ _("upcoming events") }}</h2> <div class="border border-tertiary p-6"> - {{ list_events.tiles(events_upcoming, is_favorite_events, is_scheduled_events ) }} + {{ list_events.slider(events_upcoming, is_favorite_events, is_scheduled_events ) }} </div> <hr class="my-5"> @@ -49,7 +49,7 @@ <h2>{{ _("recommended events") }}</h2> <div class="border border-tertiary p-6"> TODO: implement recommendation algo. Actual displayes faved events - {{ list_events.tiles(events_recommended, is_favorite_events, is_scheduled_events ) }} + {{ list_events.grid(events_recommended, is_favorite_events, is_scheduled_events ) }} </div> <hr class="my-5"> diff --git a/src/plainui/jinja2/plainui/assemblies_all.html b/src/plainui/jinja2/plainui/assemblies_all.html index 48b1e935c846a0065278780177ec571663d7354d..bca09a48550720cbc4d2b835cf7e2170e4c04736 100644 --- a/src/plainui/jinja2/plainui/assemblies_all.html +++ b/src/plainui/jinja2/plainui/assemblies_all.html @@ -6,13 +6,38 @@ {{ titleMacro.title(title=_("all assemblies"), share_url = url('plainui:assemblies_all', conf_slug=conf.slug), report_url = url('plainui:assemblies_all', conf_slug=conf.slug)) }} - <a href="{{ url('plainui:assemblies', conf_slug=conf.slug) }}"> - assemblies start seite - </a> - TODO: Filter assemblies + <div class="mb-2"> + <a href="{{ url('plainui:assemblies', conf_slug=conf.slug) }}" role="button" class="btn btn btn-secondary"> + {{ _("assemblies startseite") }} + </a> + {# actually we don't need a filter here. but maybe coming. + <div class="float-right"> + {% if qfilter != "official" %} + <a href="{{ url('plainui:assemblies_official', conf_slug=conf.slug) }}" role="button" class="btn btn-outline-secondary"> + {{ _("assemblies_official_only") }} + </a> + {% else %} + <a href=""{{ url('plainui:assemblies_all', conf_slug=conf.slug) }}"" role="button" class="btn btn-secondary"> + {{ _("assemblies_official_only") }} + </a> + {% endif %} - <div class="border border-tertiary p-6"> + {% if qfilter != "community" %} + <a href="{{ url('plainui:assemblies_community', conf_slug=conf.slug) }}" role="button" class="btn btn-outline-secondary"> + {{ _("assemblies_community_only") }} + </a> + {% else %} + <a href="{{ url('plainui:assemblies_all', conf_slug=conf.slug) }}" role="button" class="btn btn-secondary"> + {{ _("assemblies_community_only") }} + </a> + {% endif %} + </div> #} + </div> + <div class="border border-tertiary p-6 my-8"> {{ list_assm.list(assemblies, my_favorite_assemblies) }} </div> + + <hr class="border-top-0 my-11"> + {% endblock %} diff --git a/src/plainui/jinja2/plainui/assemblies_events.html b/src/plainui/jinja2/plainui/assemblies_events.html index cbf2f0e4c02fc6c1593b0c42da7ddf841cf7e20a..1bcc85cd3fa2933f43bcb1fcfe50edf9c6d19d07 100644 --- a/src/plainui/jinja2/plainui/assemblies_events.html +++ b/src/plainui/jinja2/plainui/assemblies_events.html @@ -5,22 +5,20 @@ {% block content %} {{ titleMacro.title(title=_("assembly events"), share_url = url('plainui:assemblies_events', conf_slug=conf.slug), - report_url = url('plainui:assemblies_events', conf_slug=conf.slug)) }} - <a href="{{ url('plainui:assemblies', conf_slug=conf.slug) }}"> - assemblies start seite - </a> + report_url = url('plainui:assemblies_events', conf_slug=conf.slug)) + }} - <h2>{{ _("running and upcoming events") }}</h2> + <h2>{{ _("Running and Upcoming Events") }}</h2> <div class="border border-tertiary p-6"> - {{ list_events.tiles(events_upcoming, is_favorite_events, is_scheduled_events ) }} + {{ list_events.slider(events_upcoming, is_favorite_events, is_scheduled_events ) }} </div> - <hr class="my-5 border-tertiary"> + <hr class="my-8"> - <h2>{{ _("all assemblies events") }}</h2> + <h2>{{ _("All Assemblies Events") }}</h2> <div class="border border-tertiary p-6"> {{ list_events.list(events_from_assemblies, is_favorite_events, is_scheduled_events ) }} </div> - + <hr class="border-top-0 my-11"> {% endblock %} diff --git a/src/plainui/jinja2/plainui/assembly.html b/src/plainui/jinja2/plainui/assembly.html index 1b5bc5c4de1f20697ff46dee2e8f0acb3ef921a8..5ca3ce53ba0bb0cec19491455e2e880b6b2db43e 100644 --- a/src/plainui/jinja2/plainui/assembly.html +++ b/src/plainui/jinja2/plainui/assembly.html @@ -17,17 +17,22 @@ , share_url = url('plainui:assembly', conf_slug=conf.slug, assembly_slug = assembly.slug) ) }} + <div class="d-flex mb-8"> + <a href="{{ url('plainui:assemblies', conf_slug=conf.slug) }}" role="button" class="mr-2 btn btn-secondary"> + assemblies start seite + </a> + <a class="mr-2 col-auto btn btn-secondary" href="{{ url('plainui:assemblies_all', conf_slug=conf.slug) }}" role="button">all assemblies</a> + </div> + {% if assembly.banner_image %} {{ imageMacro.image(image=assembly.banner_image.url, alt=assembly.banner_image.name, title=assembly.banner_image.name) }} {% endif %} {% if assembly.description != None and assembly.description != "" -%} {{- markdownMacro.markdown(markdown=assembly.description_html | safe) -}} - <hr> + <hr class="my-8 border-tertiary"> {% endif %} - <a class="col-auto" href="{{ url('plainui:assemblies_all', conf_slug=conf.slug) }}">all assemblies</a> - <a href="{{ url('plainui:assemblies', conf_slug=conf.slug) }}"> - assemblies start seite - </a> + + <div class="row"> <div class="col"> {{ tagboxMacro.tagbox(conf.slug, tags) }} @@ -37,30 +42,30 @@ </div> </div> - <hr class="my-5 border-tertiary"> + <hr class="my-8 border-tertiary"> - <h2>{{ _("assmebly rooms") }}</h2> + <h2>{{ _("assembly rooms") }}</h2> <div class="border border-tertiary p-6"> {{ list_rooms.list(assembly.rooms.all() ) }} </div> - <hr class="my-5 border-tertiary"> + <hr class="my-8 border-tertiary"> <h2>{{ _("assembly events") }}</h2> <div class="border border-tertiary p-6"> - {{ list_events.tiles(events, is_favorite_events, is_scheduled_events ) }} + {{ list_events.grid(events, is_favorite_events, is_scheduled_events ) }} </div> - <hr class="my-5 border-tertiary"> + <hr class="my-8 border-tertiary"> - <h2 class="">{{ _("selforganized sessions") }}</h2> + <h2 class="">{{ _("Self-organized Sessions") }}</h2> {% if can_create_sos -%} - <a class="btn btn-secondary mb-2 " href="{{ url('plainui:sos_new', conf_slug=conf.slug, assembly_slug=assembly.slug) }}">{{ _("new Selforganized Session") }}</a> + <a class="btn btn-secondary mb-2 " href="{{ url('plainui:sos_new', conf_slug=conf.slug, assembly_slug=assembly.slug) }}">{{ _("new Self-organized Session") }}</a> {%- endif %} - <div class="border border-tertiary p-6"> + <div class="border border-tertiary p-6 mb-8"> {{ list_events.list(sos, is_favorite_events, is_scheduled_events ) }} {% if sos_private %} - <h3 class="bg bg-info p-2 px-5 text-white text-center">non public Selforganized Sessions</h3> + <h3 class="mt-4 bg bg-info p-2 px-5 text-white text-center">non public Selforganized Sessions</h3> {{ list_events.list(sos_private, is_favorite_events, is_scheduled_events ) }} {% endif %} </div> diff --git a/src/plainui/jinja2/plainui/base.html b/src/plainui/jinja2/plainui/base.html index 9e64c25912c439cc80c364cef7c1ba88588f3b72..883f9922dcdb98650639f8627806347175569c89 100644 --- a/src/plainui/jinja2/plainui/base.html +++ b/src/plainui/jinja2/plainui/base.html @@ -1,12 +1,19 @@ {% import "plainui/components/logo.html" as logoMacro %} -{% import "plainui/components/title.html" as titleMacro with context%} +{% import "plainui/components/title.html" as titleMacro with context %} <!DOCTYPE html> -<html lang="{{ get_language() }}"> +<html lang="{{ get_language() }}" class="no-js"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="{{ static('plainui/%s.css' % (css_scope(),)) }}"> <title>{% block title %}{% endblock %}</title> + <meta name="viewport" content="width=device-width, initial-scale=1"> {% block head %}{% endblock %} + <script> + document.addEventListener('DOMContentLoaded', (e) => { + document.querySelector('html').classList.remove('no-js'); + document.querySelector('html').classList.add('js'); + }); + </script> </head> <body> <ul class="sr-only"> @@ -34,9 +41,9 @@ {% if get_messages(request) %} <div id="messages"> {% for message in get_messages(request) %} - <div class="alert{% if message.tags %} alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}" role="alert"> + <p class="alert my-8{% if message.tags %} alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %}{% endif %}" role="alert"> {{ message }} - </div> + </p> {% endfor %} </div> {% endif %} diff --git a/src/plainui/jinja2/plainui/ccc_events.html b/src/plainui/jinja2/plainui/ccc_events.html index 89538085e4cc5487165bbc23ce458c5c24018a43..b8d9d15c7c6f14245a25e341b9fb350d3afb1a36 100644 --- a/src/plainui/jinja2/plainui/ccc_events.html +++ b/src/plainui/jinja2/plainui/ccc_events.html @@ -3,7 +3,6 @@ {% import "plainui/components/livestream.html" as livestreamMacro %} {% import "plainui/components/list_events.html" as list_events with context %} - {% block title %}{{conf.name}} - {{ _("Curated Events") }}{% endblock %} {% block content %} {{ titleMacro.title(title=_("Curated Events"), diff --git a/src/plainui/jinja2/plainui/component_gallery.html b/src/plainui/jinja2/plainui/component_gallery.html index 44f5a06eee9cef14a2840cb7ff0af695363f8e33..2c5dec198b62d8984d1dda2a4141d50aecc9ab21 100644 --- a/src/plainui/jinja2/plainui/component_gallery.html +++ b/src/plainui/jinja2/plainui/component_gallery.html @@ -8,9 +8,7 @@ {% import "plainui/components/resourcesbox.html" as resboxMacro %} {% import "plainui/components/tagbox.html" as tagboxMacro %} {% import "plainui/components/listbox.html" as listboxMacro %} -{% import "plainui/components/tile.html" as tileMacro %} {% import "plainui/components/tile_board.html" as tileBoardMacro %} -{% import "plainui/components/tilesbox.html" as tilesboxMacro %} {% import "plainui/components/image.html" as imageMacro %} {% import "plainui/components/form_elements.html" as formElementsMacro %} {% import "plainui/components/three_cards.html" as threeCardsMacro %} @@ -25,11 +23,11 @@ {% set event1 = {"id": "1", "name": "event example 1", "slug": "event_slug1", "banner_image": {"url": image_url}, "schedule_start": time1, "schedule_end": time2, "schedule_duration": duration, "description": "Lorem Ipsum ...", "language": "en" } %} {% set event2 = {"id": "2", "name": "event example 2", "slug": "event_slug2", "banner_image": {"url": image_url}, "schedule_start": time1, "schedule_end": time2, "schedule_duration": duration, "description": "Lorem Ipsum ...", "language": "de" } %} -{% set events = [ event1, event2 ] %} +{% set events = [ event1, event2, event1, event2 ] %} {% set assembly1 = {"id": "1", "name": "assembly example 1", "slug": "assembly_slug1", "is_official": false,"banner_image": {"url": image_url}, "description": "Lorem Ipsum ..." } %} {% set assembly2 = {"id": "2", "name": "assembly example 2 - official", "slug": "assembly_slug2", "is_official": true, "banner_image": {"url": image_url}, "description": "Lorem Ipsum ..." } %} -{% set assemblies = [ assembly1, assembly2 ] %} +{% set assemblies = [ assembly1, assembly2, assembly1, assembly2, assembly1, assembly2 ] %} {% set room1 = {"id": "8387a222-536d-4bb6-b15a-9b3688fda7d9", "name": "room example 1", "room_type": "bbb", "capacity": 42, "occupants": 23 } %} {% set room2 = {"id": "8387a222-536d-4bb6-b15a-9b3688fda7a5", "name": "room example 2", "room_type": "workshop", "capacity": 42 } %} @@ -72,7 +70,7 @@ <dt class="h2 pb-3 mb-3 border-bottom">Slider</dt> <dd class="mb-10"> - {{ sliderMacro.slider(title="Slider") }} + {{ sliderMacro.slider(items=['eins', 'zwei', '3', '4']) }} </dd> <dt class="h2 pb-3 mb-3 border-bottom">Livestream</dt> @@ -116,6 +114,16 @@ {{ list_assm.list(assemblies, ['2']) }} </dd> + <dt class="h2 pb-3 mb-3 border-bottom">Assembly Grid</dt> + <dd class="mb-10"> + {{ list_assm.grid(assemblies, ['2']) }} + </dd> + + <dt class="h2 pb-3 mb-3 border-bottom">Assembly Slider</dt> + <dd class="mb-10"> + {{ list_assm.slider(assemblies, ['2']) }} + </dd> + <dt class="h2 pb-3 mb-3 border-bottom">Events List</dt> <dd class="mb-10"> {{ list_events.list(events, ['1'], ['2'] ) }} @@ -123,7 +131,12 @@ <dt class="h2 pb-3 mb-3 border-bottom">Events List Tiles</dt> <dd class="mb-10"> - {{ list_events.tiles(events, ['1'], ['2'] ) }} + {{ list_events.grid(events, ['1'], ['2'] ) }} + </dd> + + <dt class="h2 pb-3 mb-3 border-bottom">Events List Slider</dt> + <dd class="mb-10"> + {{ list_events.slider(events, ['1'], ['2'] ) }} </dd> <dt class="h2 pb-3 mb-3 border-bottom">Rooms List</dt> @@ -148,16 +161,6 @@ }) }} </dd> - <dt class="h2 pb-3 mb-3 border-bottom">TODO: Tile</dt> - <dd class="mb-10"> - {{tileMacro.tile({ - "name": "Item 1", - "image": { - "url": "https://picsum.photos/600/600" - } - }, "#") }} - </dd> - <dt class="h2 pb-3 mb-3 border-bottom">Three Cards</dt> <dd class="mb-10"> {{threeCardsMacro.three_cards(cards=[{ @@ -185,38 +188,6 @@ ]) }} </dd> - <dt class="h2 pb-3 mb-3 border-bottom">TODO: Tilesbox</dt> - <dd class="mb-10"> - {{ tilesboxMacro.tilesbox(conf_slug="rc3", items=[{ - "name": "Item 1", - "link": "#", - "image": { - "url": "https://picsum.photos/600/600" - } - }, - { - "name": "Item 2", - "link": "#", - "image": { - "url": "https://picsum.photos/600/600" - } - }, - { - "name": "Item 3", - "link": "#", - "image": { - "url": "https://picsum.photos/600/600" - } - }, - { - "name": "Item 4", - "link": "#", - "image": { - "url": "https://picsum.photos/600/600" - } - }]) }} - </dd> - <dt class="h2 pb-3 mb-3 border-bottom">Valid Form</dt> <dd class="mb-10"> {{ formElementsMacro.text(form_valid, 'text') }} diff --git a/src/plainui/jinja2/plainui/components/calendar.html b/src/plainui/jinja2/plainui/components/calendar.html index eff10d034620dd72e52833e17b59fa4e95161f93..fbd7af6555b8d0eb6b7f1c2b4c4d87fbfe750b99 100644 --- a/src/plainui/jinja2/plainui/components/calendar.html +++ b/src/plainui/jinja2/plainui/components/calendar.html @@ -7,7 +7,7 @@ {%- else -%} {% set time_steps = events.calendar_time_steps -%} {% set step_minutes = events.calendar_step_minutes -%} - <div class="rc3-fahrplan text-white font-headings"> + <div class="rc3-fahrplan text-white"> <div class="mr-4 rc3-fahrplan__timeline"> <h2 class="m-0 text-white rc3-fahrplan__timeline-title"></h2> {% for step in time_steps %} @@ -23,17 +23,21 @@ {% if entry.type == 'space' %} <figure class="m-0 p-0 rc3-fahrplan__room-space" style="height: {{ h(entry.minutes) }}"></figure> {% else %} - {% set color="primary" if entry.event.kind | safe == "official" else "secondary" %} - <a class="text-decoration-none"" href="{{ url('plainui:event', conf_slug=conf.slug, event_slug=entry.event.slug) }}" title="{{entry.event.name | safe}}"> + {% set color="primary" if entry.event.kind == "official" else "secondary" %} + <a class="text-decoration-none" href="{{ url('plainui:event', conf_slug=conf.slug, event_slug=entry.event.slug) }}" title="{{entry.event.name}}"> <figure class="p-1 my-0 mx-1 d-flex flex-column bg-{{color}} rc3-fahrplan__room-event" style="height: {{ h(entry.minutes) }}"> - <h2 class="m-0 text-white rc3-fahrplan__event_title">{{entry.event.name | safe}} + <h2 class="mb-1 text-white rc3-fahrplan__event_title">{{entry.event.name}} {% if entry.event.language %} - <span class="fs-medium font-weight-normal text-lowercase"> ({{entry.event.language | safe}})</span> + <span class="fs-medium font-sans-serif font-weight-normal text-lowercase"> ({{entry.event.language}})</span> {% endif %} </h2> - {% if entry.event.track_name %}<div class="fs-medium text-bold">{{entry.event.track_name | safe}}</div>{% endif %} + {% if entry.event.track_name %} + <p class="fs-medium font-weight-bold">{{entry.event.track_name}}</p> + {% endif %} {# <time datetime="" class="mr-4">{{ entry.event.schedule_start.strftime('%H:%M') }} - {{ entry.event.schedule_end.strftime('%H:%M') }}</time> #} - <div class="mt-auto fs-medium text-bold">{{ entry.event.speakers|join(', ', attribute='speaker_name') }}</div> + {% if entry.event.speakers %} + <p class="mt-auto fs-medium font-weight-bold">{{ entry.event.speakers|join(', ', attribute='speaker_name') }}</p> + {% endif %} </figure> </a> {% endif %} diff --git a/src/plainui/jinja2/plainui/components/event_info.html b/src/plainui/jinja2/plainui/components/event_info.html index 41de51424a200e0f97be69a83d7ead641f792d17..1731dadb9745d8f462ac7181043d5c9545244988 100644 --- a/src/plainui/jinja2/plainui/components/event_info.html +++ b/src/plainui/jinja2/plainui/components/event_info.html @@ -1,10 +1,12 @@ {% import "plainui/components/image.html" as imageMacro %} + {% macro eventInfo(event) -%} <div class="rc3-event-info"> - <h2> - {{ _("Event starts in") }} TODO: 00:00:00 - </h2> + <h2> + {{ _("Event starts in") }} + {{ event.schedule_start|strftdelta }} + </h2> <h2>{{ _("Event Information") }}</h2> {% if event.banner_image and event.banner_image.url %} {{ imageMacro.image(image=event.banner_image.url, alt=event.banner_image.name, title=event.banner_image.name) }} @@ -55,12 +57,6 @@ {% endif %} </dd> </dl> - <ul class="list-unstyled row m-0"> - <li>TODO: Report</li> - <li>Share</li> - <li>Favorites</li> - <li>Schedule</li> - </ul> </div> </div> {%- endmacro %} diff --git a/src/plainui/jinja2/plainui/components/form_elements.html b/src/plainui/jinja2/plainui/components/form_elements.html index af1b2865ace480be82815428c0ee51033b16e640..7ef15fca543b5bc22d52fd93ac9902965923149c 100644 --- a/src/plainui/jinja2/plainui/components/form_elements.html +++ b/src/plainui/jinja2/plainui/components/form_elements.html @@ -9,10 +9,8 @@ class="shadow-darkmorphism d-block font-headings p-3 text-center" for="id_{{name}}" > - <b> - {{ el.label }} - {%- if el.required %} *{% endif -%} - </b> + {{ el.label }} + {%- if el.required %} *{% endif -%} </label> </div> <div class="col-sm-12 col-lg-8"> @@ -29,10 +27,10 @@ {% endfor -%} > {% for err in form.errors.get(name, []) %} - <p class="d-block invalid-feedback font-headings">{{_(err)}}</p> + <p class="d-block invalid-feedback">{{_(err)}}</p> {% endfor %} {% if el.help_text %} - <div class="d-block font-headings fs-medium mt-2">{{ el.help_text | safe }}</div> + <div class="d-block fs-medium mt-2">{{ el.help_text | safe }}</div> {% endif %} </div> </div> @@ -51,10 +49,8 @@ class="shadow-darkmorphism font-headings d-block p-3 text-center" for="id_{{name}}" > - <b> - {{ el.label }} - {%- if el.required %} *{% endif -%} - </b> + {{ el.label }} + {%- if el.required %} *{% endif -%} </label> </div> <div class="col-sm-12 col-lg-8"> @@ -69,9 +65,9 @@ {% endfor -%} >{{ el.value() or '' }}</textarea> {% for err in form.errors.get(name, []) %} - <p class="d-block invalid-feedback font-headings">{{_(err)}}</p> + <p class="d-block invalid-feedback">{{_(err)}}</p> {% endfor %} - {% if el.help_text %}<div class="d-block fs-medium font-headings mt-2">{{ el.help_text | safe }}</div>{% endif %} + {% if el.help_text %}<div class="d-block fs-medium mt-2">{{ el.help_text | safe }}</div>{% endif %} </div> </div> {%- endmacro %} @@ -83,10 +79,8 @@ class="shadow-darkmorphism d-block font-headings p-3 text-center mb-0" for="id_{{name}}" > - <b> - {{ el.label }} - {%- if el.required %} *{% endif -%} - </b> + {{ el.label }} + {%- if el.required %} *{% endif -%} </p> </div> <div class="col-sm-12 col-lg-8"> @@ -101,12 +95,12 @@ {% if el.required %}required{%endif%} {% if el.disabled %}disabled{%endif%} > - <label for="id_{{name}}" class="form-check-label font-headings">{{ el.label }}</label> + <label for="id_{{name}}" class="form-check-label">{{ el.label }}</label> </div> {% for err in form.errors.get(name, []) -%} - <p class="d-block invalid-feedback font-headings">{{_(err)}}</p> + <p class="d-block invalid-feedback">{{_(err)}}</p> {% endfor %} - {%- if el.help_text %}<div class="d-block fs-medium font-headings mt-2">{{ el.help_text | safe }}</div>{% endif %} + {%- if el.help_text %}<div class="d-block fs-medium mt-2">{{ el.help_text | safe }}</div>{% endif %} </div> </div> </div> @@ -118,9 +112,9 @@ <label for="id_{{name}}" class="shadow-darkmorphism d-block font-headings p-3 text-center" - ><b> + > {{ el.label }} - </b></label> + </label> </div> <div class="col-sm-12 col-lg-8"> <div class="form-control-selectbox"> @@ -148,9 +142,9 @@ </svg> </div> {% for err in form.errors.get(name, []) %} - <p class="d-block invalid-feedback font-headings">{{_(err)}}</p> + <p class="d-block invalid-feedback">{{_(err)}}</p> {% endfor %} - {% if el.help_text %}<div class="d-block fs-medium font-headings mt-2">{{ el.help_text | safe }}</div>{% endif %} + {% if el.help_text %}<div class="d-block fs-medium mt-2">{{ el.help_text | safe }}</div>{% endif %} </div> </div> {%- endmacro %} diff --git a/src/plainui/jinja2/plainui/components/list_assemblies.html b/src/plainui/jinja2/plainui/components/list_assemblies.html index de0414df367788bfd0efb05cf4be6f671d459a8b..eee2141c40c167e8be572f3ea1876c802cb89ce8 100644 --- a/src/plainui/jinja2/plainui/components/list_assemblies.html +++ b/src/plainui/jinja2/plainui/components/list_assemblies.html @@ -8,7 +8,7 @@ {% macro list(assemblies, my_favorite_assemblies) -%} {% if assemblies %} - <ul class="list-unstyled"> + <ul class="list-unstyled mb-0"> {% for assembly in assemblies %} {{ list_el(assembly, faved=true if assembly.id | safe in my_favorite_assemblies ) }} {% endfor %} @@ -22,12 +22,91 @@ {% set link = url('plainui:assembly', conf_slug=conf.slug, assembly_slug=assembly.slug ) %} {% set color="plattform" if assembly.is_official else "assembly" %} - <li class="mt-3 d-flex border border-{{ color }} bg-gradient-{{ color }}-horizontal p-2 align-items-center font-headings"> - <a href="{{ link }}" title="{{ assembly.name | safe }}" class="text-white mr-auto"> - {{ assembly.name | safe }} + <li class="d-flex border border-{{ color }} bg-gradient-{{ color }}-horizontal p-2 align-items-center font-headings{% if not first %} mt-3{% endif %}"> + <a + href="{{ link }}" + title="{{ assembly.name }}" + class="text-white mr-auto"> + {{ assembly.name }} </a> {{ fbtns.share(link, color=color) }} {{ fbtns.fav(assembly.id, "assembly", faved, color=color) }} {{ fbtns.report(link, color=color) }} </li> {%- endmacro %} + +{% macro slider(assemblies, my_favorite_assemblies) -%} + {% if assemblies %} + <div class="rc3-slider"> + <ul class="rc3-slider__container row row-cols-1 row-cols-sm-2 row-cols-lg-3 flex-nowrap m-0 list-unstyled"> + {% for assembly in assemblies %} + <li class="rc3-slider__item col mb-2{% if loop.first %} pl-0{% endif %}{% if loop.last %} pr-0{% endif %}"> + {{ tile(assembly, faved=true if assembly.id | safe in my_favorite_assemblies) }} + </li> + {% endfor %} + </ul> + </div> + {% else %} + <p>{{_("No entries available.")}}</p> + {% endif %} +{%- endmacro %} + +{% macro grid(assemblies, my_favorite_assemblies) -%} + {% if assemblies %} + <ul class="row row-cols-1 row-cols-md-2 row-cols-xl-3 list-unstyled mb-0"> + {% for assembly in assemblies %} + <li class="col mb-3"> + {{ tile(assembly, faved=true if assembly.id | safe in my_favorite_assemblies) }} + </li> + {% endfor %} + </ul> + {% else %} + <p>{{_("No entries available.")}}</p> + {% endif %} +{%- endmacro %} + +{% macro tile(assembly, faved) -%} + {% set link = url('plainui:assembly', conf_slug=conf.slug, assembly_slug=assembly.slug ) %} + {% set color="plattform" if assembly.is_official else "assembly" %} + <article class="h-100 d-flex flex-column border border-{{ color }}-dark bg-gradient-{{ color }}-vertical"> + <a + href="{{ link }}" + class="text-decoration-none text-white" + title="{{ assembly.name | safe }}" + > + <figure class="mb-2"> + {% if assembly.banner_image %} + <img class="w-100 d-block" src="{{ assembly.banner_image.url }}" alt="{{ assembly.name | safe }}" title="{{ assembly.name | safe}}" /> + {% else %} + <img class="w-100 d-block" src="/static/plainui/img/rc3-logo-assembly.svg" alt="{{ assembly.name | safe }}" title="{{ assembly.name | safe }}" /> + {% endif %} + </figure> + <section class="m-2"> + <p class="mb-2 font-headings fs-medium"> + {{ _("Official Page") if assembly.is_official else _("Assembly Page") }} + </p> + {% if assembly.name %} + <h3 class="text-white h6 mb-2">{{ assembly.name }}</h3> + {% endif %} + + {% if assembly.description %} + <p class="fs-medium mb-2">{{ assembly.description[:120] + (assembly.description[120:] and '...') }}</p> + {% endif %} + </section> + </a> + + <footer class="mt-auto mr-2 mb-2 d-md-flex align-items-center"> + <ul class="ml-auto list-unstyled d-flex justify-content-end"> + <li> + {{ fbtns.share(link, color=color) }} + </li> + <li> + {{ fbtns.fav(assembly.id, "assembly", faved, color=color) }} + </li> + <li> + {{ fbtns.report(link, color=color) }} + </li> + </ul> + </footer> + </article> +{%- endmacro %} diff --git a/src/plainui/jinja2/plainui/components/list_events.html b/src/plainui/jinja2/plainui/components/list_events.html index b7e8c7ee0f46c20313237e6dd9f526fb5daffa61..40bd07bb19e7400d067830e295050e42a05c8934 100644 --- a/src/plainui/jinja2/plainui/components/list_events.html +++ b/src/plainui/jinja2/plainui/components/list_events.html @@ -4,15 +4,16 @@ conf csrf_input #} -{% import "plainui/components/function_btns.html" as fbtns with context%} +{% import "plainui/components/function_btns.html" as fbtns with context %} {% macro list(events, my_favorite_events, my_scheduled_events, assembly_slug=None, msg_none=_("No entries available.")) -%} {% if events %} - <ul class="list-unstyled"> + <ul class="list-unstyled mb-0"> {% for event in events %} {{ list_el( event, faved=true if event.id | safe in my_favorite_events, - scheduled=true if event.id | safe in my_scheduled_events ) }} + scheduled=true if event.id | safe in my_scheduled_events, + first=loop.first ) }} {% endfor %} </ul> {% else %} @@ -20,15 +21,15 @@ {% endif %} {%- endmacro %} -{% macro list_el(event, faved, scheduled) -%} +{% macro list_el(event, faved, scheduled, first) -%} {% set link = url('plainui:event', conf_slug=conf.slug, event_slug=event.slug ) %} - {% set color="plattform" if event.kind | safe == "official" else "assembly" %} - <li class="mt-3 d-flex border border-{{ color }} bg-gradient-{{ color }}-horizontal p-2 align-items-center font-headings"> - <a href="{{ link }}" title="{{ event.name | safe }}" class="text-white mr-auto"> - {{ event.name | safe }} + {% set color="plattform" if event.kind == "official" else "assembly" %} + <li class="d-flex border border-{{ color }} bg-gradient-{{ color }}-horizontal p-2 align-items-center font-headings{% if not first %} mt-3{% endif %}"> + <a href="{{ link }}" title="{{ event.name }}" class="text-white mr-auto"> + {{ event.name }} </a> - <time datetime="event.schedule_start" class="mr-2">{{ event.schedule_start.strftime('%x') }},</time> - <time datetime="" class="mr-4">{{ event.schedule_start.strftime('%H:%M') }} - {{ event.schedule_end.strftime('%H:%M') }}</time> + <time datetime="{{event.schedule_start}}" class="mr-2">{{ event.schedule_start.strftime('%x') }},</time> + <time class="mr-4">{{ event.schedule_start.strftime('%H:%M') }} - {{ event.schedule_end.strftime('%H:%M') }}</time> {%- if assembly and assembly.slug and (event.owner_id == request.user.id or can_manage_sos) -%} {{ icon_public(event.is_public) }} {{ fbtns.edit(url('plainui:sos_edit', conf_slug=conf.slug, assembly_slug=assembly.slug, event_slug=event.slug), color=color) }} @@ -40,13 +41,33 @@ </li> {%- endmacro %} -{% macro tiles(events, my_favorite_events, my_scheduled_events, msg_none=_("No entries available.")) -%} +{% macro slider(events, my_favorite_events, my_scheduled_events, msg_none=_("No entries available.")) -%} {% if events %} - <ul class="row list-unstyled"> + <div class="rc3-slider"> + <ul class="rc3-slider__container row row-cols-1 row-cols-sm-2 row-cols-lg-3 flex-nowrap m-0 list-unstyled"> + {% for event in events %} + <li class="rc3-slider__item col mb-2{% if loop.first %} pl-0{% endif %}{% if loop.last %} pr-0{% endif %}"> + {{ tile( event, + faved=true if event.id | safe in my_favorite_events, + scheduled=true if event.id | safe in my_scheduled_events) }} + </li> + {% endfor %} + </ul> + </div> + {% else %} + <p>{{ msg_none }}</p> + {% endif %} +{%- endmacro %} + +{% macro grid(events, my_favorite_events, my_scheduled_events, msg_none=_("No entries available.")) -%} + {% if events %} + <ul class="row row-cols-1 row-cols-md-2 row-cols-xl-3 list-unstyled mb-0"> {% for event in events %} - {{ tile( event, - faved=true if event.id | safe in my_favorite_events, - scheduled=true if event.id | safe in my_scheduled_events) }} + <li class="col mb-3"> + {{ tile( event, + faved=true if event.id | safe in my_favorite_events, + scheduled=true if event.id | safe in my_scheduled_events) }} + </li> {% endfor %} </ul> {% else %} @@ -56,34 +77,79 @@ {% macro tile(event, faved, scheduled) -%} {% set link = url('plainui:event', conf_slug=conf.slug, event_slug=event.slug ) %} - {% set color="plattform" if event.kind | safe == "official" else "assembly" %} - <li class="col-6 col-md-4 col-lg-3 mt-2 mr-2 border border-{{ color }}-dark bg-gradient-{{ color }}-vertical"> - <figure> - <a href="{{ link }}" title="{{ event.name | safe }}"> + {% set color="plattform" if event.kind == "official" else "assembly" %} + <article class="h-100 d-flex flex-column border border-{{ color }}-dark bg-gradient-{{ color }}-vertical"> + <a + href="{{ link }}" + class="text-decoration-none text-white" + title="{{ event.name }}" + > + <figure class="mb-2"> {% if event.banner_image %} - <img class="w-100 d-block" src="{{ event.banner_image.url }}" alt="{{ event.name | safe }}" title="{{ event.name | safe}}" /> + <img class="w-100 d-block" src="{{ event.banner_image.url }}" alt="{{ event.name }}" title="{{ event.name }}" /> {% else %} - <img class="w-100 d-block" src="/static/plainui/img/rc3_no_avater.png" alt="{{ event.name | safe }}" title="{{ event.name | safe}}" /> + <img class="w-100 d-block" src="{{ random_preview_image_url() }}" alt="{{ event.name }}" title="{{ event.name }}" /> {% endif %} - <time datetime="{{ event.schedule_start }}">{{ event.schedule_start.strftime('%x') }}</time> - <time datetime="{{ event.schedule_start.strftime('%H:%M') }}">{{ event.schedule_start.strftime('%H:%M') }}</time > - <time>{{ event.schedule_duration }}</time> - <p>{{ event.name | safe }}</p> - <p>TODO: speakers</p> - <p>{{ event.description[:100] + (event.description[100:] and '...') | safe }}</p> - </a> - <p>TODO: track</p> - {%- if assembly and assembly.slug and (event.owner_id == request.user.id or can_manage_sos) -%} - {{ icon_public(event.is_public) }} - {{ fbtns.edit(url('plainui:sos_edit', conf_slug=conf.slug, assembly_slug=assembly.slug, event_slug=event.slug), color=color) }} - {% endif %} + </figure> + <section class="m-2"> + {% if event.schedule_start or event.schedule_duration %} + <p class="mb-2 d-flex flex-row justify-content-between font-headings fs-medium justify-space-between text-center"> + <time datetime="{{ event.schedule_start }}"> + {{ event.schedule_start.strftime('%x') }} + </time> + <time> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clock" viewBox="0 0 16 16"> + <path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/> + <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/> + </svg> + {{ event.schedule_start.strftime('%H:%M') }} + </time> + <span>{{ event.schedule_duration }}</span> + </p> + {% endif %} + {% if event.name %} + <h3 class="text-white h6 mb-2">{{ event.name }}</h3> + {% endif %} + {% if event.speakers %} + <b class="f s-medium d-block mb-2">{{ event.speakers|join(', ', attribute='speaker_name') }}</b> + {% endif %} + {% if event.description %} + <p class="fs-medium mb-2">{{ event.description[:120] + (event.description[120:] and '...') }}</p> + {% endif %} + </section> + </a> - {{ fbtns.share(link, color=color) }} - {{ fbtns.schedule(event.id, scheduled, color=color) }} - {{ fbtns.fav(event.id, "event", faved, color=color) }} - {{ fbtns.report(link, color=color) }} - </figure> - </li> + <footer class="mt-auto mr-2 mb-2 d-md-flex align-items-center"> + {% if event.track_name %} + <p class="mb-2 mb-md-0 font-headings fs-medium"> + <small class="d-block {{ "text-tertiary" if event.kind == "official" else "text-secondary"}}"> + {{ _("%(kind)s Event on Track", kind="Official" if event.kind == "official" else "") }} + </small> + {{ event.track_name }} + </p> + {% endif %} + <ul class="ml-auto list-unstyled d-flex justify-content-end"> + {%- if assembly and assembly.slug and (event.owner_id == request.user.id or can_manage_sos) -%} + <li> + {{ icon_public(event.is_public) }} + {{ fbtns.edit(url('plainui:sos_edit', conf_slug=conf.slug, assembly_slug=assembly.slug, event_slug=event.slug), color=color) }} + </li> + {% endif %} + <li> + {{ fbtns.share(link, color=color) }} + </li> + <li> + {{ fbtns.schedule(event.id, scheduled, color=color) }} + </li> + <li> + {{ fbtns.fav(event.id, "event", faved, color=color) }} + </li> + <li> + {{ fbtns.report(link, color=color) }} + </li> + </ul> + </footer> + </article> {%- endmacro %} {% macro icon_public(is_public) -%} @@ -101,3 +167,8 @@ </svg> {% endif %} {%- endmacro %} + +{%- macro random_preview_image_url() -%} + {%- set imgs = [1,2,3,4,5,6,7] -%} + /static/plainui/img/rc3-assembly-event-0{{ imgs | random }}.png +{%- endmacro -%} diff --git a/src/plainui/jinja2/plainui/components/list_rooms.html b/src/plainui/jinja2/plainui/components/list_rooms.html index d67e26c8d4dc5160940b4c8fcb3982164b02a969..7d9b3e133e4965a980021528d1c71cbcc84dc022 100644 --- a/src/plainui/jinja2/plainui/components/list_rooms.html +++ b/src/plainui/jinja2/plainui/components/list_rooms.html @@ -44,20 +44,20 @@ {%- macro icon(type) -%} {% if type=="lecturehall" %} - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-square-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.496 6.033a.237.237 0 0 1-.24-.247C5.35 4.091 6.737 3.5 8.005 3.5c1.396 0 2.672.73 2.672 2.24 0 1.08-.635 1.594-1.244 2.057-.737.559-1.01.768-1.01 1.486v.105a.25.25 0 0 1-.25.25h-.81a.25.25 0 0 1-.25-.246l-.004-.217c-.038-.927.495-1.498 1.168-1.987.59-.444.965-.736.965-1.371 0-.825-.628-1.168-1.314-1.168-.803 0-1.253.478-1.342 1.134-.018.137-.128.25-.266.25h-.825zm2.325 6.443c-.584 0-1.009-.394-1.009-.927 0-.552.425-.94 1.01-.94.609 0 1.028.388 1.028.94 0 .533-.42.927-1.029.927z"/> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-easel-fill" viewBox="0 0 16 16"> + <path d="M8.473.337a.5.5 0 0 0-.946 0L6.954 2H2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h1.85l-1.323 3.837a.5.5 0 1 0 .946.326L4.908 11H7.5v2.5a.5.5 0 0 0 1 0V11h2.592l1.435 4.163a.5.5 0 0 0 .946-.326L12.15 11H14a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H9.046L8.473.337z"/> </svg> {% elif type=="bbb" %} - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-square-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.496 6.033a.237.237 0 0 1-.24-.247C5.35 4.091 6.737 3.5 8.005 3.5c1.396 0 2.672.73 2.672 2.24 0 1.08-.635 1.594-1.244 2.057-.737.559-1.01.768-1.01 1.486v.105a.25.25 0 0 1-.25.25h-.81a.25.25 0 0 1-.25-.246l-.004-.217c-.038-.927.495-1.498 1.168-1.987.59-.444.965-.736.965-1.371 0-.825-.628-1.168-1.314-1.168-.803 0-1.253.478-1.342 1.134-.018.137-.128.25-.266.25h-.825zm2.325 6.443c-.584 0-1.009-.394-1.009-.927 0-.552.425-.94 1.01-.94.609 0 1.028.388 1.028.94 0 .533-.42.927-1.029.927z"/> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat-quote-fill" viewBox="0 0 16 16"> + <path d="M16 8c0 3.866-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.584.296-1.925.864-4.181 1.234-.2.032-.352-.176-.273-.362.354-.836.674-1.95.77-2.966C.744 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7zM7.194 6.766a1.688 1.688 0 0 0-.227-.272 1.467 1.467 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 5.734 6C4.776 6 4 6.746 4 7.667c0 .92.776 1.666 1.734 1.666.343 0 .662-.095.931-.26-.137.389-.39.804-.81 1.22a.405.405 0 0 0 .011.59c.173.16.447.155.614-.01 1.334-1.329 1.37-2.758.941-3.706a2.461 2.461 0 0 0-.227-.4zM11 9.073c-.136.389-.39.804-.81 1.22a.405.405 0 0 0 .012.59c.172.16.446.155.613-.01 1.334-1.329 1.37-2.758.942-3.706a2.466 2.466 0 0 0-.228-.4 1.686 1.686 0 0 0-.227-.273 1.466 1.466 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 10.07 6c-.957 0-1.734.746-1.734 1.667 0 .92.777 1.666 1.734 1.666.343 0 .662-.095.931-.26z"/> </svg> {% elif type=="stage" %} - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-square-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.496 6.033a.237.237 0 0 1-.24-.247C5.35 4.091 6.737 3.5 8.005 3.5c1.396 0 2.672.73 2.672 2.24 0 1.08-.635 1.594-1.244 2.057-.737.559-1.01.768-1.01 1.486v.105a.25.25 0 0 1-.25.25h-.81a.25.25 0 0 1-.25-.246l-.004-.217c-.038-.927.495-1.498 1.168-1.987.59-.444.965-.736.965-1.371 0-.825-.628-1.168-1.314-1.168-.803 0-1.253.478-1.342 1.134-.018.137-.128.25-.266.25h-.825zm2.325 6.443c-.584 0-1.009-.394-1.009-.927 0-.552.425-.94 1.01-.94.609 0 1.028.388 1.028.94 0 .533-.42.927-1.029.927z"/> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-aspect-ratio-fill" viewBox="0 0 16 16"> + <path d="M0 12.5v-9A1.5 1.5 0 0 1 1.5 2h13A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5zM2.5 4a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0V5h2.5a.5.5 0 0 0 0-1h-3zm11 8a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-1 0V11h-2.5a.5.5 0 0 0 0 1h3z"/> </svg> {% elif type=="workshop" %} <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-screwdriver bi-hammer" viewBox="0 0 16 16"> - <path transform="translate(16), scale(-1, 1)"" fill-rule="evenodd" d="M0 1l1-1 3.081 2.2a1 1 0 0 1 .419.815v.07a1 1 0 0 0 .293.708L10.5 9.5l.914-.305a1 1 0 0 1 1.023.242l3.356 3.356a1 1 0 0 1 0 1.414l-1.586 1.586a1 1 0 0 1-1.414 0l-3.356-3.356a1 1 0 0 1-.242-1.023L9.5 10.5 3.793 4.793a1 1 0 0 0-.707-.293h-.071a1 1 0 0 1-.814-.419L0 1zm11.354 9.646a.5.5 0 0 0-.708.708l3 3a.5.5 0 0 0 .708-.708l-3-3z"/> + <path transform="translate(16), scale(-1, 1)" fill-rule="evenodd" d="M0 1l1-1 3.081 2.2a1 1 0 0 1 .419.815v.07a1 1 0 0 0 .293.708L10.5 9.5l.914-.305a1 1 0 0 1 1.023.242l3.356 3.356a1 1 0 0 1 0 1.414l-1.586 1.586a1 1 0 0 1-1.414 0l-3.356-3.356a1 1 0 0 1-.242-1.023L9.5 10.5 3.793 4.793a1 1 0 0 0-.707-.293h-.071a1 1 0 0 1-.814-.419L0 1zm11.354 9.646a.5.5 0 0 0-.708.708l3 3a.5.5 0 0 0 .708-.708l-3-3z"/> <path d="M9.812 1.952a.5.5 0 0 1-.312.89c-1.671 0-2.852.596-3.616 1.185L4.857 5.073V6.21a.5.5 0 0 1-.146.354L3.425 7.853a.5.5 0 0 1-.708 0L.146 5.274a.5.5 0 0 1 0-.706l1.286-1.29a.5.5 0 0 1 .354-.146H2.84C4.505 1.228 6.216.862 7.557 1.04a5.009 5.009 0 0 1 2.077.782l.178.129z"/> <path fill-rule="evenodd" d="M6.012 3.5a.5.5 0 0 1 .359.165l9.146 8.646A.5.5 0 0 1 15.5 13L14 14.5a.5.5 0 0 1-.756-.056L4.598 5.297a.5.5 0 0 1 .048-.65l1-1a.5.5 0 0 1 .366-.147z"/> </svg> @@ -70,8 +70,9 @@ <path fill-rule="evenodd" d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-5.784 6A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/> </svg> {% else %} - <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-square-fill" viewBox="0 0 16 16"> - <path fill-rule="evenodd" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.496 6.033a.237.237 0 0 1-.24-.247C5.35 4.091 6.737 3.5 8.005 3.5c1.396 0 2.672.73 2.672 2.24 0 1.08-.635 1.594-1.244 2.057-.737.559-1.01.768-1.01 1.486v.105a.25.25 0 0 1-.25.25h-.81a.25.25 0 0 1-.25-.246l-.004-.217c-.038-.927.495-1.498 1.168-1.987.59-.444.965-.736.965-1.371 0-.825-.628-1.168-1.314-1.168-.803 0-1.253.478-1.342 1.134-.018.137-.128.25-.266.25h-.825zm2.325 6.443c-.584 0-1.009-.394-1.009-.927 0-.552.425-.94 1.01-.94.609 0 1.028.388 1.028.94 0 .533-.42.927-1.029.927z"/> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bug-fill" viewBox="0 0 16 16"> + <path d="M4.978.855a.5.5 0 1 0-.956.29l.41 1.352A4.985 4.985 0 0 0 3 6h10a4.985 4.985 0 0 0-1.432-3.503l.41-1.352a.5.5 0 1 0-.956-.29l-.291.956A4.978 4.978 0 0 0 8 1a4.979 4.979 0 0 0-2.731.811l-.29-.956z"/> + <path d="M13 6v1H8.5v8.975A5 5 0 0 0 13 11h.5a.5.5 0 0 1 .5.5v.5a.5.5 0 1 0 1 0v-.5a1.5 1.5 0 0 0-1.5-1.5H13V9h1.5a.5.5 0 0 0 0-1H13V7h.5A1.5 1.5 0 0 0 15 5.5V5a.5.5 0 0 0-1 0v.5a.5.5 0 0 1-.5.5H13zm-5.5 9.975V7H3V6h-.5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 0-1 0v.5A1.5 1.5 0 0 0 2.5 7H3v1H1.5a.5.5 0 0 0 0 1H3v1h-.5A1.5 1.5 0 0 0 1 11.5v.5a.5.5 0 1 0 1 0v-.5a.5.5 0 0 1 .5-.5H3a5 5 0 0 0 4.5 4.975z"/> </svg> {% endif %} {%- endmacro -%} diff --git a/src/plainui/jinja2/plainui/components/livestream.html b/src/plainui/jinja2/plainui/components/livestream.html index df97f213f0cc262b2ed9a0bfadd0d00f79ef6a72..1f55752913c442718f470867eedc836c04ee56b4 100644 --- a/src/plainui/jinja2/plainui/components/livestream.html +++ b/src/plainui/jinja2/plainui/components/livestream.html @@ -1,4 +1,3 @@ -{% import "plainui/components/slider.html" as sliderMacro %} {% macro livestream(title, event, upcomingEvents, recommendedEvents) -%} <div> <h2 class="mb-5">{{ title }}</h2> @@ -15,9 +14,9 @@ </div> </div> <div class="col-7"> - {{ sliderMacro.slider(title="Upcoming Events") }} + TODO: Slider Upcoming Events <hr class="border-top-0 py-1" /> - {{ sliderMacro.slider(title="Recommended Events") }} + TODO: Slider Recommended Events </div> </div> </div> diff --git a/src/plainui/jinja2/plainui/components/slider.html b/src/plainui/jinja2/plainui/components/slider.html index d5a3b138cb7fc9a141eed7582753e7023c8dd7ca..0558439e0bda24d99540f4cf77456b30bbad0bdd 100644 --- a/src/plainui/jinja2/plainui/components/slider.html +++ b/src/plainui/jinja2/plainui/components/slider.html @@ -1,8 +1,13 @@ -{% macro slider(title, items) -%} - <section class="rc3-slider"> - <h2>{{ title }}</h2> - <div class="border border-tertiary p-6"> - TODO: Slider - </div> +{% macro slider(items) -%} + <section class="rc3-slider border border-tertiary p-6"> + {% if items %} + <ul class="rc3-slider__container row row-cols-1 row-cols-sm-2 row-cols-lg-3 flex-nowrap mb-0 list-unstyled"> + {% for item in items %} + <li class="rc3-slider__item col"> + {{ item }} + </li> + {% endfor %} + </ul> + {% endif %} </section> {%- endmacro %} diff --git a/src/plainui/jinja2/plainui/components/tagbox.html b/src/plainui/jinja2/plainui/components/tagbox.html index 89570c45d04e71262e3c8bdfb7ac81f13d9872ed..d966409259fe34665793b3fd868b1391efa7bce2 100644 --- a/src/plainui/jinja2/plainui/components/tagbox.html +++ b/src/plainui/jinja2/plainui/components/tagbox.html @@ -1,9 +1,9 @@ {% macro tagbox(conf_slug, tags) -%} <div> <h4 class="h2 mb-5">{{_("Tags")}}</h4> - <ul class="border border-tertiary p-6 list-unstyled mb-0 d-flex flex-row flex-wrap justify-content-center align-items-center"> + <ul class="border border-tertiary px-6 pt-6 pb-5 list-unstyled mb-0 d-flex flex-row flex-wrap justify-content-center align-items-center"> {%- for tag in tags %} - <li class="pr-2"> + <li class="pr-2 mb-2"> <a href="{{ url('plainui:tag', conf_slug=conf_slug, tag_slug=tag.tag.slug) }}" class="btn btn-tag-secondary">{{tag.tag.slug}}</a> </li> {% endfor -%} diff --git a/src/plainui/jinja2/plainui/components/tile.html b/src/plainui/jinja2/plainui/components/tile.html deleted file mode 100644 index 32a672ae16748fdc0a89bbd85dd068595be35af2..0000000000000000000000000000000000000000 --- a/src/plainui/jinja2/plainui/components/tile.html +++ /dev/null @@ -1,19 +0,0 @@ -{% macro tile(item, link) -%} - <figure> - <a href="{{ link }}" class="d-block bg-secondary text-white text-center p-2 mr-auto"> - {{item.name}} - </a> - <img class="w-100 d-block" src="{{ item.image.url }}" alt="{{ item.name }}" title="{{ item.name }}" /> - <a href="#" class="mr-2 btn btn-secondary">+ schedule</a> - <a href="#" class="mr-2 btn btn-secondary"> - <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-heart" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z"/> - </svg> - </a> - <a href="#" class="btn btn-secondary"> - <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-share" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" d="M13.5 1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM11 2.5a2.5 2.5 0 1 1 .603 1.628l-6.718 3.12a2.499 2.499 0 0 1 0 1.504l6.718 3.12a2.5 2.5 0 1 1-.488.876l-6.718-3.12a2.5 2.5 0 1 1 0-3.256l6.718-3.12A2.5 2.5 0 0 1 11 2.5zm-8.5 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm11 5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/> - </svg> - </a> - </figure> -{%- endmacro %} diff --git a/src/plainui/jinja2/plainui/components/tile_board.html b/src/plainui/jinja2/plainui/components/tile_board.html index e2671118681544394c592f0ebdbf7fe9324b527f..539fc9c38e125d2df2838e57fc3dc695d2a6ec8a 100644 --- a/src/plainui/jinja2/plainui/components/tile_board.html +++ b/src/plainui/jinja2/plainui/components/tile_board.html @@ -19,7 +19,7 @@ {% endif %} {% if item.text %} - {{ markdownMacro.markdown_plain(item.text | truncate( 400, false, '...', 10) | safe, "rc3-tile-board__body card-body") }} + {{ markdownMacro.markdown_plain(item.text | truncate( 400, false, '...', 10), "rc3-tile-board__body card-body") }} {% endif %} {% if item.owner_name or item.timestamp %} diff --git a/src/plainui/jinja2/plainui/components/tilesbox.html b/src/plainui/jinja2/plainui/components/tilesbox.html deleted file mode 100644 index b5de2ec88c0b5b6ce52b447ae92fc58e53cba0d1..0000000000000000000000000000000000000000 --- a/src/plainui/jinja2/plainui/components/tilesbox.html +++ /dev/null @@ -1,22 +0,0 @@ -{% import "plainui/components/tile.html" as tileMacro %} - -{% macro tilesbox(conf_slug, items, item_type="default" ) -%} - {% if items %} - <ul class="row list-unstyled"> - {% for item in items %} - {% if item_type == "event" %} - {% set link = url("plainui:event", conf_slug=conf_slug, event_slug=item.slug ) %} - {% elif item_type == "assembly" %} - {% set link = url('plainui:assembly', conf_slug=conf_slug, assembly_slug=item.slug ) %} - {% else %} - {% set link = item.link %} - {% endif %} - <li class="col-6 col-md-4 col-lg-3 mt-2"> - {{tileMacro.tile(item, link) }} - </li> - {% endfor %} - </ul> - {% else %} - <p>{{_("No entries available.")}}</p> - {% endif %} -{%- endmacro %} diff --git a/src/plainui/jinja2/plainui/dereferrer.html b/src/plainui/jinja2/plainui/dereferrer.html index 71f5420a740a1e8cfb4654afd6613901a5d9fe1b..e070c78871697cc631c5d8aaba933c4e6402190c 100644 --- a/src/plainui/jinja2/plainui/dereferrer.html +++ b/src/plainui/jinja2/plainui/dereferrer.html @@ -2,16 +2,27 @@ {% block title %}Dereferrer{% endblock %} {% block content %} -<article class="row justify-content-center align-items-center my-8"> - <section class="p-5 border border-primary text-center col-lg-8 col-xl-6"> - <h1 class="mb-3">{{ _("Hey") }}</h1> +<article class="d-flex justify-content-center align-items-center my-11"> + <section class="p-5 border border-primary text-center mw-810"> + <h1>{{ _("Hey") }}</h1> <p>{{ _("You are leaving the »RC3-area«. For external sites, streams and applications the actual owners are completely and solely responsible regarding data protection, copyright, youth protection, etc.!")}}</p> - <div class="mt-5"> - <a href='#' class="btn btn-lg btn-secondary">Link FEHLT</a> - <a href='{{ dst }}' class="btn btn-lg btn-primary" rel="external,noreferrer">{{ _("External Link") }}</a> - </div> + <ul class="row mt-5 mb-0 list-unstyled"> + <li class="col d-js-only"> + <a + href="javascript:history.back()" + class="btn btn-xl btn-block btn-secondary" + title="{{ _("Back") }}"> + {{ _("Back") }} + </a> + </li> + <li class="col"> + <a href='{{ dst }}' class="btn btn-xl btn-block btn-primary external" rel="external,noreferrer"> + {{ _("External Link") }} + </a> + </li> + </ul> </section> </article> diff --git a/src/plainui/jinja2/plainui/event.html b/src/plainui/jinja2/plainui/event.html index 71d254f81cdb795f84d5913a00b67ce6b94d1944..af73b7fc8b26280b17c6e34a83042cee5ed8f017 100644 --- a/src/plainui/jinja2/plainui/event.html +++ b/src/plainui/jinja2/plainui/event.html @@ -1,18 +1,16 @@ {% import "plainui/components/markdown.html" as markdownMacro %} {% import "plainui/components/event_info.html" as eventInfoMacro %} -{% import "plainui/components/slider.html" as sliderMacro %} {% import "plainui/components/title.html" as titleMacro %} {% import "plainui/components/tagbox.html" as tagboxMacro %} {% import "plainui/components/resourcesbox.html" as resboxMacro %} {% import "plainui/components/integrations.html" as integrations %} {% import "plainui/components/list_events.html" as list_events with context %} -{% block head %} - <script src="{{ static('plainui/js/player.js') }}" /></script> -{% endblock %} - {% extends "plainui/base.html" %} {% block title %}{{conf.name}} - Event {{event.name}}{% endblock %} +{% block head %} + <script src="{{ static('plainui/js/player.js') }}"></script> +{% endblock %} {% block content %} <article class="mt-10"> {{ titleMacro.title(event.name, @@ -26,7 +24,7 @@ ) }} {% if assembly.slug and assembly.name %} - <p>hosted by: <a href="{{ url("plainui:assembly", conf_slug=conf.slug, assembly_slug=assembly.slug) }}">{{ assembly.name | safe }}</a></p> + <p>hosted by: <a href="{{ url("plainui:assembly", conf_slug=conf.slug, assembly_slug=assembly.slug) }}">{{ assembly.name }}</a></p> {% endif %} {{ eventInfoMacro.eventInfo(event=event) }} {% if event.kind == 'official' %} @@ -48,7 +46,7 @@ <h2>{{ _("upcoming events") }}</h2> <div class="border border-tertiary p-6"> - {{ list_events.tiles(events_upcoming, is_favorite_events, is_scheduled_events ) }} + {{ list_events.slider(events_upcoming, is_favorite_events, is_scheduled_events ) }} </div> </article> diff --git a/src/plainui/jinja2/plainui/fahrplan.html b/src/plainui/jinja2/plainui/fahrplan.html index c31a8dea268f38d33c524facf5ae6dfa4a6c0da1..b7a5351cef6d88907915aa23bfa3d3604b1e7d8f 100644 --- a/src/plainui/jinja2/plainui/fahrplan.html +++ b/src/plainui/jinja2/plainui/fahrplan.html @@ -29,21 +29,21 @@ {% if track %}<input type="hidden" name="track" value="{{track.slug}}">{% endif %} <div class="row justify-content-md-center"> - <button type="submit" name="set" value="mlist" class="m-2 btn btn-primary {{ 'acitve' if mode == 'list' }}">{{ _("view as list") }}</button> - <button type="submit" name="set" value="mcalendar" class="m-2 btn btn-primary {{ 'active' if mode == 'calendar' }}">{{ _("view as calendar") }}</button> + <button type="submit" name="set" value="mlist" class="m-2 btn {{ 'btn-primary active' if mode == 'list' else 'btn-secondary'}}">{{ _("view as list") }}</button> + <button type="submit" name="set" value="mcalendar" class="m-2 btn {{ 'btn-primary active' if mode == 'calendar' else 'btn-secondary'}}">{{ _("view as calendar") }}</button> </div> <div class="row justify-content-md-center"> - <button type="submit" name="set" value="fday" class="m-2 btn btn-primary {{ 'active' if show_day_filters }}">{{ _("by day") }}</button> - <button type="submit" name="set" value="ftrack" class="m-2 btn btn-primary {{ 'active' if show_track_filters }}">{{ _("by track") }}</button> - <button type="submit" name="set" value="curated" class="m-2 btn btn-primary {{ 'active' if curated }}">{{ _("curated only") }}</button> + <button type="submit" name="set" value="fday" class="m-2 btn {{ 'btn-primary active' if show_day_filters else 'btn-secondary'}}">{{ _("by day") }}</button> + <button type="submit" name="set" value="ftrack" class="m-2 btn {{ 'btn-primary active' if show_track_filters else 'btn-secondary'}}">{{ _("by track") }}</button> + <button type="submit" name="set" value="curated" class="m-2 btn {{ 'btn-primary active' if curated else 'btn-secondary'}}">{{ _("curated only") }}</button> {# Assembly events are displayed on assmbly page. filter here by assembly will mean display serveral hundred assemblies. leave for the future - <button type="submit" name="set" value="fassembly" class="m-2 btn btn-primary {{ 'active' if show_assembly_filters }}">Assembly</button> #} + <button type="submit" name="set" value="fassembly" class="m-2 btn {{ 'btn-primary active' if show_assembly_filters else 'btn-secondary'}}">Assembly</button> #} </div> {% if show_day_filters %} <div class="row justify-content-md-center"> {% for n in range(days) %} - <button type="submit" name="set" value="d{{n if n != day else ''}}" class="m-2 btn btn-primary {{ 'active' if n == day }}">{{ _("Day %(n)s", n=n) }}</button> + <button type="submit" name="set" value="d{{n if n != day else ''}}" class="m-2 btn {{ 'btn-primary active' if n == day else 'btn-secondary'}}">{{ _("Day %(n)s", n=n) }}</button> {%- endfor %} </div> {% endif %} @@ -60,7 +60,7 @@ {% if show_track_filters %} <div class="row justify-content-md-center"> {% for t in tracks %} - <button type="submit" name="set" value="t{{t.slug if t != track else ''}}" class="m-2 btn btn-primary {{ 'active' if t == track }}">{{ t.name }}</button> + <button type="submit" name="set" value="t{{t.slug if t != track else ''}}" class="m-2 btn {{ 'btn-primary active' if t == track else 'btn-secondary'}}">{{ t.name }}</button> {%- endfor %} </div> {% endif %} diff --git a/src/plainui/jinja2/plainui/header.html b/src/plainui/jinja2/plainui/header.html index 2fbe837ef4f7fd77b1b190b69e43449e5dc705bb..3d421a8372f0bf6527d283cc9e9bb59ad1714dc2 100644 --- a/src/plainui/jinja2/plainui/header.html +++ b/src/plainui/jinja2/plainui/header.html @@ -1,6 +1,7 @@ {% set view_name = request.resolver_match.view_name %} +{% set scope = scope|default('plattform') %} <header id="header" class="rc3-header container mb-3 mt-6"> - {{ logoMacro.logo(static('plainui/img/rc3-logo-' + scope|default('plattform') + '.svg'), url('plainui:index', conf_slug=conf.slug), conf.name + " logo", conf.name + " logo") }} + {{ logoMacro.logo(static('plainui/img/rc3-logo-' + scope + '.svg'), url('plainui:index', conf_slug=conf.slug), conf.name + " logo", conf.name + " logo") }} <nav class="rc3-header__main"> <a class="btn {{ 'btn-primary' if view_name == 'plainui:world' else 'btn-secondary' }} rc3-header__main-linkbox" href="{{ url('plainui:world', conf_slug=conf.slug ) }}" title="{{ _("world") }}"> {{ _("world") }} @@ -24,8 +25,15 @@ </form> </nav> <div class="rc3-header__additional"> - <a class="btn rc3-header__additional-linkbox {{ 'btn-primary' if view_name == 'plainui:userprofile' else 'btn-secondary' }}" href="{{ url('plainui:userprofile', conf_slug=conf.slug) }}" title="{{ _("Profile") }}"> - {{ _("Profile") }} + <a class="btn rc3-header__additional-linkbox {{ 'btn-primary' if view_name == 'plainui:userprofile' else 'btn-secondary' }}" href="{{ url('plainui:userprofile', conf_slug=conf.slug) }}" title="{{ _('Profile') }}"> + {% if user.avatar_url != None %} + {# TODO: implement real avatar if set #} + {% set av_url = '/static/plainui/img/rc3-no-avatar-' + scope + '.jpeg' %} + {% else %} + {% set av_act = '-active' if view_name == 'plainui:userprofile' else '' %} + {% set av_url = '/static/plainui/img/rc3-no-avatar-' + scope + av_act + '.jpeg' %} + {% endif %} + <img class="rc3-image__img w-100 d-block" src="{{ av_url }}" alt="{{ _('Profile') }}" title="{{ _('Profile') }}" /> </a> <a class="btn rc3-header__additional-linkbox {{ 'btn-primary' if view_name == 'plainui:my_fahrplan' else 'btn-secondary' }}" href="{{ url('plainui:my_fahrplan', conf_slug=conf.slug) }}"> {{ _("My Plan") }} @@ -47,7 +55,7 @@ <a class="btn btn-block rc3-header__additional-linkbox {{ 'btn-primary' if view_name == 'plainui:personal_message' else 'btn-secondary' }}" href="{{ url('plainui:personal_message', conf_slug=conf.slug) }}" title="{{ _("Messages") }}"> {{ _("Mess ages") -}} {% set num_unread = num_of_unread_messages(request) -%} - {% if num_unread %}<span class="rc3-header__additional-linkbox-badge badge badge-info border border-primary">{{num_unread}}</span>{% endif %} + {% if num_unread %}<span class="rc3-header__additional-linkbox-badge bg-info badge badge-info border border-primary">{{num_unread}}</span>{% endif %} </a> <a class="btn rc3-header__additional-linkbox {{ 'btn-primary' if view_name == 'plainui:fahrplan' else 'btn-secondary' }}" href="{{ url('plainui:fahrplan', conf_slug=conf.slug) }}" title="{{ _("Fahrplan") }}"> {{ _("Fahr plan") }} diff --git a/src/plainui/jinja2/plainui/landing.html b/src/plainui/jinja2/plainui/landing.html index d63ccdc6c494d641c4b2b8a482ec6a4cb2aed525..bc38df408c224f89ac84b861e63940d808834bda 100644 --- a/src/plainui/jinja2/plainui/landing.html +++ b/src/plainui/jinja2/plainui/landing.html @@ -44,9 +44,9 @@ <a href="https://tickets.events.ccc.de/rc3/" target="_blank" - rel="noreferrer" + rel="external, noreferrer" title="{{ _("ticket") }}" - class="btn btn-secondary btn-box" + class="btn btn-secondary btn-box external" > {{ _("ticket") }} </a> diff --git a/src/plainui/jinja2/plainui/login.html b/src/plainui/jinja2/plainui/login.html index 8bd66a0663ec151222576e9fea2bc2641a6f32c9..4792e98dd358d89c9962de135e03efdb3a263355 100644 --- a/src/plainui/jinja2/plainui/login.html +++ b/src/plainui/jinja2/plainui/login.html @@ -22,7 +22,7 @@ {{ formElementsMacro.password(form, 'password') }} <p class="my-5 font-headings text-white text-center">{{ _("By logging in you accept our use of cookies to store your user session.") }}</p> - <ul class="row row row-cols-1 row-cols-lg-3 list-unstyled"> + <ul class="row row-cols-1 row-cols-lg-3 list-unstyled"> <li class="col mb-3 mb-lg-0"> <a href="{{ url('plainui:password_reset', conf_slug=conf.slug) }}" class="btn btn-xl btn-block btn-secondary" title="{{ _("Reset Password") }}"> {{ _("Reset Password") }} @@ -33,7 +33,7 @@ href="https://tickets.events.ccc.de/rc3/" target="_blank" rel="noreferrer" - class="btn btn-xl btn-block btn-secondary" + class="btn btn-xl btn-block btn-secondary external" title="{{ _("New Ticket") }}" > {{ _("New Ticket") }} diff --git a/src/plainui/jinja2/plainui/password_change.html b/src/plainui/jinja2/plainui/password_change.html index fe5a0326fca4473ee17cc17368091b4a7af77e9f..571eedb99a46f06557d00c70ee64e3d731437ed0 100644 --- a/src/plainui/jinja2/plainui/password_change.html +++ b/src/plainui/jinja2/plainui/password_change.html @@ -19,13 +19,13 @@ {{ form_elements.password(form, 'new_password1') }} {{ form_elements.password(form, 'new_password2') }} - {{ form_elements.errors(form) }} - <ul class="row list-unstyled"> <li class="col-12 ml-md-auto col-md-8"> <button type="submit" class="btn btn-xl btn-block btn-primary">{{ _("Change Password") }}</button> </li> </ul> + + {{ form_elements.errors(form) }} </form> {% endblock %} diff --git a/src/plainui/jinja2/plainui/password_reset_confirm.html b/src/plainui/jinja2/plainui/password_reset_confirm.html index 5081c02d96f963ff99e9165ab60673eff5744b57..0d880e319eeead2d04d74828b050e7f2b63c2b3a 100644 --- a/src/plainui/jinja2/plainui/password_reset_confirm.html +++ b/src/plainui/jinja2/plainui/password_reset_confirm.html @@ -18,8 +18,6 @@ {{ form_elements.password(form, 'new_password1') }} {{ form_elements.password(form, 'new_password2') }} - {{ form_elements.errors(form) }} - <ul class="row row-cols-md-2 list-unstyled"> <li class="col"> <a @@ -34,6 +32,9 @@ <button type="submit" class="btn btn-xl btn-block btn-primary" form="login-change">{{ _("Change Password") }}</button> </li> </ul> + + {{ form_elements.errors(form) }} + </form> {% endblock %} diff --git a/src/plainui/jinja2/plainui/personal_message_list.html b/src/plainui/jinja2/plainui/personal_message_list.html index 54e9c55c38540a1b92f3824320c5e8e475f93963..f1a697dadf7691f1e3ee70d07c375636f0d946ce 100644 --- a/src/plainui/jinja2/plainui/personal_message_list.html +++ b/src/plainui/jinja2/plainui/personal_message_list.html @@ -1,37 +1,84 @@ +{% import "plainui/components/function_btns.html" as fbtns with context %} + {% extends "plainui/base.html" %} {% block title %}{{conf.name}} - {{ _("Personal Messages") }}{% endblock %} {% block content %} -<div> - <div class="row justify-content-center"> - <div class="col-auto"><h1>{{ _("Personal Messages") }}</h1></div> - <div class="w-100"></div> - <div class="col-auto"> - <a href="{{ url('plainui:personal_message', conf_slug=conf.slug) }}">{{_("Inbox")}}</a> - <a href="{{ url('plainui:personal_message_outbox', conf_slug=conf.slug) }}">{{_("Outbox")}}</a> +{{ titleMacro.title(_("Personal Messages") ) }} + +<div class="mb-2"> + <a role="button" class="btn btn-primary mb-1" href="{{ url('plainui:personal_message', conf_slug=conf.slug) }}">{{_("Inbox")}}</a> + <a role="button" class="btn btn-primary mb-1" href="{{ url('plainui:personal_message_outbox', conf_slug=conf.slug) }}">{{_("Outbox")}}</a> + <a role="button" class="btn btn-primary mb-1" href="{{ url('plainui:personal_message_send', conf_slug=conf.slug) }}">{{_("New PM")}}</a> +</div> + +<div class="border border-tertiary p-6 text-light mx-0"> + <h2 class="w-100 bg bg-info p-2 px-5 h3 text-white text-center">{% if not sent_mode %}{{ _("Received Messages") }} {% else %}{{ _("Sent Messages") }}{% endif %}</h2> + <form method="POST" action="{{ url('plainui:personal_message_delete', conf_slug=conf.slug) }}"> + {{ csrf_input }} + <table class="table"> + <thead> + <tr> + <th scope="col">{% if not sent_mode %}{{ _("messages_from") }} {% else %}{{ _("messages_to") }}{% endif %}</th> + <th scope="col">{{ _("messages_subject") }}</th> + <th scope="col">{{ _("messages_date") }}</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + + {%- for msg in msgs %} + <tr> + <td> + {% if not sent_mode %} + {% if msg.was_read %} + <span class="ml-2" title="{{_("messages_was_read")}}"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-envelope-open" viewBox="0 0 16 16"> + <path d="M8.47 1.318a1 1 0 0 0-.94 0l-6 3.2A1 1 0 0 0 1 5.4v.818l5.724 3.465L8 8.917l1.276.766L15 6.218V5.4a1 1 0 0 0-.53-.882l-6-3.2zM15 7.388l-4.754 2.877L15 13.117v-5.73zm-.035 6.874L8 10.083l-6.965 4.18A1 1 0 0 0 2 15h12a1 1 0 0 0 .965-.738zM1 13.117l4.754-2.852L1 7.387v5.73zM7.059.435a2 2 0 0 1 1.882 0l6 3.2A2 2 0 0 1 16 5.4V14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V5.4a2 2 0 0 1 1.059-1.765l6-3.2z"/> + </svg> + </span> + {% else %} + <span class="ml-2" title="{{_("messages_is_new")}}"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-envelope-fill" viewBox="0 0 16 16"> + <path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414.05 3.555zM0 4.697v7.104l5.803-3.558L0 4.697zM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586l-1.239-.757zm3.436-.586L16 11.801V4.697l-5.803 3.546z"/> + </svg> + </span> + {% endif %} + <a href="{{ url('plainui:personal_message_send_to', conf_slug=conf.slug, recipient=msg.sender_name) }}"> + {{msg.sender_name}} + </a> + {% else %} + <a href="{{ url('plainui:personal_message_send_to', conf_slug=conf.slug, recipient=msg.recipient_name) }}"> + {{msg.recipient_name}} + </a> + {% endif %} - <a href="{{ url('plainui:personal_message_send', conf_slug=conf.slug) }}">{{_("New PM")}}</a> - <form method="POST" action="{{ url('plainui:personal_message_delete', conf_slug=conf.slug) }}"> - {{ csrf_input }} - <ul> - {%- for msg in msgs %} - <li> - {{msg.timestamp | strftime}} - <a href="{{ url('plainui:personal_message_show', conf_slug=conf.slug, msg_id=msg.id) }}">{{msg.subject}}</a> - {% if not sent_mode %} - by {{msg.sender_name}} - {% if msg.was_read %}[Read]{% endif %} - {% if msg.has_responded %}[Responded]{% endif %} - {% if msg.flagged_for_abuse %}[Flagged]{% endif %} - {% else %} - to {{msg.recipient_name}} - {%endif %} - <button class="btn btn-danger" type="submit" name="id" value="{{msg.id}}">{{ _("Delete") }}</button> - </li> - {%- endfor %} - </ul> - {{ msgs | length }} of {{ total }} - </form> - </div> - </div> + </td> + <td> + {% if not sent_mode %} + {% if msg.has_responded %} + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-reply-fill" viewBox="0 0 16 16"> + <path transform="translate(16), scale(-1, 1)" d="M9.079 11.9l4.568-3.281a.719.719 0 0 0 0-1.238L9.079 4.1A.716.716 0 0 0 8 4.719V6c-1.5 0-6 0-7 8 2.5-4.5 7-4 7-4v1.281c0 .56.606.898 1.079.62z"/> + </svg> + {% endif %} + {% endif %} + <a href="{{ url('plainui:personal_message_show', conf_slug=conf.slug, msg_id=msg.id) }}">{{msg.subject}}</a></td> + <td>{{msg.timestamp | strftime}}</td></a> + <td> + {# {% not implemented? should color the flag button instead! if msg.flagged_for_abuse %}[{ _("messages_flagged") }]{% endif %} #} + {% if not sent_mode %} + {{ fbtns.report(report_url=msg.id, kind="pn", title=_("report this message")) }} + {% endif %} + <button class="ml-2 btn-icon-big btn btn-danger" type="submit" name="id" value="{{msg.id}}" title="{{ _("messages_delete") }}"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-square-fill" viewBox="0 0 16 16"> + <path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.354 4.646L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z"/> + </svg> + </button> + </td> + </tr> + {%- endfor %} + </tbody> + </table> + {{ msgs | length }} {{ _("messages_x_of_n") }} {{ total }} + </form> </div> {% endblock %} diff --git a/src/plainui/jinja2/plainui/personal_message_send.html b/src/plainui/jinja2/plainui/personal_message_send.html index 78132c80e663dd957f99249601f12e58095c9853..5c1980e5a6fad60bfa204894544404f0d7f51a01 100644 --- a/src/plainui/jinja2/plainui/personal_message_send.html +++ b/src/plainui/jinja2/plainui/personal_message_send.html @@ -2,23 +2,22 @@ {% extends "plainui/base.html" %} {% block title %}{{conf.name}} - {{ _("Personal Messages - Send") }}{% endblock %} {% block content %} -<div> - <div class="row justify-content-center"> - <div class="col-auto"><h1>{{ _("Send Personal Message") }}</h1></div> - <div class="w-100"></div> - <div class="col-auto"> - <form method="POST"> - {{ csrf_input }} - {{ form_elements.errors(form) }} - {{ form_elements.hidden(form, 'in_reply_to') }} - {{ form_elements.text(form, 'recipient') }} - {{ form_elements.text(form, 'subject') }} - {{ form_elements.textarea(form, 'body') }} +{{ titleMacro.title(_("Send Personal Message")) }} +<form class="border border-tertiary p-6 text-light mx-0" method="POST"> + <h2 class="w-100 bg bg-info p-2 px-5 h3 text-white text-center">{{ _("new message") }}</h2> + {{ csrf_input }} - <button type="submit" class="btn btn-primary">{{ _("Send") }}</button> - </form> - </div> - </div> -</div> + {{ form_elements.errors(form) }} + {{ form_elements.hidden(form, 'in_reply_to') }} + {{ form_elements.text(form, 'recipient') }} + {{ form_elements.text(form, 'subject') }} + {{ form_elements.textarea(form, 'body') }} + + <ul class="list-unstyled row justify-content-end "> + <li class="col-1 order-last"> + <button type="submit" class="btn btn-primary order-last">{{ _("Send") }}</button> + </li> + </ul> +</form> {% endblock %} diff --git a/src/plainui/jinja2/plainui/personal_message_show.html b/src/plainui/jinja2/plainui/personal_message_show.html index 1f822bd1a01c8d18e6439d91bacf5910ef991733..91a727451238b580a44af2a8c53a8c69a54a3704 100644 --- a/src/plainui/jinja2/plainui/personal_message_show.html +++ b/src/plainui/jinja2/plainui/personal_message_show.html @@ -1,19 +1,32 @@ +{% import "plainui/components/function_btns.html" as fbtns with context %} {% from "plainui/components/markdown.html" import markdown %} + {% extends "plainui/base.html" %} {% block title %}{{conf.name}} - {{ _("Personal Message") }}{% endblock %} {% block content %} -<div> - <div class="row justify-content-center"> - <div class="col-auto"><h1>{{ _("Personal Message") }}</h1></div> - <div class="w-100"></div> - <div class="col-auto"> - <h2>{{ msg.subject }}</h2> - <div> - Sent From {{ msg.sender.username }} To {{ msg.recipient.username }} - At {{ msg.timestamp | strftime }} - </div> - {{ markdown(msg_body) }} - </div> - </div> +{{ titleMacro.title(_("Personal Messages"), report_url=msg.id, report_kind="pn") }} + +<div class="border border-tertiary p-6 text-light mx-0"> + <h2 class="w-100 bg bg-info p-2 px-5 h3 text-white text-center">{{ msg.subject }}</h2> + <h6 class="card-subtitle mb-2 text-muted">{{ _("messages_from_short") }} {{ msg.sender.username }} {{ _("messages_to_short") }} {{ msg.recipient.username }} {{ _("messages_at") }} {{ msg.timestamp | strftime }}</h6> + {{ markdown(msg_body) }} + <ul class="mt-2 list-unstyled row justify-content-end "> + <li class="col-1"> + <form method="POST" action="{{ url('plainui:personal_message_delete', conf_slug=conf.slug) }}"> + <button class="ml-2 btn-icon-big btn btn-danger" type="submit" name="id" value="{{msg.id}}" title="{{ _("messages_delete") }}"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-square-fill" viewBox="0 0 16 16"> + <path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.354 4.646L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z"/> + </svg> + </button> + </form> + </li> + {% if user.id == msg.recipient.id %} + <li class="col-1"> + <a class="btn btn-primary" href="{{ url('plainui:personal_message_send_to', conf_slug=conf.slug, recipient=msg.sender.username) ~ '?in_reply_to=' ~ msg.id | urlencode ~ '&subject=AW: ' ~ msg.subject | truncate(100) | urlencode }}"> + {{_("Reply")}} + </a> + </li> + {% endif %} + </ul> </div> {% endblock %} diff --git a/src/plainui/jinja2/plainui/profile.html b/src/plainui/jinja2/plainui/profile.html index 473edab69745dcf1fab1ca9cac5df6757996bc3b..58d6ac531663c1077df9d8b2b94d278443875e3e 100644 --- a/src/plainui/jinja2/plainui/profile.html +++ b/src/plainui/jinja2/plainui/profile.html @@ -2,77 +2,56 @@ {% import "plainui/components/list_assemblies.html" as list_assm with context %} {% import "plainui/components/list_events.html" as list_events with context %} {% import "plainui/components/form_elements.html" as form_elements %} - +{% import "plainui/components/image.html" as imageMacro %} {% block title %}{{conf.name}} - {{ _("Profile") }}{% endblock %} {% block content %} - {{ titleMacro.title( _("My Dashboard") ) }} -<div class="row border my-5 p-6"> - <div class="col mr-1"> - <div class="row"> - <h2 class="w-100 text-center bg-secondary p-2">{{ user.username }}</h2> - </div> - <div class="row"> - <div class="col-4 border p-2"> - <img class="img-fluid" src="/static/plainui/img/rc3_no_avater.png" alt="{{ _("Avatar image") }}"> - </div> - <dl clasS="col-4 border p-2"> - <dt>{{ _("username") }}</dt> - <dd>{{ user.username }}</dd> - - <dt>{{ _("email") }}</dt> - <dd>{{ user.email }}</dd> - - <dt>{{ _("last login") }}</dt> - <dd>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') }}</dd> - </dl> - </div> - <div class="row mt-2"> - <h2 class="w-100 text-center bg-secondary p-2">{{ _("overview") }}</h2> - </div> - <div class="row mt-2 border p-2"> - <div class="row w-100"> - <div class="col-9">{{ _("total events added schedule") }}</div> - <div class="col-3 text-right">TODO: </div> - </div> - <div class="row w-100"> - <div class="col-9">{{ _("total events attended") }}</div> - <div class="col-3 text-right">TODO: </div> - </div> - <div class="row w-100"> - <div class="col-9">{{ _("total assemblies involved") }}</div> - <div class="col-3 text-right">TODO: </div> - </div> - <div class="row w-100"> - <div class="col-9">{{ _("total tokens found") }}</div> - <div class="col-3 text-right">TODO: </div> - </div> - </div> - </div> - <div class="col ml-1"> - <div class="row"> - <h2 class="w-100 text-center bg-secondary p-2">{{ _("custom preferences") }}</h2> - </div> - <div class="row border p-2"> - <form method="POST"> - {{ csrf_input }} - - {{ form_elements.errors(form) }} - {{ form_elements.textarea(form, 'description') }} - {{ form_elements.text(form, 'pronouns') }} - {{ form_elements.checkbox(form, 'high_contrast') }} - {{ form_elements.checkbox(form, 'receive_audio') }} - {{ form_elements.checkbox(form, 'receive_video') }} - - <input type="submit"> - </form> - <a href="{{ url('plainui:password_change', conf_slug=conf.slug) }}">{{ _("Change Password") }}</a> + +<div class="row border p-6 m-0"> + <h2 class="w-100 bg bg-info p-2 px-5 h3 text-white text-center">{{ user.username }}</h2> + <div class="row"> + <div class="col-5"> + {{ imageMacro.image(image="/static/plainui/img/rc3-no-avatar-plattform-active.jpeg", alt=_("Avatar image"), title=_("Avatar image") ) }} </div> + <dl clasS="col-7 p-2"> + <dt>{{ _("username") }}</dt> + <dd>{{ user.username }}</dd> + + <dt>{{ _("email") }}</dt> + <dd>{{ user.email }}</dd> + + <dt>{{ _("last login") }}</dt> + <dd>{{ user.last_login.strftime('%Y-%m-%d %H:%M:%S') }}</dd> + </dl> </div> + <form class="col-12" method="POST"> + <h2 class="w-100 bg bg-info p-2 px-5 h3 text-white text-center">{{ _("custom preferences") }}</h2> + {{ csrf_input }} + + {{ form_elements.textarea(form, 'description') }} + {{ form_elements.text(form, 'pronouns') }} + {{ form_elements.select(form, 'time_zone') }} + {{ form_elements.checkbox(form, 'high_contrast') }} + {{ form_elements.checkbox(form, 'receive_audio') }} + {{ form_elements.checkbox(form, 'receive_video') }} + + <ul class="row row-cols-1 row-cols-md-2 row-cols-lg-3 list-unstyled"> + <li class="col d-none d-lg-block"> </li> + <li class="col mb-2 mb-lg-0"> + <a class="btn btn-xl btn-block btn-secondary px-5" href="{{ url('plainui:password_change', conf_slug=conf.slug) }}">{{ _("Change Password") }}</a> + </li> + <li class="col"> + <button type="submit" class="btn btn-xl btn-block btn-primary px-5">{{ _("save") }}</button> + </li> + </ul> + + {{ form_elements.errors(form) }} + </form> </div> +{# Badges disabled until further work. <div class="row border my-5 p-6"> <form action="{{ url('plainui:badge_token_submit', conf_slug=conf.slug) }}" method="POST"> {{ csrf_input }} @@ -85,23 +64,25 @@ <li>{{badge_link.badge.name}} {% if badge_link.hidden %}(HIDDEN){% endif %}</li> {% endfor %} </ul> -</div> +</div> #} -<hr class="my-5 border-primary"> +<hr class="my-8"> <h2>{{ _("My Favorites") }}</h2> <div class="border border-tertiary p-6"> <h3 class="bg bg-info p-2 px-5 text-white text-center">{{ ("Events") }}</h3> {{ list_events.list(my_favorite_events, is_favorite_events, is_fahrplan_events ) }} - <h3 class="bg bg-info p-2 px-5 text-white text-center mt-5">{{ _("Assemblies") }}</h3> + <h3 class="bg bg-info p-2 px-5 text-white text-center mt-8">{{ _("Assemblies") }}</h3> {{ list_assm.list(my_favorite_assemblies, is_favorite_assemblies ) }} </div> -<hr class="my-5 border-primary"> +<hr class="my-8"> <h2>{{ _("My Fahrplan") }}</h2> <div class="border border-tertiary p-6"> {{ list_events.list(my_fahrplan_events, is_favorite_events, is_fahrplan_events ) }} </div> +<hr class="my-11 border-top-0"> + {% endblock %} diff --git a/src/plainui/jinja2/plainui/report_content.html b/src/plainui/jinja2/plainui/report_content.html index 4e44ee2a121cebf7699d7f11642ff28415b39a89..755a6de3202b86a6be9ee61c3a70ad0fbe14f1d2 100644 --- a/src/plainui/jinja2/plainui/report_content.html +++ b/src/plainui/jinja2/plainui/report_content.html @@ -14,14 +14,13 @@ {{ formElementsMacro.hidden(form, 'kind_data') }} {{ formElementsMacro.hidden(form, 'next') }} - {{ formElementsMacro.errors(form) }} {{ formElementsMacro.select(form, 'category') }} {{ formElementsMacro.textarea(form, 'message') }} {{ formElementsMacro.textarea(form, 'message2') }} <ul class="row row-cols-1 row-cols-sm-3 list-unstyled"> <li class="col d-none d-md-block"> </li> - <li class="col mb-3 mb-sm-0"> + <li class="col mb-3 mb-sm-0 d-js-only"> <a href="javascript:history.back()" class="btn btn-xl btn-block btn-secondary" @@ -33,6 +32,7 @@ <button type="submit" class="btn btn-xl btn-block btn-primary px-5">{{ _("Send") }}</button> </li> </ul> + {{ formElementsMacro.errors(form) }} <p>{{ _("Your message will be read and processed by one of our angels. We assure you to keep your personal data safe and hidden if not needed to solve your problem.") }}</p> </form> diff --git a/src/plainui/jinja2/plainui/room.html b/src/plainui/jinja2/plainui/room.html index 5b0bba0e38d02f99f95298e536cb59b90293eb1d..c09107bf8480f042aacb3b2583b7b89df143dc21 100644 --- a/src/plainui/jinja2/plainui/room.html +++ b/src/plainui/jinja2/plainui/room.html @@ -1,4 +1,5 @@ {% import "plainui/components/integrations.html" as integrations %} +{% import "plainui/components/markdown.html" as markdownMacro %} {% extends "plainui/base.html" %} {% block title %}Conference {{conf.name}} - Room {{room.name}}{% endblock %} {% block head %} @@ -16,4 +17,18 @@ {% if voc_stream %} {{ integrations.vocPlayer(vocStream=voc_stream) }} {% endif %} + + {% if room.description %} + <h4>{{ _("Description") }}</h4> + <p>{{ markdownMacro.markdown(markdown=room.description | safe) }}</p> + {% endif %} + + <div class="mt-4"> + <h4>{{ _("Links") }}</h4> + <ul> + {% for link in room.links.all() %} + <li>{{ link.get_link_type_display() }}: <a href="#TODO_DEREFFERER">{{ link.name }}</a></li> + {% endfor %} + </ul> + </div> {% endblock %} diff --git a/src/plainui/jinja2/plainui/sos_edit.html b/src/plainui/jinja2/plainui/sos_edit.html index 22385c8d26d4cd4b4399fbaf83c78dc427760d7a..2fb083cd05996ddaa80f01cd305cb373c7b10082 100644 --- a/src/plainui/jinja2/plainui/sos_edit.html +++ b/src/plainui/jinja2/plainui/sos_edit.html @@ -1,14 +1,12 @@ {% import "plainui/components/form_elements.html" as form_elements %} {% extends "plainui/base.html" %} -{% block title %}{{conf.name}} - Edit Selforganized Session{% endblock %} +{% block title %}{{conf.name}} - Edit Self-organized Session{% endblock %} {% block content %} - {{ titleMacro.title(_("Selforganized Session")) }} + <form method="POST" class="border p-6 mx-auto my-11{% if form.errors %} border-danger{% else %} border-tertiary{% endif %}"> + <h1 class="text-center bg-info p-3 text-white h3">{{ _("Self-organized Session") }}</h1> - <form method="POST"> {{ csrf_input }} - {{ form_elements.errors(form) }} - {{ form_elements.text(form, 'name') }} {{ form_elements.select(form, 'room') }} {{ form_elements.text(form, 'schedule_start') }} @@ -17,7 +15,17 @@ {{ form_elements.text(form, 'language') }} {{ form_elements.textarea(form, 'description') }} - <a class="btn btn-secondary" href="{{ url('plainui:assembly', conf_slug=conf.slug, assembly_slug=assembly.slug) }}">{{ _("Cancel") }}</a> - <button type="submit" class="btn btn-primary">{{ _("Update") if edit_mode else _("Create") }}</button> + <ul class="row row-cols-1 row-cols-sm-3 list-unstyled"> + <li class="col d-none d-md-block"> </li> + <li class="col mb-3 mb-sm-0"> + <a class="btn btn-xl btn-block btn-secondary" title="{{ _("Back") }}" href="{{ url('plainui:assembly', conf_slug=conf.slug, assembly_slug=assembly.slug) }}">{{ _("back") }}</a> + </li> + <li class="col"> + <button type="submit" class="btn btn-xl btn-block btn-primary px-5">{{ _("Update") if edit_mode else _("Create") }}</button> + </li> + </ul> + + {{ form_elements.errors(form) }} + </form> {% endblock %} diff --git a/src/plainui/jinja2/plainui/static_page.html b/src/plainui/jinja2/plainui/static_page.html index 844e2de6a31985dc32dce1a8c283da3209183f01..95cb08b0e48e6961517b1cacba8e54bfdaa7197e 100644 --- a/src/plainui/jinja2/plainui/static_page.html +++ b/src/plainui/jinja2/plainui/static_page.html @@ -1,5 +1,4 @@ {% extends "plainui/base.html" %} -{% import "plainui/components/title.html" as titleMacro %} {% import "plainui/components/markdown.html" as markdownMacro %} {% block title %}{{conf.name}} - {{ page.title }}{% endblock %} diff --git a/src/plainui/jinja2/plainui/upcoming.html b/src/plainui/jinja2/plainui/upcoming.html index a49e022d9f009d3b623d729b87ef950798dfa9b1..9b0e45e3fcba73d1c5671e1dc2f6cd171eae373a 100644 --- a/src/plainui/jinja2/plainui/upcoming.html +++ b/src/plainui/jinja2/plainui/upcoming.html @@ -7,6 +7,6 @@ <h2>{{ _("upcoming events") }}</h2> <div class="border border-tertiary p-6"> - {{ list_events.tiles(events, my_favorite_events, my_scheduled_events ) }} + {{ list_events.slider(events, my_favorite_events, my_scheduled_events ) }} </div> {% endblock %} diff --git a/src/plainui/jinja2/plainui/user.html b/src/plainui/jinja2/plainui/user.html index 8ff85bd7ebe07b6ec008c670fd0aa48a5292c9cd..395b177b6813df4a9b40d8bae3a2da50c6a19dfb 100644 --- a/src/plainui/jinja2/plainui/user.html +++ b/src/plainui/jinja2/plainui/user.html @@ -15,7 +15,7 @@ </div> <div class="row"> <div class="col-4 border p-2"> - <img class="img-fluid" src="/static/plainui/img/rc3_no_avater.png" alt="{{ _("Avatar image") }}"> + <img class="img-fluid" src="/static/plainui/img/rc3-no-avatar-plattform-active.jpeg" alt="{{ _("Avatar image") }}"> </div> <div class="col-8 border p-2"> {{ display_user.description_html }} diff --git a/src/plainui/jinja2/plainui/world.html b/src/plainui/jinja2/plainui/world.html index b3b48328f5a753690d1afeee09469243d6d6ac10..3d67ceae24982f1c8678bb97d7f1b282691b7cfa 100644 --- a/src/plainui/jinja2/plainui/world.html +++ b/src/plainui/jinja2/plainui/world.html @@ -5,7 +5,12 @@ {% block title %}{{conf.name}} - {{ _("2D World") }}{% endblock %} {% block content %} <article> - <a href="https://play.at.rc3.world/" class="row my-8"> + <a + href="https://play.at.rc3.world/" + class="row my-8" + target="_blank" + rel="external, noreferrer" + > {{ imageMacro.image(image=static('plainui/img/start2dworld.gif'), alt="Start 2D World", title="Start 2D World") }} </a> @@ -13,7 +18,12 @@ {{ markdownMacro.markdown(markdown=page.rendered_body|safe) }} - <a href="https://play.at.rc3.world" class="row my-8"> + <a + href="https://play.at.rc3.world" + class="row my-8" + target="_blank" + rel="external, noreferrer" + > {{ imageMacro.image(image=static('plainui/img/enter2dworld.gif'), alt="Enter 2D World", title="Enter 2D World") }} </a> </article> diff --git a/src/plainui/locale/de/LC_MESSAGES/django.po b/src/plainui/locale/de/LC_MESSAGES/django.po index 6db239aa764d496379c83b6f11b5c8ffc144e230..f72e339edee1ed84586a4ee8865512a7e577c51b 100644 --- a/src/plainui/locale/de/LC_MESSAGES/django.po +++ b/src/plainui/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-12-23 11:52+0000\n" +"POT-Creation-Date: 2020-12-23 13:29+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -75,6 +75,15 @@ msgstr "" msgid "all assemblies" msgstr "" +msgid "assemblies startseite" +msgstr "" + +msgid "assemblies_official_only" +msgstr "Nur offizielle Assemblies" + +msgid "assemblies_community_only" +msgstr "Nur Community-Assemblies" + msgid "assembly events" msgstr "" @@ -84,6 +93,9 @@ msgstr "" msgid "all assemblies events" msgstr "" +msgid "assmebly events" +msgstr "" + msgid "New Selforganized Session" msgstr "" @@ -372,17 +384,56 @@ msgstr "" msgid "New PM" msgstr "" +msgid "Received Messages" +msgstr "Empfangene Nachrichten" + +msgid "Sent Messages" +msgstr "Gesendete Nachrichten" + +msgid "messages_subject" +msgstr "Betreff" + +msgid "messages_from" +msgstr "Absender" + +msgid "messages_to" +msgstr "Empfänger" + +msgid "messages_date" +msgstr "Datum" + +msgid "messages_state" +msgstr "Status" + +msgid "messages_delete" +msgstr "Löschen" + +msgid "messages_was_read" +msgstr "gelesen" + +msgid "messages_x_of_n" +msgstr "von" + msgid "Personal Messages - Send" msgstr "" msgid "Send Personal Message" -msgstr "" +msgstr "Nachricht senden" msgid "Send" -msgstr "" +msgstr "Abschicken" msgid "Personal Message" -msgstr "" +msgstr "Persönliche Nachricht" + +msgid "messages_from_short" +msgstr "Von" + +msgid "messages_to_short" +msgstr "An" + +msgid "messages_at" +msgstr "am" msgid "Avatar image" msgstr "" diff --git a/src/plainui/locale/en/LC_MESSAGES/django.po b/src/plainui/locale/en/LC_MESSAGES/django.po index 49e790d108ba72564e1eab054fe01952bc499158..4ee1cb9c6c410c4eb907f77086771e27ddfee7da 100644 --- a/src/plainui/locale/en/LC_MESSAGES/django.po +++ b/src/plainui/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-12-23 11:52+0000\n" +"POT-Creation-Date: 2020-12-23 13:29+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -75,6 +75,15 @@ msgstr "" msgid "all assemblies" msgstr "" +msgid "assemblies startseite" +msgstr "" + +msgid "assemblies_official_only" +msgstr "Only official assemblies" + +msgid "assemblies_community_only" +msgstr "only community assemblies" + msgid "assembly events" msgstr "" @@ -84,6 +93,9 @@ msgstr "" msgid "all assemblies events" msgstr "" +msgid "assmebly events" +msgstr "" + msgid "New Selforganized Session" msgstr "" @@ -360,6 +372,36 @@ msgstr "" msgid "New PM" msgstr "" +msgid "Received Messages" +msgstr "Received Messages" + +msgid "Sent Messages" +msgstr "Sent Messages" + +msgid "messages_subject" +msgstr "Subject" + +msgid "messages_from" +msgstr "From" + +msgid "messages_to" +msgstr "To" + +msgid "messages_date" +msgstr "Date" + +msgid "messages_state" +msgstr "State" + +msgid "messages_delete" +msgstr "Delete" + +msgid "messages_was_read" +msgstr "read" + +msgid "messages_x_of_n" +msgstr "of" + msgid "Personal Messages - Send" msgstr "" @@ -372,6 +414,15 @@ msgstr "" msgid "Personal Message" msgstr "" +msgid "messages_from_short" +msgstr "From" + +msgid "messages_to_short" +msgstr "To" + +msgid "messages_at" +msgstr "At" + msgid "Avatar image" msgstr "" diff --git a/src/plainui/static/plainui/img/box-arrow-up-right.svg b/src/plainui/static/plainui/img/box-arrow-up-right.svg new file mode 100644 index 0000000000000000000000000000000000000000..1d93acb91a5f657008d4fe5b0f9dc5cfe517fcce --- /dev/null +++ b/src/plainui/static/plainui/img/box-arrow-up-right.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/> + <path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/> +</svg> \ No newline at end of file diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-01.png b/src/plainui/static/plainui/img/rc3-assembly-event-01.png new file mode 100644 index 0000000000000000000000000000000000000000..78449b2de9ef56bf5e500dbd3a5f044039bce9c9 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-01.png differ diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-02.png b/src/plainui/static/plainui/img/rc3-assembly-event-02.png new file mode 100644 index 0000000000000000000000000000000000000000..a1461f8d69c7b1fd9d49d08e37c8f74f1b71f834 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-02.png differ diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-03.png b/src/plainui/static/plainui/img/rc3-assembly-event-03.png new file mode 100644 index 0000000000000000000000000000000000000000..97a8c6be6224fc3e8c91f63c8c6c530843e36b89 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-03.png differ diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-04.png b/src/plainui/static/plainui/img/rc3-assembly-event-04.png new file mode 100644 index 0000000000000000000000000000000000000000..44eb1ae552b78175d33a20d116481b99faafa061 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-04.png differ diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-05.png b/src/plainui/static/plainui/img/rc3-assembly-event-05.png new file mode 100644 index 0000000000000000000000000000000000000000..2e1fba3a41296433a19f500ec22be3d8c7d602e4 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-05.png differ diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-06.png b/src/plainui/static/plainui/img/rc3-assembly-event-06.png new file mode 100644 index 0000000000000000000000000000000000000000..957808c4824e96c4215c50be0d3eb0fd5a3ed6ad Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-06.png differ diff --git a/src/plainui/static/plainui/img/rc3-assembly-event-07.png b/src/plainui/static/plainui/img/rc3-assembly-event-07.png new file mode 100644 index 0000000000000000000000000000000000000000..d5a6b88958c78fec211f961df552a76a0c0efc49 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-assembly-event-07.png differ diff --git a/src/plainui/static/plainui/img/rc3-logo-assembly.svg b/src/plainui/static/plainui/img/rc3-logo-assembly.svg index 53d9e86d2b3925e02c43a95e815a956ce30978a1..cad15be8bd91dbf19cfb1eca14c1bb67549c52cc 100644 --- a/src/plainui/static/plainui/img/rc3-logo-assembly.svg +++ b/src/plainui/static/plainui/img/rc3-logo-assembly.svg @@ -1,3 +1,220 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="624" height="624" viewBox="0 0 624 624"> - <image width="624" height="623" xlink:href="data:img/png;base64,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"/> +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + id="pink_color_bars" + data-name="pink color bars" + width="624" + height="624" + viewBox="0 0 624 624" + version="1.1" + sodipodi:docname="rc3-logo-assembly_2.svg" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> + <metadata + id="metadata25"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1600" + inkscape:window-height="1762" + id="namedview23" + showgrid="false" + inkscape:zoom="1.1600968" + inkscape:cx="281.55154" + inkscape:cy="432.58503" + inkscape:window-x="1582" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="pink_color_bars" + inkscape:document-rotation="0" /> + <defs + id="defs4"> + <style + id="style2"> + .cls-1 { + fill: #240039; + } + + .cls-2 { + fill: #670096; + } + + .cls-3 { + fill: #450069; + } + + .cls-4 { + fill: #b239ff; + } + + .cls-5 { + fill: #fff; + fill-rule: evenodd; + } + + .cls-6 { + fill: none; + stroke: #fff; + stroke-width: 7.63px; + } + </style> + </defs> + <image + id="Layer_0" + data-name="Layer 0" + x="1" + y="1" + width="576" + height="575" + xlink:href="data:img/png;base64,iVBORw0KGgoAAAANSUhEUgAAAkAAAAI/CAYAAACf7mYiAAAKAElEQVR4nO3YsQ0DMQDDQL1//3FTpXCQOXi3ghpCz7Y7AICQs+1rcAAg5HM8QABAzD0WBwBqBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJAjgACAHAEEAOQIIAAgRwABADkCCADIEUAAQI4AAgByBBAAkCOAAIAcAQQA5AggACBHAAEAOQIIAMgRQABAjgACAHIEEACQI4AAgBwBBADkCCAAIEcAAQA5AggAyBFAAECOAAIAcgQQAJDzD6DX7ABAyPtsuxYHADK2/QCQ7ApvODTfkAAAAABJRU5ErkJggg==" /> + <rect + id="_240039" + data-name="240039" + class="cls-1" + x="143" + y="240" + width="97" + height="288" + style="fill:#001b18;fill-opacity:1" /> + <rect + id="_240039-2" + data-name="240039" + class="cls-1" + x="528" + y="432" + width="96" + height="192" + style="fill:#001b18;fill-opacity:1" /> + <rect + id="_670096" + data-name="670096" + class="cls-2" + x="528" + y="47" + width="96" + height="97" + style="fill:#01a08f;fill-opacity:1" /> + <rect + id="_670096-2" + data-name="670096" + class="cls-2" + x="432" + y="47" + width="96" + height="193" + style="fill:#01a08f;fill-opacity:1" /> + <rect + id="_670096-3" + data-name="670096" + class="cls-2" + x="336" + y="47" + width="96" + height="97" + style="fill:#01a08f;fill-opacity:1" /> + <rect + id="_670096-4" + data-name="670096" + class="cls-2" + x="336" + y="432" + width="96" + height="192" + style="fill:#01a08f;fill-opacity:1" /> + <rect + id="_670096-5" + data-name="670096" + class="cls-2" + x="240" + y="336" + width="96" + height="288" + style="fill:#01a08f;fill-opacity:1" /> + <rect + id="_450069" + data-name="450069" + class="cls-3" + x="143" + y="528" + width="97" + height="96" + style="fill:#01564d;fill-opacity:1" /> + <rect + id="_450069-2" + data-name="450069" + class="cls-3" + x="528" + y="143" + width="96" + height="289" + style="fill:#01a08f;fill-opacity:1" /> + <rect + id="_450069-3" + data-name="450069" + class="cls-3" + x="432" + y="528" + width="96" + height="96" + style="fill:#01564d;fill-opacity:1" /> + <rect + id="_450069-4" + data-name="450069" + class="cls-3" + x="240" + y="47" + width="96" + height="289" + style="fill:#01564d;fill-opacity:1" /> + <rect + id="b239ff" + class="cls-4" + x="432" + y="240" + width="96" + height="288" + style="fill:#02fae0;fill-opacity:1" /> + <rect + id="b239ff-2" + data-name="b239ff" + class="cls-4" + x="336" + y="143" + width="96" + height="290" + style="fill:#02fae0;fill-opacity:1" /> + <path + id="RC3_copy" + data-name="RC3 copy" + class="cls-5" + d="M222.656,545.361v-36.5l31.856-.154,30.774,36.649h24.742v-8.2L285.9,508.712h1.083a23.238,23.238,0,0,0,23.041-22.887V457.062a23.236,23.236,0,0,0-23.041-22.887h-88.3V545.361h23.969Zm0-60.773V458.144h63.093v26.444H222.656Zm208.609,60.773V521.237H344.2V458.144h87.062V434.021H343.12a22.964,22.964,0,0,0-22.886,22.886v65.568a22.966,22.966,0,0,0,22.886,22.886h88.145Zm96.339,0a22.571,22.571,0,0,0,22.423-22.423V499.9a21.224,21.224,0,0,0-1.392-7.655,22.542,22.542,0,0,0-3.711-6.417,20.741,20.741,0,0,0,.7-3.479,30.7,30.7,0,0,0,.232-3.635V456.443a22.44,22.44,0,0,0-22.423-22.422H463.274a21.855,21.855,0,0,0-15.928,6.572,21.474,21.474,0,0,0-6.649,15.85v9.9h23.659v-8.66h57.836v19.794H458.944v23.66h67.423V521.7H464.356v-7.422H440.7v8.659a22.7,22.7,0,0,0,22.577,22.423H527.6Z" /> + <rect + id="rahmen_weiss" + data-name="rahmen weiss" + class="cls-6" + x="1.156" + y="-0.188" + width="576.313" + height="576.343" /> </svg> diff --git a/src/plainui/static/plainui/img/rc3-no-avatar-assembly-active.jpeg b/src/plainui/static/plainui/img/rc3-no-avatar-assembly-active.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..d136d59ceb863ca0cef3709b83aae735baac8e11 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-no-avatar-assembly-active.jpeg differ diff --git a/src/plainui/static/plainui/img/rc3-no-avatar-assembly.jpeg b/src/plainui/static/plainui/img/rc3-no-avatar-assembly.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3eed660d12539ccd24a9b91dba4e135e0a396e45 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-no-avatar-assembly.jpeg differ diff --git a/src/plainui/static/plainui/img/rc3-no-avatar-plattform-active.jpeg b/src/plainui/static/plainui/img/rc3-no-avatar-plattform-active.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cd48c921abfa798e70232daad7b65f331c727c82 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-no-avatar-plattform-active.jpeg differ diff --git a/src/plainui/static/plainui/img/rc3-no-avatar-plattform.jpeg b/src/plainui/static/plainui/img/rc3-no-avatar-plattform.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f6c865ff57fbfbef092fb8f7fc566561604eb950 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-no-avatar-plattform.jpeg differ diff --git a/src/plainui/static/plainui/img/rc3-no-avatar-world.jpeg b/src/plainui/static/plainui/img/rc3-no-avatar-world.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cd48c921abfa798e70232daad7b65f331c727c82 Binary files /dev/null and b/src/plainui/static/plainui/img/rc3-no-avatar-world.jpeg differ diff --git a/src/plainui/static/plainui/img/rc3_no_avater.png b/src/plainui/static/plainui/img/rc3_no_avater.png deleted file mode 100644 index 6a8c9b22486a82198602e5240832a1dcd26c6326..0000000000000000000000000000000000000000 Binary files a/src/plainui/static/plainui/img/rc3_no_avater.png and /dev/null differ diff --git a/src/plainui/styles/_button-classes.scss b/src/plainui/styles/_button-classes.scss index 4d8a1b49dbee2d44909a7995c2d3f0d25613caea..cc6512d60b6d29597f38491829129c56c555fb92 100644 --- a/src/plainui/styles/_button-classes.scss +++ b/src/plainui/styles/_button-classes.scss @@ -110,19 +110,20 @@ cursor: not-allowed; } - &:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active { text-shadow: $btn-hover-text-shadow; + box-shadow: none; + } + + &:not(:disabled):not(.disabled):active { + text-shadow: $btn-hover-text-shadow; + background: rgba($value, 0.75); &:focus, &.focus { box-shadow: none; } } - - &:not(:disabled):not(.disabled):active { - background: rgba($value, 0.75); - } } .btn-#{$color}, @@ -165,10 +166,15 @@ cursor: not-allowed; } - &:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active, .show > &.dropdown-toggle { text-shadow: $btn-hover-text-shadow; + box-shadow: none; + } + + &:not(:disabled):not(.disabled):active { + background: rgba($value, 0.75); + text-shadow: $btn-hover-text-shadow; &:focus, &.focus { @@ -176,10 +182,6 @@ } } - &:not(:disabled):not(.disabled):active { - background: rgba($value, 0.75); - } - &:not(:disabled):not(.disabled).active, .show > &.dropdown-toggle { @if $color == "assembly" { diff --git a/src/plainui/styles/_import-fonts.scss b/src/plainui/styles/_import-fonts.scss index fa238fd53d0c9a934c3157ed5a63108988b909c3..a1a1ed6ef5518765d3380a5d06962d175fc61f0d 100644 --- a/src/plainui/styles/_import-fonts.scss +++ b/src/plainui/styles/_import-fonts.scss @@ -1,12 +1,3 @@ -@font-face{ - font-family:"Orbitron"; - font-weight: 400; - font-style: normal; - font-display: swap; - src:url("fonts/orbitron-light-webfont.woff") format("woff"), - url("fonts/orbitron-light-webfont.ttf") format("truetype"); -} - @font-face{ font-family:"Orbitron"; font-weight: 700; diff --git a/src/plainui/styles/_util-classes.scss b/src/plainui/styles/_util-classes.scss index f73176b0153e316066a79035eed899f76e23ce18..abdefd3aac9d0f2677ca569063a580e8e6f80e51 100644 --- a/src/plainui/styles/_util-classes.scss +++ b/src/plainui/styles/_util-classes.scss @@ -84,11 +84,16 @@ h6, .font-headings { font-family: $headings-font-family; + font-weight: 700; +} + +.font-sans-serif { + font-family: $font-family-sans-serif; } h6, .h6 { - font-weight: 400; + text-transform: unset; } .mw-664 { @@ -99,6 +104,26 @@ h6, max-width: 50.625rem; } +.external { + white-space: nowrap; + + &::after { + content: ""; + display: inline-block; + width: $spacer; + height: $spacer; + margin-left: map-get($spacers, 1); + background: currentColor; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url("img/box-arrow-up-right.svg"); + } +} + +.no-js .d-js-only { + display: none !important; +} + // Basic Bootstrap table - only use the basic table // - if you need another configuration update the bootstrap variables .table { diff --git a/src/plainui/styles/components/_header.scss b/src/plainui/styles/components/_header.scss index 193ffe5525be4ed5fd420e8e0805203d49bf84eb..c1cdc10f5b4c8855473823e1b658a371cd2aa2f8 100644 --- a/src/plainui/styles/components/_header.scss +++ b/src/plainui/styles/components/_header.scss @@ -54,6 +54,7 @@ position: absolute !important; left: -0.4rem !important; top: -0.4rem !important; + padding: 0.1rem 0.5rem !important; } &-box-2x1 { diff --git a/src/plainui/styles/components/_index.scss b/src/plainui/styles/components/_index.scss index 25aa310f3e16a969d230c5c42ff3863ff6e8ad8a..c4396267c3d62be3292f7927f5f618f1eaa2e1d8 100644 --- a/src/plainui/styles/components/_index.scss +++ b/src/plainui/styles/components/_index.scss @@ -7,3 +7,4 @@ @import "syntaxhilite"; @import "player"; @import "tile-board"; +@import "slider"; diff --git a/src/plainui/styles/components/_markdown.scss b/src/plainui/styles/components/_markdown.scss index def2cc48577549a3d34af6ec2230d0d78970fa2d..0286993a5234e44574016e17bfe84df7e5825087 100644 --- a/src/plainui/styles/components/_markdown.scss +++ b/src/plainui/styles/components/_markdown.scss @@ -23,6 +23,7 @@ hr { border-width: 1px; margin: map-get($spacers,4) 0; + border-color: $markdown-border-color; } table { diff --git a/src/plainui/styles/components/_slider.scss b/src/plainui/styles/components/_slider.scss new file mode 100644 index 0000000000000000000000000000000000000000..a54c530a61197533361b54f53c3f20f0c4522c15 --- /dev/null +++ b/src/plainui/styles/components/_slider.scss @@ -0,0 +1,13 @@ +.rc3-slider { + &__container { + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scroll-snap-type: y mandatory; + scroll-padding: map-get($spacers, 2); + } + + &__item { + scroll-snap-align: start; + } +} diff --git a/src/plainui/styles/utils/_bootstrap-theme-assembly.scss b/src/plainui/styles/utils/_bootstrap-theme-assembly.scss index 47dd4c6357af731b5a28059477331fb3c2e08a35..e020c53bb5cbdc7a331398aa9b3021cb78a573f3 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-assembly.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-assembly.scss @@ -43,6 +43,7 @@ $link-hover-color: $primary; $border-color: $secondary; $hr-border-color: $primary; +$markdown-border-color: $primary; $table-head-color: $gray-100; diff --git a/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss b/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss index 5885b3b54226f9d3aa87fba4763f5ee9a7ac62d2..36724b90e02e8f71fa743b0808124b9585230226 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss @@ -42,6 +42,7 @@ $card-cap-bg: $info; $link-hover-color: $primary; $hr-border-color: $secondary; +$markdown-border-color: $primary; $table-head-color: $primary; $input-color: $gray-100; diff --git a/src/plainui/styles/utils/_bootstrap-theme-plattform.scss b/src/plainui/styles/utils/_bootstrap-theme-plattform.scss index 1ba6f1353052ccd8a8c0cb6432740dbfdb4ce192..c23d459e867dd4f0bb15a36d0c250c1b87eee942 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-plattform.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-plattform.scss @@ -43,7 +43,8 @@ $link-color: map-get($plattform, "font-pink"); $link-hover-color: $link-color; $border-color: $tertiary; -$hr-border-color: $secondary; +$hr-border-color: $primary; +$markdown-border-color: $secondary; $table-head-color: $gray-100; $input-color: $gray-100; diff --git a/src/plainui/styles/utils/_bootstrap-theme-world.scss b/src/plainui/styles/utils/_bootstrap-theme-world.scss index 09582e11f5c86a011b2adfd2b7759b0998ad8cce..75f2bba70906f0dd3a82ef94507692225aa70557 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-world.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-world.scss @@ -38,7 +38,8 @@ $card-cap-bg: $info; $link-hover-color: $primary; $border-color: $tertiary; -$hr-border-color: $secondary; +$hr-border-color: $primary; +$markdown-border-color: $primary; $input-color: $gray-100; $input-focus-color: $gray-100; diff --git a/src/plainui/styles/utils/_forms.scss b/src/plainui/styles/utils/_forms.scss index c7dfba0871f23920cea6b024911103d096e0fcd2..e57a11af84dd12af14ab34545557a390354f24ab 100644 --- a/src/plainui/styles/utils/_forms.scss +++ b/src/plainui/styles/utils/_forms.scss @@ -1,8 +1,7 @@ @use "./fonts"; @use "./button"; -$input-font-family: fonts.$headings-font-family; -$input-font-weight: 400; +$input-font-family: fonts.$font-family-sans-serif; $input-padding-y: 1rem; $input-padding-x: 1rem; diff --git a/src/plainui/tests.py b/src/plainui/tests.py index d0b81c5a75bede27199b8327990d938c2f6a92df..131b2e631b9aa11f16f06c19e11796efa9973bed 100644 --- a/src/plainui/tests.py +++ b/src/plainui/tests.py @@ -9,7 +9,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.core import mail from django.http import SimpleCookie -from django.test import TestCase, override_settings +from django.test import TestCase, override_settings, modify_settings from django.urls import reverse from django.utils import timezone from django.utils.formats import localize @@ -95,7 +95,7 @@ class ViewsTest(TestCase): hidden_conf = Conference(name='foo_self.conf', slug='slug42', is_public=False) hidden_conf.save() - with override_settings(DEBUG=False): + with override_settings(DEBUG=False), modify_settings(MIDDLEWARE={'remove': ['debug_toolbar.middleware.DebugToolbarMiddleware']}, INSTALLED_APPS={'remove': ['debug_toolbar']}): # noqa: E501 resp = self.client.get(reverse('plainui:conferences'), follow=False) self.assertRedirects(resp, reverse('plainui:landing', kwargs={'conf_slug': self.conf.slug})) @@ -144,7 +144,7 @@ class ViewsTest(TestCase): self.assertEqual(resp.context_data['conf'], self.conf) self.assertEqual(resp.context_data['event'], event) self.assertEqual(list(resp.context_data['tags']), [tagitem]) - self.assertEqual(resp.context_data['scope'], '') + self.assertEqual(resp.context_data['scope'], 'plattform') self.assertEqual(list(resp.context_data['suggested']), [suggested_event2, suggested_event]) event.kind = Event.Kind.ASSEMBLY @@ -201,6 +201,8 @@ class ViewsTest(TestCase): self.conf.start = datetime(2020, 12, 24, 10, tzinfo=utc) self.conf.end = datetime(2020, 12, 27, 10, tzinfo=utc) self.conf.save() + self.user.time_zone = 'UTC' + self.user.save() assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state=Assembly.State.PLACED) assembly.save() @@ -227,7 +229,7 @@ class ViewsTest(TestCase): 'schedule_start': '25.12.2020 14:00', 'schedule_duration': '42:00', }) - self.assertRedirects(resp, reverse('plainui:assembly', kwargs={'conf_slug': self.conf.slug, 'assembly_slug': assembly.slug})) + self.assertRedirects(resp, reverse('plainui:sos_edit', kwargs={'conf_slug': self.conf.slug, 'assembly_slug': assembly.slug, 'event_slug': 'sos-1'})) sos = assembly.events.get(kind=Event.Kind.SELF_ORGANIZED) self.assertEqual(sos.conference, self.conf) self.assertEqual(sos.assembly, assembly) @@ -552,10 +554,12 @@ class ViewsTest(TestCase): resp = self.client.post(reverse('plainui:userprofile', kwargs={'conf_slug': self.conf.slug}), { 'description': 'new_description', 'high_contrast': 'on', + 'time_zone': 'Europe/Berlin', }) self.assertRedirects(resp, reverse('plainui:userprofile', kwargs={'conf_slug': self.conf.slug})) self.user.refresh_from_db() self.assertEqual(self.user.description, 'new_description') + self.assertEqual(self.user.time_zone, 'Europe/Berlin') self.assertTrue(self.user.high_contrast) self.assertFalse(self.user.receive_audio) self.assertFalse(self.user.receive_video) @@ -1375,10 +1379,13 @@ class ViewsTest(TestCase): self.assertRedirects(resp, reverse('plainui:board_private', kwargs={'conf_slug': self.conf.slug})) self.assertTrue(BulletinBoardEntry.objects.filter(pk=e2.pk).exists()) + @override_settings(TIME_ZONE='UTC') def test_Fahrplan(self): self.conf.start = datetime(2020, 1, 1, 0, 0, 0, tzinfo=utc) self.conf.end = datetime(2020, 1, 3, 0, 0, 0, tzinfo=utc) self.conf.save() + self.user.time_zone = 'UTC' + self.user.save() assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state=Assembly.State.PLACED) assembly.save() @@ -1428,10 +1435,23 @@ class ViewsTest(TestCase): 'calendar_step_minutes': 30, }) + # some random requests that test building the filter content + resp = self.client.get(reverse('plainui:fahrplan', kwargs={'conf_slug': self.conf.slug}), {'mode': 'calendar', 'show_day_filters': 'y'}) + self.assertEqual(resp.context_data['conf'], self.conf) + + resp = self.client.get(reverse('plainui:fahrplan', kwargs={'conf_slug': self.conf.slug}), {'mode': 'calendar', 'show_assembly_filters': 'y'}) + self.assertEqual(resp.context_data['conf'], self.conf) + + resp = self.client.get(reverse('plainui:fahrplan', kwargs={'conf_slug': self.conf.slug}), {'mode': 'calendar', 'show_track_filters': 'y'}) + self.assertEqual(resp.context_data['conf'], self.conf) + + @override_settings(TIME_ZONE='UTC') def test_MyFahrplan(self): self.conf.start = datetime(2020, 1, 1, 0, 0, 0, tzinfo=utc) self.conf.end = datetime(2020, 1, 3, 0, 0, 0, tzinfo=utc) self.conf.save() + self.user.time_zone = 'UTC' + self.user.save() assembly = Assembly(conference=self.conf, slug='assembly1', name='Assembly1', state=Assembly.State.PLACED) assembly.save() diff --git a/src/plainui/urls.py b/src/plainui/urls.py index 6c2bff8c7cd901c3274e4a4b70276d5a9e5a8606..d496fe3fe090d9e8a9e0369ae62bd11a614ebb50 100644 --- a/src/plainui/urls.py +++ b/src/plainui/urls.py @@ -54,6 +54,8 @@ urlpatterns = [ path('<slug:conf_slug>/upcoming', views.UpcomingView.as_view(), name='upcoming'), path('<slug:conf_slug>/assemblies', views.AssembliesView.as_view(), name='assemblies'), path('<slug:conf_slug>/assemblies/all', views.AssembliesAllView.as_view(), name='assemblies_all'), + path('<slug:conf_slug>/assemblies/all/official', views.AssembliesAllView.as_view(), {'qfilter': 'official'}, name='assemblies_official'), + path('<slug:conf_slug>/assemblies/all/community', views.AssembliesAllView.as_view(), {'qfilter': 'community'}, name='assemblies_community'), path('<slug:conf_slug>/assemblies/events', views.AssembliesEventsView.as_view(), name='assemblies_events'), path('<slug:conf_slug>/assembly/<slug:assembly_slug>/', views.AssemblyView.as_view(), name='assembly'), path('<slug:conf_slug>/assembly/<slug:assembly_slug>/sos/new', views.SelfOrganizedSessionEditView.as_view(), name='sos_new'), diff --git a/src/plainui/views.py b/src/plainui/views.py index a53a3d7f5cc842150aa1b288ef4d16ed253e0731..5b97ccafa5aeaf169f464760452bc37f26915b11 100644 --- a/src/plainui/views.py +++ b/src/plainui/views.py @@ -15,6 +15,7 @@ from django.urls import reverse 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 from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import View, TemplateView @@ -140,7 +141,7 @@ class EventView(ConferenceRequiredMixin, TemplateView): context['suggested'] = [ s.event2 for s in event.suggestions.select_related('event2').exclude(event2=event.pk).exclude(event2__is_public=False).order_by('-like_ratio')[:5] ] - context['scope'] = '' if event.kind == Event.Kind.OFFICIAL else 'assembly' + context['scope'] = 'plattform' if event.kind == Event.Kind.OFFICIAL else 'assembly' return context @@ -220,7 +221,9 @@ class SelfOrganizedSessionEditView(ConferenceRequiredMixin, FormView): return initial def get_success_url(self): - return reverse('plainui:assembly', kwargs={'conf_slug': self.kwargs['conf_slug'], 'assembly_slug': self.kwargs['assembly_slug']}) + return reverse('plainui:sos_edit', kwargs={'conf_slug': self.kwargs['conf_slug'], + 'assembly_slug': self.kwargs['assembly_slug'], + 'event_slug': self.sos.slug}) def form_valid(self, form): self.sos.name = form.cleaned_data['name'] @@ -297,9 +300,15 @@ class AssembliesAllView(ConferenceRequiredMixin, TemplateView): def get_context_data(self, conf_slug, **kwargs): context = super().get_context_data(conf_slug=conf_slug, **kwargs) + user = self.request.user context['conf'] = self.conf - context['assemblies'] = Assembly.objects.conference_accessible(self.conf) + context['qfilter'] = kwargs.get('qfilter', None) + context['assemblies'] = Assembly.objects.accessible_by_user(user, self.conf) + if context['qfilter'] == 'official': + context['assemblies'] = context['assemblies'].filter(is_official=True) + elif context['qfilter'] == 'community': + context['assemblies'] = context['assemblies'].filter(is_official=False) context['my_favorite_assemblies'] = _session_get_favorite_assemblies(self.request.session, self.request.user) context['scope'] = 'assembly' return context @@ -365,8 +374,8 @@ class ProfileView(ConferenceRequiredMixin, UpdateView): def get_object(self, *args, **kwargs): return self.request.user - def get_context_data(self): - context = super().get_context_data() + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) context['conf'] = self.conf user = self.request.user @@ -693,6 +702,7 @@ class PersonalMessageSendView(ConferenceRequiredMixin, FormView): def get_initial(self): initial = super().get_initial() initial['recipient'] = self.kwargs.get('recipient', '') + initial['subject'] = self.request.POST.get('subject', self.request.GET.get('subject', '')) initial['in_reply_to'] = self.request.POST.get('in_reply_to', self.request.GET.get('in_reply_to', '')) return initial @@ -988,11 +998,12 @@ def _organize_events_for_calendar(conf, events): room_events[0]['event'].schedule_start if room_events[0]['type'] == 'event' else room_events[1]['event'].schedule_start for (_, room_events) in rooms_with_events ) - calendar_start = calendar_start.replace(minute=0, second=0, microsecond=0) + 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 = (calendar_end + timedelta(0, 3599)).replace(minute=0, second=0, microsecond=0) + calendar_end = localtime(calendar_end) # put timestamps from calendar_start to calendar_end in 30 minute steps into calendar_time_steps calendar_time_steps = [] @@ -1117,7 +1128,11 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): day = None context['day'] = day - curated = None if curated is None else curated == 'y' + # curated could be in genereal none (don't filter), false (only non curated), true (only curated) + # not easy to toggle three states with one button and beeing easy to display + # for now, the button says "curated only", so it does that. + # curated = None if curated is None else curated == 'y' + # context['curated'] = curated is not None context['curated'] = curated is not None if show_assembly_filters: @@ -1130,7 +1145,7 @@ class FahrplanView(ConferenceRequiredMixin, TemplateView): context['assembly'] = assembly = None if show_track_filters: - tracks = ConferenceTrack.objects.conference_accessible(self.conf) + tracks = ConferenceTrack.objects.filter(conference=self.conf, is_public=True) context['tracks'] = tracks track = tracks.get(slug=track) if track else None context['track'] = track @@ -1296,11 +1311,13 @@ class ReportContentView(ConferenceRequiredMixin, FormView): try: static_page = StaticPage.objects.conference_accessible(conference=self.conf).get(slug='report_content') except StaticPage.DoesNotExist: - static_page = StaticPage(conference=self.conf, title='Page Missing', rendered_body='Please configure the static page "report_content" (or change slug... ).') + static_page = StaticPage( + conference=self.conf, title='Page Missing', rendered_body='Please configure the static page "report_content" (or change slug... ).' + ) return super().get_context_data( conf=self.conf, - page = static_page, + page=static_page, **kwargs ) @@ -1318,8 +1335,13 @@ class ReportContentView(ConferenceRequiredMixin, FormView): def form_valid(self, form): form.send_report_mail(self.email_template_name, self.request) - messages.success(self.request, gettext("Thank you for your help to make this plattform safer and better! ") \ - + gettext("Please give us some time to find a solution and keep an eye on your Messages, we may contact you.")) + 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." + ) + ) redirect_to = form.cleaned_data['next'] url_is_safe = url_has_allowed_host_and_scheme( diff --git a/src/rc3platform/settings/base.py b/src/rc3platform/settings/base.py index b77169629854e5ac715c376c672ed60feb1f758a..4ef40a5bd8ac6d4c3c6a8be1a05980e8c602bfcf 100644 --- a/src/rc3platform/settings/base.py +++ b/src/rc3platform/settings/base.py @@ -67,6 +67,7 @@ MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'core.middleware.TimezoneMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -154,7 +155,7 @@ LANGUAGE_CODE = 'en-us' LANGUAGE_COOKIE_NAME = 'RC3_LANG' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Europe/Berlin' USE_I18N = True @@ -185,10 +186,16 @@ REST_FRAMEWORK = { ] } +# List of disallowed assembly slugs +FORBIDDEN_ASSEMBLY_SLUGS = ['visit', 'maps', 'api', 'pusher'] + # Mail configuration MAIL_REPLY_TO = [] SUPPORT_HTML_MAILS = False +# API access +API_USERS = [] + # SSO SSO_COOKIE_NAME = 'SSO_TOKEN' SSO_COOKIE_DOMAIN = '.localhost' @@ -200,8 +207,18 @@ SSO_SECRET = None PRETIX_ISSUER = 'tickets.events.ccc.de' # expected value in the 'iss' field of pretix PRETIX_SECRET_KEY = None # the JWT shared secret with Pretix +# ---------------------------------- # External Components Integration +# ---------------------------------- + +# BigBlueButton BIGBLUEBUTTON_API_URL = None BIGBLUEBUTTON_API_TOKEN = None +BIGBLUEBUTTON_END_MEETING_CALLBACK = None # url bbb will call to notify us of ending meetings +# BIGBLUEBUTTON_END_MEETING_CALLBACK = 'https://rc3.world/api/bbb_meeting_end' + +# Hangar HANGAR_URL = None + +# Workadventure WORKADVENTURE_URL_SCHEME = 'http://play.{assembly_slug}.localhost:8080/?u={username}'