class EventRelation(with_metaclass(ModelBase, *get_model_bases())): ''' This is for relating data to an Event, there is also a distinction, so that data can be related in different ways. A good example would be, if you have events that are only visible by certain users, you could create a relation between events and users, with the distinction of 'visibility', or 'ownership'. event: a foreign key relation to an Event model. 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 'viewer' relation and an 'owner' relation for example. 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. ''' 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, null=True) 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)
def test_get_model_bases_with_custom_dict_specific(self): model_mock = mock.Mock() expected_result = [model_mock] self.import_string_mock.return_value = model_mock actual_result = get_model_bases('ClassName') self.assertListEqual(actual_result, expected_result) self.import_string_mock.assert_called_once_with('path.to.module.AbstractClass')
def test_get_model_bases_with_custom_dict_specific(self): model_mock = mock.Mock() expected_result = [model_mock] self.import_string_mock.return_value = model_mock actual_result = get_model_bases('ClassName') self.assertListEqual(actual_result, expected_result) self.import_string_mock.assert_called_once_with( 'path.to.module.AbstractClass')
def test_get_model_bases_with_custom_list(self): model_mock1 = mock.Mock() model_mock2 = mock.Mock() expected_result = [model_mock1, model_mock2] self.import_string_mock.side_effect = [model_mock1, model_mock2] actual_result = get_model_bases('ClassName') self.assertListEqual(actual_result, expected_result) self.import_string_mock.assert_any_call('path.to.module.AbstractClass1') self.import_string_mock.assert_any_call('path.to.module.AbstractClass2') self.assertEqual(self.import_string_mock.call_count, 2)
def test_get_model_bases_with_custom_list(self): model_mock1 = mock.Mock() model_mock2 = mock.Mock() expected_result = [model_mock1, model_mock2] self.import_string_mock.side_effect = [model_mock1, model_mock2] actual_result = get_model_bases('ClassName') self.assertListEqual(actual_result, expected_result) self.import_string_mock.assert_any_call( 'path.to.module.AbstractClass1') self.import_string_mock.assert_any_call( 'path.to.module.AbstractClass2') self.assertEqual(self.import_string_mock.call_count, 2)
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)
class Occurrence(with_metaclass(ModelBase, *get_model_bases('Occurrence'))): #ORIGINAL event = models.ForeignKey(Event, on_delete=models.CASCADE, verbose_name=_("event")) title = models.CharField(_("Title"), max_length=255, blank=True) start = models.DateTimeField(_("Start"), db_index=True) end = models.DateTimeField(_("End"), db_index=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) #BOOKING reservation_spots = models.IntegerField(_("Reservation Spots"), null=True) spots_free = models.IntegerField(null=True, default=35) description = models.TextField(_("description"), blank=True) #PRICING price = models.DecimalField(_("price"), max_digits=100, decimal_places=2, default=0.00) sale_price = models.DecimalField(_("Sale Price"), decimal_places=2, max_digits=20, null=True, blank=True) #QUERYING BY DATE guide = models.ForeignKey('tour.Guide', blank=True, null=True, related_name='guide') #GOOGLE MAPS latitude = models.DecimalField(_("latitude"), max_digits=50, decimal_places=10, blank=True, null=True, db_index=True) longitude = models.DecimalField(_("longitude"), max_digits=50, decimal_places=10, blank=True, null=True, db_index=True) tour_type = models.CharField(_("tour_type"), max_length=255, blank=True) tour_icon = models.CharField(_("tour_icon"), max_length=255, blank=True, null=True) sail = models.NullBooleanField(_('sail'), default=False) bike = models.NullBooleanField(_('bike'), default=False) trail = models.NullBooleanField(_('trail'), default=False) #TOURS SEARCH BY RADIUS class Meta(object): verbose_name = _("occurrence") verbose_name_plural = _("occurrences") app_label = 'schedule' index_together = (('start', 'end'), ('latitude', 'longitude')) 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 if not self.reservation_spots and self.event_id: self.reservation_spots = self.event.reservation_spots if not self.spots_free and self.event.id: self.spots_free = self.event.reservation_spots if not self.latitude and self.event.id: self.latitude = self.event.latitude if not self.longitude and self.event.id: self.longitude = self.event.longitude if not self.tour_type and self.event.id: self.tour_type = self.event.longitude if not self.price and self.event_id: self.price = self.event.price if not self.sale_price and self.event_id: self.sale_price = self.event.sale_price if not self.tour_icon and self.event.tour_type: if self.event.tour_type == "Boat": self.tour_icon = "sail" self.sail = True elif self.event.tour_type == "Bike": self.tour_icon = "hike" self.bike = True else: self.tour_icon = "trail" self.trail = True 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, }) @property 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, }) @property 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 with %(spots_free)s/%(spots_total)s left" ) % { 'start': date(self.start, django_settings.DATE_FORMAT), 'end': date(self.end, django_settings.DATE_FORMAT), 'spots_free': self.spots_free, 'spots_total': self.reservation_spots } 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) def get_tour_time(self): return ugettext( "%(start)s to %(end)s with %(spots_free)s/%(spots_total)s left" ) % { 'start': date(self.start, django_settings.DATE_FORMAT), 'end': date(self.end, django_settings.DATE_FORMAT), 'spots_free': self.spots_free, 'spots_total': self.reservation_spots } def get_spots_avaliable(self): if self.spots_free != 0: return ugettext("%(spots_total)s spots avaliable") % { 'spots_free': self.spots_free, } else: return ugettext("no spots currently avaliable") def get_price(self): if self.sale_price is not None: return self.sale_price else: return self.price def get_html_price(self): if self.sale_price is not None: html_text = "<span class='sale-price'>%s</span> <span class='og-price'>%s</span>" % ( self.sale_price, self.price) else: html_text = "<span class='price'>%s</span>" % (self.price) return mark_safe(html_text) #_____IN THE WORKS FOR PRICING TOURS________ def add_to_cart(self): return "%s?item=%s&qty=1" % (reverse("cart"), self.id) def remove_from_cart(self): return "%s?item=%s&qty=1&delete=True" % (reverse("cart"), self.id) def get_tour_title(self): return "%s" % (self.title) #_____IN THE WORKS FOR PRICING TOURS________ #GOOGLE MAPS ONLY FIND A PLACE WITHIN A DISTANCE OF THE TYPED IN LOCATION (NOT WORKING) # class LocationManager(models.Manager): # def nearby_locations(self, latitude=0, longitude=0, radius=10, max_results=100, use_miles=True): # if use_miles: # distance_unit = 3959 # else: # distance_unit = 6371 # # from django.db import connection, transaction # cursor = connection.cursor() # if django_settings.DATABASE_ENGINE == 'sqlite3': # connection.connection.create_function('acos', 1, math.acos) # connection.connection.create_function('cos', 1, math.cos) # connection.connection.create_function('radians', 1, math.radians) # connection.connection.create_function('sin', 1, math.sin) # # sql = """SELECT id, (%f * acos( cos( math.radians(%f) ) * cos( math.radians( latitude ) ) * # cos( math.radians( longitude ) - math.radians(%f) ) + sin( math.radians(%f) ) * sin( math.radians( latitude ) ) ) ) # AS distance FROM schedule_occurrence WHERE distance < %d # ORDER BY distance LIMIT 0 , %d;""" % (distance_unit, latitude, longitude, latitude, int(radius), max_results) # cursor.execute(sql) # ids = [row[0] for row in cursor.fetchall()] # # return self.filter(id__in=ids)
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. ''' TOUR_FIELDS = [('Boat', 'Boat'), ('Bike', 'Bike'), ('Walk', 'Walk')] NUMBER_FIELDS = [(5, 5), (10, 10), (15, 15), (20, 20), (25, 25), (30, 30), (35, 35), (50, 50)] start = models.DateTimeField(_("Start"), db_index=True) end = models.DateTimeField( _("End"), db_index=True, help_text=_("The end time must be later than the start time.")) title = models.CharField(_("Title"), max_length=255) image = models.ImageField(upload_to=upload_location, null=True, blank=True) reservation_spots = models.IntegerField(_("Reservation Spots"), null=True, \ blank=True, choices=NUMBER_FIELDS, help_text=_("How large is your tour group?"), db_index=True,) price = models.DecimalField(max_digits=100, decimal_places=2, default=0.00) sale_price = models.DecimalField(decimal_places=2, max_digits=20, null=True, blank=True) #GEOCODING: MAKE SO THAT the POSTSAVERECEIVER SAVES THE SPOT AND THEN PUTS PUTS IT IN A GEOLOCATION POINT. city = models.TextField(_("City"), blank=True) location = models.CharField(_("Location Point"), null=True, \ blank=True, help_text=_("Where does the tour meetup? Please enter like such. 331 NW 26th St, Corvallis OR, 97330"), db_index=True, max_length=200) latitude = models.DecimalField(max_digits=50, decimal_places=10, blank=True, null=True) longitude = models.DecimalField(max_digits=50, decimal_places=10, blank=True, null=True) tour_type = models.CharField(_("Meetup Location"), max_length=255, null=True, \ blank=True, choices=TOUR_FIELDS, help_text=_("What type of tour is this?"), db_index=True,) tour_icon = models.CharField(_("tour_icon"), max_length=255, blank=True, null=True) #ENDGEOCODING description = models.TextField(_("description"), blank=True) creator = models.ForeignKey(django_settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("creator"), related_name='creator') 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) objects = EventManager() 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 @property def get_image_url(self): img = self.image_set.first() if img: return img.image.url return img def get_absolute_url(self): return reverse('event', args=[self.id]) def get_occurrences(self, start, end, clear_prefetch=True): """ >>> rule = Rule(frequency = "MONTHLY", name = "Monthly") >>> rule.save() >>> event = Event(rule=rule, start=datetime.datetime(2008,1,1,tzinfo=pytz.utc), end=datetime.datetime(2008,1,2)) >>> event.rule <Rule: Monthly> >>> occurrences = event.get_occurrences(datetime.datetime(2008,1,24), datetime.datetime(2008,3,2)) >>> ["%s to %s" %(o.start, o.end) for o in occurrences] ['2008-02-01 00:00:00+00:00 to 2008-02-02 00:00:00+00:00', '2008-03-01 00:00:00+00:00 to 2008-03-02 00:00:00+00:00'] Ensure that if an event has no rule, that it appears only once. >>> event = Event(start=datetime.datetime(2008,1,1,8,0), end=datetime.datetime(2008,1,1,9,0)) >>> occurrences = event.get_occurrences(datetime.datetime(2008,1,24), datetime.datetime(2008,3,2)) >>> ["%s to %s" %(o.start, o.end) for o in occurrences] [] """ # Explanation of clear_prefetch: # # Periods, and their subclasses like Week, call # prefetch_related('occurrence_set') on all events in their # purview. This reduces the database queries they make from # len()+1 to 2. However, having a cached occurrence_set on the # Event model instance can sometimes cause Events to have a # different view of the state of occurrences than the Period # managing them. # # E.g., if you create an unsaved occurrence, move it to a # different time [which saves the event], keep a reference to # the moved occurrence, & refetch all occurrences from the # Period without clearing the prefetch cache, you'll end up # with two Occurrences for the same event but different moved # states. It's a complicated scenario, but can happen. (See # tests/test_occurrence.py#test_moved_occurrences, which caught # this bug in the first place.) # # To prevent this, we clear the select_related cache by default # before we call an event's get_occurrences, but allow Period # to override this cache clear since it already fetches all # occurrence_sets via prefetch_related in its get_occurrences. 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): 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) 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) 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) # Was this before I pushed it out. # return self._create_occurrence(next_occurrence) 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: event_params['count'] = 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
def test_get_model_bases_with_no_setting(self): from django.db.models import Model expected_result = [Model] actual_result = get_model_bases('Event') self.assertListEqual(actual_result, expected_result)
def test_get_model_bases_with_custom_dict_default(self): from django.db.models import Model expected_result = [Model] actual_result = get_model_bases('Event') self.assertListEqual(actual_result, expected_result)
class Occurrence(with_metaclass(ModelBase, *get_model_bases())): event = models.ForeignKey(Event, on_delete=models.CASCADE, verbose_name=_("event")) title = models.CharField(_("title"), max_length=255, blank=True, null=True) description = models.TextField(_("description"), blank=True, null=True) start = models.DateTimeField(_("start")) end = models.DateTimeField(_("end")) 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) class Meta(object): verbose_name = _("occurrence") verbose_name_plural = _("occurrences") app_label = 'schedule' def __init__(self, *args, **kwargs): super(Occurrence, self).__init__(*args, **kwargs) if self.title is None and self.event_id: self.title = self.event.title if self.description is None 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)
class Occurrence(with_metaclass(ModelBase, *get_model_bases())): event = models.ForeignKey(Event, verbose_name=_("event")) start = models.DateTimeField(_("start")) end = models.DateTimeField(_("end")) 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) class Meta(object): verbose_name = _("occurrence") verbose_name_plural = _("occurrences") app_label = 'schedule' unique_together = ('event', 'start') # the api uses event and start as the primary key objects = models.Manager() def moved(self): return self.original_start != self.start or self.original_end != self.end moved = property(moved) def cancel(self): self.cancelled = True self.save() def get_lookups(self): ''' returns lookup values for this instance as dictionary {key:value, key:value} to use for url reverse ''' lookups = {'event_id': self.event_id} lookups.update({'pk':self.pk} if self.pk else {'start': self.start}) return lookups def move(self, new_start, new_end): self.start = new_start self.end = new_end self.save() def uncancel(self): self.cancelled = False self.save() # these get url functions do not apply to the rest api 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)
class Event(with_metaclass(ModelBase, *get_model_bases())): ''' This model stores meta data for a date. You can relate this data to many other models. ''' start = models.DateTimeField(_("start")) end = models.DateTimeField(_("end"), help_text=_("The end time must be later than the start time.")) title = models.CharField(_("title"), max_length=255, blank=True, null=True) description = models.TextField(_("description"), null=True, blank=True) created_on = models.DateTimeField(_("created on"), auto_now_add=True) updated_on = models.DateTimeField(_("updated on"), auto_now=True) rule = models.ForeignKey(Rule, null=True, blank=True, verbose_name=_("Repeats"), help_text=_("Select '----' for a one time only event.")) rule_params = JSONField(_("repeat params"), null=True, blank=True) end_recurring_period = models.DateTimeField(_("end recurring period"), null=True, blank=True, help_text=_("This date is ignored for one time only events.")) calendar = models.ForeignKey(Calendar, null=True, blank=True, verbose_name=_("calendar")) objects = EventManager() class Meta(object): verbose_name = _('event') verbose_name_plural = _('events') app_label = 'schedule' 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): """ >>> rule = Rule(frequency = "MONTHLY", name = "Monthly") >>> rule.save() >>> event = Event(rule=rule, start=datetime.datetime(2008,1,1,tzinfo=pytz.utc), end=datetime.datetime(2008,1,2)) >>> event.rule <Rule: Monthly> >>> occurrences = event.get_occurrences(datetime.datetime(2008,1,24), datetime.datetime(2008,3,2)) >>> ["%s to %s" %(o.start, o.end) for o in occurrences] ['2008-02-01 00:00:00+00:00 to 2008-02-02 00:00:00+00:00', '2008-03-01 00:00:00+00:00 to 2008-03-02 00:00:00+00:00'] Ensure that if an event has no rule, that it appears only once. >>> event = Event(start=datetime.datetime(2008,1,1,8,0), end=datetime.datetime(2008,1,1,9,0)) >>> occurrences = event.get_occurrences(datetime.datetime(2008,1,24), datetime.datetime(2008,3,2)) >>> ["%s to %s" %(o.start, o.end) for o in occurrences] [] ` """ 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): if self.rule is not None: params = self.rule.get_params() or {} params.update(self.rule_params or {}) frequency = self.rule.rrule_frequency() return rrule.rrule(frequency, dtstart=self.start, **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) def get_occurrence(self, date): if timezone.is_naive(date) and django_settings.USE_TZ: date = timezone.make_aware(date, timezone.utc) rule = self.get_rrule_object() if rule: next_occurrence = rule.after(date, inc=True) else: next_occurrence = self.start if next_occurrence == date: try: return Occurrence.objects.get(event=self, original_start=date) except Occurrence.DoesNotExist: return self._create_occurrence(next_occurrence) def _get_occurrence_list(self, start, end): """ returns a list of occurrences for this event from start to end. """ difference = (self.end - self.start) if self.rule is not None: occurrences = [] if self.end_recurring_period and self.end_recurring_period < end: end = self.end_recurring_period rule = self.get_rrule_object() o_starts = [] o_starts.append(rule.between(start, end, inc=True)) o_starts.append(rule.between(start - (difference // 2), end - (difference // 2), inc=True)) o_starts.append(rule.between(start - difference, end - difference, inc=True)) for occ in o_starts: for o_start in occ: o_end = o_start + difference 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, tzinfo=pytz.utc): """ returns a generator that produces unpresisted occurrences after the datetime ``after``. """ if after is None: after = timezone.now() rule = self.get_rrule_object() if rule is None: if self.end > after: yield self._create_occurrence(self.start, self.end) raise StopIteration date_iter = iter(rule) difference = self.end - self.start while True: o_start = next(date_iter) if self.end_recurring_period and o_start > self.end_recurring_period: raise StopIteration o_end = o_start + difference if o_end > after: yield self._create_occurrence(o_start, o_end) def occurrences_after(self, after=None): """ returns a generator that produces occurrences after the datetime ``after``. Includes all of the persisted Occurrences. """ occ_replacer = OccurrenceReplacer(self.occurrence_set.all()) generator = self._occurrences_after_generator(after) while True: next_occurence = next(generator) yield occ_replacer.get_occurrence(next_occurence)
class Rule(with_metaclass(ModelBase, *get_model_bases())): """ 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 https://dateutil.readthedocs.io/en/stable/rrule.html) ** count 重复次数,如指定到期时间,可无 ** bymonth: MO, TU, WE, TH, FR, SA, SU 指定星期, 数字指定日期; ** bymonthday 每月日期,数字指定日期; ** byweekday: MO, TU, WE, TH, FR, SA, SU # ** bysetpos # ** byyearday 每年日期,数字指定天数,暂停使用; # ** byweekno 数字 0至6表示周一到周末;暂停使用 # ** 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"), null=True, blank=True) is_public = models.BooleanField("is public", default=False) class Meta(object): verbose_name = _('rule') verbose_name_plural = _('rules') app_label = 'schedule' def rrule_frequency(self): compatibiliy_dict = { 'DAILY': DAILY, 'MONTHLY': MONTHLY, 'WEEKLY': WEEKLY, 'YEARLY': YEARLY, 'HOURLY': HOURLY, # 'MINUTELY': MINUTELY, # 'SECONDLY': SECONDLY } return compatibiliy_dict[self.frequency] def get_params(self): """ {'count': 5, 'byweekday': [MO, TU, WE, TH]} """ if self.params is None: return {} return json.loads(self.params) def __str__(self): """Human readable string for Rule""" return 'Rule %s params %s' % (self.name, self.params)
class Rule(with_metaclass(ModelBase, *get_model_bases())): """ 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"), null=True, 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): compatibiliy_dict = { 'DAILY': DAILY, 'MONTHLY': MONTHLY, 'WEEKLY': WEEKLY, 'YEARLY': YEARLY, 'HOURLY': HOURLY, 'MINUTELY': MINUTELY, 'SECONDLY': SECONDLY } return compatibiliy_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} """ if self.params is None: return {} 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)
class Event(with_metaclass(ModelBase, *get_model_bases())): ''' This model stores meta data for a date. You can relate this data to many other models. ''' start = models.DateTimeField(_("start")) end = models.DateTimeField( _("end"), help_text=_("The end time must be later than the start time.")) title = models.CharField(_("title"), max_length=255) description = models.TextField(_("description"), null=True, blank=True) creator = models.ForeignKey(django_settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True, verbose_name=_("creator"), related_name='creator') 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, 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"), null=True, blank=True, max_length=10) objects = EventManager() class Meta(object): verbose_name = _('event') verbose_name_plural = _('events') app_label = 'schedule' 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): """ >>> rule = Rule(frequency = "MONTHLY", name = "Monthly") >>> rule.save() >>> event = Event(rule=rule, start=datetime.datetime(2008,1,1,tzinfo=pytz.utc), end=datetime.datetime(2008,1,2)) >>> event.rule <Rule: Monthly> >>> occurrences = event.get_occurrences(datetime.datetime(2008,1,24), datetime.datetime(2008,3,2)) >>> ["%s to %s" %(o.start, o.end) for o in occurrences] ['2008-02-01 00:00:00+00:00 to 2008-02-02 00:00:00+00:00', '2008-03-01 00:00:00+00:00 to 2008-03-02 00:00:00+00:00'] Ensure that if an event has no rule, that it appears only once. >>> event = Event(start=datetime.datetime(2008,1,1,8,0), end=datetime.datetime(2008,1,1,9,0)) >>> occurrences = event.get_occurrences(datetime.datetime(2008,1,24), datetime.datetime(2008,3,2)) >>> ["%s to %s" %(o.start, o.end) for o in occurrences] [] """ # Explanation of clear_prefetch: # # Periods, and their subclasses like Week, call # prefetch_related('occurrence_set') on all events in their # purview. This reduces the database queries they make from # len()+1 to 2. However, having a cached occurrence_set on the # Event model instance can sometimes cause Events to have a # different view of the state of occurrences than the Period # managing them. # # E.g., if you create an unsaved occurrence, move it to a # different time [which saves the event], keep a reference to # the moved occurrence, & refetch all occurrences from the # Period without clearing the prefetch cache, you'll end up # with two Occurrences for the same event but different moved # states. It's a complicated scenario, but can happen. (See # tests/test_occurrence.py#test_moved_occurrences, which caught # this bug in the first place.) # # To prevent this, we clear the select_related cache by default # before we call an event's get_occurrences, but allow Period # to override this cache clear since it already fetches all # occurrence_sets via prefetch_related in its get_occurrences. 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): if self.rule is not None: params, empty = 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 not empty: return rrule.rrule(frequency, dtstart=dtstart, **params) else: year = self.start.year - 1 return rrule.rrule(frequency, dtstart=dtstart, until=self.start.replace(year=year)) 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) 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 for this event from start to end. """ difference = (self.end - self.start) if self.rule is not None: use_naive = timezone.is_naive(start) # Use the timezone from the start date tzinfo = timezone.utc if start.tzinfo: tzinfo = start.tzinfo occurrences = [] if self.end_recurring_period and self.end_recurring_period < end: end = self.end_recurring_period rule = self.get_rrule_object(tzinfo) start = (start - difference).replace(tzinfo=None) end = (end - difference) if timezone.is_aware(end): end = tzinfo.normalize(end).replace(tzinfo=None) o_starts = [] o_starts.append(rule.between(start, end, inc=True)) o_starts.append( rule.between(start - (difference // 2), end - (difference // 2), inc=True)) o_starts.append( rule.between(start - difference, end - difference, inc=True)) for occ in o_starts: for o_start in occ: o_start = tzinfo.localize(o_start) if use_naive: o_start = timezone.make_naive(o_start, tzinfo) o_end = o_start + difference 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_occurences`` 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) if self.end_recurring_period and self.end_recurring_period and o_start > self.end_recurring_period: break 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_occurences=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_occurences`` 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_occurences and index > max_occurences - 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 empty = False 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 sp in rule_params[param]: event_params[param] = [sp] else: event_params = {'count': 0} empty = True break else: event_params[param] = rule_params[param] return event_params, empty @property def event_params(self): event_params, empty = 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
class Calendar(with_metaclass(ModelBase, *get_model_bases())): ''' 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 = _('calendar') app_label = 'schedule' def __str__(self): return self.name @property def events(self): return self.event_set def create_relation(self, obj, distinction=None, 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, in_datetime=datetime.datetime.now, tzinfo=pytz.utc): """ 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. in_datetime is the datetime you want to check against. It defaults to datetime.datetime.now """ 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): return reverse('calendar_home', kwargs={'calendar_slug': self.slug}) def add_event_url(self): return reverse('calendar_create_event', args=[self.slug])
class Rule(with_metaclass(ModelBase, *get_model_bases())): """ 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"), null=True, blank=True) class Meta(object): verbose_name = _('rule') verbose_name_plural = _('rules') app_label = 'schedule' def rrule_frequency(self): compatibiliy_dict = { 'DAILY': DAILY, 'MONTHLY': MONTHLY, 'WEEKLY': WEEKLY, 'YEARLY': YEARLY, 'HOURLY': HOURLY, 'MINUTELY': MINUTELY, 'SECONDLY': SECONDLY } return compatibiliy_dict[self.frequency] 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]} """ if self.params is None: return {} params = self.params.split(';') param_dict = defaultdict(list) for param in params: param = param.split(':') if len(param) == 2: param = dict([(str(param[0]), [int(p) for p in param[1].split(',')])]) param_dict.update(param) return param_dict def __str__(self): """Human readable string for Rule""" return 'Rule %s params %s' % (self.name, self.params)