2024-10-05 18:57:07 +02:00

333 lines
11 KiB
Python

from datetime import datetime, timedelta, date, time
import calendar
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.template.defaultfilters import date as _date
import logging
logger = logging.getLogger(__name__)
def daterange(start, end, step=timedelta(1)):
if end is None:
yield start
else:
curr = start
while curr <= end:
yield curr
curr += step
class DayInCalendar:
midnight = time(0, 0, 0)
def __init__(self, d, on_requested_interval=True):
self.date = d
now = date.today()
self.week = d.isocalendar()[1]
self.in_past = d < now
self.today = d == now
self.tomorrow = d == now + timedelta(days=+1)
self.events = []
self.on_requested_interval = on_requested_interval
self.events_by_category = {}
self.time_intervals = None
self.id = d.strftime('%Y-%m-%d')
def is_in_past(self):
return self.in_past
def is_today(self):
return self.today
def is_tomorrow(self):
return self.tomorrow
def is_ancestor_uuid_event_from_other(self, event):
for e in self.events:
if event.is_ancestor_by_uuid(e):
return True
return False
def remove_event_with_ancestor_uuid_if_exists(self, event):
removed = False
for i, e in enumerate(self.events):
if e.is_ancestor_by_uuid(event):
# remove e from events_by_category
for k, v in self.events_by_category.items():
if e in v:
self.events_by_category[k].remove(e)
# remove e from the events
del self.events[i]
removed = True
if removed:
# remove empty events_by_category
self.events_by_category = dict(
[(k, v) for k, v in self.events_by_category.items() if len(v) > 0]
)
def add_event(self, event):
if event.contains_date(self.date):
if self.is_ancestor_uuid_event_from_other(event):
# we do not add a generic event if a specific is already present
pass
else:
self.remove_event_with_ancestor_uuid_if_exists(event)
self._add_event_internal(event)
def _add_event_internal(self, event):
from .models import Category
from copy import deepcopy
# copy event
local_event = deepcopy(event)
# set values
if local_event.start_day != self.date:
local_event.start_day = self.date
local_event.start_time = None
if local_event.end_day != self.date:
local_event.end_day = None
local_event.end_time = None
# add event to the day
self.events.append(local_event)
# add in its category
if local_event.category is None:
cat = Category.default_name
else:
cat = local_event.category.name
if cat not in self.events_by_category:
self.events_by_category[cat] = []
self.events_by_category[cat].append(local_event)
def filter_events(self):
self.events.sort(
key=lambda e: DayInCalendar.midnight
if e.start_time is None
else e.start_time
)
def events_by_category_ordered(self):
from .models import Category
cats = Category.objects.order_by('position')
result = []
for c in cats:
if c.name in self.events_by_category:
result.append((c.name, self.events_by_category[c.name]))
return result
def build_time_intervals(self, all_day_name, all_day_short_name, interval_names, interval_short_names, interval_markers):
self.time_intervals = [IntervalInDay(self.date, i, n[0], n[1]) for i, n in
enumerate(zip([all_day_name] + interval_names, [all_day_short_name] + interval_short_names))]
nm2 = datetime.now() + timedelta(hours=-2)
for e in self.events:
if e.start_time is None:
self.time_intervals[0].add_event(e)
else:
dt = datetime.combine(e.start_day, e.start_time)
if dt >= nm2:
ok = False
for i in range(len(interval_markers)):
if dt < interval_markers[i]:
self.time_intervals[i + 1].add_event(e)
ok = True
break
if not ok:
self.time_intervals[-1].add_event(e)
def get_time_intervals(self):
if self.time_intervals is None:
if self.is_today():
all_day_name = _('All day today')
interval_names = [_('This morning'), _('This noon'), _('This afternoon'), _('This evening')]
elif self.is_tomorrow():
name = _("Tomorrow")
all_day_name = _('All day tomorrow')
interval_names = [_('%s morning') % name, _('%s noon') % name, _('%s afternoon') % name, _('%s evening') % name]
else:
name = _date(self.date, "l")
all_day_name = _('All day %s') % name
interval_names = [_('%s morning') % name, _('%s noon') % name, _('%s afternoon') % name, _('%s evening') % name]
all_day_short_name = _('All day')
interval_short_names = [_('Morning'), _('Noon'), _('Afternoon'), _('Evening')]
interval_markers = [datetime.combine(self.date, time(h, m)) for h, m in [(11, 30), (13, 0), (18, 0)]]
self.build_time_intervals(all_day_name, all_day_short_name, interval_names, interval_short_names, interval_markers)
return self.time_intervals
class IntervalInDay(DayInCalendar):
def __init__(self, d, id, name, short_name):
self.name = name
self.short_name = short_name
super().__init__(d)
self.id = self.id + '-' + str(id)
class CalendarList:
def __init__(self, firstdate, lastdate, filter=None, exact=False):
self.firstdate = firstdate
self.lastdate = lastdate
self.now = date.today()
self.filter = filter
if exact:
self.c_firstdate = self.firstdate
self.c_lastdate = self.lastdate
else:
# start the first day of the first week
self.c_firstdate = firstdate + timedelta(days=-firstdate.weekday())
# end the last day of the last week
self.c_lastdate = lastdate + timedelta(days=6 - lastdate.weekday())
self.calendar_days = None
def build_internal(self):
# create a list of DayInCalendars
self.create_calendar_days()
# fill DayInCalendars with events
self.fill_calendar_days()
# finally, sort each DayInCalendar
for i, c in self.calendar_days.items():
c.filter_events()
def get_calendar_days(self):
if self.calendar_days is None:
self.build_internal()
return self.calendar_days
def today_in_calendar(self):
return self.firstdate <= self.now and self.lastdate >= self.now
def all_in_past(self):
return self.lastdate < self.now
def fill_calendar_days(self):
if self.filter is None:
from .models import Event
qs = Event.objects.all()
else:
qs = self.filter.qs
startdatetime = timezone.make_aware(datetime.combine(self.c_firstdate, time.min), timezone.get_default_timezone())
lastdatetime = timezone.make_aware(datetime.combine(self.c_lastdate, time.max), timezone.get_default_timezone())
self.events = qs.filter(
(Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime))
| (
Q(recurrence_dtend__isnull=False)
& ~(
Q(recurrence_dtstart__gt=lastdatetime)
| Q(recurrence_dtend__lt=startdatetime)
)
)
).order_by("start_time").prefetch_related("exact_location").prefetch_related("category")
firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
firstdate = timezone.make_aware(firstdate, timezone.get_default_timezone())
lastdate = datetime.fromordinal(self.c_lastdate.toordinal())
if lastdate.tzinfo is None or lastdate.tzinfo.utcoffset(lastdate) is None:
lastdate = timezone.make_aware(lastdate, timezone.get_default_timezone())
for e in self.events:
for e_rec in e.get_recurrences_between(firstdate, lastdate):
for d in daterange(e_rec.start_day, e_rec.end_day):
if d.__str__() in self.calendar_days:
self.calendar_days[d.__str__()].add_event(e_rec)
def create_calendar_days(self):
# create daylist
self.calendar_days = {}
for d in daterange(self.c_firstdate, self.c_lastdate):
self.calendar_days[d.strftime("%Y-%m-%d")] = DayInCalendar(
d, d >= self.firstdate and d <= self.lastdate
)
def is_single_week(self):
return hasattr(self, "week")
def is_full_month(self):
return hasattr(self, "month")
def calendar_days_list(self):
return list(self.get_calendar_days().values())
def time_intervals_list(self, onlyfirst=False):
ds = self.calendar_days_list()
result = []
for d in ds:
tis = d.get_time_intervals()
for t in tis:
if len(t.events) > 0:
result.append(t)
if onlyfirst:
break
return result
def time_intervals_list_first(self):
return self.time_intervals_list(True)
def export_to_ics(self):
from .models import Event
events = [event for day in self.get_calendar_days().values() for event in day.events]
return Event.export_to_ics(events)
class CalendarMonth(CalendarList):
def __init__(self, year, month, filter):
self.year = year
self.month = month
r = calendar.monthrange(year, month)
first = date(year, month, 1)
last = date(year, month, r[1])
super().__init__(first, last, filter)
def get_month_name(self):
return self.firstdate.strftime("%B")
def next_month(self):
return self.lastdate + timedelta(days=7)
def previous_month(self):
return self.firstdate + timedelta(days=-7)
class CalendarWeek(CalendarList):
def __init__(self, year, week, filter):
self.year = year
self.week = week
first = date.fromisocalendar(self.year, self.week, 1)
last = date.fromisocalendar(self.year, self.week, 7)
super().__init__(first, last, filter)
def next_week(self):
return self.firstdate + timedelta(days=7)
def previous_week(self):
return self.firstdate + timedelta(days=-7)
class CalendarDay(CalendarList):
def __init__(self, date, filter=None):
super().__init__(date, date, filter, exact=True)
def get_events(self):
return self.calendar_days_list()[0].events