Example #1
0
class CalendarRelation(
        with_metaclass(ModelBase, *get_model_bases('CalendarRelation'))):
    '''
    This is for relating data to a Calendar, and possible all of the events for
    that calendar, there is also a distinction, so that the same type or kind of
    data can be related in different ways.  A good example would be, if you have
    calendars that are only visible by certain users, you could create a
    relation between calendars and users, with the distinction of 'visibility',
    or 'ownership'.  If inheritable is set to true, all the events for this
    calendar will inherit this relation.

    calendar: a foreign key relation to a Calendar object.
    content_type: a foreign key relation to ContentType of the generic object
    object_id: the id of the generic object
    content_object: the generic foreign key to the generic object
    distinction: a string representing a distinction of the relation, User could
    have a 'veiwer' relation and an 'owner' relation for example.
    inheritable: a boolean that decides if events of the calendar should also
    inherit this relation

    DISCLAIMER: while this model is a nice out of the box feature to have, it
    may not scale well.  If you use this, keep that in mind.
    '''

    calendar = models.ForeignKey(Calendar,
                                 on_delete=models.CASCADE,
                                 verbose_name=_("calendar"))
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.IntegerField()
    content_object = fields.GenericForeignKey('content_type', 'object_id')
    distinction = models.CharField(_("distinction"), max_length=20)
    inheritable = models.BooleanField(_("inheritable"), default=True)

    objects = CalendarRelationManager()

    class Meta(object):
        verbose_name = _('calendar relation')
        verbose_name_plural = _('calendar relations')
        app_label = 'schedule'

    def __str__(self):
        return '%s - %s' % (self.calendar, self.content_object)
Example #2
0
class EventRelation(
        with_metaclass(ModelBase, *get_model_bases('EventRelation'))):
    event = models.ForeignKey(Event,
                              on_delete=models.CASCADE,
                              verbose_name=_("event"))
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.IntegerField()
    content_object = fields.GenericForeignKey('content_type', 'object_id')
    distinction = models.CharField(_("distinction"), max_length=20)

    objects = EventRelationManager()

    class Meta(object):
        verbose_name = _("event relation")
        verbose_name_plural = _("event relations")
        app_label = 'schedule'

    def __str__(self):
        return '%s(%s)-%s' % (self.event.title, self.distinction,
                              self.content_object)
Example #3
0
class Calendar(with_metaclass(ModelBase, *get_model_bases('Calendar'))):
    '''
    This is for grouping events so that batch relations can be made to all
    events.  An example would be a project calendar.

    name: the name of the calendar
    events: all the events contained within the calendar.
    >>> calendar = Calendar(name = 'Test Calendar')
    >>> calendar.save()
    >>> data = {
    ...         'title': 'Recent Event',
    ...         'start': datetime.datetime(2008, 1, 5, 0, 0),
    ...         'end': datetime.datetime(2008, 1, 10, 0, 0)
    ...        }
    >>> event = Event(**data)
    >>> event.save()
    >>> calendar.events.add(event)
    >>> data = {
    ...         'title': 'Upcoming Event',
    ...         'start': datetime.datetime(2008, 1, 1, 0, 0),
    ...         'end': datetime.datetime(2008, 1, 4, 0, 0)
    ...        }
    >>> event = Event(**data)
    >>> event.save()
    >>> calendar.events.add(event)
    >>> data = {
    ...         'title': 'Current Event',
    ...         'start': datetime.datetime(2008, 1, 3),
    ...         'end': datetime.datetime(2008, 1, 6)
    ...        }
    >>> event = Event(**data)
    >>> event.save()
    >>> calendar.events.add(event)
    '''

    name = models.CharField(_("name"), max_length=200)
    slug = models.SlugField(_("slug"), max_length=200)
    objects = CalendarManager()

    class Meta(object):
        verbose_name = _('calendar')
        verbose_name_plural = _('calendars')
        app_label = 'schedule'

    def __str__(self):
        return self.name

    @property
    def events(self):
        return self.event_set

    def create_relation(self, obj, distinction='', inheritable=True):
        """
        Creates a CalendarRelation between self and obj.

        if Inheritable is set to true this relation will cascade to all events
        related to this calendar.
        """
        CalendarRelation.objects.create_relation(self, obj, distinction,
                                                 inheritable)

    def get_recent(self, amount=5):
        """
        This shortcut function allows you to get events that have started
        recently.

        amount is the amount of events you want in the queryset. The default is
        5.
        """
        return self.events.order_by('-start').filter(
            start__lt=timezone.now())[:amount]

    def occurrences_after(self, date=None):
        return EventListManager(self.events.all()).occurrences_after(date)

    def get_absolute_url(self):
        if USE_FULLCALENDAR:
            return reverse('fullcalendar', kwargs={'calendar_slug': self.slug})
        return reverse('calendar_home', kwargs={'calendar_slug': self.slug})

    def add_event_url(self):
        return reverse('calendar_create_event', args=[self.slug])
Example #4
0
class Event(with_metaclass(ModelBase, *get_model_bases('Event'))):
    '''
    This model stores meta data for a date.  You can relate this data to many
    other models.
    '''
    start = models.DateTimeField(_("start"), db_index=True)
    end = models.DateTimeField(
        _("end"),
        null=True,
        blank=True,
        help_text=_("The end time must be later than the start time."))
    title = models.CharField(_("title"), max_length=255)
    description = models.TextField(_("description"), blank=True)
    creator = models.ForeignKey(DjangoUser,
                                on_delete=models.CASCADE,
                                verbose_name=_("user"),
                                related_name='user')

    created_on = models.DateTimeField(_("created on"), auto_now_add=True)
    updated_on = models.DateTimeField(_("updated on"), auto_now=True)
    rule = models.ForeignKey(
        Rule,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        verbose_name=_("rule"),
        help_text=_("Select '----' for a one time only event."))
    end_recurring_period = models.DateTimeField(
        _("end recurring period"),
        null=True,
        blank=True,
        db_index=True,
        help_text=_("This date is ignored for one time only events."))
    calendar = models.ForeignKey(Calendar,
                                 on_delete=models.CASCADE,
                                 null=True,
                                 blank=True,
                                 verbose_name=_("calendar"))

    color_event = models.CharField(_("Color event"), blank=True, max_length=10)

    by_week_day = models.CharField(_("By week day"),
                                   blank=True,
                                   max_length=26,
                                   default="")
    objects = EventManager()

    all_day = models.BooleanField(default=True)

    class Meta(object):
        verbose_name = _('event')
        verbose_name_plural = _('events')
        app_label = 'schedule'
        index_together = (('start', 'end'), )

    def __str__(self):
        return ugettext('%(title)s: %(start)s - %(end)s') % {
            'title': self.title,
            'start': date(self.start, django_settings.DATE_FORMAT),
            'end': date(self.end, django_settings.DATE_FORMAT),
        }

    @property
    def seconds(self):
        return (self.end - self.start).total_seconds()

    @property
    def minutes(self):
        return float(self.seconds) / 60

    @property
    def hours(self):
        return float(self.seconds) / 3600

    def get_absolute_url(self):
        return reverse('event', args=[self.id])

    def get_occurrences(self, start, end, clear_prefetch=True):

        if clear_prefetch:
            persisted_occurrences = self.occurrence_set.select_related(
                None).all()
        else:
            persisted_occurrences = self.occurrence_set.all()
        occ_replacer = OccurrenceReplacer(persisted_occurrences)
        occurrences = self._get_occurrence_list(start, end)
        final_occurrences = []
        for occ in occurrences:
            # replace occurrences with their persisted counterparts
            if occ_replacer.has_occurrence(occ):
                p_occ = occ_replacer.get_occurrence(occ)
                # ...but only if they are within this period
                if p_occ.start < end and p_occ.end >= start:
                    final_occurrences.append(p_occ)
            else:
                final_occurrences.append(occ)
        # then add persisted occurrences which originated outside of this period but now
        # fall within it
        final_occurrences += occ_replacer.get_additional_occurrences(
            start, end)
        return final_occurrences

    def get_rrule_object(self, tzinfo):
        from dateutil.rrule import WEEKLY, MO, TU, WE, TH, FR, SA, SU
        if self.rule is None:
            return
        params = self._event_params()
        frequency = self.rule.rrule_frequency()
        if timezone.is_naive(self.start):
            dtstart = self.start
        else:
            dtstart = tzinfo.normalize(self.start).replace(tzinfo=None)

        if self.end_recurring_period is None:
            until = None
        elif timezone.is_naive(self.end_recurring_period):
            until = self.end_recurring_period
        else:
            until = tzinfo.normalize(
                self.end_recurring_period.astimezone(tzinfo)).replace(
                    tzinfo=None)

        if frequency == WEEKLY:
            list_week_day = []
            for day in self.by_week_day.split(','):
                if day in ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]:
                    list_week_day.append(eval(day))

            if list_week_day:
                params["byweekday"] = tuple(list_week_day)

            return rrule.rrule(
                frequency,
                dtstart=dtstart,
                until=until,
                **params,
            )

        return rrule.rrule(frequency, dtstart=dtstart, until=until, **params)

    def _create_occurrence(self, start, end=None):
        if end is None:
            end = start + (self.end - self.start)

        return Occurrence(event=self,
                          start=start,
                          end=end,
                          original_start=start,
                          original_end=end,
                          all_day=self.all_day)

    def get_occurrence(self, date):
        use_naive = timezone.is_naive(date)
        tzinfo = timezone.utc
        if timezone.is_naive(date):
            date = timezone.make_aware(date, timezone.utc)
        if date.tzinfo:
            tzinfo = date.tzinfo
        rule = self.get_rrule_object(tzinfo)
        if rule:
            next_occurrence = rule.after(
                tzinfo.normalize(date).replace(tzinfo=None), inc=True)
            next_occurrence = tzinfo.localize(next_occurrence)
        else:
            next_occurrence = self.start
        if next_occurrence == date:
            try:
                return Occurrence.objects.get(event=self, original_start=date)
            except Occurrence.DoesNotExist:
                if use_naive:
                    next_occurrence = timezone.make_naive(
                        next_occurrence, tzinfo)
                return self._create_occurrence(next_occurrence)

    def _get_occurrence_list(self, start, end):
        """
        Returns a list of occurrences that fall completely or partially inside
        the timespan defined by start (inclusive) and end (exclusive)
        """
        if self.rule is not None:
            duration = self.end - self.start
            use_naive = timezone.is_naive(start)

            # Use the timezone from the start date
            tzinfo = timezone.utc
            if start.tzinfo:
                tzinfo = start.tzinfo

            # Limit timespan to recurring period
            occurrences = []
            if self.end_recurring_period and self.end_recurring_period < end:
                end = self.end_recurring_period

            start_rule = self.get_rrule_object(tzinfo)
            start = start.replace(tzinfo=None)
            if timezone.is_aware(end):
                end = tzinfo.normalize(end).replace(tzinfo=None)

            o_starts = []

            # Occurrences that start before the timespan but ends inside or after timespan
            closest_start = start_rule.before(start, inc=False)
            if closest_start is not None and closest_start + duration > start:
                o_starts.append(closest_start)

            # Occurrences starts that happen inside timespan (end-inclusive)
            occs = start_rule.between(start, end, inc=True)
            # The occurrence that start on the end of the timespan is potentially
            # included above, lets remove if thats the case.
            if len(occs) > 0:
                if occs[-1] == end:
                    occs.pop()
            # Add the occurrences found inside timespan
            o_starts.extend(occs)

            # Create the Occurrence objects for the found start dates
            for o_start in o_starts:
                o_start = tzinfo.localize(o_start)
                if use_naive:
                    o_start = timezone.make_naive(o_start, tzinfo)
                o_end = o_start + duration
                occurrence = self._create_occurrence(o_start, o_end)
                if occurrence not in occurrences:
                    occurrences.append(occurrence)
            return occurrences
        else:
            # check if event is in the period
            if self.start < end and self.end > start:
                return [self._create_occurrence(self.start)]
            else:
                return []

    def _occurrences_after_generator(self, after=None):
        """
        returns a generator that produces unpresisted occurrences after the
        datetime ``after``. (Optionally) This generator will return up to
        ``max_occurrences`` occurrences or has reached ``self.end_recurring_period``, whichever is smallest.
        """

        tzinfo = timezone.utc
        if after is None:
            after = timezone.now()
        elif not timezone.is_naive(after):
            tzinfo = after.tzinfo
        rule = self.get_rrule_object(tzinfo)
        if rule is None:
            if self.end > after:
                yield self._create_occurrence(self.start, self.end)
            return
        date_iter = iter(rule)
        difference = self.end - self.start
        loop_counter = 0
        for o_start in date_iter:
            o_start = tzinfo.localize(o_start)
            o_end = o_start + difference
            if o_end > after:
                yield self._create_occurrence(o_start, o_end)

            loop_counter += 1

    def occurrences_after(self, after=None, max_occurrences=None):
        """
        returns a generator that produces occurrences after the datetime
        ``after``.  Includes all of the persisted Occurrences. (Optionally) This generator will return up to
        ``max_occurrences`` occurrences or has reached ``self.end_recurring_period``, whichever is smallest.
        """
        if after is None:
            after = timezone.now()
        occ_replacer = OccurrenceReplacer(self.occurrence_set.all())
        generator = self._occurrences_after_generator(after)
        trickies = list(
            self.occurrence_set.filter(original_start__lte=after,
                                       start__gte=after).order_by('start'))
        for index, nxt in enumerate(generator):
            if max_occurrences and index > max_occurrences - 1:
                break
            if (len(trickies) > 0
                    and (nxt is None or nxt.start > trickies[0].start)):
                yield trickies.pop(0)
            yield occ_replacer.get_occurrence(nxt)

    @property
    def event_start_params(self):
        start = self.start
        params = {
            'byyearday': start.timetuple().tm_yday,
            'bymonth': start.month,
            'bymonthday': start.day,
            'byweekno': start.isocalendar()[1],
            'byweekday': start.weekday(),
            'byhour': start.hour,
            'byminute': start.minute,
            'bysecond': start.second
        }
        return params

    @property
    def event_rule_params(self):
        return self.rule.get_params()

    def _event_params(self):
        freq_order = freq_dict_order[self.rule.frequency]
        rule_params = self.event_rule_params
        start_params = self.event_start_params
        event_params = {}

        if len(rule_params) == 0:
            return event_params

        for param in rule_params:
            # start date influences rule params
            if (param in param_dict_order
                    and param_dict_order[param] > freq_order
                    and param in start_params):
                sp = start_params[param]
                if sp == rule_params[param] or (hasattr(
                        rule_params[param], '__iter__')
                                                and sp in rule_params[param]):
                    event_params[param] = [sp]
                else:
                    event_params[param] = rule_params[param]
            else:
                event_params[param] = rule_params[param]

        return event_params

    @property
    def event_params(self):
        event_params = self._event_params()
        start = self.effective_start
        if not start:
            empty = True
        elif self.end_recurring_period and start > self.end_recurring_period:
            empty = True
        return event_params, empty

    @property
    def effective_start(self):
        if self.pk and self.end_recurring_period:
            occ_generator = self._occurrences_after_generator(self.start)
            try:
                return next(occ_generator).start
            except StopIteration:
                pass
        elif self.pk:
            return self.start
        return None

    @property
    def effective_end(self):
        if self.pk and self.end_recurring_period:
            params, empty = self.event_params
            if empty or not self.effective_start:
                return None
            elif self.end_recurring_period:
                occ = None
                occ_generator = self._occurrences_after_generator(self.start)
                for occ in occ_generator:
                    pass
                return occ.end
        elif self.pk:
            return datetime.max
        return None
Example #5
0
class Occurrence(with_metaclass(ModelBase, *get_model_bases('Occurrence'))):
    event = models.ForeignKey(Event,
                              on_delete=models.CASCADE,
                              verbose_name=_("event"))
    all_day = models.BooleanField(default=True)

    title = models.CharField(_("title"), max_length=255, blank=True)
    description = models.TextField(_("description"), blank=True)
    start = models.DateTimeField(_("start"), db_index=True)
    end = models.DateTimeField(_("end"), db_index=True, null=True)
    cancelled = models.BooleanField(_("cancelled"), default=False)
    original_start = models.DateTimeField(_("original start"))
    original_end = models.DateTimeField(_("original end"))
    created_on = models.DateTimeField(_("created on"), auto_now_add=True)
    updated_on = models.DateTimeField(_("updated on"), auto_now=True)
    color_event = models.CharField(_("Color event"), blank=True, max_length=10)

    class Meta(object):
        verbose_name = _("occurrence")
        verbose_name_plural = _("occurrences")
        app_label = 'schedule'
        index_together = (('start', 'end'), )

    def __init__(self, *args, **kwargs):
        super(Occurrence, self).__init__(*args, **kwargs)
        if not self.title and self.event_id:
            self.title = self.event.title
        if not self.description and self.event_id:
            self.description = self.event.description

    def moved(self):
        return self.original_start != self.start or self.original_end != self.end

    moved = property(moved)

    def move(self, new_start, new_end):
        self.start = new_start
        self.end = new_end
        self.save()

    def cancel(self):
        self.cancelled = True
        self.save()

    def uncancel(self):
        self.cancelled = False
        self.save()

    @property
    def seconds(self):
        return (self.end - self.start).total_seconds()

    @property
    def minutes(self):
        return float(self.seconds) / 60

    @property
    def hours(self):
        return float(self.seconds) / 3600

    def get_absolute_url(self):
        if self.pk is not None:
            return reverse('occurrence',
                           kwargs={
                               'occurrence_id': self.pk,
                               'event_id': self.event.id
                           })
        return reverse('occurrence_by_date',
                       kwargs={
                           'event_id': self.event.id,
                           'year': self.start.year,
                           'month': self.start.month,
                           'day': self.start.day,
                           'hour': self.start.hour,
                           'minute': self.start.minute,
                           'second': self.start.second,
                       })

    def get_cancel_url(self):
        if self.pk is not None:
            return reverse('cancel_occurrence',
                           kwargs={
                               'occurrence_id': self.pk,
                               'event_id': self.event.id
                           })
        return reverse('cancel_occurrence_by_date',
                       kwargs={
                           'event_id': self.event.id,
                           'year': self.start.year,
                           'month': self.start.month,
                           'day': self.start.day,
                           'hour': self.start.hour,
                           'minute': self.start.minute,
                           'second': self.start.second,
                       })

    def get_edit_url(self):
        if self.pk is not None:
            return reverse('edit_occurrence',
                           kwargs={
                               'occurrence_id': self.pk,
                               'event_id': self.event.id
                           })
        return reverse('edit_occurrence_by_date',
                       kwargs={
                           'event_id': self.event.id,
                           'year': self.start.year,
                           'month': self.start.month,
                           'day': self.start.day,
                           'hour': self.start.hour,
                           'minute': self.start.minute,
                           'second': self.start.second,
                       })

    def __str__(self):
        return ugettext("%(start)s to %(end)s") % {
            'start': date(self.start, django_settings.DATE_FORMAT),
            'end': date(self.end, django_settings.DATE_FORMAT)
        }

    def __lt__(self, other):
        return self.end < other.end

    def __eq__(self, other):
        return (isinstance(other, Occurrence)
                and self.original_start == other.original_start
                and self.original_end == other.original_end)
Example #6
0
class Rule(with_metaclass(ModelBase, *get_model_bases('Rule'))):
    """
    This defines a rule by which an event will recur.  This is defined by the
    rrule in the dateutil documentation.

    * name - the human friendly name of this kind of recursion.
    * description - a short description describing this type of recursion.
    * frequency - the base recurrence period
    * param - extra params required to define this type of recursion. The params
      should follow this format:

        param = [rruleparam:value;]*
        rruleparam = see list below
        value = int[,int]*

      The options are: (documentation for these can be found at
      http://labix.org/python-dateutil#head-470fa22b2db72000d7abe698a5783a46b0731b57)
        ** count
        ** bysetpos
        ** bymonth
        ** bymonthday
        ** byyearday
        ** byweekno
        ** byweekday
        ** byhour
        ** byminute
        ** bysecond
        ** byeaster
    """
    name = models.CharField(_("name"), max_length=32)
    description = models.TextField(_("description"))
    frequency = models.CharField(_("frequency"), choices=freqs, max_length=10)
    params = models.TextField(_("params"), blank=True)

    _week_days = {
        'MO': MO,
        'TU': TU,
        'WE': WE,
        'TH': TH,
        'FR': FR,
        'SA': SA,
        'SU': SU
    }

    class Meta(object):
        verbose_name = _('rule')
        verbose_name_plural = _('rules')
        app_label = 'schedule'

    def rrule_frequency(self):
        compatibility_dict = {
            'DAILY': DAILY,
            'MONTHLY': MONTHLY,
            'WEEKLY': WEEKLY,
            'YEARLY': YEARLY,
            'HOURLY': HOURLY,
            'MINUTELY': MINUTELY,
            'SECONDLY': SECONDLY
        }
        return compatibility_dict[self.frequency]

    def _weekday_or_number(self, param):
        '''
        Receives a rrule parameter value, returns a upper case version
        of the value if its a weekday or an integer if its a number
        '''
        try:
            return int(param)
        except (TypeError, ValueError):
            uparam = str(param).upper()
            if uparam in Rule._week_days:
                return Rule._week_days[uparam]

    def get_params(self):
        """
        >>> rule = Rule(params = "count:1;bysecond:1;byminute:1,2,4,5")
        >>> rule.get_params()
        {'count': 1, 'byminute': [1, 2, 4, 5], 'bysecond': 1}
        """
        params = self.params.split(';')
        param_dict = []
        for param in params:
            param = param.split(':')
            if len(param) != 2:
                continue

            param = (
                str(param[0]).lower(),
                [
                    x for x in
                    [self._weekday_or_number(v) for v in param[1].split(',')]
                    if x is not None
                ],
            )

            if len(param[1]) == 1:
                param_value = self._weekday_or_number(param[1][0])
                param = (param[0], param_value)
            param_dict.append(param)
        return dict(param_dict)

    def __str__(self):
        """Human readable string for Rule"""
        return 'Rule %s params %s' % (self.name, self.params)