333 lines
11 KiB
Python
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
|