Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More
cal
: Calendar functionality¶
This page gives developer information about the lino_xl.lib.cal
plugin,
which adds general calendar functionality to your Lino application.
We assume that you have read the User Guide: The cal plugin.
See also Defining holidays.
This plugin is often used together with lino_xl.lib.calview
, which defines
calendar views.
Side note: Code snippets (lines starting with >>>
) in this document get
tested as part of our development workflow. The following
initialization snippet tells you which demo project is being used in
this document.
>>> from lino import startup
>>> startup('lino_book.projects.avanti1.settings')
>>> from lino.api.doctest import *
Plugin settings¶
This plugin adds the following settings, which a server administrator can
configure in the settings.py
.
Calendar entries¶
- class lino_xl.lib.cal.Event¶
The Django model that represents a calendar entry.
The internal model name is
Event
for historical reasons. Users see it as calendar entry:>>> print(rt.models.cal.Event._meta.verbose_name) Calendar entry
- start_date¶
The starting date of this calendar entry. May not be empty.
- start_time¶
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
).
- 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.
- summary¶
A one-line descriptive text.
- description¶
A longer descriptive text.
- user¶
The responsible user.
- assigned_to¶
Another user who is expected to take responsibility for this entry.
See
lino.modlib.users.Assignable.assigned_to
.
- event_type¶
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.
- state¶
The state of this entry. The state can change according to rules defined by the workflow.
- owner¶
The event generator that generated and controls this calendar entry.
- transparent¶
Whether this entry should allow other entries at the same time.
- guests_edited¶
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()
.
- when_text¶
Shows the start date and time of the calendar entry.
See
lino_xl.lib.cal.utils.when_text()
for details.
- when_html¶
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 openingEntriesByDay
if they want.
- show_conflicting¶
A
ShowSlaveTable
button which opens theConflictingEvents
table for this event.
- notify_before¶
The multiples of a time unit (
notify_unit
) before which to send a notification to the corresponding user.
- notify_unit¶
The unit of time to compute before sending a notification. Example values are: minute, hour, day, week et cetera.
A pointer to
NotifyBeforeUnits
.
- notified¶
Stores a boolean value as an indication of whether the user has been notified or NOT.
- update_guests()¶
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
- update_events()¶
Create or update all other automatic calendar entries of this series.
- show_today()¶
Show all calendar entries today.
See
ShowEntriesByDay
.
- get_conflicting_events(self)¶
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.
- has_conflicting_events(self)¶
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.
- suggest_guests(self)¶
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.
- _get_calendar(self)¶
Returns the
Calendar
which contains this entry, or None if no subscription is found.The
Calendar
is resolved from the setting given bycal.calendar_fieldnames
.Needed for ext.ensible calendar panel.
It is advised to never override this method. Instead, configure the related
Calendar
using,cal.calendar_fieldnames
.
When the
lino_xl.lib.google
is installed. It inserts some other fields into this model. See:GoogleCalendarEventSynchronized
>>> show_fields(rt.models.cal.Event,
... 'start_date start_time end_date end_time user summary description event_type state')
...
- 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.
- Responsible user (user) : The responsible user.
- Short description (summary) : A one-line descriptive text.
- Description (description) : A longer descriptive text.
- Calendar entry type (event_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.
Repeaters¶
- class lino_xl.lib.cal.EntryRepeater¶
- repeater¶
The calendar entry from which this repeater has been created.
Event Notification¶
Sending an event notification is handled by get_notification_queryset()
and send_event_notifications()
functions in the cal module.
Below is an example query generated by get_notification_queryset()
.
PS: The three dots (…) at the end of the query is a replacement for approximately the current datetime and has done so for technical reasons.
>>> qs = rt.models.cal.get_notification_queryset()
>>> print(qs.query)
...
SELECT "cal_event"."id", "cal_event"."modified", "cal_event"."created",
"cal_event"."project_id", "cal_event"."start_date", "cal_event"."start_time",
"cal_event"."end_date", "cal_event"."end_time", "cal_event"."build_time",
"cal_event"."build_method", "cal_event"."user_id", "cal_event"."assigned_to_id",
"cal_event"."owner_type_id", "cal_event"."owner_id", "cal_event"."summary",
"cal_event"."description", "cal_event"."auto_type", "cal_event"."priority",
"cal_event"."event_type_id", "cal_event"."transparent",
"cal_event"."guests_edited", "cal_event"."room_id", "cal_event"."state",
"cal_event"."notify_before", "cal_event"."notify_unit", "cal_event"."notified",
CASE WHEN "cal_event"."start_time" IS NULL THEN 07:00:00 ELSE
"cal_event"."start_time" END AS "start_timed",
(COALESCE("cal_event"."start_date", ) || COALESCE((COALESCE( , ) ||
COALESCE(CASE WHEN "cal_event"."start_time" IS NULL THEN 07:00:00 ELSE
"cal_event"."start_time" END, )), )) AS "start_datetime", CASE WHEN
"cal_event"."notify_unit" = 10 THEN (("cal_event"."notify_before" * 60) *
1000000) WHEN "cal_event"."notify_unit" = 20 THEN ((("cal_event"."notify_before"
* 60) * 1000000) * 60) WHEN "cal_event"."notify_unit" = 30 THEN
(((("cal_event"."notify_before" * 60) * 1000000) * 60) * 24) WHEN
"cal_event"."notify_unit" = 40 THEN ((((("cal_event"."notify_before" * 60) *
1000000) * 60) * 24) * 7) ELSE NULL END AS "notify_before_duration",
strftime(%Y-%m-%d %H:%M:%f, (django_format_dtdelta('-',
(COALESCE("cal_event"."start_date", ) || COALESCE((COALESCE( , ) ||
COALESCE(CASE WHEN "cal_event"."start_time" IS NULL THEN 07:00:00 ELSE
"cal_event"."start_time" END, )), )), CASE WHEN "cal_event"."notify_unit" = 10
THEN (("cal_event"."notify_before" * 60) * 1000000) WHEN
"cal_event"."notify_unit" = 20 THEN ((("cal_event"."notify_before" * 60) *
1000000) * 60) WHEN "cal_event"."notify_unit" = 30 THEN
(((("cal_event"."notify_before" * 60) * 1000000) * 60) * 24) WHEN
"cal_event"."notify_unit" = 40 THEN ((((("cal_event"."notify_before" * 60) *
1000000) * 60) * 24) * 7) ELSE NULL END))) AS "schedule" FROM "cal_event" WHERE
(NOT "cal_event"."notified" AND "cal_event"."notify_before" IS NOT NULL AND
"cal_event"."start_date" IS NOT NULL AND strftime(%Y-%m-%d %H:%M:%f,
(django_format_dtdelta('-', (COALESCE("cal_event"."start_date", ) ||
COALESCE((COALESCE( , ) || COALESCE(CASE WHEN ("cal_event"."start_time" IS NULL)
THEN 07:00:00 ELSE "cal_event"."start_time" END, )), )), CASE WHEN
("cal_event"."notify_unit" = 10) THEN (("cal_event"."notify_before" * 60) *
1000000) WHEN ("cal_event"."notify_unit" = 20) THEN
((("cal_event"."notify_before" * 60) * 1000000) * 60) WHEN
("cal_event"."notify_unit" = 30) THEN (((("cal_event"."notify_before" * 60) *
1000000) * 60) * 24) WHEN ("cal_event"."notify_unit" = 40) THEN
((((("cal_event"."notify_before" * 60) * 1000000) * 60) * 24) * 7) ELSE NULL
END))) <= ...)
A dummy event for the demonstration purposes:
>>> import datetime as dt
>>> event = rt.models.cal.Event(summary="Kong's Birthday",
... start_date=dt.date(2021, 12, 12), start_time=dt.time(0,0),
... user=rt.models.users.User.objects.get(username='robin'))
>>> event.full_clean()
>>> event.save()
Sending an event notification also stores the message in the database. Below on
the third line of the code we used send_event_notifications()
to simulate
sending of an event notification where we passed the event from the previous
code block into an array. For consistency the argument passed onto the
send_event_notifications()
method should be a queryset that is retrieved
from the database using get_notification_queryset()
.
>>> from django.utils import timezone
>>> now = timezone.now()
>>> rt.models.cal.send_event_notifications([event])
>>> messages = rt.models.notify.Message.objects.filter(created__gte=now)
>>> assert messages.count() == 1
An inspection of the message content:
>>> messages[0].subject
'Upcoming calendar event at 2021-12-12T00:00:00'
>>> messages[0].body
...
'<div class="row-fluid">\n<h1>[cal_entry ... Kong\'s Birthday]</h1>\n\n<p style="background-color:#eeeeee; padding:6pt;">\n\n<b>Date :</b> 2021-12-12\n\n\n<br/><b>Time : 00:00:00-None\n\n\n\n\n</p>\n<div>\n\n</div>\n\n</div>'
Cleaning up:
>>> messages[0].delete()
>>> event.delete()
- lino_xl.lib.cal.event_notification_scheduler()¶
A hook for
schedule
that makeslinod
send out notifications for upcoming calendar events every 5 minutes.
- lino_xl.lib.cal.get_notification_queryset()¶
Returns notifiable calendar entries computing the attributes
Event.notify_before
,Event.notify_unit
andEvent.notified
.
- lino_xl.lib.cal.send_event_notifications()¶
Send out notifications to the site users interested in this calendar entry.
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.
>>> rt.show(cal.EntryStates)
======= ============ ============ ============= ============= ======== ============= =========
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
======= ============ ============ ============= ============= ======== ============= =========
- class lino_xl.lib.cal.EntryStates¶
The list of possible states of a calendar entry.
- class lino_xl.lib.cal.EntryState¶
Every calendar entry state is an instance of this and has some attributes.
- fill_guests¶
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
- guest_state¶
Force the given guest state for all guests when an entry is set to this state and when
EventType.force_guest_states
is True.
- transparent¶
Whether an entry in this state is considered transparent, i.e. dos not conflict with other entries at the same moment.
- fixed¶
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
andEventEvents.pending
.
- noauto¶
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.
- edit_guests¶
Old name for
fill_guests
.
Calendar entry types¶
Every calendar entry has a field Event.event_type
that points to its
calendar entry type.
- class lino_xl.lib.cal.EventType¶
Django model representing a calendar entry type.
The possible value of the
Event.event_type
field.- default_duration¶
An optional default duration for calendar entries of this type.
If this field is set, Lino will help with entering
Event.end_time
andEvent.start_date
of an calendar entries by changing theend_time
of an entry when thestart_date
is changed (and thestart_time
when theend_date
)
- event_label¶
Default text for summary of new entries.
- is_appointment¶
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
.
- max_days¶
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
- locks_user¶
Whether calendar entries of this type make the user unavailable for other locking events at the same time.
- max_conflicting¶
How many conflicting events should be tolerated.
- transparent¶
Allow entries of this type to conflict with other events.
- force_guest_states¶
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.
- fill_presences¶
Whether guests should be automatically filled for calendar entries of this type.
- fill_guests¶
Planned name for
fill_presences
.
- class lino_xl.lib.cal.EventTypes¶
The list of entry types defined on this site.
This is usually filled in the
std
demo fixture of the application.>>> rt.show(cal.EventTypes) =========== =============== ================== ================== ================ ============= ===================== ================= 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 =========== =============== ================== ================== ================ ============= ===================== =================
Calendars¶
Calendar entries can be grouped into “calendars”.
- class lino_xl.lib.cal.Calendar¶
The django model representing a calendar.
- name¶
A babel field containing the designation of the calendar.
- description¶
A multi-line description of the calendar.
- color¶
The color to use for entries of this calendar (in
lino_xl.lib.extensible
).
When the
lino_xl.lib.google
is installed. It inserts some other fields into this model. See:GoogleSynchronized
.
- class lino_xl.lib.cal.Calendars¶
>>> rt.show(cal.Calendars)
...
==== ============= ================== ================== ============= =======
ID Designation Designation (de) Designation (fr) Description color
---- ------------- ------------------ ------------------ ------------- -------
1 General Allgemein Général 1
**1**
==== ============= ================== ================== ============= =======
Note that the default implementation has no “Calendar” field per
calendar entry. The Event model instead has a Event._get_calendar()
which
internally uses the following settings cal.calendar_fieldnames
.
You might extend Event
and the cal plugin as follows:
# .../cal/models.py
from lino_xl.lib.cal.models import *
class Event(Event):
calendar = dd.ForeignKey('cal.Calendar')
# .../cal/__init__.py
...
class Plugin(...):
...
calendar_fieldnames = "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 site user and uses the setting:
# .../lino_welfare/modlib/cal/__init__.py
...
class Plugin(...):
...
calendar_fieldnames = "assigned_to__calendar|user__calendar"
...
Or in Lino Voga there is one calendar per Room, and uses the following setting:
# .../lino_voga/lib/cal/__init__.py
...
class Plugin(...):
...
calendar_fieldnames = "room__calendar"
...
Event generators¶
An event generator is something that can generate automatic calendar entries. Examples of event generators include
A course, workshop or activity as used by Welfare, Voga and Avanti (subclasses of
lino_xl.lib.courses.Course
).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
andlino_welfare.modlib.jobs.Contract
)
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.
- class lino_xl.lib.cal.EventGenerator¶
Base class for things that generate a series of events.
See Event generators.
- update_all_guests()¶
Update the presence lists of all calendar events generated by this.
- do_update_events()¶
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
(inresolve_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
lino.modlib.system.RecurrenceSet
.
- class lino_xl.lib.cal.UpdateEntries¶
Generate or update the automatic events controlled by this object.
This action is installed as
EventGenerator.do_update_events
.See also Automatic calendar entries.
- class lino_xl.lib.cal.UpdateEntriesByEvent¶
Update all events of this series.
This is installed as
update_events
onEvent
.
Miscellaneous¶
- class lino_xl.lib.cal.NotifyBeforeUnits¶
The list of choices for
Event.notify_unit
.
Recurrences¶
The recurrence expresses how often something is to be repeated. For example when generating automatic calendar entries, or when scheduling background task.
Lino supports the following recurrences:
>>> rt.show(cal.Recurrences)
======= ============= ====================
value name text
------- ------------- --------------------
O once once
N never never
s secondly secondly
m minutely minutely
h hourly hourly
D daily daily
W weekly weekly
M monthly monthly
Y yearly yearly
P per_weekday per weekday
E easter Relative to Easter
======= ============= ====================
Note: per_weekday
exists only for background compatibility. It is an alias
for weekly
.
Adding a duration unit
>>> start_date = i2d(20160327)
>>> cal.Recurrences.never.add_duration(start_date, 1)
>>> cal.Recurrences.once.add_duration(start_date, 1)
Traceback (most recent call last):
...
Exception: Invalid DurationUnit once
>>> cal.Recurrences.daily.add_duration(start_date, 1)
datetime.date(2016, 3, 28)
>>> cal.Recurrences.weekly.add_duration(start_date, 1)
datetime.date(2016, 4, 3)
>>> cal.Recurrences.monthly.add_duration(start_date, 1)
datetime.date(2016, 4, 27)
>>> cal.Recurrences.yearly.add_duration(start_date, 1)
datetime.date(2017, 3, 27)
>>> cal.Recurrences.easter.add_duration(start_date, 1)
datetime.date(2017, 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 lino.modlib.system.RecurrenceSet
and EventGenerator
).
>>> def demo(every_unit, **kwargs):
... kwargs.setdefault('start_date', i2d(20160628))
... kwargs.update(every_unit=cal.Recurrences.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
>>> rt.show(cal.EventTypes, 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)
True
>>> isinstance(obj, cal.EventGenerator)
True
>>> obj.tuesday = True
>>> obj.every = 2
>>> obj.every_unit = cal.Recurrences.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))
28/06/2016
30/08/2016
01/11/2016
03/01/2017
07/03/2017
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))
28/06/2012
28/08/2012
28/10/2012
28/12/2012
28/02/2013
Conflicting events¶
The demo database contains two appointments on Ash Wednesday and two on Rose Monday. These conflicting calendar events are visible as checkdata messages (see checkdata : High-level integrity tests).
>>> chk = checkdata.Checkers.get_by_value('cal.ConflictingEventsChecker')
>>> rt.show(checkdata.MessagesByChecker, chk)
...
================= ======================================= ==================================================
Responsible Database object Message text
----------------- --------------------------------------- --------------------------------------------------
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 `Réunion (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)
>>> rt.show(cal.ConflictingEvents, 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
============ ============ ========== ======== ====== ==================
Calendar entries by day¶
The Event.show_today
action of a calendar entry opens a window showing
all calendar entries on the same day as this entry. It is implemented as a
custom action class ShowEntriesByDay
.
- class lino_xl.lib.cal.ShowEntriesByDay¶
Show all calendar events of the same day.
>>> obj = cal.Event.objects.get(id=123)
>>> ar = rt.login("robin", renderer=settings.SITE.kernel.default_renderer)
>>> obj.show_today.run_from_ui(ar)
>>> ar.response
{'eval_js': 'window.App.runAction({ "actorId": "cal.EntriesByDay", "an": "grid",
"rp": null, "status": { "base_params": { }, "param_values": { "end_date":
"01.03.2017", "event_type": null, "event_typeHidden": null, "presence_guest":
null, "presence_guestHidden": null, "project": null, "projectHidden": null,
"room": null, "roomHidden": null, "start_date": "01.03.2017", "user": null,
"userHidden": null } } })'}
Transparent calendar entries¶
The entry type “Internal” is marked “transparent”.
>>> obj = cal.EventType.objects.get(id=4)
>>> obj
EventType #4 ('Internal')
>>> obj.transparent
True
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”, …
- class lino_xl.lib.cal.Guest¶
The Django model representing a guest.
- event¶
The calendar event to which this presence applies.
- partner¶
The partner to which this presence applies.
- role¶
The role of this partner in this presence.
- state¶
The state of this presence. See
GuestStates
.
The following three fields are injected by the
reception
plugin:- waiting_since¶
Time when the visitor arrived (checked in).
- busy_since¶
Time when the visitor was received by agent.
- gone_since¶
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.
- class lino_xl.lib.cal.GuestRole¶
The role of a guest expresses what the partner is going to do there.
- class lino_xl.lib.cal.GuestRoles¶
Global table of guest roles.
- class lino_xl.lib.cal.GuestState¶
The current state of a guest.
- class lino_xl.lib.cal.GuestStates¶
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
.>>> rt.show(cal.GuestStates) ======= =========== ============ =========== ============= value name Afterwards text Button text ------- ----------- ------------ ----------- ------------- 10 invited No Invited ? 40 present Yes Present ☑ 50 missing Yes Missing ☉ 60 excused No Excused ⚕ 90 cancelled No Cancelled ☒ ======= =========== ============ =========== =============
- class lino_xl.lib.cal.UpdateGuests¶
- class lino_xl.lib.cal.UpdateAllGuests¶
Presence lists¶
To introduce the problem:
runserver
inlino_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 lino_xl.lib.cal.management.commands.watch_calendars
.
- class lino_xl.lib.cal.RemoteCalendar¶
Django model for representing a remote calendar.
Rooms¶
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.
- class lino_xl.lib.cal.Room¶
Django model for representing a room.
- name¶
The designation of the room. This is not required to be unique.
- display_color¶
The color to use when displaying entries in this room in the calendar view.
- class lino_xl.lib.cal.Rooms¶
Base class for all list of rooms.
- class lino_xl.lib.cal.AllRooms¶
Show a list of all rooms.
Subscriptions¶
A suscription is when a user subscribes to a calendar. It corresponds to what the extensible CalendarPanel calls “Calendars”
- class lino_xl.lib.cal.BaseSubscription¶
- User:
points to the author (recipient) of this subscription
- calendar¶
Pointer to the
Calendar
for theBaseSubscription
.
Whether this subscription should initially be displayed as a hidden calendar.
- class lino_xl.lib.cal.Subscription¶
Django model for representing a subscription.
A subclass of
BaseSubscription
- class lino_xl.lib.cal.Subscriptions¶
- class lino_xl.lib.cal.SubscriptionsByUser¶
- class lino_xl.lib.cal.SubscriptionsByCalendar¶
Tasks¶
A task is when a user plans to do something (and optionally wants to get reminded about it).
- class lino_xl.lib.cal.Task¶
Django model for representing a subscription.
- priority¶
How urgent this task is.
Choicelist field pointing to
lino_xl.lib.xl.Priorities
.
- state¶
The state of this Task. one of
TaskStates
.
- class lino_xl.lib.cal.TaskStates¶
Possible values for the state of a
Task
. The list of choices for theTask.state
field.
- class lino_xl.lib.cal.Tasks¶
Global table of all tasks for all users.
- class lino_xl.lib.cal.TasksByUser¶
Shows the list of tasks for this user.
- class lino_xl.lib.cal.MyTasks¶
Shows my tasks whose start date is today or in the future.
Recurrent calendar entries¶
- class lino_xl.lib.cal.EventPolicy¶
Django model used to store a recurrency policy.
- event_type¶
Generated calendar entries will have this type.
- class lino_xl.lib.cal.EventPolicies¶
Global table of all possible recurrency policies.
- class lino_xl.lib.cal.RecurrentEvent¶
Django model used to store a recurrent event.
- name¶
- every_unit¶
Inherited from
RecurrenceSet.every_unit
.
- event_type¶
- description¶
- 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.
- class lino_xl.lib.cal.RecurrentEvents¶
The list of all recurrent events (
RecurrentEvent
).
Miscellaneous¶
- class lino_xl.lib.cal.Events¶
Table which shows all calendar events.
Filter parameters:
- show_appointments¶
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.
- presence_guest¶
Show only entries that have a presence for the specified guest.
- project¶
Show only entries assigned to this project, where project is defined by
lino.core.site.Site.project_model
.
- assigned_to¶
Show only entries assigned to this user.
- observed_event¶
- event_type¶
Show only entries having this type.
- state¶
Show only entries having this state.
- user¶
Show only entries having this user as author.
- class lino_xl.lib.cal.ConflictingEvents¶
Shows events conflicting with this one (the master).
- class lino_xl.lib.cal.EntriesByDay¶
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.
- class lino_xl.lib.cal.EntriesByController¶
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.
- class lino_xl.lib.cal.EntriesByProject¶
- class lino_xl.lib.cal.OneEvent¶
Show a single calendar event.
- class lino_xl.lib.cal.MyEntries¶
Shows the appointments for which I am responsible.
This data table 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”).
- class lino_xl.lib.cal.MyAssignedEvents¶
Shows the calendar entries that are assigned to me.
That is, having
Event.assigned_to
field refers to the requesting user.This data table also generates a welcome message “X events have been assigned to you” when it is not empty.
- class lino_xl.lib.cal.OverdueAppointments¶
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.
- class lino_xl.lib.cal.MyOverdueAppointments¶
Like OverdueAppointments, but only for myself.
- class lino_xl.lib.cal.MyUnconfirmedAppointments¶
Shows my appointments in the near future which are in suggested or draft state.
Appointments before today are not shown. The parameters
end_date
andstart_date
can manually be modified in the parameters panel.The state filter (draft or suggested) cannot be removed.
- class lino_xl.lib.cal.Guests¶
The default table of presences.
- class lino_xl.lib.cal.GuestsByEvent¶
- class lino_xl.lib.cal.GuestsByRole¶
- class lino_xl.lib.cal.MyPresences¶
Shows all my presences in calendar events, independently of their state.
- class lino_xl.lib.cal.MyPendingPresences¶
Received invitations waiting for my feedback (accept or reject).
- class lino_xl.lib.cal.GuestsByPartner¶
Show the calendar entries having this partner as a guest.
This might get deprecated some day. You probably prefer
EntriesByGuest
.
- class lino_xl.lib.cal.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.
- class lino_xl.lib.cal.Reservation¶
Base class for
lino_xl.lib.rooms.models.Booking
andlino.modlib.courses.models.Course
.Inherits from both
EventGenerator
andlino.modlib.system.RecurrenceSet
.- room¶
- max_date¶
Don’t generate calendar entries beyond this date.
Miscellaneous¶
- class lino_xl.lib.cal.AccessClasses¶
The sitewide list of access classes.
Plugin configuration¶
See Plugin
.
Data checkers¶
- class lino_xl.lib.cal.ConflictingEventsChecker¶
Check whether this entry conflicts with other events.
- class lino_xl.lib.cal.ObsoleteEventTypeChecker¶
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.
- class lino_xl.lib.cal.LongEntryChecker¶
Check for entries which last longer than the maximum number of days allowed by their type.
- class lino_xl.lib.cal.EventGuestChecker¶
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.
- lino_xl.lib.cal.check_subscription(user, 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")
Duration('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
Duration('0:30')
So when we create an entry which starts at 8:00, Lino will automaticallt set end_time to 8:30
>>> entry = cal.Event(start_date=dd.today(), 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_date=dd.today(), 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
datetime.date(2017, 2, 15)
>>> entry.end_date
User roles¶
Besides the user roles defined in lino.modlib.office
this plugins also
defines two specific roles.
- class lino_xl.lib.cal.CalendarReader¶
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.
- class lino_xl.lib.cal.GuestOperator¶
Can see presences and guests of a calendar entry.
- class lino_xl.lib.cal.GuestUser¶
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
lino.modlib.office.OfficeUser
Positions¶
The lino.modlib.system.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
(lino.modlib.system.RecurrenceSet.every_unit
) is yearly, monthly, weekly or daily. It is
silently ignored with other frequencies. When this field is set, the value of
lino.modlib.system.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):
... obj.name = "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(today, ses.logger)
... 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.Recurrences.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.Recurrences.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.Recurrences.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.Recurrences.once
>>> show(obj, i2d(20191213))
On Wednesday 15 February 2017
Am Mittwoch, 15. Februar 2017
Le mercredi 15 février 2017
Every 3 years (with Easter):
>>> obj = cal.RecurrentEvent()
>>> obj.every_unit = cal.Recurrences.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.Recurrences.daily
>>> obj.positions = "7"
>>> show(obj, i2d(20191213))
...
Every day
Jeden Tag
Chaque jour
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.
Utilities¶
>>> import datetime
>>> from lino_xl.lib.cal.utils import dt2kw, when_text, parse_rrule
dt2kw()
examples:
>>> dt = datetime.datetime(2013, 12, 25, 17, 15, 0)
>>> dt2kw(dt,'foo') == {'foo_date': datetime.date(2013, 12, 25), 'foo_time': datetime.time(17, 15)}
True
when_text()
examples:
>>> print(when_text(datetime.date(2013,12,25)))
Wed 25/12/2013
>>> print(when_text(
... datetime.date(2013,12,25), datetime.time(17,15,00)))
Wed 25/12/2013 (17:15)
>>> print(when_text(None))
Recurrence rules¶
- recurrence rule¶
A text that describes a “recurrence rule” according to the syntax defined in RFC 5545.
Examples for the parse_rrule()
function.
Every last Friday of the month:
>>> parse_rrule("RRULE:FREQ=MONTHLY;BYDAY=-1FR")
(<system.Recurrences.monthly:M>, 1, None, {'BYDAY': [(-1, 'FR')]})
Every year ((from start date):
>>> parse_rrule("RRULE:FREQ=YEARLY")
(<system.Recurrences.yearly:Y>, 1, None, {})