Uploads in Lino Welfare

This document describes the lino_xl.lib.uploads plugin as used by Lino Welfare.

All code snippets on this page (lines starting with >>>) are being tested as part of our development workflow. The following snippet initializes a demo project to use throughout this page.

>>> import lino
>>> lino.startup('lino_welfare.projects.gerd.settings.doctests')
>>> from lino.api.doctest import *

Lino Welfare uses the lino_xl.lib.uploads plugin together with the lino_xl.lib.coachings plugin, which changes some aspects.

>>> print(cal.TasksByController.detail_layout.main)

    start_date due_date id workflow_buttons
    summary
    project user delegated
    owner created:20 modified:20
    description #notes.NotesByTask

Configuring upload types

This is the list of upload types:

>>> rt.login('rolf').show(uploads.UploadTypes)
==== ============================ ======== ================ ============= ========================= ====================== ============================
 ID   Bezeichnung                  Wanted   Upload-Bereich   Max. number   Ablaufwarnung (Einheit)   Ablaufwarnung (Wert)   Upload shortcut
---- ---------------------------- -------- ---------------- ------------- ------------------------- ---------------------- ----------------------------
 11   Arbeitserlaubnis             Ja       Allgemein        1             monatlich                 2
 10   Aufenthaltserlaubnis         Ja       Allgemein        1             monatlich                 2
 15   Behindertenausweis           Nein     Allgemein        -1                                      1
 16   Diplom                       Ja       Allgemein        -1                                      1
 12   Führerschein                 Ja       Allgemein        1             monatlich                 1
 1    Identifizierendes Dokument   Ja       Allgemein        1             monatlich                 2                      Identifizierendes Dokument
 17   Personalausweis              Nein     Allgemein        -1                                      1
 2    Source document              Ja       Allgemein        1             monatlich                 2                      Source document
 13   Vertrag                      Nein     Allgemein        -1                                      1
 14   Ärztliche Bescheinigung      Nein     Allgemein        -1                                      1
                                                             **0**                                   **14**
==== ============================ ======== ================ ============= ========================= ====================== ============================

Two clients and their uploads

The following newcomer has uploaded 2 identifying documents. One of these is no longer valid, and we know it: needed has been unchecked. The other is still valid but will expire in 3 days.

>>> newcomer = pcsw.Client.objects.get(pk=121)
>>> print(newcomer)
DERICUM Daniel (121)

The UploadsByProject summary shows a summary grouped by upload type.

>>> rt.show(uploads.UploadsByProject, newcomer)
Identifizierendes Dokument: `14 <…>`__, `13 <…>`__
Diplom: `11 <…>`__
Personalausweis: `12 <…>`__
>>> rt.show(uploads.UploadsByProject, newcomer, nosummary=True)
============================ ============ ======= =============================== ===================
 Upload-Art                   Gültig bis   Nötig   Beschreibung                    Hochgeladen durch
---------------------------- ------------ ------- ------------------------------- -------------------
 Identifizierendes Dokument   25.05.14     Ja      Identifizierendes Dokument 14   Theresia Thelen
 Identifizierendes Dokument   22.04.14     Nein    Identifizierendes Dokument 13   Theresia Thelen
 Personalausweis              26.06.15     Nein    Personalausweis 12              Hubert Huppertz
 Diplom                       26.06.15     Nein    Diplom 11                       Hubert Huppertz
============================ ============ ======= =============================== ===================

Here is another beneficiary with three uploads:

>>> oldclient = pcsw.Client.objects.get(pk=124)
>>> print(str(oldclient))
DOBBELSTEIN Dorothée (124)
>>> rt.show(uploads.UploadsByProject, oldclient)
Aufenthaltserlaubnis: `residence_permit.pdf <…>`__ `⎙ </media/uploads/2014/05/residence_permit.pdf>`__
Arbeitserlaubnis: `work_permit.pdf <…>`__ `⎙ </media/uploads/2014/05/work_permit.pdf>`__
Führerschein: `driving_license.pdf <…>`__ `⎙ </media/uploads/2014/05/driving_license.pdf>`__
>>> rt.show(uploads.UploadsByProject, oldclient, nosummary=True)
====================== ============ ======= ============================================================================================= ===================
 Upload-Art             Gültig bis   Nötig   Beschreibung                                                                                  Hochgeladen durch
---------------------- ------------ ------- --------------------------------------------------------------------------------------------- -------------------
 Führerschein           01.06.14     Ja      `Führerschein driving_license.pdf </media/uploads/2014/05/driving_license.pdf>`__             Caroline Carnol
 Arbeitserlaubnis       30.08.14     Ja      `Arbeitserlaubnis work_permit.pdf </media/uploads/2014/05/work_permit.pdf>`__                 Alicia Allmanns
 Aufenthaltserlaubnis   18.03.15     Ja      `Aufenthaltserlaubnis residence_permit.pdf </media/uploads/2014/05/residence_permit.pdf>`__   Theresia Thelen
====================== ============ ======= ============================================================================================= ===================

My uploads

Most users can open two tables which show “their” uploads.

>>> print(str(uploads.MyUploads.label))
Meine Upload-Dateien
>>> print(str(uploads.MyExpiringUploads.label))
Meine ablaufenden Upload-Dateien

This is the MyUploads table for Theresia:

>>> rt.login('theresia').show(uploads.MyUploads)
==== ============================ ============================ ============ ============ ======= ============================================================================================= ======================================
 ID   Klient                       Upload-Art                   Gültig von   Gültig bis   Nötig   Beschreibung                                                                                  Datei
---- ---------------------------- ---------------------------- ------------ ------------ ------- --------------------------------------------------------------------------------------------- --------------------------------------
 15   DOBBELSTEIN Dorothée (124)   Aufenthaltserlaubnis                      18.03.15     Ja      `Aufenthaltserlaubnis residence_permit.pdf </media/uploads/2014/05/residence_permit.pdf>`__   uploads/2014/05/residence_permit.pdf
 14   DERICUM Daniel (121)         Identifizierendes Dokument                25.05.14     Ja      Identifizierendes Dokument 14
 13   DERICUM Daniel (121)         Identifizierendes Dokument                22.04.14     Nein    Identifizierendes Dokument 13
==== ============================ ============================ ============ ============ ======= ============================================================================================= ======================================

And the same for Caroline:

>>> rt.login('caroline').show(uploads.MyUploads)
==== ============================ ============== ============ ============ ======= =================================================================================== =====================================
 ID   Klient                       Upload-Art     Gültig von   Gültig bis   Nötig   Beschreibung                                                                        Datei
---- ---------------------------- -------------- ------------ ------------ ------- ----------------------------------------------------------------------------------- -------------------------------------
 17   DOBBELSTEIN Dorothée (124)   Führerschein                01.06.14     Ja      `Führerschein driving_license.pdf </media/uploads/2014/05/driving_license.pdf>`__   uploads/2014/05/driving_license.pdf
==== ============================ ============== ============ ============ ======= =================================================================================== =====================================

This is the MyExpiringUploads table for Hubert:

>>> rt.login('hubert').show(uploads.MyExpiringUploads)
========================= ============================ ============================== =================== ============ ============ =======
 Klient                    Upload-Art                   Beschreibung                   Hochgeladen durch   Gültig von   Gültig bis   Nötig
------------------------- ---------------------------- ------------------------------ ------------------- ------------ ------------ -------
 AUSDEMWALD Alfons (116)   Identifizierendes Dokument   Identifizierendes Dokument 3   Hubert Huppertz                  17.05.15     Ja
 AUSDEMWALD Alfons (116)   Source document              Source document 4              Hubert Huppertz                  17.05.15     Ja
========================= ============================ ============================== =================== ============ ============ =======

Theresia does not coach anybody, so the MyExpiringUploads table is empty for her:

>>> rt.login('theresia').show(uploads.MyExpiringUploads)
Keine Daten anzuzeigen

Shortcut fields

>>> id_document = uploads.UploadType.objects.get(shortcut=uploads.Shortcuts.id_document)
>>> rt.show(uploads.UploadsByType, id_document)
=================== ========================= ============================ ======= ============ ============ ======= ===============================
 Hochgeladen durch   Klient                    Upload-Art                   Datei   Gültig von   Gültig bis   Nötig   Beschreibung
------------------- ------------------------- ---------------------------- ------- ------------ ------------ ------- -------------------------------
 Theresia Thelen     DERICUM Daniel (121)      Identifizierendes Dokument                        25.05.14     Ja      Identifizierendes Dokument 14
 Theresia Thelen     DERICUM Daniel (121)      Identifizierendes Dokument                        22.04.14     Nein    Identifizierendes Dokument 13
 Hubert Huppertz     AUSDEMWALD Alfons (116)   Identifizierendes Dokument                        17.05.15     Ja      Identifizierendes Dokument 3
=================== ========================= ============================ ======= ============ ============ ======= ===============================

Let’s have a closer look at the id_document shortcut field for some customers.

The response to this AJAX request is in JSON, and we want to inspect the id_document field using BeautifulSoup:

>>> uri = "pcsw/Clients/{0}".format(newcomer.pk)
>>> soup = get_json_soup('romain', uri, 'id_document')

This is an upload shortcut field whose target has more than one row. Which means that it has two buttons.

>>> div = soup.div
>>> len(div.contents)
3

The first button NO LONGER opens a detail window on the last uploaded file. This feature (was it one?)

>>> div.contents[0]
<a href='javascript:Lino.uploads.UploadsByController.insert.run(null,{
"base_params": {...}, "data_record": { "data": {
"description": "", "disabled_fields": { "camera_stream": true, "file_size":
true, "mimetype": true }, "end_date": null, "file": "", "needed": true, "type":
"Source document", "typeHidden": 2 }, "phantom": true, "title": "Ins\u00e9rer
Fichier t\u00e9l\u00e9charg\u00e9" }, "param_values": { "coached_by": null,
"coached_byHidden": null, "end_date": null, "observed_event": "Est active",
"observed_eventHidden": "20", "start_date": null, "upload_type": null,
"upload_typeHidden": null, "user": null, "userHidden": null }, "record_id": null
})' style="vertical-align:-30%;" title="Datei von Ihrem PC zum Server
hochladen."><img alt="add" src="/static/images/mjames/add.png"/></a>

<a href=’javascript:Lino.uploads.Uploads.detail.run(null,{ “base_params”: { }, “param_values”: { … }, “record_id”: 14 })’ style=”text-decoration:none”>Letzte</a>

The second item is just the comma that separates the two buttons:

>>> div.contents[1]
', '

The second button opens the list of uploads. Here is the full HTML definition of that button. Until 20250306 this button pointed to UploadsByProject but now to UploadsByController, and the text of the link changed from “Alle 2 Dateien” to “⏏”.

>>> btn = div.contents[2]
>>> btn
<a href='...' style="text-decoration:none"
title="Manage the list of uploaded files."> ⏏ </a>

Already its href is quite long:

>>> btn['href']
'javascript:Lino.uploads.UploadsByController.grid.run(null,{
"base_params": { "mk": 121, "mt": 58, "type": 2 }, "param_values": {
"coached_by": null, "coached_byHidden": null, "end_date": null,
"observed_event": "Est active", "observed_eventHidden": "20", "start_date":
null, "upload_type": null, "upload_typeHidden": null, "user": null,
"userHidden": null }, "record_id": 121 })'

This code would call the run() method of the grid view of the uploads.UploadsByController table with two positional arguments, the first being null and the second being a big JavaScript object. Let’s inspect this second argument of this second button.

>>> arg = btn['href'][58:-1]
>>> print(arg)
{ ... }
>>> d = AttrDict(json.loads(arg))

It has 3 keys:

>>> keys = list(d.keys())
>>> keys.sort()
>>> print(json.dumps(keys))
["base_params", "param_values", "record_id"]
>>> d.record_id
121
>>> d.base_params
{'mk': 121, 'mt': 58, 'type': 2}
>>> pprint(d.param_values)
...
{'coached_by': None,
 'coached_byHidden': None,
 'end_date': None,
 'observed_event': 'Est active',
 'observed_eventHidden': '20',
 'start_date': None,
 'upload_type': None,
 'upload_typeHidden': None,
 'user': None,
 'userHidden': None}

Uploads by client

UploadsByProject shows all the uploads of a given client, but it has a customized get_slave_summary.

The following example is going to use client #124 as master.

>>> obj = oldclient

Here we use lino.api.doctest.get_json_soup() to inspect what the summary view of UploadsByProject returns for this client.

>>> soup = get_json_soup('rolf', 'pcsw/Clients/124', 'uploads.UploadsByProject')
>>> print(soup.get_text())
...
Identifizierendes Dokument: Source document: Aufenthaltserlaubnis:
residence_permit.pdf ⎙Arbeitserlaubnis: work_permit.pdf ⎙Führerschein:
driving_license.pdf ⎙Diplom:

The HTML fragment contains five links:

>>> links = soup.find_all('a')
>>> len(links)
9

The first link would run the insert action on UploadsByProject, with the owner set to this client

>>> btn = links[0]
>>> print(btn.string)
None
>>> print(btn.img['src'])
/static/images/mjames/add.png
>>> print(btn)
...
<a href='javascript:Lino.uploads.UploadsByProject.insert.run(null,{...})'
style="vertical-align:-30%;" title="Neue(n/s) Upload-Datei
erstellen."><img alt="add" src="/static/images/mjames/add.png"/></a>
>>> print(links[2].get('href'))
...
javascript:Lino.uploads.UploadsByProject.detail.run(null,{ ..., "record_id": 15 })
>>> print(links[3].get('href'))
...
/media/uploads/2014/05/residence_permit.pdf

Now let’s inspect the javascript of the first button

>>> dots = btn['href'][57:-1]
>>> print(dots)
{ ... }
>>> d = AttrDict(json.loads(dots))
>>> pprint(d)
...
{'base_params': {'mk': 124, 'mt': 58, 'type_id': 1},
 'data_record': {'data': {'description': '',
                          'disabled_fields': {'camera_stream': True,
                                              'file_size': True,
                                              'mimetype': True},
                          'end_date': None,
                          'file': '',
                          'needed': True,
                          'project': 'DOBBELSTEIN Dorothée (124)',
                          'projectHidden': 124,
                          'start_date': None,
                          'type': 'Identifizierendes Dokument',
                          'typeHidden': 1},
                 'phantom': True,
                 'title': 'Upload-Datei erstellen'},
 'param_values': {'coached_by': None,
                  'coached_byHidden': None,
                  'end_date': None,
                  'observed_event': 'Ist aktiv',
                  'observed_eventHidden': '20',
                  'start_date': None,
                  'upload_type': None,
                  'upload_typeHidden': None,
                  'user': None,
                  'userHidden': None},
 'record_id': None}