diff --git a/src/plainui/jinja2.py b/src/plainui/jinja2.py index 7470c509f996be3834214c8a01e7fcad85c2ea9e..626f28b03f56134e76ee9c22e3e9ac2d022a6dd9 100644 --- a/src/plainui/jinja2.py +++ b/src/plainui/jinja2.py @@ -1,9 +1,10 @@ -from datetime import datetime +from datetime import datetime, timedelta from django.contrib.messages import get_messages from django.templatetags.static import static from django.urls import reverse from django.utils.formats import localize from django.utils.functional import LazyObject +from django.utils.html import json_script from django.utils.timezone import localdate, localtime from django.utils.translation import ugettext, ungettext, get_language from django.contrib.humanize.templatetags.humanize import NaturalTimeFormatter @@ -35,6 +36,17 @@ def custom_timedelta(tdelta): return NaturalTimeFormatter.string_for(tdelta) +def custom_timedelta_short(tdelta: timedelta): + if not isinstance(tdelta, timedelta): + return '' + s = tdelta.seconds + h = s // 3600 + s -= h * 3600 + m = s // 60 + s -= m * 60 + return '%02d:%02d:%02d' % (h, m, s) + + def custom_strftime(date): if not isinstance(date, datetime): return '' @@ -99,12 +111,14 @@ def environment(**options): 'css_scope': css_scope, 'get_language': get_language, 'get_messages': get_messages, + 'json_script': json_script, 'num_of_unread_messages': num_of_unread_messages, 'static': static, 'url': url, 'show_vars': show_vars, }) env.filters['strftdelta'] = custom_timedelta + env.filters['strftdelta_short'] = custom_timedelta_short 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/base.html b/src/plainui/jinja2/plainui/base.html index 883f9922dcdb98650639f8627806347175569c89..162b1da653c2d9bb148d5f4d2cf7b3cd2216589b 100644 --- a/src/plainui/jinja2/plainui/base.html +++ b/src/plainui/jinja2/plainui/base.html @@ -69,4 +69,5 @@ </div> </body> {# <script async src="/static/plainui/js/tools.js" /></script> #} + <script async src="{{ static('plainui/js/modal.js') }}"></script> </html> diff --git a/src/plainui/jinja2/plainui/components/calendar.html b/src/plainui/jinja2/plainui/components/calendar.html index fbd7af6555b8d0eb6b7f1c2b4c4d87fbfe750b99..8ae27820a15acaf427aaa95dd0fd96c920884f26 100644 --- a/src/plainui/jinja2/plainui/components/calendar.html +++ b/src/plainui/jinja2/plainui/components/calendar.html @@ -1,7 +1,7 @@ {% macro h(minutes) -%} {{minutes*2}}px {%- endmacro %} -{% macro calendar(events, my_favorite_events, my_scheduled_events, msg_none=_("No entries available.")) -%} +{% macro calendar(events, my_favorite_events, my_scheduled_events, msg_none=_("No entries available."), public=False) -%} {% if not events -%} {{ msg_none }} {%- else -%} @@ -24,7 +24,19 @@ <figure class="m-0 p-0 rc3-fahrplan__room-space" style="height: {{ h(entry.minutes) }}"></figure> {% else %} {% 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}}"> + <a class="text-decoration-none" {% if public %}onclick="fahrplanModal('{{entry.event.id}}')" href="#"{% else %}href="{{ url('plainui:event', conf_slug=conf.slug, event_slug=entry.event.slug) }}"{% endif %} title="{{entry.event.name}}"> + {%- if public %} + {{ json_script({ + 'title': entry.event.name | escape, + 'description_html': entry.event.description_html, + 'schedule_start': entry.event.schedule_start | strftime, + 'schedule_duration': entry.event.schedule_duration | strftdelta_short, + 'room_name': room.name | escape, + 'track_name': entry.event.track_name | escape, + 'language': entry.event.language | escape, + 'speakers': entry.event.speakers|join(', ', attribute='speaker_name') | escape + }, entry.event.id) }} + {% endif %} <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="mb-1 text-white rc3-fahrplan__event_title">{{entry.event.name}} {% if entry.event.language %} diff --git a/src/plainui/jinja2/plainui/components/event_info.html b/src/plainui/jinja2/plainui/components/event_info.html index 1731dadb9745d8f462ac7181043d5c9545244988..bfd5ec6b81599535c17b2fcc1806d22f8968f0cd 100644 --- a/src/plainui/jinja2/plainui/components/event_info.html +++ b/src/plainui/jinja2/plainui/components/event_info.html @@ -1,7 +1,7 @@ {% import "plainui/components/image.html" as imageMacro %} -{% macro eventInfo(event) -%} +{% macro eventInfo(event, speakers) -%} <div class="rc3-event-info"> <h2> {{ _("Event starts in") }} @@ -27,7 +27,7 @@ <dt>{{ _("Speakers") }}</dt> <dd> {% for speaker in speakers %} - {{ speaker.participant.first_name }} {{ speaker.participant.last_name }}{% if not loop.last %}, {% endif %} + {{ speaker.participant.username }}{% if not loop.last %}, {% endif %} {% else %} {{ _("No Speakers publicated yet") }} {% endfor %} diff --git a/src/plainui/jinja2/plainui/event.html b/src/plainui/jinja2/plainui/event.html index c54ace95a0d484be03291e4f2c93dba0f70fc8ff..2e2fbc8b1c51e63aaa98a9578be561155fadf0cc 100644 --- a/src/plainui/jinja2/plainui/event.html +++ b/src/plainui/jinja2/plainui/event.html @@ -26,7 +26,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 }}</a></p> {% endif %} - {{ eventInfoMacro.eventInfo(event=event) }} + {{ eventInfoMacro.eventInfo(event, speakers) }} {% if event.kind == 'official' %} {{ integrations.vocPlayer(vocLecture=event.slug) }} {% endif %} diff --git a/src/plainui/jinja2/plainui/public_fahrplan.html b/src/plainui/jinja2/plainui/public_fahrplan.html index ec539237ae473c75585def291bd91612c50d38c7..24f5d7bef13f346f581e9451f25d10d0bcfaaa29 100644 --- a/src/plainui/jinja2/plainui/public_fahrplan.html +++ b/src/plainui/jinja2/plainui/public_fahrplan.html @@ -10,6 +10,6 @@ {% if mode == 'list' %} {{ list_events.list(events, [], []) }} {% elif mode == 'calendar' %} - {{ calendar(events, [], []) }} + {{ calendar(events, [], [], public=True) }} {% endif %} {% endblock %} diff --git a/src/plainui/static/plainui/js/modal.js b/src/plainui/static/plainui/js/modal.js new file mode 100644 index 0000000000000000000000000000000000000000..857d8d2ae3601a174c16c7dcd5a578a93fa03a37 --- /dev/null +++ b/src/plainui/static/plainui/js/modal.js @@ -0,0 +1,59 @@ + +function showModal(title, content) { + let root = document.createElement('div'); + root.innerHTML = ` +<div class="modal show modal-close" tabindex="-1" style="display:block;"> + <div class="modal-dialog modal-lg"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">${title}</h5> + <button type="button" class="close modal-close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + ${content} + </div> + </div> + </div> +</div> +`; + let modal_backdrop = document.createElement('div'); + modal_backdrop.classList.add('modal-backdrop', 'show', 'modal-close') + document.body.appendChild(root); + document.body.appendChild(modal_backdrop); + + let hide = function() { + console.log('hide_fn'); + document.body.removeChild(root); + document.body.removeChild(modal_backdrop); + } + + modal_backdrop.addEventListener('click', hide); + let close_elements = root.getElementsByClassName('modal-close'); + for(let i = 0; i < close_elements.length; ++i) { + let el = close_elements[i]; + el.addEventListener('click', hide) + } + + root.getElementsByClassName('modal-dialog')[0].addEventListener('click', function(e) { e.stopPropagation() }); + + document.body.appendChild(root); +} + +function fahrplanModal(data_elem_id) { + let data_elem = document.getElementById(data_elem_id) + let data = JSON.parse(data_elem.innerText); + let body = ` + <div> + <p><b>Start time: </b>${data.schedule_start}</p> + <p><b>Duration: </b>${data.schedule_duration}</p> + <p><b>Room: </b>${data.room_name}</p> + <p><b>Track: </b>${data.track_name}</p> + <p><b>Language: </b>${data.language}</p> + <p><b>Speakers: </b>${data.speakers}</p> + </div> + <div class="rc3-markdown">${data.description_html}</div> + ` + showModal(data.title, body); +} diff --git a/src/plainui/static/plainui/js/tools.js b/src/plainui/static/plainui/js/tools.js index 38d09f3d7baa0319ea6199171d7ae6fb8c8077e1..5580500b19a3837b5580b90558fb43029ad9d51e 100644 --- a/src/plainui/static/plainui/js/tools.js +++ b/src/plainui/static/plainui/js/tools.js @@ -4,7 +4,7 @@ var docReady = (callback) => { else document.addEventListener("DOMContentLoaded", callback); } -docReady(() => { +docReady(() => { // one click listener to rule them all :) document.addEventListener('click',function onclicked(e){ documentClicked(e); diff --git a/src/plainui/styles/_bootstrap.scss b/src/plainui/styles/_bootstrap.scss index 48648eeeb306aa11759679bbe93ba8822459b147..46e5a433adab152dae3833fdb2997b2f4dee7fe3 100644 --- a/src/plainui/styles/_bootstrap.scss +++ b/src/plainui/styles/_bootstrap.scss @@ -36,9 +36,9 @@ @import "bootstrap/scss/progress"; @import "bootstrap/scss/media"; // @import "bootstrap/scss/list-group"; -// @import "bootstrap/scss/close"; + @import "bootstrap/scss/close"; // @import "bootstrap/scss/toasts"; -// @import "bootstrap/scss/modal"; + @import "bootstrap/scss/modal"; // @import "bootstrap/scss/tooltip"; // @import "bootstrap/scss/popover"; @import "bootstrap/scss/carousel"; diff --git a/src/plainui/styles/utils/_bootstrap-theme-assembly.scss b/src/plainui/styles/utils/_bootstrap-theme-assembly.scss index e020c53bb5cbdc7a331398aa9b3021cb78a573f3..87326e277c2232b389dba9ded30c2cdac72e69bf 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-assembly.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-assembly.scss @@ -62,3 +62,5 @@ $input-btn-focus-box-shadow: 0px 0px 3px 3px #FFFFFF, 0px 0px 2px 2px $tertiary, $btn-hover-text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.75); $box-shadow-morphism: 2px 2px 4px #000000, -2px -2px 4px #162D38, inset 0px 0px 15px #0E1D24; $btn-active-box-shadow: $input-btn-focus-box-shadow; + +$modal-content-bg: $gray-900; diff --git a/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss b/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss index 36724b90e02e8f71fa743b0808124b9585230226..06a87017317dd7847eaf0822b20fce44a88614bc 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-high-contrast.scss @@ -59,3 +59,5 @@ $input-btn-focus-box-shadow: 0px 0px 3px 3px #BAF0FF, 0px 0px 2px 2px $tertiary, $btn-hover-text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.75); $box-shadow-morphism: 2px 2px 4px #000000, -2px -2px 4px #1A1638, inset 2px 2px 8px $dark; $btn-active-box-shadow: $input-btn-focus-box-shadow; + +$modal-content-bg: $gray-900; diff --git a/src/plainui/styles/utils/_bootstrap-theme-plattform.scss b/src/plainui/styles/utils/_bootstrap-theme-plattform.scss index c23d459e867dd4f0bb15a36d0c250c1b87eee942..e6f291fab42b926ac6a4e17f9b24899a06399b33 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-plattform.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-plattform.scss @@ -63,3 +63,5 @@ $input-btn-focus-box-shadow: 0px 0px 3px 3px #BAF0FF, 0px 0px 2px 2px $tertiary, $btn-hover-text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.75); $box-shadow-morphism: 2px 2px 4px #000000, -2px -2px 4px #1A1638; $btn-active-box-shadow: $input-btn-focus-box-shadow; + +$modal-content-bg: $gray-900; diff --git a/src/plainui/styles/utils/_bootstrap-theme-world.scss b/src/plainui/styles/utils/_bootstrap-theme-world.scss index 75f2bba70906f0dd3a82ef94507692225aa70557..11223724351fe3f776752d4bc69162e36e6dca8d 100644 --- a/src/plainui/styles/utils/_bootstrap-theme-world.scss +++ b/src/plainui/styles/utils/_bootstrap-theme-world.scss @@ -55,3 +55,5 @@ $input-btn-focus-box-shadow: 0px 0px 3px 3px #BAF0FF, 0px 0px 2px 2px $tertiary, $btn-hover-text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.75); $box-shadow-morphism: 2px 2px 4px #000000, -2px -2px 4px #1A1638; $btn-active-box-shadow: $input-btn-focus-box-shadow; + +$modal-content-bg: $gray-900; diff --git a/src/plainui/views.py b/src/plainui/views.py index 5b97ccafa5aeaf169f464760452bc37f26915b11..aca532f9b3dfc85ab3d2007165f7cbaa536abfd2 100644 --- a/src/plainui/views.py +++ b/src/plainui/views.py @@ -24,7 +24,7 @@ from django.views.generic.edit import FormView, UpdateView from core import integrations from core.forms import PasswordResetForm from core.models import Assembly, AssemblyLikeCount, Badge, Conference, ConferenceMember, ConferenceMemberTicket, ConferenceTag, \ - ConferenceTrack, DirectMessage, Event, EventLikeCount, EventParticipant, PlatformUser, Room, RoomLink, StaticPage, TagItem, UserBadge + ConferenceTrack, DirectMessage, Event, EventAttachment, EventLikeCount, EventParticipant, PlatformUser, Room, RoomLink, StaticPage, TagItem, UserBadge from core.models.ticket import TicketValidationError from core.search import search from core.utils import render_markdown @@ -124,7 +124,7 @@ class EventView(ConferenceRequiredMixin, TemplateView): context = super().get_context_data(conf_slug=conf_slug, **kwargs) context['conf'] = self.conf - event = get_object_or_404(Event.objects.conference_accessible(self.conf), slug=event_slug) + event = get_object_or_404(Event.objects.conference_accessible(self.conf).select_related('assembly'), slug=event_slug) context['event'] = event favorites = _session_get_favorite_events(self.request.session, self.request.user) personal_calendar = _session_get_scheduled_events(self.request.session, self.request.user) @@ -133,7 +133,7 @@ class EventView(ConferenceRequiredMixin, TemplateView): context['is_favorite_events'] = favorites context['is_scheduled_events'] = personal_calendar context['events_upcoming'] = _event_filter(self.request.user, self.conf, upcoming=True) - context['speakers'] = event.participants.filter(is_public=True, role=EventParticipant.Role.SPEAKER) + context['speakers'] = event.participants.filter(is_public=True, role=EventParticipant.Role.SPEAKER).select_related('participant') context['tags'] = TagItem.objects.select_related('tag').filter( tag__is_public=True, target_type=ContentType.objects.get_for_model(Event), target_id=event.pk ) @@ -141,6 +141,11 @@ 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['attachments'] = EventAttachment.objects.filter( + event=event, + visibility__in=[EventAttachment.Visibility.PUBLIC, EventAttachment.Visibility.CONFERENCE] + ) context['scope'] = 'plattform' if event.kind == Event.Kind.OFFICIAL else 'assembly' return context