mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Add day calendar to organizer page (#2100)
Co-authored-by: Richard Schreiber <wiffbi@gmail.com> Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
<div class="day-calendar cal-size-{{ raster_to_shortest_ratio }}{% if no_headlines %} no-headlines{% endif %}"
|
||||
data-raster-size="{{ raster_size }}"
|
||||
data-duration="{{ calendar_duration }}"
|
||||
data-start="{{ start|date:"c" }}"
|
||||
data-timezone="{{ cal_tz }}">
|
||||
<h3 aria-hidden="true" class="day-row-name"><span hidden>{% trans "Time of day" %}</span></h3>
|
||||
<ul aria-hidden="true" class="day-timeline ticks">
|
||||
{% for t in time_ticks %}
|
||||
<li data-offset="{{ t.offset|date:"H:i" }}"
|
||||
data-duration="{{ t.duration|date:"H:i" }}"
|
||||
data-start="{{ t.start|date:"H:i" }}"
|
||||
class="text-muted">{{ t.start|date:"TIME_FORMAT" }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for series, collection in collections %}
|
||||
<h3 class="day-row-name">
|
||||
{% if series %}
|
||||
<a href="{% eventurl series "presale:event.index" %}">
|
||||
{{ series.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="sr-only">{% trans "Single events" context "day calendar" %}</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<ul class="day-timeline" data-concurrency="{{ collection.concurrency }}">
|
||||
{% for t in time_ticks %}
|
||||
{% if not forloop.counter|divisibleby:2 %}
|
||||
<li data-offset="{{ t.offset|date:"H:i" }}"
|
||||
data-duration="{{ t.duration|date:"H:i" }}" class="tick"> </li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for event in collection.events %}
|
||||
<li data-offset="{{ event.offset_rastered|date:"H:i" }}"
|
||||
data-offset-shift="{{ event.offset_shift_start }}:{{ event.offset_shift_end }}"
|
||||
data-duration="{{ event.duration_rastered }}"
|
||||
data-concurrency="{{ event.concurrency }}">
|
||||
<a class="event {% if event.continued %}continued{% else %} {% spaceless %}
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% if event.event.best_availability_state == 100 %}
|
||||
available
|
||||
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
|
||||
waitinglist
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
reserved
|
||||
{% elif event.event.best_availability_state < 20 %}
|
||||
soldout
|
||||
{% endif %}
|
||||
{% elif event.event.presale_is_running %}
|
||||
running
|
||||
{% elif event.event.presale_has_ended %}
|
||||
over
|
||||
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
|
||||
soon
|
||||
{% else %}
|
||||
soon
|
||||
{% endif %}
|
||||
{% endspaceless %}{% endif %}"
|
||||
href="{{ event.url }}">
|
||||
{% if show_names|default_if_none:True %}
|
||||
<span class="event-name">
|
||||
{{ event.event.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if not event.continued %}
|
||||
{% if event.time %}
|
||||
<span class="event-time" data-time="{{ event.event.date_from.isoformat }}"
|
||||
data-timezone="{{ event.timezone }}" data-time-short>
|
||||
<span class="fa fa-clock-o" aria-hidden="true"></span>
|
||||
{% if not show_names|default_if_none:True %}
|
||||
<strong>
|
||||
{% endif %}
|
||||
<time datetime="{{ event.time|date:"H:i" }}">{{ event.time|date:"TIME_FORMAT" }}</time>
|
||||
{% if event.time_end %}
|
||||
<span role="img" aria-label="{% trans "to" context "timerange" %}">–</span>
|
||||
<time datetime="{{ event.time_end|date:"H:i" }}">{{ event.time_end|date:"TIME_FORMAT" }}</time>
|
||||
{% endif %}
|
||||
{% if not show_names|default_if_none:True %}
|
||||
</strong>
|
||||
{% endif %}
|
||||
{% if multiple_timezones %}
|
||||
{{ event.timezone }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="event-status">
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% if event.event.best_availability_state == 100 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
|
||||
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Reserved" %}
|
||||
{% elif event.event.best_availability_state < 20 %}
|
||||
{% if event.event.has_paid_item %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sold out" %}
|
||||
{% else %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span>
|
||||
{% trans "Fully booked" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif event.event.presale_is_running %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
|
||||
{% elif event.event.presale_has_ended %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sale over" %}
|
||||
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span>
|
||||
{% blocktrans with start_date=event.event.presale_start|date:"SHORT_DATE_FORMAT" %}
|
||||
from {{ start_date }}
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Soon" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -20,21 +20,26 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12 text-left flip">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" %}" type="button" class="btn btn-default">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" "" %}" type="button" class="btn btn-default">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "old" "" "month" "" "year" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "week" "old" "" "month" "" "year" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "old" "" "week" "" %}"
|
||||
<a href="?{% url_replace request "style" "calendar" "old" "" "week" "" "date" "" %}"
|
||||
type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" %}"
|
||||
class="btn btn-default">
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
{% extends "pretixpresale/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Event overview" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% if organizer_homepage_text %}
|
||||
<div>
|
||||
{{ organizer_homepage_text | rich_text }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{{ date|date:"DATE_FORMAT" }}</h3>
|
||||
<form class="form-inline" method="get" id="monthselform"
|
||||
action="{% eventurl request.organizer "presale:organizer.index" %}">
|
||||
{% for f, v in request.GET.items %}
|
||||
{% if f != "date" %}
|
||||
<input type="hidden" name="{{ f }}" value="{{ v }}">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-sm-4 hidden-xs text-left flip">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "year" "" "date" "" %}"
|
||||
type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" "date" "" %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar-plus-o" aria-hidden="true"></span>
|
||||
{% trans "iCal" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-sm-4 col-xs-12 text-center">
|
||||
<input class="datepickerfield form-control" value="{{ date|date:"SHORT_DATE_FORMAT" }}" name="date">
|
||||
<button type="submit" class="js-hidden btn btn-default">
|
||||
{% trans "Go" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-4 hidden-xs text-right flip">
|
||||
{% if has_before %}
|
||||
<a href="?{% url_replace request "date" before.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-arrow-left" aria-hidden="true"></span>
|
||||
{{ before|date:"SHORT_DATE_FORMAT" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if has_after %}
|
||||
<a href="?{% url_replace request "date" after.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
{{ after|date:"SHORT_DATE_FORMAT" }}
|
||||
<span class="fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% include "pretixpresale/fragment_day_calendar.html" with show_avail=request.organizer.settings.event_list_availability %}
|
||||
<div class="col-sm-4 visible-xs text-center">
|
||||
{% if has_before %}
|
||||
<a href="?{% url_replace request "date" before.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-arrow-left" aria-hidden="true"></span>
|
||||
{{ before|date:"SHORT_DATE_FORMAT" }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if has_after %}
|
||||
<a href="?{% url_replace request "date" after.date.isoformat %}"
|
||||
class="btn btn-default">
|
||||
{{ after|date:"SHORT_DATE_FORMAT" }}
|
||||
<span class="fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if multiple_timezones %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
Note that the events in this view are in different timezones.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -21,22 +21,27 @@
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-6 col-xs-12 text-left flip">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "date" "" %}" type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "year" "" %}"
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "year" "" "date" "" %}"
|
||||
type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" %}"
|
||||
class="btn btn-default">
|
||||
|
||||
@@ -28,21 +28,26 @@
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "list" "week" "" "year" "" "month" "" "date" ""%}" type="button"
|
||||
class="btn btn-default active">
|
||||
<span class="fa fa-list" aria-hidden="true"></span>
|
||||
{% trans "List" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "page" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "week" "month" "" "old" "" "page" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Week" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "page" "" %}" type="button"
|
||||
<a href="?{% url_replace request "style" "calendar" "week" "" "old" "" "page" "" "date" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Month" %}
|
||||
</a>
|
||||
<a href="?{% url_replace request "style" "day" "week" "" "month" "" "old" "" "page" "" %}" type="button"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-th" aria-hidden="true"></span>
|
||||
{% trans "Day" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "page" "" "old" "" "week" "" "style" "" "month" "" "year" "" %}"
|
||||
class="btn btn-default">
|
||||
|
||||
@@ -31,13 +31,15 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import calendar
|
||||
import hashlib
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from functools import reduce
|
||||
from urllib.parse import quote
|
||||
|
||||
import dateutil
|
||||
import isoweek
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
@@ -376,6 +378,10 @@ class OrganizerIndex(OrganizerViewMixin, EventListMixin, ListView):
|
||||
cv = CalendarView()
|
||||
cv.request = request
|
||||
return cv.get(request, *args, **kwargs)
|
||||
elif style == "day":
|
||||
cv = DayCalendarView()
|
||||
cv.request = request
|
||||
return cv.get(request, *args, **kwargs)
|
||||
elif style == "week":
|
||||
cv = WeekCalendarView()
|
||||
cv.request = request
|
||||
@@ -441,6 +447,11 @@ def add_events_for_days(request, baseqs, before, after, ebd, timezones):
|
||||
)) and event.settings.show_times
|
||||
else None
|
||||
),
|
||||
'time_end_today': (
|
||||
datetime_to.time().replace(tzinfo=None)
|
||||
if date_to == d and event.settings.show_times
|
||||
else None
|
||||
),
|
||||
'url': eventreverse(event, 'presale:event.index'),
|
||||
'timezone': event.settings.timezone,
|
||||
})
|
||||
@@ -516,6 +527,11 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
|
||||
)) and s.show_times
|
||||
else None
|
||||
),
|
||||
'time_end_today': (
|
||||
datetime_to.time().replace(tzinfo=None)
|
||||
if date_to == d and s.show_times
|
||||
else None
|
||||
),
|
||||
'event': se,
|
||||
'url': (
|
||||
eventreverse(se.event, 'presale:event.redeem',
|
||||
@@ -714,6 +730,334 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
|
||||
return ebd
|
||||
|
||||
|
||||
class DayCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
|
||||
template_name = 'pretixpresale/organizers/calendar_day.html'
|
||||
|
||||
def _set_date_to_next_event(self):
|
||||
next_ev = filter_qs_by_attr(Event.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
Q(date_from__gte=now()) | Q(date_to__isnull=False, date_to__gte=now()),
|
||||
organizer=self.request.organizer,
|
||||
live=True,
|
||||
is_public=True,
|
||||
date_from__gte=now(),
|
||||
), self.request).order_by('date_from').first()
|
||||
next_sev = filter_qs_by_attr(SubEvent.objects.using(settings.DATABASE_REPLICA).filter(
|
||||
Q(date_from__gte=now()) | Q(date_to__isnull=False, date_to__gte=now()),
|
||||
event__organizer=self.request.organizer,
|
||||
event__is_public=True,
|
||||
event__live=True,
|
||||
active=True,
|
||||
is_public=True,
|
||||
), self.request).select_related('event').order_by('date_from').first()
|
||||
|
||||
datetime_from = None
|
||||
if (next_ev and next_sev and next_sev.date_from < next_ev.date_from) or (next_sev and not next_ev):
|
||||
datetime_from = next_sev.date_from
|
||||
next_ev = next_sev.event
|
||||
elif next_ev:
|
||||
datetime_from = next_ev.date_from
|
||||
|
||||
if datetime_from:
|
||||
self.tz = pytz.timezone(next_ev.settings.timezone)
|
||||
self.date = datetime_from.astimezone(self.tz).date()
|
||||
else:
|
||||
self.tz = self.request.organizer.timezone
|
||||
self.date = now().astimezone(self.tz).date()
|
||||
|
||||
def _set_date(self):
|
||||
if 'date' in self.request.GET:
|
||||
self.tz = self.request.organizer.timezone
|
||||
try:
|
||||
self.date = dateutil.parser.parse(self.request.GET.get('date')).date()
|
||||
except ValueError:
|
||||
self.date = now().astimezone(self.tz).date()
|
||||
else:
|
||||
self._set_date_to_next_event()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._set_date()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
before = datetime(
|
||||
self.date.year, self.date.month, self.date.day, 0, 0, 0, tzinfo=UTC
|
||||
) - timedelta(days=1)
|
||||
after = datetime(
|
||||
self.date.year, self.date.month, self.date.day, 0, 0, 0, tzinfo=UTC
|
||||
) + timedelta(days=1)
|
||||
|
||||
ctx['date'] = self.date
|
||||
ctx['cal_tz'] = self.tz
|
||||
ctx['before'] = before
|
||||
ctx['after'] = after
|
||||
|
||||
ctx['has_before'], ctx['has_after'] = has_before_after(
|
||||
self.request.organizer.events.filter(
|
||||
sales_channels__contains=self.request.sales_channel.identifier
|
||||
),
|
||||
SubEvent.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
event__is_public=True,
|
||||
event__live=True,
|
||||
event__sales_channels__contains=self.request.sales_channel.identifier
|
||||
),
|
||||
before,
|
||||
after,
|
||||
)
|
||||
|
||||
ebd = self._events_by_day(before, after)
|
||||
if not ebd[self.date]:
|
||||
return ctx
|
||||
|
||||
events = ebd[self.date]
|
||||
shortest_duration = self._get_shortest_duration(events).total_seconds() // 60
|
||||
# pick the next biggest tick_duration based on shortest_duration, max. 180 minutes
|
||||
tick_duration = next((d for d in [5, 10, 15, 30, 60, 120, 180] if d >= shortest_duration), 180)
|
||||
|
||||
raster_size = min(self._get_raster_size(events), tick_duration)
|
||||
events, start, end = self._rasterize_events(events, tick_duration=tick_duration, raster_size=raster_size)
|
||||
calendar_duration = self._get_time_duration(start, end)
|
||||
ctx["calendar_duration"] = self._format_duration(calendar_duration)
|
||||
ctx['time_ticks'] = self._get_time_ticks(start, end, tick_duration)
|
||||
ctx['start'] = datetime.combine(self.date, start)
|
||||
ctx['raster_size'] = raster_size
|
||||
# ctx['end'] = end
|
||||
# size of each grid-column is based on shortest event duration and raster_size
|
||||
# raster_size is based on start/end times, so it could happen we have a small raster but long running events
|
||||
# raster_size will always be smaller or equals tick_duration
|
||||
ctx['raster_to_shortest_ratio'] = round((8 * raster_size) / shortest_duration)
|
||||
|
||||
ctx['events'] = events
|
||||
|
||||
events_by_series = self._grid_for_template(events)
|
||||
ctx['collections'] = events_by_series
|
||||
ctx['no_headlines'] = not any([series for series, events in events_by_series])
|
||||
ctx['multiple_timezones'] = self._multiple_timezones
|
||||
return ctx
|
||||
|
||||
def _get_raster_size(self, events):
|
||||
# get best raster-size for min. # of columns in grid
|
||||
# due to grid-col-calculations in CSS raster_size cannot be bigger than 60 (minutes)
|
||||
|
||||
# all start- and end-times (minute-part) except full hour
|
||||
times = [
|
||||
e["time"].minute for e in events if e["time"] and e["time"].minute
|
||||
] + [
|
||||
e["time_end_today"].minute for e in events if "time_end_today" in e and e["time_end_today"] and e["time_end_today"].minute
|
||||
]
|
||||
if not times:
|
||||
# no time other than full hour, so raster can be 1 hour/60 minutes
|
||||
return 60
|
||||
gcd = reduce(math.gcd, set(times))
|
||||
return next((d for d in [5, 10, 15, 30, 60] if d >= gcd), 60)
|
||||
|
||||
def _get_time_duration(self, start, end):
|
||||
midnight = time(0, 0)
|
||||
return datetime.combine(
|
||||
self.date if end != midnight else self.date + timedelta(days=1),
|
||||
end
|
||||
) - datetime.combine(
|
||||
self.date,
|
||||
start
|
||||
)
|
||||
|
||||
def _format_duration(self, duration):
|
||||
return ":".join([
|
||||
"%02d" % i for i in (
|
||||
(duration.days * 24) + (duration.seconds // 3600),
|
||||
(duration.seconds // 60) % 60
|
||||
)
|
||||
])
|
||||
|
||||
def _floor_time(self, t, raster_size=5):
|
||||
# raster_size based on minutes, might be factored into a helper class with a timedelta as raster
|
||||
minutes = t.hour * 60 + t.minute
|
||||
if minutes % raster_size:
|
||||
minutes = (minutes // raster_size) * raster_size
|
||||
return t.replace(hour=minutes // 60, minute=minutes % 60)
|
||||
return t
|
||||
|
||||
def _ceil_time(self, t, raster_size=5):
|
||||
# raster_size based on minutes, might be factored into a helper class with a timedelta as raster
|
||||
minutes = t.hour * 60 + t.minute
|
||||
if not minutes % raster_size:
|
||||
return t
|
||||
minutes = math.ceil(minutes / raster_size) * raster_size
|
||||
minute = minutes % 60
|
||||
hour = minutes // 60
|
||||
if hour > 23:
|
||||
hour = hour % 24
|
||||
return t.replace(minute=minute, hour=hour)
|
||||
|
||||
def _rasterize_events(self, events, tick_duration, raster_size=5):
|
||||
rastered_events = []
|
||||
start, end = self._get_time_range(events)
|
||||
start = self._floor_time(start, raster_size=tick_duration)
|
||||
end = self._ceil_time(end, raster_size=tick_duration)
|
||||
|
||||
midnight = time(0, 0)
|
||||
for e in events:
|
||||
e["offset_shift_start"] = 0
|
||||
if e["continued"]:
|
||||
e["time_rastered"] = midnight
|
||||
elif e["time"].minute % raster_size:
|
||||
e["time_rastered"] = e["time"].replace(minute=(e["time"].minute // raster_size) * raster_size)
|
||||
e["offset_shift_start"] = e["time"].minute % raster_size
|
||||
else:
|
||||
e["time_rastered"] = e["time"]
|
||||
|
||||
e["offset_shift_end"] = 0
|
||||
if "time_end_today" in e and e["time_end_today"]:
|
||||
if e["time_end_today"].minute % raster_size:
|
||||
minute = math.ceil(e["time_end_today"].minute / raster_size) * raster_size
|
||||
hour = e["time_end_today"].hour
|
||||
if minute > 59:
|
||||
minute = minute % 60
|
||||
hour = (hour + 1) % 24
|
||||
e["time_end_today_rastered"] = e["time_end_today"].replace(minute=minute, hour=hour)
|
||||
e["offset_shift_end"] = raster_size - e["time_end_today"].minute % raster_size
|
||||
else:
|
||||
e["time_end_today_rastered"] = e["time_end_today"]
|
||||
else:
|
||||
e["time_end_today"] = e["time_end_today_rastered"] = time(0, 0)
|
||||
|
||||
e["duration_rastered"] = self._format_duration(datetime.combine(
|
||||
self.date if e["time_end_today_rastered"] != midnight else self.date + timedelta(days=1),
|
||||
e["time_end_today_rastered"]
|
||||
) - datetime.combine(
|
||||
self.date,
|
||||
e['time_rastered']
|
||||
))
|
||||
|
||||
e["offset_rastered"] = datetime.combine(self.date, time(0, 0)) + self._get_time_duration(start, e["time_rastered"])
|
||||
|
||||
rastered_events.append(e)
|
||||
|
||||
return rastered_events, start, end
|
||||
|
||||
def _get_shortest_duration(self, events):
|
||||
midnight = time(0, 0)
|
||||
durations = [
|
||||
datetime.combine(
|
||||
self.date if e.get('time_end_today') and e['time_end_today'] != midnight else self.date + timedelta(days=1),
|
||||
e['time_end_today'] if e.get('time_end_today') else time(0, 0)
|
||||
)
|
||||
-
|
||||
datetime.combine(
|
||||
self.date,
|
||||
time(0, 0) if e['continued'] else e['time']
|
||||
)
|
||||
for e in events
|
||||
]
|
||||
return min([d for d in durations])
|
||||
|
||||
def _get_time_range(self, events):
|
||||
if any(e['continued'] for e in events) or any(e['time'] is None for e in events):
|
||||
starting_at = time(0, 0)
|
||||
else:
|
||||
starting_at = min(e['time'] for e in events)
|
||||
|
||||
if any(e.get('time_end_today') is None for e in events):
|
||||
ending_at = time(0, 0)
|
||||
else:
|
||||
ending_at = max(e['time_end_today'] for e in events)
|
||||
|
||||
return starting_at, ending_at
|
||||
|
||||
def _get_time_ticks(self, start, end, tick_duration):
|
||||
ticks = []
|
||||
tick_duration = timedelta(minutes=tick_duration)
|
||||
|
||||
# convert time to datetime for timedelta calc
|
||||
start = datetime.combine(self.date, start)
|
||||
end = datetime.combine(self.date, end)
|
||||
if end <= start:
|
||||
end = end + timedelta(days=1)
|
||||
|
||||
tick_start = start
|
||||
offset = datetime.utcfromtimestamp(0)
|
||||
duration = datetime.utcfromtimestamp(tick_duration.total_seconds())
|
||||
while tick_start < end:
|
||||
tick = {
|
||||
"start": tick_start,
|
||||
"duration": duration,
|
||||
"offset": offset,
|
||||
}
|
||||
ticks.append(tick)
|
||||
tick_start += tick_duration
|
||||
offset += tick_duration
|
||||
|
||||
return ticks
|
||||
|
||||
def _grid_for_template(self, events):
|
||||
midnight = time(0, 0)
|
||||
rows_by_collection = defaultdict(list)
|
||||
|
||||
# We sort the events into "collections": all subevents from the same
|
||||
# event series together and all non-series events into a "None"
|
||||
# collection. Then, we look if there's already an event in the
|
||||
# collection that overlaps, in which case we need to split the
|
||||
# collection into multiple rows.
|
||||
for counter, e in enumerate(events):
|
||||
collection = e['event'].event if isinstance(e['event'], SubEvent) else None
|
||||
|
||||
placed_in_row = False
|
||||
for row in rows_by_collection[collection]:
|
||||
if any(
|
||||
(e['time_rastered'] < o['time_end_today_rastered'] or o['time_end_today_rastered'] == midnight) and
|
||||
(o['time_rastered'] < e['time_end_today_rastered'] or e['time_end_today_rastered'] == midnight)
|
||||
for o in row
|
||||
):
|
||||
continue
|
||||
row.append(e)
|
||||
placed_in_row = True
|
||||
break
|
||||
|
||||
if not placed_in_row:
|
||||
rows_by_collection[collection].append([e])
|
||||
|
||||
# flatten rows to one stream of events with attribute row
|
||||
# for better keyboard-tab-order in html
|
||||
for collection in rows_by_collection:
|
||||
for i, row in enumerate(rows_by_collection[collection]):
|
||||
concurrency = i + 1
|
||||
for e in row:
|
||||
e["concurrency"] = concurrency
|
||||
rows_by_collection[collection] = {
|
||||
"concurrency": len(rows_by_collection[collection]),
|
||||
"events": sorted([e for row in rows_by_collection[collection] for e in row], key=lambda d: d['time'] or time(0, 0)),
|
||||
}
|
||||
|
||||
def sort_key(c):
|
||||
collection, row = c
|
||||
if collection is None:
|
||||
return ''
|
||||
else:
|
||||
return str(collection.name)
|
||||
return sorted(rows_by_collection.items(), key=sort_key)
|
||||
|
||||
def _events_by_day(self, before, after):
|
||||
ebd = defaultdict(list)
|
||||
timezones = set()
|
||||
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
|
||||
settings.DATABASE_REPLICA
|
||||
).filter(
|
||||
sales_channels__contains=self.request.sales_channel.identifier
|
||||
), before, after, ebd, timezones)
|
||||
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
event__is_public=True,
|
||||
event__live=True,
|
||||
event__sales_channels__contains=self.request.sales_channel.identifier
|
||||
).prefetch_related(
|
||||
'event___settings_objects', 'event__organizer___settings_objects'
|
||||
)), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
|
||||
self._multiple_timezones = len(timezones) > 1
|
||||
return ebd
|
||||
|
||||
|
||||
@method_decorator(cache_page(300), name='dispatch')
|
||||
class OrganizerIcalDownload(OrganizerViewMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -315,7 +315,11 @@ $(function () {
|
||||
$("#monthselform select").change(function () {
|
||||
$(this).closest("form").get(0).submit();
|
||||
});
|
||||
|
||||
$("#monthselform input").on("dp.change", function () {
|
||||
if ($(this).data("DateTimePicker")) { // prevent submit after dp init
|
||||
this.form.submit();
|
||||
}
|
||||
});
|
||||
var update_cart_form = function () {
|
||||
var is_enabled = $(".product-row input[type=checkbox]:checked, .variations input[type=checkbox]:checked, .product-row input[type=radio]:checked, .variations input[type=radio]:checked").length;
|
||||
if (!is_enabled) {
|
||||
@@ -468,9 +472,11 @@ $(function () {
|
||||
$("span[data-timezone], small[data-timezone]").each(function() {
|
||||
var t = moment.tz($(this).attr("data-time"), $(this).attr("data-timezone"))
|
||||
var tz = moment.tz.zone($(this).attr("data-timezone"))
|
||||
var tpl = '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner text-nowrap"></div></div>';
|
||||
|
||||
$(this).tooltip({
|
||||
'title': gettext("Time zone:") + " " + tz.abbr(t)
|
||||
"title": gettext("Time zone:") + " " + tz.abbr(t),
|
||||
"template": tpl
|
||||
});
|
||||
if (t.tz(tz.name).format() !== t.tz(local_tz).format()) {
|
||||
var $add = $("<span>")
|
||||
@@ -488,7 +494,8 @@ $(function () {
|
||||
}
|
||||
$add.insertAfter($(this));
|
||||
$add.tooltip({
|
||||
'title': gettext("Time zone:") + " " + moment.tz.zone(local_tz).abbr(t),
|
||||
"title": gettext("Time zone:") + " " + moment.tz.zone(local_tz).abbr(t),
|
||||
"template": tpl
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -501,6 +508,80 @@ $(function () {
|
||||
});
|
||||
}
|
||||
|
||||
// Day calendar
|
||||
$(".day-calendar [data-concurrency]").each(function() {
|
||||
var c = parseInt(this.getAttribute("data-concurrency"), 10);
|
||||
if (c > 9) this.style.setProperty('--concurrency', c);
|
||||
})
|
||||
$(".day-calendar").each(function() {
|
||||
var timezone = this.getAttribute("data-timezone");
|
||||
var startTime = moment.tz(this.getAttribute("data-start"), timezone);
|
||||
|
||||
var currentTime = moment().tz(timezone);
|
||||
if (!currentTime.isSame(startTime, 'day')) {
|
||||
// Not on same day
|
||||
return;
|
||||
}
|
||||
|
||||
// scroll to best matching tick
|
||||
var currentTimeCmp = parseInt(currentTime.format("Hmm"), 10);
|
||||
var ticks = this.querySelectorAll(".ticks li");
|
||||
var currentTick;
|
||||
var t;
|
||||
for (var i=0, max=ticks.length; i < max; i++) {
|
||||
currentTick = ticks[i]
|
||||
t = parseInt(currentTick.getAttribute("data-start").replace(":", ""), 10);
|
||||
if (t > currentTimeCmp) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentTick.scrollIntoView({behavior:"smooth", inline: "center"});
|
||||
|
||||
|
||||
var thisCalendar = this;
|
||||
var currentTimeInterval;
|
||||
|
||||
var timeFormat = document.body.getAttribute("data-timeformat");
|
||||
var timeFormatParts = timeFormat.match(/([a-zA-Z_\s]+)([^a-zA-Z_\s])(.*)/);
|
||||
if (!timeFormatParts) timeFormatParts = [timeFormat];
|
||||
if (timeFormatParts.length > 1) timeFormatParts.shift();
|
||||
var currentTimeBar = $('<div class="current-time-bar" aria-hidden="true"><time></time></div>').appendTo(this);
|
||||
var currentTimeDisplay = currentTimeBar.find("time");
|
||||
var currentTimeDisplayParts = [];
|
||||
timeFormatParts.forEach(function(format) {
|
||||
currentTimeDisplayParts.push([format, $("<span></span>").appendTo(currentTimeDisplay)])
|
||||
});
|
||||
var duration = this.getAttribute("data-duration").split(":").reduce(function(previousValue, currentValue, currentIndex) {
|
||||
return previousValue + (currentIndex ? parseInt(currentValue, 10) * 60 : parseInt(currentValue, 10) * 60 * 60);
|
||||
}, 0);
|
||||
function setCurrentTimeBar() {
|
||||
var currentTime = moment().tz(timezone);
|
||||
var currentTimeDelta = Math.floor((currentTime - startTime)/1000);
|
||||
if (currentTimeDelta < 0 || currentTimeDelta > duration) {
|
||||
// Too early || Too late
|
||||
window.clearInterval(currentTimeInterval);
|
||||
currentTimeBar.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = thisCalendar.querySelector("h3").getBoundingClientRect().width;
|
||||
var dx = Math.round(offset + (thisCalendar.scrollWidth-offset)*(currentTimeDelta/duration));
|
||||
currentTimeDisplayParts.forEach(function(part) {
|
||||
part[1].text(currentTime.format(part[0]));
|
||||
});
|
||||
if (currentTimeDisplay.get(0).getBoundingClientRect().width + dx >= thisCalendar.scrollWidth) {
|
||||
currentTimeBar.addClass("swap-side");
|
||||
}
|
||||
else {
|
||||
currentTimeBar.removeClass("swap-side");
|
||||
}
|
||||
thisCalendar.style.setProperty('--current-time-offset', dx + "px");
|
||||
}
|
||||
currentTimeInterval = window.setInterval(setCurrentTimeBar, 15*1000);
|
||||
$(window).on("resize", setCurrentTimeBar);
|
||||
setCurrentTimeBar();
|
||||
});
|
||||
|
||||
// Lightbox
|
||||
lightbox.init();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.table-calendar, .week-calendar {
|
||||
.table-calendar, .week-calendar, .day-calendar {
|
||||
td, th {
|
||||
width: 14.29%;
|
||||
}
|
||||
@@ -113,6 +113,266 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.day-calendar {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 7fr;
|
||||
gap: 0;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 16px; /* use padding to make space for link focus-outline and overlayed scrollbar */
|
||||
}
|
||||
.cal-size-8 {--col-min-size: 9em}
|
||||
.cal-size-7 {--col-min-size: 7.8em}
|
||||
.cal-size-6 {--col-min-size: 6.6em}
|
||||
.cal-size-5 {--col-min-size: 5.4em}
|
||||
.cal-size-4 {--col-min-size: 4.2em}
|
||||
.cal-size-3 {--col-min-size: 3em}
|
||||
.cal-size-2 {--col-min-size: 1.8em}
|
||||
.cal-size-1 {--col-min-size: 0.6em}
|
||||
.cal-size-0 {--col-min-size: 0.6em}
|
||||
|
||||
.day-calendar .current-time-bar {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: -2px;
|
||||
height: calc(100% - 10px); /* remove 5px padding for link focus-outline */
|
||||
width: 1px;
|
||||
background-color: $brand-primary;
|
||||
outline: 1px solid #fff;
|
||||
transform: translateX(var(--current-time-offset, 0));
|
||||
}
|
||||
[dir=rtl] .day-calendar .current-time-bar {
|
||||
left: auto;
|
||||
right: -2px;
|
||||
transform: translateX(calc(-1 * var(--current-time-offset, 0)));
|
||||
}
|
||||
.day-calendar .current-time-bar time {
|
||||
background: $brand-primary;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
color: #fff;
|
||||
padding: 1px 3px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
.day-calendar .current-time-bar time,
|
||||
[dir=rtl] .day-calendar .current-time-bar.swap-side time {
|
||||
left: 0;
|
||||
}
|
||||
.day-calendar .current-time-bar.swap-side time,
|
||||
[dir=rtl] .day-calendar .current-time-bar time {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
.day-calendar.no-headlines {
|
||||
display: block;
|
||||
}
|
||||
.day-calendar.no-headlines h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.day-calendar .current-time-bar time span:nth-child(2) {
|
||||
animation: watch-blinker 2s step-end infinite;
|
||||
}
|
||||
@-webkit-keyframes watch-blinker {
|
||||
0% { visibility: visible; }
|
||||
75% { visibility: hidden; }
|
||||
}
|
||||
|
||||
@-moz-keyframes watch-blinker {
|
||||
0% { visibility: visible; }
|
||||
75% { visibility: hidden; }
|
||||
}
|
||||
|
||||
@keyframes watch-blinker {
|
||||
0% { visibility: visible; }
|
||||
75% { visibility: hidden; }
|
||||
}
|
||||
|
||||
.day-row-name {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 3px 8px 3px 0;
|
||||
background-color: rgba(255,255,255,.9);
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.day-calendar a.event {
|
||||
margin: 1px;
|
||||
}
|
||||
.day-calendar a.event .event-time {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
[data-raster-size="5"] {--raster-size: 5}
|
||||
[data-raster-size="10"] {--raster-size: 10}
|
||||
[data-raster-size="15"] {--raster-size: 15}
|
||||
[data-raster-size="30"] {--raster-size: 30}
|
||||
[data-raster-size="60"] {--raster-size: 60}
|
||||
|
||||
/*
|
||||
concurrency has two meanings:
|
||||
on .day-timeline it means total nummber concurrently running events at any given time
|
||||
on .day-timeline>li it means the current concurrency of this li-element
|
||||
if concurrency is higher than 9, JavaScript (currently in pretixpresale/js/ui/main.js) sets --concurrency accordingly
|
||||
*/
|
||||
[data-concurrency="1"] {--concurrency: 1}
|
||||
[data-concurrency="2"] {--concurrency: 2}
|
||||
[data-concurrency="3"] {--concurrency: 3}
|
||||
[data-concurrency="4"] {--concurrency: 4}
|
||||
[data-concurrency="5"] {--concurrency: 5}
|
||||
[data-concurrency="6"] {--concurrency: 6}
|
||||
[data-concurrency="7"] {--concurrency: 7}
|
||||
[data-concurrency="8"] {--concurrency: 8}
|
||||
[data-concurrency="9"] {--concurrency: 9}
|
||||
|
||||
|
||||
[data-offset^="01"] {--offset-hour: 1}
|
||||
[data-offset^="02"] {--offset-hour: 2}
|
||||
[data-offset^="03"] {--offset-hour: 3}
|
||||
[data-offset^="04"] {--offset-hour: 4}
|
||||
[data-offset^="05"] {--offset-hour: 5}
|
||||
[data-offset^="06"] {--offset-hour: 6}
|
||||
[data-offset^="07"] {--offset-hour: 7}
|
||||
[data-offset^="08"] {--offset-hour: 8}
|
||||
[data-offset^="09"] {--offset-hour: 9}
|
||||
[data-offset^="10"] {--offset-hour: 10}
|
||||
[data-offset^="11"] {--offset-hour: 11}
|
||||
[data-offset^="12"] {--offset-hour: 12}
|
||||
[data-offset^="13"] {--offset-hour: 13}
|
||||
[data-offset^="14"] {--offset-hour: 14}
|
||||
[data-offset^="15"] {--offset-hour: 15}
|
||||
[data-offset^="16"] {--offset-hour: 16}
|
||||
[data-offset^="17"] {--offset-hour: 17}
|
||||
[data-offset^="18"] {--offset-hour: 18}
|
||||
[data-offset^="19"] {--offset-hour: 19}
|
||||
[data-offset^="20"] {--offset-hour: 20}
|
||||
[data-offset^="21"] {--offset-hour: 21}
|
||||
[data-offset^="22"] {--offset-hour: 22}
|
||||
[data-offset^="23"] {--offset-hour: 23}
|
||||
[data-offset^="24"] {--offset-hour: 24}
|
||||
|
||||
[data-offset$="05"] {--offset-minute: 5}
|
||||
[data-offset$="10"] {--offset-minute: 10}
|
||||
[data-offset$="15"] {--offset-minute: 15}
|
||||
[data-offset$="20"] {--offset-minute: 20}
|
||||
[data-offset$="25"] {--offset-minute: 25}
|
||||
[data-offset$="30"] {--offset-minute: 30}
|
||||
[data-offset$="35"] {--offset-minute: 35}
|
||||
[data-offset$="40"] {--offset-minute: 40}
|
||||
[data-offset$="45"] {--offset-minute: 45}
|
||||
[data-offset$="50"] {--offset-minute: 50}
|
||||
[data-offset$="55"] {--offset-minute: 55}
|
||||
|
||||
/* minute based micro adjustment offset from 5-minute raster */
|
||||
[data-offset-shift^="1"] {--offset-start: 1}
|
||||
[data-offset-shift^="2"] {--offset-start: 2}
|
||||
[data-offset-shift^="3"] {--offset-start: 3}
|
||||
[data-offset-shift^="4"] {--offset-start: 4}
|
||||
|
||||
[data-offset-shift$="1"] {--offset-end: 1}
|
||||
[data-offset-shift$="2"] {--offset-end: 2}
|
||||
[data-offset-shift$="3"] {--offset-end: 3}
|
||||
[data-offset-shift$="4"] {--offset-end: 4}
|
||||
|
||||
|
||||
[data-duration^="00"] {--duration-hour: 0}
|
||||
[data-duration^="01"] {--duration-hour: 1}
|
||||
[data-duration^="02"] {--duration-hour: 2}
|
||||
[data-duration^="03"] {--duration-hour: 3}
|
||||
[data-duration^="04"] {--duration-hour: 4}
|
||||
[data-duration^="05"] {--duration-hour: 5}
|
||||
[data-duration^="06"] {--duration-hour: 6}
|
||||
[data-duration^="07"] {--duration-hour: 7}
|
||||
[data-duration^="08"] {--duration-hour: 8}
|
||||
[data-duration^="09"] {--duration-hour: 9}
|
||||
[data-duration^="10"] {--duration-hour: 10}
|
||||
[data-duration^="11"] {--duration-hour: 11}
|
||||
[data-duration^="12"] {--duration-hour: 12}
|
||||
[data-duration^="13"] {--duration-hour: 13}
|
||||
[data-duration^="14"] {--duration-hour: 14}
|
||||
[data-duration^="15"] {--duration-hour: 15}
|
||||
[data-duration^="16"] {--duration-hour: 16}
|
||||
[data-duration^="17"] {--duration-hour: 17}
|
||||
[data-duration^="18"] {--duration-hour: 18}
|
||||
[data-duration^="19"] {--duration-hour: 19}
|
||||
[data-duration^="20"] {--duration-hour: 20}
|
||||
[data-duration^="21"] {--duration-hour: 21}
|
||||
[data-duration^="22"] {--duration-hour: 22}
|
||||
[data-duration^="23"] {--duration-hour: 23}
|
||||
[data-duration^="24"] {--duration-hour: 24}
|
||||
|
||||
[data-duration$="00"] {--duration-minute: 0}
|
||||
[data-duration$="05"] {--duration-minute: 5}
|
||||
[data-duration$="10"] {--duration-minute: 10}
|
||||
[data-duration$="15"] {--duration-minute: 15}
|
||||
[data-duration$="20"] {--duration-minute: 20}
|
||||
[data-duration$="25"] {--duration-minute: 25}
|
||||
[data-duration$="30"] {--duration-minute: 30}
|
||||
[data-duration$="35"] {--duration-minute: 35}
|
||||
[data-duration$="40"] {--duration-minute: 40}
|
||||
[data-duration$="45"] {--duration-minute: 45}
|
||||
[data-duration$="50"] {--duration-minute: 50}
|
||||
[data-duration$="55"] {--duration-minute: 55}
|
||||
|
||||
.day-timeline {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: grid;
|
||||
gap: 0;
|
||||
--grid-cols: calc(var(--duration-hour, 0) * 60/var(--raster-size, 5) + var(--duration-minute, 0) / var(--raster-size, 5));
|
||||
grid-template-columns: repeat(var(--grid-cols), minmax(var(--col-min-size, 3em), 1fr));
|
||||
grid-template-rows: repeat(var(--concurrency, 1), auto);
|
||||
}
|
||||
|
||||
.day-timeline > li {
|
||||
grid-column: calc(1 + var(--offset-hour, 0)*60/var(--raster-size, 5) + var(--offset-minute, 0)/var(--raster-size, 5)) / span calc(var(--duration-hour, 0)*60/var(--raster-size, 5) + var(--duration-minute, 0)/var(--raster-size, 5));
|
||||
grid-row: var(--concurrency, 1) / span 1;
|
||||
}
|
||||
.day-timeline > li:focus-within {
|
||||
z-index: 2; /* move to front so focus-outline overlays other elements */
|
||||
}
|
||||
|
||||
.ticks li, .tick {
|
||||
grid-row: 1 / span var(--concurrency, 1) !important;
|
||||
}
|
||||
.ticks li {
|
||||
padding: 8px 3px 5px;
|
||||
line-height: 1;
|
||||
border-top-left-radius: $border-radius-base;
|
||||
border-top-right-radius: $border-radius-base;
|
||||
}
|
||||
.ticks li:nth-of-type(odd), .tick {
|
||||
background-color: $table-bg-accent;
|
||||
}
|
||||
.day-timeline:nth-last-of-type(1) li {
|
||||
border-bottom-left-radius: $border-radius-base;
|
||||
border-bottom-right-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.day-calendar a.event {
|
||||
--duration: calc(var(--duration-hour, 0)*60 + var(--duration-minute));
|
||||
margin-left: calc(var(--offset-start, 0) / var(--duration) * 100%);
|
||||
margin-right: calc(var(--offset-end, 0) / var(--duration) * 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media (min-width: $screen-md-min) {
|
||||
.week-calendar {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user