Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

Activities in Lino Avanti

This document specifies how activities are being used in Lino Avanti.

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.

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

The methods for managing courses and enrolments in Lino Avanti is implemented by the lino_avanti.lib.courses plugin which extends lino_xl.lib.courses.

Courses

This plugin extends the Course model as follows.

class lino_avanti.lib.courses.Course
can_excuse

Whether end users can select the workflow state “excused” for presences of calendar entries generated by this course.

update_missing_rates(self)

Calculate the missing rates for all enrolments of this course.

See Missing rates below.

Enrolments

class lino_avanti.lib.courses.EnrolmentsByCourse

Same as lino_xl.lib.courses.EnrolmentsByCourse, but shows additional information about the pupil and the enrolment.

class lino_avanti.lib.courses.Enrolment

Inherits from lino_xl.lib.courses.Enrolment but adds four specific “enrolment options”:

needs_childcare

Whether this pupil has small children to care about.

needs_bus

Whether this pupil needs public transportation for moving.

needs_school

Whether this pupil has school children to care about.

needs_evening

Whether this pupil is available only for evening courses.

missing_rate

How many times the pupil was missing when a lesson took place. In percent.

class lino_avanti.lib.courses.PresencesByEnrolment

Shows the presences of this pupil for this course.

Filling the guest list of a calendar entry

>>> 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
======= ============ ============ ============= ============= ======== ============= =========
>>> cal.EntryStates.suggested.fill_guests
True

Topics

>>> rt.show('courses.Topics')
==== ================== ================== ==================
 ID   Designation        Designation (de)   Designation (fr)
---- ------------------ ------------------ ------------------
 1    Citizen course     Citizen course     Citizen course
 2    Language courses   Language courses   Language courses
==== ================== ================== ==================
>>> language_courses = courses.Topic.objects.get(pk=2)
>>> rt.show('courses.ActivitiesByTopic', language_courses)
====================================== =========== ============= ================== =========== ============= =========== ========
 Activity                               When        Times         Available places   Confirmed   Free places   Requested   Trying
-------------------------------------- ----------- ------------- ------------------ ----------- ------------- ----------- --------
 `Alphabetisation (16/01/2017) <…>`__   Every day   09:00-12:00   5                  3           0             3           2
 `Alphabetisation (16/01/2017) <…>`__   Every day   14:00-17:00   15                 2           0             4           13
 `Alphabetisation (16/01/2017) <…>`__   Every day   18:00-20:00   15                 12          0             11          3
 **Total (3 rows)**                                               **35**             **17**      **0**         **18**      **18**
====================================== =========== ============= ================== =========== ============= =========== ========

API note: ActivitiesByTopic is a table with a remote master key:

>>> courses.ActivitiesByTopic.master
<class 'lino_xl.lib.courses.models.Topic'>
>>> print(courses.ActivitiesByTopic.master_key)
line__topic

Course lines

>>> rt.show('courses.LinesByTopic', language_courses)
==================== ====================== ====================== ====================== ================== ============ ===================== ===================== ============ ==============
 Reference            Designation            Designation (de)       Designation (fr)       Topic              Layout       Calendar entry type   Manage presences as   Recurrency   Repeat every
-------------------- ---------------------- ---------------------- ---------------------- ------------------ ------------ --------------------- --------------------- ------------ --------------
                      Alphabetisation        Alphabetisation        Alphabetisation        Language courses   Activities   Lesson                Pupil                 weekly       1
                      German A1+             German A1+             German A1+             Language courses   Activities   Lesson                Pupil                 weekly       1
                      German A2              German A2              German A2              Language courses   Activities   Lesson                Pupil                 weekly       1
                      German A2 (women)      German A2 (women)      German A2 (women)      Language courses   Activities   Lesson                Pupil                 weekly       1
                      German for beginners   German for beginners   German for beginners   Language courses   Activities   Lesson                Pupil                 weekly       1
 **Total (5 rows)**                                                                                                                                                                 **5**
==================== ====================== ====================== ====================== ================== ============ ===================== ===================== ============ ==============

Instructor versus author

Note the difference between the instructor of a course and the author.

The author can modify dates and enroll new participants. The teacher can just enter presences and absences for existing participants in existing events.

>>> rt.show('courses.AllActivities')
================= ============ =============== ========== =========== =============
 Activity line     Start date   Instructor      Author     When        Times
----------------- ------------ --------------- ---------- ----------- -------------
 Alphabetisation   16/01/2017   Laura Lieblig   nelly      Every day   18:00-20:00
 Alphabetisation   16/01/2017   Laura Lieblig   nathalie   Every day   14:00-17:00
 Alphabetisation   16/01/2017   Laura Lieblig   martina    Every day   09:00-12:00
================= ============ =============== ========== =========== =============
>>> rt.login('laura').show(courses.MyCoursesGiven)
... 
============ ====================================== =========== ============= ====== =============
 Start date   Activity                               When        Times         Room   Workflow
------------ -------------------------------------- ----------- ------------- ------ -------------
 16/01/2017   `Alphabetisation (16/01/2017) <…>`__   Every day   09:00-12:00          **Started**
 16/01/2017   `Alphabetisation (16/01/2017) <…>`__   Every day   14:00-17:00          **Started**
 16/01/2017   `Alphabetisation (16/01/2017) <…>`__   Every day   18:00-20:00          **Started**
============ ====================================== =========== ============= ====== =============

Who can see the detail of an activity?

Here is why we introduced the get_request_detail_action.

The detail_link in MyCourses and MyCoursesGiven didn’t work for UserTypes.teacher because the custom Course.get_detail_action() method, returned the detail_action of CoursesByLayout, for which a teacher doesn’t have view permission.

The Course.get_detail_action() method may indeed return different detail actions based on the activity layout.

But at least in Lino Avanti, the MyCoursesGiven table (for which the teacher does have permission) uses the same detail layout as CoursesByLayout. The custom Course.get_detail_action() method had to become more intelligent and loop over all tables that use the same detail layout as CoursesByLayout

>>> #ses = rt.login('laura', renderer=settings.SITE.kernel.default_ui.renderer)
>>> ses = rt.login('laura')
>>> laura = ses.get_user()
>>> ut = laura.user_type
>>> ut
<users.UserTypes.teacher:100>
>>> courses.Activities.required_roles
{<class 'lino_xl.lib.courses.roles.CoursesUser'>}
>>> courses.Activities.default_action.allow_view(ut)
False
>>> obj  = courses.Course.objects.filter(teacher=laura.partner).first()
>>> obj
Course #1 ('Alphabetisation (16/01/2017)')

For example, the owner field in lino_xl.lib.cal.MyEntries now uses MyActivities, which happens to be the first activity table having the same detail layout as Activities and for which the user has view permission:

>>> ar = cal.MyEntries.request(parent=ses)
>>> ba = obj.get_detail_action(ar)
>>> print(ba.actor)
courses.MyActivities

The detail_link in MyCoursesGiven now remains on the same actor because it has the same detail layout:

>>> ar = courses.MyCoursesGiven.request(parent=ses)
>>> ba = obj.get_detail_action(ar)
>>> print(ba.actor)
courses.MyCoursesGiven

Calendar entries generated by a course

Teachers can of course also see the list of calendar entries for a course.

>>> obj = courses.Course.objects.get(pk=1)
>>> rt.login('laura').show('cal.EntriesByController', obj)
... 
February 2017: `Fri 24. <…>`__? `Thu 23. <…>`__? `Tue 21. <…>`__? `Mon 20. <…>`__? `Fri 17. <…>`__? `Thu 16. <…>`__? `Tue 14. <…>`__? `Mon 13. <…>`__? `Fri 10. <…>`__? `Thu 09. <…>`__? `Tue 07. <…>`__☑ `Mon 06. <…>`__☑ `Fri 03. <…>`__☒ `Thu 02. <…>`__☑
January 2017: `Tue 31. <…>`__☑ `Mon 30. <…>`__☑ `Fri 27. <…>`__☑ `Thu 26. <…>`__☑ `Tue 24. <…>`__☑ `Mon 23. <…>`__☑ `Fri 20. <…>`__☑ `Thu 19. <…>`__☒ `Tue 17. <…>`__☑ `Mon 16. <…>`__☑
Suggested : 10 ,  Draft : 0 ,  Took place : 12 ,  Cancelled : 2 **New**

Even though Nathalie is author of the morning course, it is Laura (the teacher) who is responsible for the individual events.

>>> rt.login('laura').show('cal.MyEntries')
... 
====================================== ======== ===================================
 Calendar entry                         Client   Workflow
-------------------------------------- -------- -----------------------------------
 `Lesson 19 (16.02.2017 09:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 19 (16.02.2017 14:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 19 (16.02.2017 18:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 20 (17.02.2017 09:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 20 (17.02.2017 14:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 20 (17.02.2017 18:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 21 (20.02.2017 09:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 21 (20.02.2017 14:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 21 (20.02.2017 18:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 22 (21.02.2017 09:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 22 (21.02.2017 14:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 22 (21.02.2017 18:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 23 (23.02.2017 09:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 23 (23.02.2017 14:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 23 (23.02.2017 18:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 24 (24.02.2017 09:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 24 (24.02.2017 14:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
 `Lesson 24 (24.02.2017 18:00) <…>`__            [▽] **? Suggested** → [☐] [☑] [☒]
====================================== ======== ===================================

Reminders

class lino_avanti.lib.courses.Reminder

A reminder is when a coaching worker sends a written letter to a client reminding him or her that they have a problem with their presences.

class lino_avanti.lib.courses.Reminders

The table of all reminders.

class lino_avanti.lib.courses.RemindersByPupil

Shows all reminders that have been issued for this pupil.

This is an example of Slave tables with remote master.

class lino_avanti.lib.courses.ReminderStates

The list of possible states of a reminder.

>>> rt.show(courses.ReminderStates)
======= =========== =========== =============
 value   name        text        Button text
------- ----------- ----------- -------------
 10      draft       Draft
 20      sent        Sent
 30      ok          OK
 40      final       Final
 90      cancelled   Cancelled
======= =========== =========== =============
class lino_avanti.lib.courses.EnrolmentChecker

Checks for the following data problems:

  • More than 2 times absent.

  • Missed more than 10% of meetings.

Templates

courses/Enrolment/Default.odt

Prints an “Integration Course Agreement”.

courses/Reminder/Default.odt

Prints a reminder to be sent to the client.

Help texts

Test whether the help texts have been loaded and translated correctly:

>>> fld = courses.EnrolmentsByCourse.model._meta.get_field('needs_childcare')
>>> print(fld.help_text)
Whether this pupil has small children to care about.

Test whether translations of help texts are working correctly:

>>> from django.utils import translation
>>> with translation.override('de'):
...     print(fld.help_text)
Ob dieser Teilnehmer Kleinkinder zu betreuen hat.

Presence sheet

>>> obj = courses.Course.objects.get(id=2)
>>> print(obj.user)
nathalie
>>> from pathlib import Path
>>> url = '/api/courses/Activities/2?'
>>> url += 'fv=01.02.2017&fv=28.02.2017&fv=false&fv=true&'
>>> url += 'an=print_presence_sheet_html&sr=2'
>>> test_client.force_login(rt.login('robin').user)
>>> res = test_client.get(url)  
weasy2html render ['courses/Course/presence_sheet.weasy.html'] -> .../cache/weasy2html/courses.Course-2.html ('en', {})
>>> res.status_code
200
>>> rv = AttrDict(json.loads(res.content.decode()))
>>> open_url = rv.open_url
>>> print(open_url)
/media/cache/weasy2html/courses.Course-2.html
>>> open_url = open_url[1:]
>>> fn = settings.SITE.site_dir / open_url
>>> html = fn.read_text()
>>> soup = BeautifulSoup(html, "lxml")
>>> links = soup.find_all('a')
>>> len(links)
0

Number of rows:

>>> len(soup.find_all('tr'))
29

Number of columns:

>>> len(soup.find('tr').find_all('td'))
17

Total number of cells is 13*17:

>>> cells = soup.find_all('td')
>>> len(cells)
493
>>> cells[0]
<td>No.</td>
>>> cells[1]
<td>Participant</td>
>>> print(cells[3].decode())  
<td>02.02.

<br/><font size="1">11 (☑)</font>
</td>
>>> cells[17]
<td>1</td>
>>> print(cells[18].decode())
<td><p>Mr Armán Berndt</p></td>
>>> print(cells[20].decode())  
<td align="center" valign="middle">☉
  </td>

Description sheet

Here is a similar series of tests for the “description sheet”. This time we want to test the German translation as well. For this we change the language of its author to “de”.

>>> nathalie.language = "de"
>>> nathalie.save()
>>> url = '/api/courses/Activities/2?'
>>> url += 'fv=01.02.2017&fv=28.02.2017&fv=false&fv=true&'
>>> url += 'an=print_description&sr=2'
>>> res = test_client.get(url)  
weasy2html render ['courses/Course/description.weasy.html'] -> .../cache/weasy2html/courses.Course-2.html ('de', {})

Restore Nathalie’s language:

>>> nathalie.language = "en"
>>> nathalie.save()
>>> res.status_code
200
>>> res.status_code
200
>>> rv = AttrDict(json.loads(res.content.decode()))
>>> url = rv.open_url
>>> print(url)
/media/cache/weasy2html/courses.Course-2.html
>>> url = url[1:]
>>> fn = settings.SITE.site_dir / url
>>> html = fn.read_text()
>>> soup = BeautifulSoup(html, "lxml")
>>> links = soup.find_all('a')
>>> len(links)
0

Number of rows:

>>> len(soup.find_all('tr'))
20

Number of columns:

>>> len(soup.find('tr').find_all('td'))
5

Total number of cells is 13*17:

>>> headings = soup.find_all('h2')
>>> headings[0]
<h2>Teilnehmeraufstellung</h2>
>>> cells = soup.find_all('td')
>>> len(cells)
100
>>> cells[0]
<td>Nr.</td>
>>> cells[1]
<td>Teilnehmer</td>

Course layouts

The ActivityLayouts choicelist in Lino Avanti defines only one layout.

>>> rt.show(courses.ActivityLayouts)
======= ========= ============ ============================
 value   name      text         Table
------- --------- ------------ ----------------------------
 C       default   Activities   courses.ActivitiesByLayout
======= ========= ============ ============================

Missing rates

The Course.update_missing_rates() action is automatically performed for all courses once per day in the evening. Users can run it manually by clicking the button on a course.

>>> print(courses.Course.update_missing_rates.button_text)
 ☉
>>> print(courses.Course.update_missing_rates.label)
Update missing rates
>>> print(courses.Course.update_missing_rates.help_text)
Calculate the missing rates for all enrolments of this course.
class lino_avanti.lib.courses.DitchingEnrolments

List of enrolments with high absence rate for review by their coach.

>>> with translation.override("de"):
... 
...    print(str(courses.DitchingEnrolments.label))
...    print(str(courses.DitchingEnrolments.help_text))
Abwesenheitskontrolle
Liste der Einschreibungen mit hoher Abwesenheitsrate zwecks Kontrolle durch den Begleiter.
>>> rt.login("romain").show(courses.DitchingEnrolments)
============== ================================= ============================== =================
 Missing rate   Participant                       Activity                       Primary coach
-------------- --------------------------------- ------------------------------ -----------------
 54,17          ABID Abdul Báásid (162/romain)    Alphabetisation (16/01/2017)   Romain Raffault
 54,17          CISSE Chátá (150/romain)          Alphabetisation (16/01/2017)   Romain Raffault
 50,00          BEK-MURZIN Agápiiá (160/romain)   Alphabetisation (16/01/2017)   Romain Raffault
============== ================================= ============================== =================

Clients with more than one enrolment

>>> from django.db.models import Count
>>> qs = rt.models.avanti.Client.objects.order_by('name').all()
>>> qs = qs.annotate(
...     ecount=Count('enrolments_by_pupil'))
>>> qs = qs.filter(ecount__gt=1)
>>> obj = qs[0]
>>> rt.show(courses.EnrolmentsByPupil, obj, header_level=4)
Enrolments in Activities of ABAD Aábdeen (114/nathalie) (Also Cancelled)
========================================================================
================= ============================== ======== ======== ===============
 Date of request   Activity                       Author   Remark   Workflow
----------------- ------------------------------ -------- -------- ---------------
 13/02/2017        Alphabetisation (16/01/2017)   nelly             **Requested**
 13/02/2017        Alphabetisation (16/01/2017)   sandra            **Trying**
================= ============================== ======== ======== ===============

Note that missing rates are also computed for non-confirmed enrolments, and that there are even non-zero rates for such cases.

Miscellaneous

The following failed before 20230620:

>>> url = '/choices/courses/EnrolmentsByPupil/course?mk=127&request_date='
>>> show_choices("rolf", url) 

Alphabetisation (16.01.17)
Alphabetisation (16.01.17)
Alphabetisation (16.01.17)

Until 20231206 the reaction to an invalid date in an action parameter form raised a ValueError, which was reported to the site admins. Until 20241008 it raised a warning to be reported to the user. Since 20241008 Lino parses an invalid date as None because validation and warning is done by the front end.

>>> url = '/api/courses/Activities/2?an=print_presence_sheet&dm=detail&end_date=31.072.2023&fmt=json&pv&pv&pv&pv&pv&pv&pv&pv&pv&pv&rp=weak-key-75&show_remarks=false&show_states=true&start_date=01.07.2023'
>>> test_client.get(url)  
weasy2pdf render ['courses/Course/presence_sheet.weasy.html'] -> .../book/lino_book/projects/avanti1/media/cache/weasy2pdf/courses.Course-2.pdf ('en', {})
<HttpResponse status_code=200, "application/json">