cal : Calendar functionality

This page gives developer information about the plugin, which adds general calendar functionality to your Lino application.

We assume that you have read the User Guide: cal : The calendar module. See also Defining holidays. This plugin is often used together with lino_xl.lib.calview, which defines calendar views.

This is a tested document. The following instructions are used for initialization:

>>> from lino import startup
>>> startup('lino_book.projects.avanti1.settings')
>>> from lino.api.doctest import *

Calendar entries


The Django model that represents a calendar entry.

The internal model name is Event for historical reasons. Users see it as calendar entry:

>>> print(
Calendar entry

The starting date of this calendar entry. May not be empty.


The ending date of this calendar entry. Leave empty for same-day entries.


The starting time. If this is empty, the entry is considered an all-day entry.

Changing this field will change the end_time field as well if the entry's type has a default duration (EventType.default_duration).


The ending time. If this is before the starting time, and no ending date is given, Lino considers the entry to end the day after.


A one-line descriptive text.


A longer descriptive text.


The responsible user.


Another user who is expected to take responsibility for this entry.

See lino.modlib.users.Assignable.assigned_to.


The type of this calendar entry.

Every calendar entry should have this field pointing to a given EventType, which holds extended configurable information about this entry.


The state of this entry. The state can change according to rules defined by the workflow.


The event generator that generated and controls this calendar entry.

See Automatic calendar entries.


Whether this entry should allow other entries at the same time.


Whether the guests list of this event has been modified by an end user.

Once an end user has modified something in the list of guests, Lino will no longer touch the list during update_guests().


Shows the start date and time of the calendar entry.

See for details.


Shows the date and time of the calendar entry with a link that opens all entries on that day.

Deprecated because it is usually irritating. It's better to use when_text, and users open the detail window as usually by double-clicking on the row. And then they have an action on each entry for opening EntriesByDay if they want.


A ShowSlaveTable button which opens the ConflictingEvents table for this event.


Populate or update the list of participants for this calendar entry according to the suggestions.

Calls suggest_guests() to instantiate them.

  • No guests are added when loading from dump

  • The entry must be in a state which allows editing the guests

  • Deletes existing guests in state invited that are no longer suggested


Create or update all other automatic calendar entries of this series.


Return a QuerySet of calendar entries that conflict with this one. Must work also when called on an unsaved instance. May return None to indicate an empty queryset. Applications may override this to add specific conditions.


Whether this entry has any conflicting entries.

This is roughly equivalent to asking whether get_conflicting_events() returns more than 0 events.

Except when this event's type tolerates more than one events at the same time.


Yield the list of unsaved Guest instances to be added to this calendar entry.

This method is called from update_guests().

get_event_summary(self, ar)

How this event should be summarized in contexts where possibly another user is looking (i.e. currently in invitations of guests, or in the extensible calendar panel).

before_ui_save(self, ar)

Mark the entry as "user modified" by setting a default state. This is important because EventGenerators may not modify any user-modified Events.

auto_type_changed(self, ar)

Called when the number of this automatically generated entry (auto_type ) has changed.

The default updates the summary.


Returns the Calendar which contains this entry, or None if no subscription is found.

Needed for ext.ensible calendar panel.

The default implementation returns None. Override this if your app uses Calendars.

>>> show_fields(,
...     'start_date start_time end_date end_time user summary description event_type state')
| Internal name | Verbose name        | Help text                                                            |
| start_date    | Start date          | The starting date of this calendar entry.  May not be empty.         |
| start_time    | Start time          | The starting time.  If this is empty, the entry is considered an     |
|               |                     | all-day entry.                                                       |
| end_date      | End Date            | The ending date of this calendar entry.                              |
|               |                     | Leave empty for same-day entries.                                    |
| end_time      | End Time            | The ending time.  If this is before the starting time, and no ending |
|               |                     | date is given, Lino considers the entry to end the day after.        |
| user          | Responsible user    | The responsible user.                                                |
| summary       | Short description   | A one-line descriptive text.                                         |
| description   | Description         | A longer descriptive text.                                           |
| event_type    | Calendar entry type | The type of this calendar entry.                                     |
| state         | State               | The state of this entry. The state can change according to           |
|               |                     | rules defined by the workflow.                                       |

Lifecycle of a calendar entry

Every calendar entry has a given state, which can change according to rules defined by the application.

The default list of choices for this field contains the following values.

======= ============ ============ ============= ============= ======== ============= =========
 value   name         text         Button text   Fill guests   Stable   Transparent   No auto
------- ------------ ------------ ------------- ------------- -------- ------------- ---------
 10      suggested    Suggested    ?             Yes           No       No            No
 20      draft        Draft        ☐             Yes           No       No            No
 50      took_place   Took place   ☑             No            Yes      No            No
 70      cancelled    Cancelled    ☒             No            Yes      Yes           Yes
======= ============ ============ ============= ============= ======== ============= =========

The list of possible states of a calendar entry.


Every calendar entry state is an instance of this and has some attributes.


Whether the presences of an entry in this state are filled in automatically. If this is True (and if the entry type's fill_presences is True as well), the presences cannot be modified manually by the used.

TODO: rename this to fill_presences


Force the given guest state for all guests when an entry is set to this state and when EventType.force_guest_states is True.


Whether an entry in this state is considered transparent, i.e. dos not conflict with other entries at the same moment.


Whether an entry in this state is considered "stable" when differentiating between "stable" and "pending" entries.

This does not influence editability of the entry.

See EventEvents.stable and EventEvents.pending.


Whether switching to this state will clear the entry's auto_type field, i.e. it is no longer considered an automatically generated entry, IOW it "escapes" from its entry generator.


Old name for fill_guests.

Calendar entry types

Every calendar entry has a field Event.event_type that points to its calendar entry type.


Django model representing a calendar entry type.

The possible value of the Event.event_type field.


An optional default duration for calendar entries of this type.

If this field is set, Lino will help with entering Event.end_time and Event.start_date of an calendar entries by changing the end_time of an entry when the start_date is changed (and the start_time when the end_date)


Default text for summary of new entries.


Whether entries of this type are considered "appointments" (i.e. whose time and place have been agreed upon with other users or external parties).

Certain tables show only entries whose type has the is_appointment field checked. See show_appointments.


The maximal number of days allowed as duration. 0 means no limit.

If this is 1, Lino will set end_date to None.

See also LongEntryChecker


Whether calendar entries of this type make the user unavailable for other locking events at the same time.


How many conflicting events should be tolerated.


Allow entries of this type to conflict with other events.


Whether presence states should be forced to those defined by the entry state.

This will have an effect only if the application developer has defined a mapping from entry state to the guest state by setting EntryState.guest_state for at least one entry state.

Lino Tera uses this for individual and family therapies where they don't want to manage presences of every participant. When an appointment is set to "Took place", Lino sets all guests to "Present". See Calendar in Lino Tera for a usage example.


Whether guests should be automatically filled for calendar entries of this type.


Planned name for fill_presences.


The list of entry types defined on this site.

This is usually filled in the std demo fixture of the application.

=========== =============== ================== ================== ================ ============= ===================== =================
 Reference   Designation     Designation (de)   Designation (fr)   Planner column   Appointment   Automatic presences   Locks all rooms
----------- --------------- ------------------ ------------------ ---------------- ------------- --------------------- -----------------
             Absences        Abwesenheiten      Absences           External         Yes           No                    No
             First contact   First contact      First contact                       Yes           No                    No
             Holidays        Feiertage          Jours fériés       External         No            No                    Yes
             Internal        Intern             Interne            Internal         No            No                    No
             Lesson          Lesson             Lesson                              Yes           No                    No
             Meeting         Versammlung        Réunion            External         Yes           No                    No
=========== =============== ================== ================== ================ ============= ===================== =================


Calendar entries can be grouped into "calendars".


The django model representing a calendar.


The color to use for entries of this calendar (in lino_xl.lib.extensible).

==== ============= ================== ================== ============= =======
 ID   Designation   Designation (de)   Designation (fr)   Description   color
---- ------------- ------------------ ------------------ ------------- -------
 1    General       Allgemein          Général                          1
==== ============= ================== ================== ============= =======

Note that the default implementation has no "Calendar" field per calendar entry. The Event model instead has a Event.get_calendar() method.

You might extend Event in your plugin as follows:

from import *
class Event(Event):

    calendar = dd.ForeignKey('cal.Calendar')

    def get_calendar(self):
        return self.calendar

But in other cases it would create unnecessary complexity to add such a field. For example in Lino Welfare there is one calendar per User, and thus the get_calendar method is implemented as follows:

def get_calendar(self):
    if self.user is not None:
        return self.user.calendar

Or in Lino Voga there is one calendar per Room. Thus the get_calendar method is implemented as follows:

def get_calendar(self):
    if is not None:

Event generators

An event generator is something that can generate automatic calendar entries. Examples of event generators include

  • Holidays

  • A course, workshop or activity as used by Welfare, Voga and Avanti (subclasses of

  • A reservation of a room in Lino Voga (lino_xl.lib.rooms.Reservation).

  • A coaching contract with a client in Lino Welfare (lino_welfare.modlib.isip.Contract and

The main effect of the EventGenerator mixin is to add the UpdateEntries action. The generated calendar entries are "controlled" by their generator (their owner field points to the generator) and have a non-empty Event.auto_type field.

The event generator itself does not necessarily contain all fields needed for specifying which events should be generated. These fields are implemented by another model mixin named RecurrenceSet. A recurrence set is something that specifies which calendar events should get generated.


Base class for things that generate a series of events.

See Event generators.


Update the presence lists of all calendar events generated by this.


Create or update the automatic calendar entries controlled by this generator.

This is the :guilabel:` ⚡ ` button.

See UpdateEntries.

get_wanted_auto_events(self, ar)

Return a tuple of (wanted, unwanted), where wanted is a list of calendar entries to be saved, and unwanted a list of events to be deleted.

care_about_conflicts(self, we)

Whether this event generator should try to resolve conflicts for the given calendar entry we (in resolve_conflicts())

resolve_conflicts(self, we, ar, rset, until)

Check whether given entry we conflicts with other entries and move it to a new date if necessary. Returns (a) the entry's start_date if there is no conflict, (b) the next available alternative date if the entry conflicts with other existing entries and should be moved, or (c) None if there are conflicts but no alternative date could be found.

ar is the action request who asks for this. rset is the RecurrenceSet.


Generate or update the automatic events controlled by this object.

This action is installed as EventGenerator.do_update_events.

See also Automatic calendar entries.


Update all events of this series.

This is installed as update_events on Event.


The recurrency expresses how often something is to be repeated.

When generating automatic calendar events, Lino supports the following date recurrenies:

======= ============= ====================
 value   name          text
------- ------------- --------------------
 O       once          once
 D       daily         daily
 W       weekly        weekly
 M       monthly       monthly
 Y       yearly        yearly
 P       per_weekday   per weekday
 E       easter        Relative to Easter
======= ============= ====================

List of possible choices for a 'recurrency' field.

Note that a recurrency (an item of this choicelist) is also a DurationUnit.


Repeat events yearly, moving them together with the Easter data of that year.

Lino computes the offset (number of days) between this rule's start_date and the Easter date of that year, and generates subsequent events so that this offset remains the same.

Adding a duration unit

>>> start_date = i2d(20160327)
>>> cal.Recurrencies.once.add_duration(start_date, 1)
Traceback (most recent call last):
Exception: Invalid DurationUnit once
>>> cal.Recurrencies.daily.add_duration(start_date, 1), 3, 28)
>>> cal.Recurrencies.weekly.add_duration(start_date, 1), 4, 3)
>>> cal.Recurrencies.monthly.add_duration(start_date, 1), 4, 27)
>>> cal.Recurrencies.yearly.add_duration(start_date, 1), 3, 27)
>>> cal.Recurrencies.easter.add_duration(start_date, 1), 4, 16)

Recurrent events

This plugin defines a database model RecurrentEvent used for example to generate holidays. See also Defining holidays.

We are going to use this model for demonstrating some more features (which it inherits from RecurrenceSet and EventGenerator).

>>> def demo(every_unit, **kwargs):
...     kwargs.setdefault('start_date', i2d(20160628))
...     kwargs.update(every_unit=cal.Recurrencies.get_by_name(every_unit))
...     obj = cal.RecurrentEvent(**kwargs)
...     for lng in 'en', 'de', 'fr':
...         with translation.override(lng):
...             print(obj.weekdays_text)
>>> demo('weekly', tuesday=True)  
Every Tuesday
Jeden Dienstag
Chaque Mardi
>>> demo('weekly', tuesday=True, every=2)
Every second Tuesday
Jeden zweiten Dienstag
Chaque deuxième Mardi
>>> demo('weekly', tuesday=True, every=9)
Every ninth Tuesday
Jeden neunten Dienstag
Chaque neuvième Mardi
>>> demo('monthly', every=2)
Every 2 months
Alle 2 Monate
Tous les 2 mois
>>>, column_names="id name")
==== =============== ================== ==================
 ID   Designation     Designation (de)   Designation (fr)
---- --------------- ------------------ ------------------
 1    Absences        Abwesenheiten      Absences
 5    First contact   First contact      First contact
 2    Holidays        Feiertage          Jours fériés
 4    Internal        Intern             Interne
 6    Lesson          Lesson             Lesson
 3    Meeting         Versammlung        Réunion
==== =============== ================== ==================
>>> obj = cal.RecurrentEvent(start_date=i2d(20160628))
>>> isinstance(obj, cal.RecurrenceSet)
>>> isinstance(obj, cal.EventGenerator)
>>> obj.tuesday = True
>>> obj.every = 2
>>> obj.every_unit = cal.Recurrencies.monthly
>>> obj.event_type = cal.EventType.objects.get(id=1)
>>> obj.max_events = 5
>>> ses = rt.login('robin')
>>> wanted, unwanted = obj.get_wanted_auto_events(ses)
>>> for e in wanted:
...     print(dd.fds(e.start_date))

Note that above dates are not exactly every 2 months because

  • they are only on Tuesdays

  • Lino also avoids conflicts with existing events

>>> cal.Event.objects.order_by('start_date')[0]
Event #1 ("New Year's Day (01.01.2013)")
>>> obj.monday = True
>>> obj.wednesday = True
>>> obj.thursday = True
>>> obj.friday = True
>>> obj.saturday = True
>>> obj.sunday = True
>>> obj.start_date=i2d(20120628)
>>> wanted, unwanted = obj.get_wanted_auto_events(ses)
>>> for e in wanted:
...     print(dd.fds(e.start_date))

Conflicting events

The demo database contains two appointments on Ash Wednesday and two on Rose Monday. These conflicting calendar events are visible as data problems (see checkdata : High-level integrity tests).

>>> chk = checkdata.Checkers.get_by_value('cal.ConflictingEventsChecker')
>>>, chk)
================= ================================= ==================================================
 Responsible       Database object                   Message
----------------- --------------------------------- --------------------------------------------------
 Robin Rood        *Ash Wednesday (01.03.2017)*      Event conflicts with 2 other events.
 Robin Rood        *Rosenmontag (27.02.2017)*        Event conflicts with 2 other events.
 Robin Rood        *Breakfast (27.02.2017 10:20)*    Event conflicts with Rosenmontag (27.02.2017).
 Romain Raffault   *Rencontre (27.02.2017 11:10)*    Event conflicts with Rosenmontag (27.02.2017).
 Robin Rood        *Seminar (01.03.2017 08:30)*      Event conflicts with Ash Wednesday (01.03.2017).
 Romain Raffault   *Evaluation (01.03.2017 09:40)*   Event conflicts with Ash Wednesday (01.03.2017).
================= ================================= ==================================================
>>> obj = cal.Event.objects.get(id=123)
>>> print(obj)
Ash Wednesday (01.03.2017)
>>>, obj)
============ ============ ========== ======== ====== ==================
 Start date   Start time   End Time   Client   Room   Responsible user
------------ ------------ ---------- -------- ------ ------------------
 01/03/2017   08:30:00     09:45:00                   Robin Rood
 01/03/2017   09:40:00     11:10:00                   Romain Raffault
============ ============ ========== ======== ====== ==================

Transparent calendar entries

The entry type "Internal" is marked "transparent".

>>> obj = cal.EventType.objects.get(id=4)
>>> obj
EventType #4 ('Internal')
>>> obj.transparent

The guests of a calendar entry

A calendar entry can have a list of guests (also called presences or participants depending on the context). A guest is the fact that a given person is expected to attend or has been present at a given calendar entry. Depending on the context the guests of a calendar entry may be labelled "guests", "participants", "presences", ...


The Django model representing a guest.


The calendar event to which this presence applies.


The partner to which this presence applies.


The role of this partner in this presence.


The state of this presence. See GuestStates.

The following three fields are injected by the reception plugin:


Time when the visitor arrived (checked in).


Time when the visitor was received by agent.


Time when the visitor left (checked out).

Every participant of a calendar entry can have a "role". For example in a class meeting you might want to differentiate between the teacher and the pupils.


The role of a guest expresses what the partner is going to do there.


Global table of guest roles.


Global choicelist of possible guest states.

Possible values for the state of a participation. The list of choices for the Guest.state field.

The actual content can be redefined by other plugins, e.g. lino_xl.lib.reception.

======= ========= ============ ========= =============
 value   name      Afterwards   text      Button text
------- --------- ------------ --------- -------------
 10      invited   No           Invited   ?
 40      present   Yes          Present   ☑
 50      missing   Yes          Missing   ☉
 60      excused   No           Excused   ⚕
======= ========= ============ ========= =============

See Event.update_guests().


See EventGenerator.update_all_guests().

Presence lists

To introduce the problem:

  • runserver in lino_book.projects.avanti1 and sign in as robin.

  • create a calendar entry, leave it in draft mode

  • Note that you cannot manually enter a guest in the presences list.

This is a situation where we want Lino to automatically keep the list of guests synchronized with the "suggested guests" for this meeting. For example in Lino Avanti when we have a course with participants (enrolments), and we have generated a series of calendar entries having their suggested guests filled already, and now one participant cancels their enrolment. We want Lino to update all participants of meetings that are still in draft state. The issue is that Lino doesn't correctly differentiate between those two situations:

  • manually enter and manage the list of guests

  • fill guests automatically and keep it synchronized with the guests suggested by the entry generator.

Lino should not let me manually create a guest when the entry is in "fill guests" mode.

The Event.update_guests action is always called in the Event.after_ui_save() method. That's okay, but in our case the action obviously comes to the conclusion that we do want to update our guests. More precisely the event state obviously has EntryState.edit_guests set to False, and the entry type has fill_presences set to True. The solution is to simply set

  • The Event.can_edit_guests_manually() method which encapsulates this condition.

  • That method is now also used to decide whether the presences lists can be modified manually.

Note the difference between "guest" and "presence". The model name is currently still cal.Guest, but this should be renamed to cal.Presence. Because the "guest" is actually the field of a presence which points to the person who is the guest.

Remote calendars

A remote calendar is a set of calendar entries stored on another server. Lino periodically synchronized the local data from the remote server, and local modifications will be sent back to the remote calendar.

The feature is not currently being used anywhere.

See also


Django model for representing a remote calendar.


A room is location where calendar entries can happen. For a given room you can see the EntriesByRoom that happened (or will happen) there. A room has a multilingual name which can be used in printouts.

Applications might change the user label for this model e.g. to "Team" (as done in Lino Presto) if the application is not interested in physical rooms.


Django model for representing a room.


The designation of the room. This is not required to be unique.


The color to use when displaying entries in this room in the calendar view.

See DisplayColors.


Base class for all list of rooms.


Show a list of all rooms.


The detail layout for Rooms and subclasses.


A suscription is when a user subscribes to a calendar. It corresponds to what the extensible CalendarPanel calls "Calendars"


Django model for representing a subscription.


points to the author (recipient) of this subscription




A task is when a user plans to do something (and optionally wants to get reminded about it).


Django model for representing a subscription.


How urgent this task is.

Choicelist field pointing to lino_xl.lib.xl.Priorities.


The state of this Task. one of TaskStates.


Possible values for the state of a Task. The list of choices for the Task.state field.


Global table of all tasks for all users.


Shows the list of tasks for this user.


Shows my tasks whose start date is today or in the future.

Recurrent calendar entries


Django model used to store a recurrency policy.


Generated calendar entries will have this type.


Global table of all possible recurrency policies.


Django model used to store a recurrent event.




Inherited from RecurrentSet.every_unit.

care_about_conflicts(self, we)

Recurrent events don't care about conflicts. A holiday won't move just because some other event has been created before on that date.


The list of all recurrent events (RecurrentEvent).



Table which shows all calendar events.

Filter parameters:


Whether only appointments should be shown. "Yes" means only appointments, "No" means no appointments and leaving it to blank shows both types of events.

An appointment is an event whose event type has appointment checked.


Show only entries that have a presence for the specified guest.


Show only entries assigned to this project, where project is defined by


Show only entries assigned to this user.


Show only entries having this type.


Show only entries having this state.


Show only entries having this user as author.


Shows events conflicting with this one (the master).


This table is usually labelled "Appointments today". It has no "date" column because it shows events of a given date.It is ordred with increasing times.

The default filter parameters are set to show only appointments.


Displays the calendar entries at a given Room.


Shows the calendar entries controlled by this database object.

If the master is an EventGenerator, then this includes especially the entries which were automatically generated.


Show a single calendar event.


Shows the appointments for which I am responsible.

This data view shows today's and all future appointments of the requesting user. The default filter parameters are set to show only appointments (show_appointments is set to "Yes").


Like MyEntries, but only today.


Shows the calendar entries that are assigned to me.

That is, having Event.assigned_to field refers to the requesting user.

This data view also generates a welcome message "X events have been assigned to you" when it is not empty.


Shows overdue appointments, i.e. appointments that happened before today and are still in a nonstable state.

show_appointments is set to "Yes", observed_event is set to "Unstable", end_date is set to today.


Like OverdueAppointments, but only for myself.


Shows my appointments in the near future which are in suggested or draft state.

Appointments before today are not shown. The parameters end_date and start_date can manually be modified in the parameters panel.

The state filter (draft or suggested) cannot be removed.


The default table of presences.


Shows all my presences in calendar events, independently of their state.


Received invitations waiting for my feedback (accept or reject).


Mixin for models that express a set of repeating calendar events. See Event generators.


The start date of the first meeting to be generated.


The end date of the first meeting to be generated. Leave this field empty if the meetings last less than one day.


The frequency of periodic iteration: daily, weekly, monthly or yearly.


The interval between each periodic iteration.

For example, when every is yearly, an every_unit of 2 means once every two years. The default value is 1.


Space-separated list of one or several positions within the recurrency cycle.

Each position is a positive or negative integer expressing which occurrence is to be taken from the recurrency period. For example if positions is -1 and every_unit is monthly, we get the last day of every month.

Inspired by dateutil.rrule.


Maximum number of calendar entries to generate.


A virtual field returning the textual formulation of the weekdays where the recurrence occurs.

Usage examples see cal : Calendar functionality.


Show the calendar entries having this partner as a guest.

This might get deprecated some day. You probably prefer EntriesByGuest.


Show the calendar entries having this partner as a guest.

Similar to GuestsByPartner, but EntriesByGuest can be used to create a new calendar entry. It also makes sure that the new entry has at least one guest, namely the partner who is the master. Because otherwise, if the user creates an entry and forgets to manually add our master as a guest, they would not see the new entry.


Base class for lino_xl.lib.rooms.models.Booking and

Inherits from both EventGenerator and RecurrenceSet.


Don't generate calendar entries beyond this date.

Display colors


A list of colors to be specified for displaying.

========= ========= =========
 value     name      text
--------- --------- ---------
 White     White     White
 Silver    Silver    Silver
 Gray      Gray      Gray
 Black     Black     Black
 Red       Red       Red
 Maroon    Maroon    Maroon
 Yellow    Yellow    Yellow
 Olive     Olive     Olive
 Lime      Lime      Lime
 Green     Green     Green
 Aqua      Aqua      Aqua
 Teal      Teal      Teal
 Blue      Blue      Blue
 Navy      Navy      Navy
 Fuchsia   Fuchsia   Fuchsia
 Purple    Purple    Purple
========= ========= =========

The days of the week


A choicelist with the seven days of a week.

======= =========== ===========
 value   name        text
------- ----------- -----------
 1       monday      Monday
 2       tuesday     Tuesday
 3       wednesday   Wednesday
 4       thursday    Thursday
 5       friday      Friday
 6       saturday    Saturday
 7       sunday      Sunday
======= =========== ===========

The five workdays of the week (Monday to Friday).

Duration units

The calendar plugin defines DurationUnits choicelist, a site-wide list of duration units. In a default configuration it has the following values:

======= ========= =========
 value   name      text
------- --------- ---------
 s       seconds   seconds
 m       minutes   minutes
 h       hours     hours
 D       days      days
 W       weeks     weeks
 M       months    months
 Y       years     years
======= ========= =========

The list of possible duration units defined by this application.

This is used as the selection list for the duration_unit <Event.duration_unit> field of a calendar entry.

Every item is an instance of DurationUnit.


Base class for the choices in the DurationUnits choicelist.

add_duration(unit, orig, value)

Return a date or datetime obtained by adding value times this unit to the specified value orig. Returns None is orig is empty.

This is intended for use as a curried magic method of a specified list item:

Duration units can be used for arithmetic operation on durations. For example:

>>> from import DurationUnits
>>> start_date = i2d(20111026)
>>> DurationUnits.months.add_duration(start_date, 2), 12, 26)
>>> from lino.utils import i2d
>>> start_date = i2d(20111026)
>>> DurationUnits.months.add_duration(start_date, 2), 12, 26)
>>> DurationUnits.months.add_duration(start_date, -2), 8, 26)
>>> start_date = i2d(20110131)
>>> DurationUnits.months.add_duration(start_date, 1), 2, 28)
>>> DurationUnits.months.add_duration(start_date, -1), 12, 31)
>>> DurationUnits.months.add_duration(start_date, -2), 11, 30)
>>> start_date = i2d(20140401)
>>> DurationUnits.months.add_duration(start_date, 3), 7, 1)
>>> DurationUnits.years.add_duration(start_date, 1), 4, 1)



The sitewide list of access classes.


Show all calendar events of the same day.


Model mixin inherited by both Event and Task.


Contains the sequence number if this is an automatically generated component. Otherwise this field is empty.

Automatically generated components behave differently at certain levels.

Data checkers


Check whether this entry conflicts with other events.


Check whether the type of this calendar entry should be updated.

This can happen when the configuration has changed and there are automatic entries which had been generated using the old configuration.


Check for entries which last longer than the maximum number of days allowed by their type.


Check for calendar entries without participants.

No participants although N suggestions exist. -- This is probably due to some problem in the past, so we repair this by adding the suggested guests., calendar)

Check whether the given subscription exists. If not, create it.

Default duration and start time

Note the difference between a DurationField and a TimeField:

>>> fld = cal.EventType._meta.get_field('default_duration')
>>> fld.__class__
<class 'lino.core.fields.DurationField'>
>>> fld.to_python("1:00")
>>> fld = cal.Event._meta.get_field('start_time')
>>> fld.__class__
<class 'lino.core.fields.TimeField'>
>>> fld.to_python("1:00")
datetime.time(1, 0)
>>> et = cal.EventType.objects.get(planner_column=cal.PlannerColumns.internal)
>>> et.default_duration

So when we create an entry which starts at 8:00, Lino will automaticallt set end_time to 8:30

>>> entry = cal.Event(, start_time="8:00", event_type=et)
>>> entry.full_clean()
>>> entry.end_time
datetime.time(8, 30)

It works also across midnight:

>>> entry = cal.Event(, start_time="23:55", event_type=et)
>>> entry.full_clean()
>>> entry.start_time
datetime.time(23, 55)
>>> entry.end_time
datetime.time(0, 25)
>>> entry.start_date, 2, 15)
>>> entry.end_date

User roles

Besides the user roles defined in this plugins also defines two specific roles.


Can read public calendar entries. This is a kind of minimal calendar functionality that can be given to anonymous users, as done e.g. by Lino Vilma.


Can see presences and guests of a calendar entry.


Can manage presences.

Usage example in Lino Voga where users of type pupil cannot create or edit calendar entries, but can manage their participation in existing entries.

Most calendar functionality requires


The RecurrenceSet.positions field allows to specify rules like "every last Friday of the month".

If given, it must be a space-separated list of positive or negative integers. Each given integer N means the Nth occurrence inside the frequency period.

The positions field makes sense only when frequency (RecurrenceSet.every_unit) is yearly, monthly, weekly or daily. It is silently ignored with other frequencies. When this field is set, the value of RecurrenceSet.every is ignored.

Lino uses dateutil.rrule when positions are used, so the edge cases described there might apply.

The following examples use a utility function:

>>> settings.SITE.verbose_client_info_message = True
>>> def show(obj, today):
... = "test"
...     obj.full_clean()
...     for lng in ('en', 'de', 'fr'):
...         with translation.override(lng):
...             print(obj.weekdays_text)
...     ses = rt.login("robin")
...     for i in range(5):
...         today = obj.get_next_suggested_date(ses, today)
...         if today is None:
...             break
...         print(dd.fdf(today))
...     if len(ses.response):
...         print(ses.response)

Every last Friday of the month:

>>> obj = cal.RecurrentEvent()
>>> obj.friday = True
>>> obj.positions = "-1"
>>> obj.every_unit = cal.Recurrencies.monthly
>>> show(obj, i2d(20191001))
Every last Friday of the month
Jeden letzten Freitag des Monats
Chaque dernier Vendredi du mois
Friday, 25 October 2019
Friday, 29 November 2019
Friday, 27 December 2019
Friday, 31 January 2020
Friday, 28 February 2020

Every last working day of the month:

>>> obj.monday = True
>>> obj.tuesday = True
>>> obj.wednesday = True
>>> obj.thursday = True
>>> show(obj, i2d(20191001))
Every last working day of the month
Jeden letzten Arbeitstag des Monats
Chaque dernier jour ouvrable du mois
Thursday, 31 October 2019
Friday, 29 November 2019
Tuesday, 31 December 2019
Friday, 31 January 2020
Friday, 28 February 2020

The first and third Wednesday of every month:

>>> obj = cal.RecurrentEvent()
>>> obj.wednesday = True
>>> obj.positions = "1 3"
>>> obj.every_unit = cal.Recurrencies.monthly
>>> show(obj, i2d(20191001))
Every first and third Wednesday of the month
Jeden ersten und dritten Mittwoch des Monats
Chaque premier et troisième Mercredi du mois
Wednesday, 2 October 2019
Wednesday, 16 October 2019
Wednesday, 6 November 2019
Wednesday, 20 November 2019
Wednesday, 4 December 2019
>>> obj = cal.RecurrentEvent()
>>> obj.friday = True
>>> obj.monday = True
>>> obj.positions = "2"
>>> obj.every_unit = cal.Recurrencies.monthly
>>> show(obj, i2d(20191213))
Every second Monday and Friday of the month
Jeden zweiten Montag und Freitag des Monats
Chaque deuxième Lundi et Vendredi du mois
Monday, 6 January 2020
Friday, 7 February 2020
Friday, 6 March 2020
Monday, 6 April 2020
Monday, 4 May 2020
>>> obj.positions = "-2"
>>> obj.monday = False
>>> show(obj, i2d(20191213))
Every second last Friday of the month
Jeden zweitletzten Freitag des Monats
Chaque avant-dernier Vendredi du mois
Friday, 20 December 2019
Friday, 24 January 2020
Friday, 21 February 2020
Friday, 20 March 2020
Friday, 17 April 2020
>>> obj = cal.RecurrentEvent()
>>> obj.positions = "2"
>>> obj.every_unit = cal.Recurrencies.once
>>> show(obj, i2d(20191213))
On Wednesday, 15 February 2017
Am Mittwoch, 15. Februar 2017
Le mercredi 15 février 2017
{'info_message': "No next date when recurrency is 'once'."}

Every 3 years (with Easter):

>>> obj = cal.RecurrentEvent()
>>> obj.every_unit = cal.Recurrencies.easter
>>> obj.positions = "2"  # silently ignored
>>> obj.every = 3
>>> show(obj, i2d(20191213))
Every 3 years (with Easter)
Alle 3 Jahre (mit Ostern)
Tous les 3 ans (avec Pâques)
Friday, 9 December 2022
Friday, 12 December 2025
Friday, 8 December 2028
Friday, 5 December 2031
Friday, 1 December 2034

A rule that yields no date at all

>>> obj = cal.RecurrentEvent()
>>> obj.every_unit = cal.Recurrencies.daily
>>> obj.positions = "7"
>>> show(obj, i2d(20191213))
Every day
Jeden Tag
Chaque jour
{'info_message': "No date matches your recursion rule({'freq': 3, 'count': 2, 'dtstart':, 12, 14), 'interval': 1, 'bysetpos': [7]})."}

For example, a bysetpos of -1 if combined with a MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will result in the last work day of every month.