diff --git a/recurrence/base.py b/recurrence/base.py index be6bd77..cf74b09 100644 --- a/recurrence/base.py +++ b/recurrence/base.py @@ -291,10 +291,18 @@ class Recurrence: recurrence in the set (according to the rfc2445 spec). With `include_dtstart == False` `dtstart` is only the rule's starting point like in python's `dateutil.rrule`. + + `include_dtend` : bool + Defines if `dtend` is included in the recurrence set as + the last occurrence. With `include_dtend == True` it is + both the ending point for recurrences and the last + recurrence in the set (according to the rfc2445 spec). + With `include_dtend == False` `dtend` is only the rule's + ending point like in python's `dateutil.rrule`. """ def __init__( self, dtstart=None, dtend=None, rrules=(), exrules=(), - rdates=(), exdates=(), include_dtstart=True + rdates=(), exdates=(), include_dtstart=True, include_dtend=True ): """ Create a new recurrence. @@ -312,6 +320,7 @@ def __init__( self.rdates = list(rdates) self.exdates = list(exdates) self.include_dtstart = include_dtstart + self.include_dtend = include_dtend def __iter__(self): return self.occurrences() @@ -542,6 +551,7 @@ def to_dateutil_rruleset(self, dtstart=None, dtend=None, cache=False): dtstart = dtstart or self.dtstart dtend = dtend or self.dtend include_dtstart = self.include_dtstart + include_dtend = self.include_dtend if dtend: dtend = normalize_offset_awareness(dtend or self.dtend, dtstart) @@ -568,7 +578,7 @@ def to_dateutil_rruleset(self, dtstart=None, dtend=None, cache=False): rruleset.rdate(rdate) elif not dtend: rruleset.rdate(rdate) - if dtend is not None: + if include_dtend and dtend is not None: rruleset.rdate(dtend) for exdate in self.exdates: @@ -918,7 +928,7 @@ def serialize_rule(rule): return u'\n'.join(u'%s:%s' % i for i in items) -def deserialize(text, include_dtstart=True): +def deserialize(text, include_dtstart=True, include_dtend=True): """ Deserialize a rfc2445 formatted string. @@ -1084,7 +1094,7 @@ def deserialize_dt(text): for item in param_text.split(','): exdates.append(deserialize_dt(item)) - return Recurrence(dtstart, dtend, rrules, exrules, rdates, exdates, include_dtstart) + return Recurrence(dtstart, dtend, rrules, exrules, rdates, exdates, include_dtstart, include_dtend) def rule_to_text(rule, short=False): diff --git a/recurrence/fields.py b/recurrence/fields.py index d86aa7a..7775c7a 100644 --- a/recurrence/fields.py +++ b/recurrence/fields.py @@ -9,8 +9,9 @@ class RecurrenceField(fields.Field): """Field that stores a `recurrence.base.Recurrence` to the database.""" - def __init__(self, include_dtstart=True, **kwargs): + def __init__(self, include_dtstart=True, include_dtend=True, **kwargs): self.include_dtstart = include_dtstart + self.include_dtend = include_dtend super(RecurrenceField, self).__init__(**kwargs) def get_internal_type(self): @@ -20,7 +21,7 @@ def to_python(self, value): if value is None or isinstance(value, recurrence.Recurrence): return value value = super(RecurrenceField, self).to_python(value) or u'' - return recurrence.deserialize(value, self.include_dtstart) + return recurrence.deserialize(value, self.include_dtstart, self.include_dtend) def from_db_value(self, value, *args, **kwargs): return self.to_python(value) diff --git a/tests/test_fields.py b/tests/test_fields.py index b30e26c..646a44c 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -218,6 +218,68 @@ def test_include_dtstart_from_object(): datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] +def test_include_dtend_from_field(): + rule = Rule( + recurrence.WEEKLY, + byday=recurrence.MONDAY + ) + + limits = Recurrence( + rrules=[rule] + ) + + value = recurrence.serialize(limits) + + model_field = recurrence.fields.RecurrenceField() # Test with include_dtend=True (default) + rec_obj = model_field.to_python(value) + assert rec_obj == limits + # 14th of August (dtend) is expected but only for inc=True + assert [occ for occ in rec_obj.to_dateutil_rruleset(datetime(2015, 8, 2), datetime(2015, 8, 14))] == [ + datetime(2015, 8, 2, 0, 0), datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0), datetime(2015, 8, 14, 0, 0) + ] + + model_field = recurrence.fields.RecurrenceField(include_dtend=False) # Test with include_dtend=False + rec_obj = model_field.to_python(value) + assert rec_obj == limits + # 14th of August (dtend) is not expected regardless of inc + assert [occ for occ in rec_obj.to_dateutil_rruleset(datetime(2015, 8, 2), datetime(2015, 8, 14))] == [ + datetime(2015, 8, 2, 0, 0), datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0)] + + +def test_include_dtend_from_object(): + rule = Rule( + recurrence.WEEKLY, + byday=recurrence.MONDAY + ) + + limits = Recurrence( # include_dtend=True (default) + rrules=[rule] + ) + + assert [occ for occ in limits.to_dateutil_rruleset(datetime(2015, 8, 2), datetime(2015, 8, 14))] == [ + datetime(2015, 8, 2, 0, 0), datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0), + datetime(2015, 8, 14, 0, 0) + ] + + limits = Recurrence( # include_dtend=False (dtend is expected to not be included) + include_dtend=False, + rrules=[rule] + ) + + assert [occ for occ in limits.to_dateutil_rruleset(datetime(2015, 8, 2), datetime(2015, 8, 14))] == [ + datetime(2015, 8, 2, 0, 0), datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0) + ] + + limits = Recurrence( # include_dtend=False (dtend dtstart are expected to not be included) + include_dtstart=False, + include_dtend=False, + rrules=[rule] + ) + + assert [occ for occ in limits.to_dateutil_rruleset(datetime(2015, 8, 2), datetime(2015, 8, 14))] == [ + datetime(2015, 8, 3, 0, 0), datetime(2015, 8, 10, 0, 0) + ] + def test_none_fieldvalue(): field = RecurrenceField() value = None