Using RecurrenceField
¶
Getting occurrences between two dates¶
Once you’ve created a model with a RecurrenceField
, you’ll
probably want to use it to figure out what dates are involved in a
particular recurrence pattern.
Note
Whether you want to use between
or occurrences will depend on what sort of rules you’re dealing
with. In reality, a model like Course probably has rules like
“every Thursday for 10 weeks”, so occurrences
will work fine,
since the rules have natural limits. For other uses (e.g. find me
every date this year for a club that runs every Wednesday), you’ll
want to use between
.
between
takes two dates (the start and end date), and returns a list
of datetime
objects matching the recurrence pattern between those
dates. It is used like this (using the Course
model from above):
from datetime import datetime
from myapp.models import Course
course = Course.objects.get(pk=1)
course.recurrences.between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0)
)
This won’t include occurrences if they occur on the start and end
dates specified. If you want to include those, pass in inc
like
this:
course.recurrences.between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0),
inc=True
)
Warning
Slightly confusingly, between
will only return you dates after
the current date, if used as above (provided those dates fall
between the two first parameters to between
). Read on for how
to get all the occurrences between two dates.
To get all the occurrences between two dates (including dates that
are before the current time, but after the provided start date),
you’ll also need to set dtstart
, like this:
course.recurrences.between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0),
dtstart=datetime(2010, 1, 1, 0, 0, 0),
inc=True
)
That will get you all occurrences between 1st January 2010, and 31st December 2014, including any occurrences on 1st January 2010 and 31st December 2014, if your recurrence pattern matches those dates.
The effective starting date for any recurrence pattern is essentially
the later of the first argument and dtstart
. To minimize
confusion, you probably want to set them both to the same value.
Warning
Note that per default dtstart
will be the first occurence in
your list if specified, according to RFC 2445. This practice
deviates from how dateutil.rrule
handles dtstart
and can
therefore lead to confusion. Read on for how you can control this
behavior for your own recurrence patterns.
To switch off the automatic inclusion of dtstart
into the
occurence list, set include_dtstart=False
as an argument for the
RecurrenceField
whose behavior you want to change:
class Course(models.Model):
title = models.CharField(max_length=200)
recurrences = RecurrenceField(include_dtstart=False)
With this change any dtstart
value will only be an occurence if
it matches the pattern specified in recurrences
. This also works
for instantiating Recurrence
objects directly:
pattern = recurrence.Recurrence(
rrules=[recurrence.Rule(recurrence.WEEKLY, byday=recurrence.MONDAY)],
include_dtstart=False).between(
datetime(2010, 1, 1, 0, 0, 0),
datetime(2014, 12, 31, 0, 0, 0),
dtstart=datetime(2010, 1, 1, 0, 0, 0),
inc=True
)
)
Getting all occurrences¶
occurrences
is particularly useful where your recurrence pattern
is limited by the rules generating occurrences (e.g. “every Tuesday
for 10 weeks”, or “every Tuesday until 23rd April 2014”).
You can get a generator which you can iterate over to get all
occurrences using occurrences
:
dates = course.recurrences.occurrences()
You can optionally provide dtstart
to specify the first
occurrence, and dtend
to specify the final occurrence.
You can index into the returned object, to (for example) get the first session of our course model:
dates = course.recurrences.occurrences()
first_instance = dates[0]
Warning
Looping over the entire generator returned by example above might
be extremely slow and resource hungry if dtstart
or dtend
are not provided. Without dtstart
, we implicitly are looking
for occurrences after the current date. Without dtend
, we’ll
look for all occurrences up to (and including) the year 9999,
which is probably not what you want. The the code above counts all
occurrences of our course from tomorrow until 31st December, 9999.
Counting occurrences¶
The function count
works fairly similarly:
course.recurrences.count()
It is roughly equivalent to:
len(list(course.recurrences.occurrences()))
Note the warning in occurrences before using
count
(or converting the generator returned by occurrences()
to a list), if you are not providing both dtstart
and dtend
.
Getting the next or previous occurrences¶
If you want to get the next or previous occurrence in a given
pattern, you can use after
or before
, respectively. As with
between
, you can choose whether you want to be inclusive of the
datetime
passed in by setting inc
. If no next or previous
occurrence exists, None
is returned.
course = Course.objects.get(pk=1)
# Get the first course on or after 1st January 2010 (this won't do
# quite what you expect)
course.recurrences.after(
datetime(2010, 1, 1, 0, 0, 0),
inc=True
)
As with between
, if you don’t specify a dtstart
, it will
implicitly be the current time, so the above code will, to be more
precise, give you the first course on or after 1st January 2010, or
on or after the current date, whichever is later. Since you probably
don’t want that behaviour, you’ll probably want to specify
dtstart
, as follows:
course = Course.objects.get(pk=1)
# Get the first course on or after 1st January 2010
course.recurrences.after(
datetime(2010, 1, 1, 0, 0, 0),
inc=True,
dtstart=datetime(2010, 1, 1, 0, 0, 0),
)
For similar reasons, using before
really requires that
dtstart
is provided, to give a start date to the recurrence
pattern. This makes some sense if you consider a recurrence pattern
like “every Monday, occurring 5 times”. Without dtstart
, it’s
unclear what before
should return - since it’s impossible to know
whether the pattern has started, and if so when. For example, if it
started 5 years ago, before
should return a date approximately 5
years ago, whereas if it started two weeks ago, before
should
return the last Monday
(or the provided date, if inc
is True,
and the provided date is a Monday).
Getting textual descriptions of patterns¶
Recurrence patterns can have multiple rules for inclusion (e.g. every week, on a Tuesday) and exclusion (e.g. except when it’s the first Tuesday of the month), together with specific dates to include or exclude (regardless of whether they’re part of the inclusion or exclusion rules).
You’ll often want to display a simple textual description of the rules involved.
To take our Course
example again, you can get access to the
relevant inclusion rules by accessing the rrules
member of the
RecurrenceField
attribute of your model (called recurrences
in our example, though you can call it whatever you like), and to the
exclusion rules by accessing the exrules
member. From there you
can get textual descriptions, like this:
course = Course.objects.get(pk=1)
text_rules_inclusion = []
for rule in course.recurrences.rrules:
text_rules_inclusion.append(rule.to_text())
Similar code would work equally well for exrules
.