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**
==================== ====================== ====================== ====================== ================== ============ ===================== ===================== ============ ==============
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">