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:
Raphael Michel
2021-11-19 14:59:35 +01:00
committed by GitHub
parent f6f3bbcce6
commit 34e4f7e0fc
8 changed files with 938 additions and 14 deletions

View File

@@ -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">&nbsp;</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>

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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):

View File

@@ -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();
});

View File

@@ -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;