Développer le serveur d’impression

Le serveur d’impression est développé et maintenu par le Crans. Il est distribué sous licence GPL v3, et peut être trouvé à l’adresse https://gitlab.crans.org/nounous/django-printer. Les contributions sont les bienvenues.

Le serveur est développé en Python, et utilise le framework Django. Il est recommandé de se familiariser avec la documentation de Django, qui est extrêmement bien faite et bien traduite qui plus est, avant de se lancer dans le projet.

Installation initiale

Cloner localement le dépôt

Commencez par installer Git avec votre gestionnaire de paquet favori (désolé les utilisateur⋅rices de Windows, mais utilisez un vrai environnement de travail), puis récupérez simplement le projet :

$ git clone https://gitlab.crans.org/nounous/django-printer.git

Le serveur étant codé en Python, vous aurez notamment besoin de Python ainsi que d’un environnement virtuel vous permettant de confiner vos dépendances.

$ sudo apt install python3 python3-pip python3-venv  # Debian/Ubuntu
$ sudo pacman -Sy python python-pip python-venv  # Arch Linux
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install -r requirements.txt

Création de la base de données

Par défaut, la base de données est stockée dans un fichier db.sqlite3 à la racine du projet, ce qui est recommandé pour développer. Pour importer le schéma de base de données, il suffit de lancer la commande suivante :

(venv) $ ./manage.py migrate

Fichiers statiques

Afin de permettre l’utilisation des fichiers statiques, notamment les fichiers Javascript et CSS, il vous suffit d’appeler cette commande :

(venv) $ ./manage.py collectstatic

Installation serveur d’impression

Deux options sont possibles pour développer cette application : installer CUPS ou ne pas le faire.

Installer CUPS permet d’envoyer de réelles instructions au serveur d’impression, s’assurant que tout fonctionne correctement.

Ne pas l’installer au contraire ignorera tous les appels à CUPS. Voir la section sur la configuration pour cette option.

Il est recommandé d’utiliser CUPS avec une imprimante virtuelle, l’option sans CUPS ne servant que pour les tests unitaires.

Pour cela, installez les paquets cups et cups-pdf, le premier gérant le serveur d’impression et le second un modèle d’imprimante virtuelle imprimant des PDF. Pensez bien à vous rendre sur la page d’administration de CUPS (normalement à l’adresse https://localhost:631/) et ajoutez bien l’imprimante. Notez bien quelque part le nom de l’imprimante ainsi créée. Assurez-vous enfin que le service cups est bien lancé avant de lancer le serveur Web.

Configuration du serveur

Rendez-vous dans le dossier printer, et copiez le fichier settings_local.example.py en settings_local.py. Vous êtes libres de le modifier localement.

Un point sur les paramètres gérés par le site en plus de ceux de Django :

NOTE_KFET_URL = 'https://note.crans.org'
NOTE_KFET_CLIENT_ID = 'CHANGE_ME'
NOTE_KFET_CLIENT_SECRET = 'CHANGE_ME'
DESTINATION_NOTE_ID = 2088
DESTINATION_NOTE_ALIAS = 'Crans'

Ces paramètres sont utilisés pour l’authentification des utilisateur⋅rices. avec la Note Kfet. En développement, afin de ne pas payer réellement, il est possible d’utiliser le serveur https://note-dev.crans.org. La note 2088 correspond à la note du club Crans sur la Note Kfet 2020 en production. Les identifiant et secret d’application sont à récupérer dans la gestion des applications OAuth2 de la Note Kfet.

# These parameters may be useful for testing purposes.
# This disables the interaction with Note Kfet (except login redirect)
# and/or the printer.
# Unless in a testing environment (eg. in a continuous integration or without
# any additional dependency), you should leave this to False.
IGNORE_NOTE_KFET = False
IGNORE_CUPS = False

Ces paramètres sont essentiellement utilisés pour l’intégration de tests unitaires, afin de les faire tourner sans serveur d’impression ni Note Kfet. Ignorer la Note Kfet ne dispensera pas d’authentification, mais ne créera pas de transaction Note Kfet après une impression. Ignorer CUPS ne créera pas de tâche d’impression. Il n’est pas recommandé de mettre ces valeurs à True en dehors d’un contexte de tests unitaires.

 # This is the common name of the printer that is installed in the CUPS server
PRINTER_NAME = 'Lexmark_X950_Series'

Il s’agit ici du nom de l’imprimante connu par CUPS. Dans le cas d’une imprimante PDF virtuelle, le nom sera sans doute Virtual_PDF_Printer, mais vous pouvez le définir librement.

Il s’agit ici de l’adresse de l’imprimante, pour la contacter pour scan. Elle doit être accessible par votre machine. Le second paramètre permet d’ignorer la vérification du certificat HTTPS, qui peut être obsolète notamment dans le cas de l’imprimante actuelle du Crans, ou en cas de redirection de ports rendant le nom de domaine utilisé non valide. Laisser à False n’empêche pas une connexion HTTPS chiffrée, elle sera simplement non vérifiée.

# To avoid spam, you can define the maximum scanning jobs a user can request.
# If set to 0, no limit is applied.
MAX_SIMULTANEOUS_SCANNING_JOBS = 5

Ce nombre permet de limiter le nombre de tâches de numérisations simultanées pour un⋅e même utilisateur⋅rice. Cela évite de pouvoir spammer les tâches. Si vous ne souhaitez pas de limite, mettez à 0.

# This address is the address of the server that will receive the scanned file.
# This may be allowed in your firewall and contactable by the printer.
SCANNER_SERVER_ADDRESS = '127.0.0.1'
SCANNER_SERVER_PORT = 9751

Il s’agit ici de l’adresse et du port du serveur de réception des scans. Cette adresse et ce port seront transmis à l’imprimante.

# Uncomment and adapt to use a LDAP server for authentication
# AUTHENTICATION_BACKENDS = ["django_auth_ldap.backend.LDAPBackend"]
# AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com:1636/"
# AUTH_LDAP_CONNECTION_OPTIONS = {
#     ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_ALLOW,
#     ldap.OPT_X_TLS_NEWCTX: 0,
#     ldap.OPT_REFERRALS: 0,
# }
# AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=passwd,dc=example,dc=com"

# AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
#     "ou=group,dc=example,dc=com",
#     ldap.SCOPE_SUBTREE,
#     "(objectClass=posixGroup)",
# )

# AUTH_LDAP_GROUP_TYPE = PosixGroupType()

# AUTH_LDAP_MIRROR_GROUPS = True

# AUTH_LDAP_USER_FLAGS_BY_GROUP = {
#     "is_active": "cn=_user,ou=group,dc=example,dc=com",
#     "is_staff": "cn=_user,ou=group,dc=example,dc=com",
#     "is_superuser": "cn=_nounou,ou=group,dc=example,dc=com",
# }

Ce bout de configuration permet de configurer une intégration LDAP, pour se connecter notamment à l’interface d’administration. Cette configuration est facultative.

Plus d’informations sur la documentation du module : https://django-auth-ldap.readthedocs.io/en/latest/

Lancement du serveur de développement

Pour lancer un serveur de développement, il suffit de lancer la commande python manage.py runserver. Il s’agit d’une fonctionnalité prévue par Django uniquement à des fins de développement. Cela lance un serveur Web sur le port 8000, il suffit alors de se rendre sur l’adresse http://localhost:8000 sur votre navigateur pour accéder à l’interface Web. À noter que ce serveur dispose d’un mécanisme de rechargement automatique, il n’est donc pas nécessaire de le relancer en cas de modification du code.

Structure du code

Le code est composé d’une unique application Django nommée printer. Cette application est composée de trois modèles :

PrintableFile

Ce modèle représente une tâche d’impression soumise. Un tel objet peut être ajouté via le formulaire Web principal, qui permet notamment de choisir le fichier à envoyer et les paramètres d’impressions. Ils peuvent également être gérés depuis l’interface d’administration.

Les différents champs sont les suivants :

  • file : le fichier soumis à l’impression

  • date : la date de création de la tâche. Gérée automatiquement.

  • status : le statut de la tâche, parmi « en attente », « retenu », « en cours de traitement », « stoppé », « annulé », « avorté » et « complété ». Géré automatiquement.

  • amount : quantité de copies à imprimer.

  • format : au choix parmi A5, A4 et A3, permettant de choisir le format de papier de sortie.

  • orientation : orientation du papier de sortie, parmi « portrait » et « paysage ».

  • color : booléen indiquant si le papier de sortie doit être imprimé en couleur ou en noir et blanc.

  • double_sided : au choix parmi « Recto », « Recto-verso sur bord long » ou « Recto-verso sur bord court ».

  • pages_per_sheet : au choix parmi 1, 2, 4, 6, 9 et 16. Permet de choisir le nombre de pages imprimées par feuille.

  • page_ranges : désigne les pages à imprimer. Cette option est facultative.

  • booklet : si cette option est activée, l’ordre des pages imprimé sera réordonné de manière à pouvoir agrafer la sortie et former un livret. Cette option implique l’impression paysage recto-verso sur bord court.

  • note_id : champ non public. Il s’agit de l’identifiant de la note à débiter.

  • username : champ non public. Pseudo de la personne qui a été facturée.

  • sender : champ non public. Pseudo de la personne qui a émis la tâche d’impression.

  • transaction_id : champ non public. Identifiant de la transaction note associée à la tâche d’impression.

  • pages : nombre de pages total imprimées. Tient compte de l’ordre des pages éventuellement défini.

  • unit_price : prix unitaire d’impression d’un document, valeur calculée puis stockée en centimes.

  • job_id : champ non public. Identifiant de la tâche d’impression enregistrée dans CUPS.

Différentes fonctions et propriétés sont définies au besoin :

  • total_price : prix total de la tâche en centimes

  • total_price_euros : prix total de la tâche, converti en euros joliment

  • unit_price_euros : prix unitaire d’une impression, converti en euros joliment

  • calculate_unit_price() : calcul du prix unitaire d’une impression, à partir des paramètres de coût définis.

  • make_transaction(token) : facture sur la Note Kfet la tâche en utilisant le jeton d’authentification fourni.

  • send_confirmation_email(token) : envoie un mail de confirmation à la personne qui a émis la tâche. Si un jeton est fourni, vient récupérer l’adresse depuis la Note Kfet.

  • booklet_order() -> list[int] : retourne l’ordre des pages imprimées pour un livret, selon le nombre de pages total. Tiens compte d’une éventuelle modification de l’ordre des pages, même si cela n’est pas recommandé.

  • replace_with_booklet() : dans le cas d’une impression livret, crée un nouveau PDF avec les pages dans l’ordre défini par la fonction précédente.

  • calculate_pages_count() -> int : récupère le nombre de pages du PDF.

  • cups_connection() -> cups.Connection : récupère la connexion à CUPS et ses bindings.

  • job_attributes -> dict : récupère les attributs de la tâche CUPS sous la forme d’un dictionnaire.

  • cups_options -> dict : génère les options comprises par CUPS à partir des attributs du modèle

  • print() : lance l’impression

  • check_status(force: bool) : récupère le statut de la tâche CUPS si elle n’est pas marquée comme terminée (sauf si le paramètre force est fini)

Scan

Ce modèle représente une tâche de scan soumise. Un tel objet peut être ajouté via le formulaire Web dédié au scan, qui permet notamment de choisir les paramètres de scan. Ils peuvent également être gérés depuis l’interface d’administration.

Les différents champs sont les suivants :

  • file : optionnel, il s’agit du fichier récupéré depuis l’imprimante. Il est non nul ssi le scan est terminé avec succès.

  • date : date de lancement du scan. Géré automatiquement.

  • username : champ non public. Pseudo de la personne qui a demandé le scan.

  • profile_name : nom du document à récupérer.

  • shortcut_id : champ non public. Identifiant du raccourci à taper sur l’imprimante.

  • status : au choix parmi « En attente », et « Terminé ». Géré automatiquement.

  • size : au choix parmi « lettre », « B5 », « A5 », « A4 » et « A3 ». Détermine le format de document scanné.

  • resolution : au choix parmi 75, 150, 200, 300, 400, 600 et 1200. Détermine la résolution du scan, en pixels par pouce.

  • type : détermine le format de sortie du scan, parmi PDF, JPEG, TIFF, BMP, GIF. Seul le PDF permet de supporter plusieurs pages.

  • orientation : au choix parmi « portrait » et « paysage ».

  • compression : JPEG est recommandé.

  • depth : au choix parmi 1 (noir et blanc), 8 et 24.

  • pages : nombre de pages scannées.

Les propriétés et fonctions suivantes sont également définies :

  • extension -> str : extension du fichier de sortie, déterminé à partir du type de fichier.

  • expected_filename -> str : renvoie le chemin de sortie prévisionnel du fichier.

  • calculate_pages_count -> int : lit le fichier scanné et calcule le nombre de pages.

  • printer_options() -> dict : calcule les paramètres à transmettre à l’imprimante pour le scan.

  • create_shortcut() -> int : crée un raccourci sur l’imprimante pour le scan avec les bons paramètres.

  • delete_task() : supprime la tâche sur l’imprimante.

  • check_status() : vérifie si le fichier est bien arrivé ou non.

AccessToken

Ce modèle représente un jeton d’accès à la Note Kfet. Un tel objet peut être généré par la Note Kfet, et servir à authentifier les utilisateurs.

Il contient les attributs suivants :

  • username : pseudo de l’utilisateur⋅rice

  • access_token : jeton d’accès permettant la connexion

  • expires_in : durée de validité du jeton

  • scopes : liste des scopes autorisés

  • refresh_token : jeton de rafraîchissement permettant de générer un nouveau jeton d’accès

  • expires_at : date de fin de validité du jeton

Plusieurs fonctions sont définies :

  • refresh() : utilise le jeton de rafraîchissement pour générer un nouveau jeton d’accès

  • refresh_if_expired() : rafraîchit le jeton si celui-ci est expiré uniquement

  • auth_header() -> dict : renvoie les en-têtes d’authentification pour les requêtes à la Note Kfet

  • fetch_user_data() -> dict : récupère les informations de l’utilisateur⋅rice courant⋅e depuis la Note Kfet

  • fetch_email_address(username: str) -> str : récupère l’adresse email de l’utilisateur⋅rice demandé⋅e

  • get_token(request) -> AccessToken : récupère le jeton d’accès associé à la requête en cours

Documentation automatique

Django-admin propose une documentation automatique des vues et modèles. Elle est disponible sur /admin/doc/, mais nécessite d’être authentifié⋅e pour y accéder.