Source code for ls.joyous.models.calendar

# ------------------------------------------------------------------------------
# Joyous calendar models
# ------------------------------------------------------------------------------
import datetime as dt
import calendar
from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.http import Http404
from django import forms
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from wagtail.core.models import Page, Site
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import HelpPanel, FieldPanel, MultiFieldPanel
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.search import index
from .. import __version__
from ..edit_handlers import ConcealedPanel
from ..holidays import Holidays
from ..utils.names import WEEKDAY_NAMES, MONTH_NAMES, MONTH_ABBRS
from ..utils.weeks import week_info, gregorian_to_week_date, num_weeks_in_year
from ..utils.weeks import weekday_abbr, weekday_name
from ..utils.mixins import ProxyPageMixin
from ..fields import MultipleSelectField
from . import (getAllEventsByDay, getAllEventsByWeek, getAllUpcomingEvents,
               getAllPastEvents, getEventFromUid, getAllEvents)
from ..forms import FormDefender, BorgPageForm

# ------------------------------------------------------------------------------
class CalendarPageForm(BorgPageForm):
    importHandler = None
    exportHandler = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @classmethod
    def registerImportHandler(cls, handler):
        class Panel(ConcealedPanel):
            def _show(self):
                page = getattr(self, 'instance', None)
                if not page:
                    return False
                hasReq = hasattr(page, '__joyous_edit_request')
                if not hasReq:
                    return False
                # only a user with edit and publishing rights should be able
                # to import iCalendar files
                perms = page.permissions_for_user(self.request.user)
                return perms.can_publish() and perms.can_edit()

        cls.importHandler = handler
        uploadWidget = forms.FileInput(attrs={'accept': "text/calendar,"
                                                        "application/zip,"
                                                        ".ics,.zip"})
        cls.declared_fields['upload'] = forms.FileField(
                                            label=_("Upload"),
                                            required=False,
                                            widget=uploadWidget)
        cls.declared_fields['utc2local'] = forms.BooleanField(
                                            label=_("Convert UTC to localtime?"),
                                            required=False,
                                            initial=True)
        CalendarPage.settings_panels.append(Panel([
              FieldPanel('upload'),
              FieldPanel('utc2local'),
            ], heading=_("Import")))

    @classmethod
    def registerExportHandler(cls, handler):
        class Panel(ConcealedPanel):
            def _show(self):
                page = getattr(self, 'instance', None)
                return page and page.url is not None and page.live

        cls.exportHandler = handler
        CalendarPage.settings_panels.append(Panel([
              HelpPanel(template="joyous/edit_handlers/export_panel.html")
            ], heading=_("Export")))

    def save(self, commit=True):
        page = super().save(commit=False)
        request = getattr(page, '__joyous_edit_request', None)

        if self.importHandler and request:
            delattr(page, '__joyous_edit_request')
            utc2local = self.cleaned_data.get('utc2local')
            upload = self.cleaned_data.get('upload')
            if upload is not None:
                self.importHandler.load(page, request, upload, utc2local=utc2local)

        if commit:
            page.save()
        return page

# ------------------------------------------------------------------------------
DatePictures = {"YYYY":  r"((?:19|20)\d\d)",
                "MM":    r"(1[012]|0?[1-9])",
                "Mon":   r"({})".format("|".join(m.lower()[:3] for m in MONTH_ABBRS[1:])),
                "DD":    r"(3[01]|[12]\d|0?[1-9])",
                "WW":    r"(5[0-3]|[1-4]\d|0?[1-9])"}

EVENTS_VIEW_CHOICES = [('L', _("List View")),
                       ('W', _("Weekly View")),
                       ('M', _("Monthly View"))]
# TODO ('D', _("Daily View"))

# ------------------------------------------------------------------------------
[docs]class CalendarPage(RoutablePageMixin, Page, metaclass=FormDefender): """CalendarPage displays all the events which are in the same site.""" class Meta: verbose_name = _("calendar page") verbose_name_plural = _("calendar pages") EventsPerPage = getattr(settings, "JOYOUS_EVENTS_PER_PAGE", 25) holidays = Holidays() subpage_types = ['joyous.SimpleEventPage', 'joyous.MultidayEventPage', 'joyous.RecurringEventPage', 'joyous.MultidayRecurringEventPage'] base_form_class = CalendarPageForm intro = RichTextField(_("intro"), blank=True, help_text=_("Introductory text.")) view_choices = MultipleSelectField(_("view choices"), blank=True, default=["L","W","M"], choices=EVENTS_VIEW_CHOICES) default_view = models.CharField(_("default view"), default="M", max_length=15, choices=EVENTS_VIEW_CHOICES) search_fields = Page.search_fields[:] content_panels = Page.content_panels + [ FieldPanel('intro', classname="full"), ] settings_panels = Page.settings_panels + [ MultiFieldPanel([ FieldPanel('view_choices'), FieldPanel('default_view')], heading=_("View Options")), ]
[docs] @route(r"^$") @route(r"^{YYYY}/$".format(**DatePictures)) def routeDefault(self, request, year=None): """Route a request to the default calendar view.""" eventsView = request.GET.get('view', self.default_view) if eventsView in ("L", "list"): return self.serveUpcoming(request) elif eventsView in ("W", "weekly"): return self.serveWeek(request, year) else: return self.serveMonth(request, year)
[docs] @route(r"^{YYYY}/{Mon}/$(?i)".format(**DatePictures)) def routeByMonthAbbr(self, request, year, monthAbbr): """Route a request with a month abbreviation to the monthly view.""" month = (DatePictures['Mon'].index(monthAbbr.lower()) // 4) + 1 return self.serveMonth(request, year, month)
[docs] @route(r"^month/$") @route(r"^{YYYY}/{MM}/$".format(**DatePictures)) def serveMonth(self, request, year=None, month=None): """Monthly calendar view.""" myurl = self.get_url(request) def myUrl(urlYear, urlMonth): if 1900 <= urlYear <= 2099: return myurl + self.reverse_subpage('serveMonth', args=[urlYear, urlMonth]) today = timezone.localdate() if year is None: year = today.year if month is None: month = today.month year = int(year) month = int(month) lastDay = dt.date(year, month, calendar.monthrange(year, month)[1]) if year == today.year and month == today.month: weekYear, weekNum, dow = gregorian_to_week_date(today) else: weekYear, weekNum, dow = gregorian_to_week_date(dt.date(year, month, 7)) weeklyUrl = myurl + self.reverse_subpage('serveWeek', args=[weekYear, weekNum]) listUrl = myurl + self.reverse_subpage('serveUpcoming') prevMonth = month - 1 prevMonthYear = year if prevMonth == 0: prevMonth = 12 prevMonthYear -= 1 nextMonth = month + 1 nextMonthYear = year if nextMonth == 13: nextMonth = 1 nextMonthYear += 1 cxt = self._getCommonContext(request) cxt.update({'year': year, 'month': month, 'yesterday': today - dt.timedelta(1), 'lastweek': today - dt.timedelta(7), 'lastDay': lastDay, 'prevMonthUrl': myUrl(prevMonthYear, prevMonth), 'nextMonthUrl': myUrl(nextMonthYear, nextMonth), 'prevYearUrl': myUrl(year - 1, month), 'nextYearUrl': myUrl(year + 1, month), 'weeklyUrl': weeklyUrl, 'listUrl': listUrl, 'thisMonthUrl': myUrl(today.year, today.month), 'monthName': MONTH_NAMES[month], 'weekdayAbbr': weekday_abbr, 'events': self._getEventsByWeek(request, year, month)}) cxt.update(self._getExtraContext("month")) return TemplateResponse(request, "joyous/calendar_month.html", cxt)
[docs] @route(r"^week/$") @route(r"^{YYYY}/W{WW}/$".format(**DatePictures)) def serveWeek(self, request, year=None, week=None): """Weekly calendar view.""" myurl = self.get_url(request) def myUrl(urlYear, urlWeek): if (urlYear < 1900 or urlYear > 2099 or urlYear == 2099 and urlWeek == 53): return None if urlWeek == 53 and num_weeks_in_year(urlYear) == 52: urlWeek = 52 return myurl + self.reverse_subpage('serveWeek', args=[urlYear, urlWeek]) today = timezone.localdate() thisYear, thisWeekNum, dow = gregorian_to_week_date(today) if year is None: year = thisYear if week is None: week = thisWeekNum year = int(year) week = int(week) firstDay, lastDay, prevYearNumWeeks, yearNumWeeks = week_info(year, week) if week == 53 and yearNumWeeks == 52: raise Http404("Only 52 weeks in {}".format(year)) eventsInWeek = self._getEventsByDay(request, firstDay, lastDay) if firstDay.year >= 1900: monthlyUrl = myurl + self.reverse_subpage('serveMonth', args=[firstDay.year, firstDay.month]) else: monthlyUrl = myurl + self.reverse_subpage('serveMonth', args=[1900, 1]) listUrl = myurl + self.reverse_subpage('serveUpcoming') lastDayOfMonth = dt.date(firstDay.year, firstDay.month, calendar.monthrange(firstDay.year, firstDay.month)[1]) prevWeek = week - 1 prevWeekYear = year if prevWeek == 0: prevWeek = prevYearNumWeeks prevWeekYear -= 1 nextWeek = week + 1 nextWeekYear = year if nextWeek > yearNumWeeks: nextWeek = 1 nextWeekYear += 1 cxt = self._getCommonContext(request) cxt.update({'year': year, 'week': week, 'yesterday': today - dt.timedelta(1), 'lastweek': None, 'lastDay': lastDayOfMonth, 'prevWeekUrl': myUrl(prevWeekYear, prevWeek), 'nextWeekUrl': myUrl(nextWeekYear, nextWeek), 'prevYearUrl': myUrl(year - 1, week), 'nextYearUrl': myUrl(year + 1, week), 'thisWeekUrl': myUrl(thisYear, thisWeekNum), 'monthlyUrl': monthlyUrl, 'listUrl': listUrl, 'weekName': _("Week {weekNum}").format(weekNum=week), 'weekdayAbbr': weekday_abbr, 'events': [eventsInWeek]}) cxt.update(self._getExtraContext("week")) return TemplateResponse(request, "joyous/calendar_week.html", cxt)
[docs] @route(r"^day/$") @route(r"^{YYYY}/{MM}/{DD}/$".format(**DatePictures)) def serveDay(self, request, year=None, month=None, dom=None): """The events of the day list view.""" myurl = self.get_url(request) today = timezone.localdate() if year is None: year = today.year if month is None: month = today.month if dom is None: dom = today.day year = int(year) month = int(month) dom = int(dom) daysInMonth = calendar.monthrange(year, month)[1] if dom > daysInMonth: raise Http404("Only {} days in month".format(daysInMonth)) day = dt.date(year, month, dom) daysEvents = self._getEventsOnDay(request, day).all_events if len(daysEvents) == 1: event = daysEvents[0].page return redirect(event.get_url(request)) eventsPage = self._paginate(request, daysEvents) monthlyUrl = myurl + self.reverse_subpage('serveMonth', args=[year, month]) weekYear, weekNum, dow = gregorian_to_week_date(dt.date(year, month, 7)) weeklyUrl = myurl + self.reverse_subpage('serveWeek', args=[weekYear, weekNum]) listUrl = myurl + self.reverse_subpage('serveUpcoming') cxt = self._getCommonContext(request) cxt.update({'year': year, 'month': month, 'dom': dom, 'day': day, 'monthlyUrl': monthlyUrl, 'weeklyUrl': weeklyUrl, 'listUrl': listUrl, 'monthName': MONTH_NAMES[month], 'weekdayName': WEEKDAY_NAMES[day.weekday()], 'events': eventsPage}) cxt.update(self._getExtraContext("day")) return TemplateResponse(request, "joyous/calendar_list_day.html", cxt)
[docs] @route(r"^upcoming/$") def serveUpcoming(self, request): """Upcoming events list view.""" myurl = self.get_url(request) today = timezone.localdate() monthlyUrl = myurl + self.reverse_subpage('serveMonth', args=[today.year, today.month]) weekYear, weekNum, dow = gregorian_to_week_date(today) weeklyUrl = myurl + self.reverse_subpage('serveWeek', args=[weekYear, weekNum]) listUrl = myurl + self.reverse_subpage('servePast') upcomingEvents = self._getUpcomingEvents(request) eventsPage = self._paginate(request, upcomingEvents) cxt = self._getCommonContext(request) cxt.update({'weeklyUrl': weeklyUrl, 'monthlyUrl': monthlyUrl, 'listUrl': listUrl, 'events': eventsPage}) cxt.update(self._getExtraContext("upcoming")) return TemplateResponse(request, "joyous/calendar_list_upcoming.html", cxt)
[docs] @route(r"^past/$") def servePast(self, request): """Past events list view.""" myurl = self.get_url(request) today = timezone.localdate() monthlyUrl = myurl + self.reverse_subpage('serveMonth', args=[today.year, today.month]) weekYear, weekNum, dow = gregorian_to_week_date(today) weeklyUrl = myurl + self.reverse_subpage('serveWeek', args=[weekYear, weekNum]) listUrl = myurl + self.reverse_subpage('serveUpcoming') pastEvents = self._getPastEvents(request) eventsPage = self._paginate(request, pastEvents) cxt = self._getCommonContext(request) cxt.update({'weeklyUrl': weeklyUrl, 'monthlyUrl': monthlyUrl, 'listUrl': listUrl, 'events': eventsPage}) cxt.update(self._getExtraContext("past")) return TemplateResponse(request, "joyous/calendar_list_past.html", cxt)
[docs] @route(r"^mini/{YYYY}/{MM}/$".format(**DatePictures)) def serveMiniMonth(self, request, year=None, month=None): """Serve data for the MiniMonth template tag.""" if request.headers.get('x-requested-with') != 'XMLHttpRequest': raise Http404("/mini/ is for ajax requests only") today = timezone.localdate() if year is None: year = today.year if month is None: month = today.month year = int(year) month = int(month) cxt = self._getCommonContext(request) cxt.update({'year': year, 'month': month, 'calendarUrl': self.get_url(request), 'monthName': MONTH_NAMES[month], 'weekdayInfo': zip(weekday_abbr, weekday_name), 'events': self._getEventsByWeek(request, year, month)}) cxt.update(self._getExtraContext("mini")) return TemplateResponse(request, "joyous/includes/minicalendar.html", cxt)
[docs] @classmethod def can_create_at(cls, parent): return super().can_create_at(parent) and cls._allowAnotherAt(parent)
[docs] @classmethod def _allowAnotherAt(cls, parent): """You can only create one of these pages per site.""" site = parent.get_site() if site is None: return False return not cls.peers().descendant_of(site.root_page).exists()
[docs] @classmethod def peers(cls): """Return others of the same concrete type.""" contentType = ContentType.objects.get_for_model(cls) return cls.objects.filter(content_type=contentType)
def _getCommonContext(self, request): cxt = self.get_context(request) cxt.update({'version': __version__, 'themeCSS': getattr(settings, "JOYOUS_THEME_CSS", ""), 'today': timezone.localdate(), # Init these variables to prevent template DEBUG messages 'listLink': None, 'weeklyLink': None, 'monthlyLink': None, }) return cxt def _getExtraContext(self, route): return {}
[docs] def _getEventsOnDay(self, request, day): """Return all the events in this site for a given day.""" return self._getEventsByDay(request, day, day)[0]
[docs] def _getEventsByDay(self, request, firstDay, lastDay): """ Return the events in this site for the dates given, grouped by day. """ home = Site.find_for_request(request).root_page return getAllEventsByDay(request, firstDay, lastDay, home=home, holidays=self.holidays)
[docs] def _getEventsByWeek(self, request, year, month): """ Return the events in this site for the given month grouped by week. """ home = Site.find_for_request(request).root_page return getAllEventsByWeek(request, year, month, home=home, holidays=self.holidays)
[docs] def _getUpcomingEvents(self, request): """Return the upcoming events in this site.""" home = Site.find_for_request(request).root_page return getAllUpcomingEvents(request, home=home, holidays=self.holidays)
[docs] def _getPastEvents(self, request): """Return the past events in this site.""" home = Site.find_for_request(request).root_page return getAllPastEvents(request, home=home, holidays=self.holidays)
[docs] def _getEventFromUid(self, request, uid): """Try and find an event with the given UID in this site.""" event = getEventFromUid(request, uid) # might raise exception home = Site.find_for_request(request).root_page if event.get_ancestors().filter(id=home.id).exists(): # only return event if it is in the same site return event
[docs] def _getAllEvents(self, request): """Return all the events in this site.""" home = Site.find_for_request(request).root_page return getAllEvents(request, home=home, holidays=self.holidays)
def _paginate(self, request, events): paginator = Paginator(events, self.EventsPerPage) try: eventsPage = paginator.page(request.GET.get('page')) except PageNotAnInteger: eventsPage = paginator.page(1) except EmptyPage: eventsPage = paginator.page(paginator.num_pages) return eventsPage
# ------------------------------------------------------------------------------
[docs]class SpecificCalendarPage(ProxyPageMixin, CalendarPage): """ SpecificCalendarPage displays only the events which are its children """ class Meta(ProxyPageMixin.Meta): verbose_name = _("specific calendar page") verbose_name_plural = _("specific calendar pages") is_creatable = False # creation is disabled by default
[docs] @classmethod def _allowAnotherAt(cls, parent): """Don't limit creation.""" return True
[docs] def _getEventsByDay(self, request, firstDay, lastDay): """ Return my child events for the dates given, grouped by day. """ return getAllEventsByDay(request, firstDay, lastDay, home=self, holidays=self.holidays)
[docs] def _getEventsByWeek(self, request, year, month): """Return my child events for the given month grouped by week.""" return getAllEventsByWeek(request, year, month, home=self, holidays=self.holidays)
[docs] def _getUpcomingEvents(self, request): """Return my upcoming child events.""" return getAllUpcomingEvents(request, home=self, holidays=self.holidays)
[docs] def _getPastEvents(self, request): """Return my past child events.""" return getAllPastEvents(request, home=self, holidays=self.holidays)
[docs] def _getEventFromUid(self, request, uid): """Try and find a child event with the given UID.""" event = getEventFromUid(request, uid) # might raise exception if event.get_ancestors().filter(id=self.id).exists(): # only return event if it is a descendant return event
[docs] def _getAllEvents(self, request): """Return all my child events.""" return getAllEvents(request, home=self, holidays=self.holidays)
# ------------------------------------------------------------------------------
[docs]class GeneralCalendarPage(ProxyPageMixin, CalendarPage): """ GeneralCalendarPage displays all the events no matter where they are """ class Meta(ProxyPageMixin.Meta): verbose_name = _("general calendar page") verbose_name_plural = _("general calendar pages") is_creatable = False # creation is disabled by default
[docs] @classmethod def _allowAnotherAt(cls, parent): """You can only create one of these pages.""" return not cls.peers().exists()
[docs] def _getEventsByDay(self, request, firstDay, lastDay): """ Return all events for the dates given, grouped by day. """ return getAllEventsByDay(request, firstDay, lastDay, holidays=self.holidays)
[docs] def _getEventsByWeek(self, request, year, month): """Return all events for the given month grouped by week.""" return getAllEventsByWeek(request, year, month, holidays=self.holidays)
[docs] def _getUpcomingEvents(self, request): """Return all the upcoming events.""" return getAllUpcomingEvents(request, holidays=self.holidays)
[docs] def _getPastEvents(self, request): """Return all the past events.""" return getAllPastEvents(request, holidays=self.holidays)
[docs] def _getEventFromUid(self, request, uid): """Try and find an event with the given UID.""" return getEventFromUid(request, uid) # might raise exception
[docs] def _getAllEvents(self, request): """Return all the events.""" return getAllEvents(request, holidays=self.holidays)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------