Sith/search/search_index.json

1 line
992 KiB
JSON

{"config":{"lang":["fr"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Documentation du site de l'association des \u00e9tudiants de l'UTBM","text":"<p>Bonjour, camarade.</p> <p>Si tu es ici, c'est sans doute pour mieux connaitre le fonctionnement interne du site AE. Peut-\u00eatre m\u00eame as-tu envie de contribuer \u00e0 son d\u00e9veloppement. Si tel est le cas, nous ne pouvons faire autrement que te souhaiter la bienvenue sur cette documentation et esp\u00e9rer que ce que nous avons \u00e9crit est assez clair et assez complet pour satisfaire ton esprit en qu\u00eate de connaissances.</p> <p>Et si tu viens pour d'autres motifs, \u00e7a ne change rien, soit le bienvenu (ou la bienvenue) quand m\u00eame.</p> <p>Pour que tu saches o\u00f9 chercher quelles informations, voici comment nous avons d\u00e9coup\u00e9 la documentation :</p> <ul> <li>Dans un premier temps, les explications, o\u00f9 nous discutons tout simplement de ce \u00e0 quoi nous pensions en faisant ceci ou cela, quels motifs ont guid\u00e9 tel ou tel choix... Bref, nous discutons du projet en tant qu'ensemble de choix, qu'en tant qu'accumulation de d\u00e9tails techniques. Des d\u00e9tails techniques, tu en trouveras quand m\u00eame, mais sans doute moins que dans les autres parties.</li> <li>Deuxi\u00e8mement, les tutoriels, o\u00f9 nous expliquons pas \u00e0 pas comment installer le projet et commencer \u00e0 travailler dessus. Vois-le comme une notice d'assemblage.</li> <li>Troisi\u00e8mement, les recettes, ou how-to, o\u00f9 nous pr\u00e9senterons des conseils, des proc\u00e9dures \u00e0 suivre et des patterns qui t'aideront peut-\u00eatre.</li> <li>Enfin, la r\u00e9f\u00e9rence, o\u00f9 sera donn\u00e9e en dur les d\u00e9tails techniques les plus intimes du projet, tir\u00e9s directement du code, qui ne seront sans doute pas d'une grande utilit\u00e9 pour comprendre le projet dans sa globalit\u00e9, mais qui seront bien plus utiles pour appr\u00e9hender l'impl\u00e9mentation de telle ou telle partie du code.</li> </ul>"},{"location":"explanation/","title":"Accueil","text":""},{"location":"explanation/#objectifs","title":"Objectifs","text":"<p>Le but de ce projet est de fournir \u00e0 l'Association des \u00c9tudiants de l'UTBM une plate-forme pratique et centralis\u00e9e de ses services. Le Sith de l'AE tient \u00e0 jour le registre des cotisations \u00e0 l'association, prend en charge la tr\u00e9sorerie, les ventes de produits et services, la diffusion d\u2019\u00e9v\u00e9nements, la gestion de la laverie et bien plus encore. </p> <p>C'est un projet b\u00e9n\u00e9vole qui tire ses origines des ann\u00e9es 2000. Il s'agit de la troisi\u00e8me version du site de l'AE. Son d\u00e9veloppement a commenc\u00e9 en 2015. C'est une r\u00e9\u00e9criture compl\u00e8te en rupture totale des deux versions qui l'ont pr\u00e9c\u00e9d\u00e9e.</p>"},{"location":"explanation/#pourquoi-reecrire-le-site","title":"Pourquoi r\u00e9\u00e9crire le site","text":"<p>L'ancienne version du site, sobrement baptis\u00e9e ae2, pr\u00e9sentait un nombre impressionnant de fonctionnalit\u00e9s. Il avait \u00e9t\u00e9 \u00e9crit en PHP et se basait sur son propre framework maison.</p> <p>Malheureusement, son entretien \u00e9tait plus ou moins hasardeux et son framework reposait sur des principes assez diff\u00e9rents de ce qui se fait aujourd'hui, rendant la maintenance difficile. De plus, la version de PHP qu'il utilisait \u00e9tait plus que d\u00e9pr\u00e9ci\u00e9e et \u00e0 l'heure de l'arriv\u00e9e de PHP 7 et de sa non-r\u00e9trocompatibilit\u00e9 il \u00e9tait vital de faire quelque chose. Il a donc \u00e9t\u00e9 d\u00e9cid\u00e9 de le r\u00e9\u00e9crire.</p>"},{"location":"explanation/#la-philosophie-initiale","title":"La philosophie initiale","text":"<p>Pour \u00e9viter les erreurs du pass\u00e9, ce projet met l'accent sur la maintenabilit\u00e9. Le choix des technologies ne s'est donc pas fait uniquement sur le fait qu'elle soit r\u00e9centes, mais \u00e9galement sur leur robustesse, leur fiabilit\u00e9 et leur potentiel \u00e0 \u00eatre maintenu loin dans le futur.</p> <p>La maintenabilit\u00e9 passe \u00e9galement par le choix minutieux des d\u00e9pendances qui doivent, elles aussi, passer l'\u00e9preuve du temps pour \u00e9viter qu'elles ne mettent le projet en danger.</p> <p>Cela passe \u00e9galement par la minimisation des frameworks employ\u00e9s de mani\u00e8re \u00e0 r\u00e9duire un maximum les connaissances n\u00e9cessaires pour contribuer au projet et donc simplifier la prise en main. La simplicit\u00e9 est \u00e0 privil\u00e9gier si elle est possible.</p> <p>Le projet doit \u00eatre simple \u00e0 installer et \u00e0 d\u00e9ployer.</p> <p>Le projet \u00e9tant \u00e0 destination d'\u00e9tudiants, il est pr\u00e9f\u00e9rable de minimiser les ressources utilis\u00e9es par l'utilisateur final. Il faut qu'il soit au maximum \u00e9conome en bande passante et calcul c\u00f4t\u00e9 client.</p> <p>Le projet est un logiciel libre et est sous licence GPL. Aucune d\u00e9pendance propri\u00e9taire n'est accept\u00e9e.</p>"},{"location":"explanation/#la-philosophie-10-ans-plus-tard","title":"La philosophie, 10 ans plus tard","text":"<p>Malgr\u00e9 la bonne volont\u00e9 et le travail colossal fourni par les developpeurs de la version actuelle du projet, force est de constater que nombre d'erreurs ont malheureusement \u00e9t\u00e9 commises : usage complexe et excessif de certains m\u00e9canismes OO, r\u00e9\u00e9criture maison de fonctionnalit\u00e9s de Django, syst\u00e8me de gestion des permissions rigide et co\u00fbteux en requ\u00eate \u00e0 la base de donn\u00e9es...</p> <p>Mais malgr\u00e9 tout \u00e7a, le site tourne. En effet, force est de constater que le pari initial de choisir un framework stable et durable a pay\u00e9. Aujourd'hui encore, Django est activement maintenu, ses mises \u00e0 jour sont r\u00e9guli\u00e8res sans pour autant n\u00e9cessiter beaucoup de changements lors des changements de version majeure.</p> <p>Quant aux erreurs qui ont \u00e9t\u00e9 commises, que celui qui n'a jamais reconsid\u00e9r\u00e9 a posteriori que ce qui lui semblait une bonne architecture \u00e9tait en fait un ensemble brinquebalant, leur jette la premi\u00e8re pierre.</p> <p>La solidit\u00e9 des fondations ayant \u00e9t\u00e9 prouv\u00e9e par l'\u00e9preuve du temps, le travail restant \u00e0 accomplir n'est pas de r\u00e9\u00e9crire encore une fois le site en utilisant encore d'autres technologies, mais plut\u00f4t de raboter les surcouches du site, pour refixer le plus solidement possiblement le projet sur ces fondations.</p>"},{"location":"explanation/archives/","title":"Archives","text":"<p>Page g\u00e9n\u00e9r\u00e9e</p> <p>Cette page est g\u00e9n\u00e9r\u00e9e \u00e0 partir du wiki GitHub du projet puisqu'elle h\u00e9berge du contenu binaire lourd qui encombrerait le repo git. La page originale ici peut \u00eatre plus \u00e0 jour puisque le contenu de cette page date de la derni\u00e8re g\u00e9n\u00e9ration de la documentation.</p> <p>Cette page contient certains des pdf qui ont pu \u00eatre produits dans le cadre du travail pass\u00e9 sur le site. Vous ne trouverez pas des informations compl\u00e8tes et \u00e0 jour. En revanche, vous trouverez des morceaux d'histoire qui vous permettront de mieux comprendre ceux \u00e0 quoi pensaient les anciens d\u00e9veloppeurs et, par extension, vous comprendrez peut-\u00eatre un peu mieux le site.</p>"},{"location":"explanation/archives/#comptes-rendus","title":"Comptes-rendus","text":"<ul> <li>CR 2016.01.05</li> <li>CR 2016.11.10</li> <li>CR 2016.11.16</li> <li>CR 2016.12.1</li> <li>CR 2016.12.8</li> <li>CR 2016.12.15</li> </ul>"},{"location":"explanation/archives/#rapports-de-twto","title":"Rapports de TW/TO","text":""},{"location":"explanation/archives/#skia","title":"Skia","text":"<p>Rapport Skia</p>"},{"location":"explanation/archives/#skia-et-loj","title":"Skia et LoJ","text":"<p>Rapport Skia+LoJ</p>"},{"location":"explanation/archives/#sli","title":"Sli","text":"<ul> <li>Rapport TO52 Sli</li> <li>Rapport PA00</li> <li>Cahier des charges PA00</li> </ul>"},{"location":"explanation/conventions/","title":"Conventions","text":"<p>Cette page traite des conventions utilis\u00e9es dans le d\u00e9veloppement du site.</p>"},{"location":"explanation/conventions/#langue","title":"Langue","text":"<p>Les noms, de fonctions, de classe, de fichiers et de dossiers sont en anglais. De m\u00eame, les commentaires et les docstrings sont r\u00e9dig\u00e9s en anglais.</p> <p>En revanche la documentation est r\u00e9dig\u00e9e en fran\u00e7ais. En effet, les d\u00e9veloppeurs et les d\u00e9veloppeuses qui ont \u00e9t\u00e9, sont et seront amen\u00e9s \u00e0 travailler sur le site sont presque tous des francophones. Or, la bonne compr\u00e9hension prime. Une documentation, qui se doit d'utiliser au mieux les mots justes, compris de mani\u00e8re juste, gagne \u00e0 \u00eatre \u00e9crite en langue vernaculaire, lorsqu'on est assur\u00e9 qu'une coop\u00e9ration internationale est peu probable.</p> <p>De la sorte, on s'assure au mieux que les r\u00e9dacteurs et r\u00e9dactrices s'expriment bien et que, r\u00e9ciproquement, les lecteurs et lectrices comprennent au mieux.</p> <p>A ce titre, on ne vous en voudra pas si vous r\u00e9digez des commentaires ou des docstrings en fran\u00e7ais.</p> <p>En revanche, le code en lui-m\u00eame doit rester imp\u00e9rativement en anglais ; les instructions \u00e9tant en langue anglaise, introduire des mots fran\u00e7ais au milieu cr\u00e9e un contraste qui nuit \u00e0 la compr\u00e9hension.</p> <p>De mani\u00e8re g\u00e9n\u00e9rale, demandez-vous juste \u00e0 qui vous \u00eates en train d'\u00e9crire :</p> <ul> <li>si vous \u00e9crivez pour la machine, c'est en anglais</li> <li>si vous \u00e9crivez pour des \u00eatres humains, c'est en fran\u00e7ais</li> </ul>"},{"location":"explanation/conventions/#gestion-de-version","title":"Gestion de version","text":"<p>Le projet utilise Git pour g\u00e9rer les versions et GitHub pour h\u00e9berger le d\u00e9p\u00f4t distant.</p> <p>L'arbre poss\u00e8de deux branches prot\u00e9g\u00e9es : <code>master</code> et <code>taiste</code>.</p> <p><code>master</code> est la branche contenant le code tel qu'il tourne effectivement sur le vrai site de l'AE. Celle-ci doit, autant que faire se peut, rester impeccable.</p> <p><code>taiste</code> est la branche de r\u00e9f\u00e9rence pour le d\u00e9veloppement. Cette derni\u00e8re est r\u00e9guli\u00e8rement d\u00e9ploy\u00e9e sur le site de test. Elle permet de s'assurer que les diverses modifications fonctionnent bien entre elles et fonctionnent bien sur le serveur, avant d'\u00eatre envoy\u00e9es sur master.</p>"},{"location":"explanation/conventions/#gestion-des-branches","title":"Gestion des branches","text":"<p>Toutes les modifications appliqu\u00e9es sur <code>taiste</code> doivent se faire via des Pull Requests depuis les diff\u00e9rentes branches de d\u00e9veloppement. Toutes les modifications appliqu\u00e9es sur <code>master</code> doivent se faire via des Pull Requests depuis <code>taiste</code>, ou bien depuis une branche de hotfix, dans le cas o\u00f9 il faut r\u00e9parer un bug urgent apparu de mani\u00e8re impromptue.</p> <p>Aucun <code>push</code> direct n'est admis, ni sur l'une, ni sur l'autre branche.</p> <p>En obligeant \u00e0 passer par des PR, on s'assure qu'au moins une autre personne aura lu votre code et que les outils de test et de v\u00e9rification de code auront valid\u00e9 vos modifications.</p> <p>Par extension du mode de travail par PR, les branches <code>master</code> et <code>taiste</code> ne peuvent recevoir du code que sous la forme de merge commits.</p> <p>De plus, ces branches doivent recevoir, mais jamais donner (\u00e0 part entre elles). Lorsqu'une modification a \u00e9t\u00e9 effectu\u00e9e sur <code>taiste</code> et que vous souhaitez la r\u00e9cup\u00e9rer dans une de vos branches, vous devez proc\u00e9der par <code>rebase</code>, et non par <code>merge</code>.</p> <p>En d'autres termes, vous devez respecter les deux r\u00e8gles suivantes :</p> <ol> <li>Les branches <code>master</code> et <code>taiste</code> ne doivent contenir que des merge commits</li> <li>Seules les branches <code>master</code> et <code>taiste</code> peuvent contenir des merge commits</li> </ol> Bien \u2714\ufe0fPas bien \u274c <pre><code>gitGraph:\n commit id: \"initial commit\"\n branch bar\n checkout main\n checkout bar\n commit id: \"baz\"\n checkout main\n merge bar id: \"Merge branch bar\"\n branch foo\n commit id: \"foo a\"\n commit id: \"foo b\"\n commit id: \"foo c\"\n checkout main\n merge foo id: \"Merge branch foo\"</code></pre> <pre><code>gitGraph:\n commit\n branch bar\n branch foo\n commit id: \"foo a\"\n commit id: \"foo b\"\n checkout main\n checkout bar\n commit id: \"baz\"\n checkout main\n merge bar id: \"Merge branch bar\"\n checkout foo\n merge main id: \"Merge branch main\"\n commit id: \"foo c\"\n checkout main\n merge foo id: \"Merge branch foo\"</code></pre>"},{"location":"explanation/conventions/#style-de-code","title":"Style de code","text":""},{"location":"explanation/conventions/#conventions-de-nommage","title":"Conventions de nommage","text":"<p>Les conventions de nommage sont celles de la PEP8 :</p> <ul> <li>les classes sont en PascalCase (ex: <code>class SacredGraal</code>)</li> <li>les constantes sont en MACRO_CASE (ex: <code>FAVOURITE_COLOUR = \"blue\"</code>)</li> <li>les fonctions et les variables sont en snake_case (ex: <code>swallow_origin = \"african\"</code>)</li> <li>les fichiers et dossiers contenant du code sont en snake_case</li> <li>les fichiers et les dossiers contenant de la documentation sont en kebab-case</li> </ul> <p>En parall\u00e8le de la casse, les r\u00e8gles de formatage du code sont celles du formateur Ruff. Ces r\u00e8gles sont automatiquement appliqu\u00e9es quand vous faites tourner Ruff, donc vous n'avez pas \u00e0 trop vous poser de questions de ce c\u00f4t\u00e9-l\u00e0.</p> <p>En ce qui concerne les templates Jinja et les fichiers SCSS, la norme de formatage est celle par d\u00e9faut de <code>djHTML</code>.</p> <p>Pour Javascript, nous utilisons biome. C'est \u00e0 la fois un formateur et un linter avec tr\u00e8s peu de configuration, un peu comme ruff.</p> <p>Le javascript dans les templates jinja</p> <p>Biome n'est pas capable de lire dans les fichiers jinja, c'est sa principale limitation.</p> <p>Il est donc recommand\u00e9 d'\u00e9viter de mettre trop de Javascript directement dans jinja mais de pr\u00e9f\u00e9rer des fichiers d\u00e9di\u00e9s.</p>"},{"location":"explanation/conventions/#qualite-du-code","title":"Qualit\u00e9 du code","text":"<p>Pour s'assurer de la qualit\u00e9 du code, Ruff et Biome sont \u00e9galement utilis\u00e9s.</p> <p>Tout comme pour le format, Ruff et Biome doivent tourner avant chaque commit.</p> <p>to edit or not to edit</p> <p>Vous constaterez sans doute que <code>ruff format</code> modifie votre code, mais que <code>ruff check</code> vous signale juste une liste d'erreurs sans rien modifier.</p> <p>En effet, <code>ruff format</code> ne s'occupe que de la forme du code, alors que <code>ruff check</code> regarde la logique du code. Si Ruff modifiait automatiquement la logique du code, \u00e7a serait un coup \u00e0 introduire plus de bugs que \u00e7a n'en r\u00e9soud.</p> <p>Il existe cependant certaines cat\u00e9gories d'erreurs que Ruff peut r\u00e9parer de mani\u00e8re s\u00fbre. Pour appliquer ces r\u00e9parations, faites :</p> <pre><code>ruff check --fix\n</code></pre> <p>Biome se comporte d'une mani\u00e8re tr\u00e8s similaire</p> <pre><code>npx @biomejs/biome check # Liste toutes les erreurs et leurs cat\u00e9gories\nnpx @biomejs/biome check --write # Applique tous les fix consid\u00e9r\u00e9s safe et formate le code\n</code></pre>"},{"location":"explanation/conventions/#documentation","title":"Documentation","text":"<p>La documentation est \u00e9crite en markdown, avec les fonctionnalit\u00e9s offertes par MkDocs, MkDocs-material et leurs extensions.</p> <p>La documentation est int\u00e9gralement en fran\u00e7ais, \u00e0 l'exception des exemples, qui suivent les conventions donn\u00e9es plus haut.</p>"},{"location":"explanation/conventions/#decoupage","title":"D\u00e9coupage","text":"<p>La s\u00e9paration entre les diff\u00e9rentes parties de la documentation se fait en suivant la m\u00e9thodologie Diataxis. On compte quatre sections :</p> <ol> <li>Explications : parlez dans cette section de ce qui est bon \u00e0 savoir sans que \u00e7a touche aux d\u00e9tails pr\u00e9cis de l'impl\u00e9mentation. Si vous parlez de pourquoi un choix a \u00e9t\u00e9 fait ou que vous montrez grossi\u00e8rement les contours d'une partie du projet, c'est une explication.</li> <li>Tutoriels : parlez dans cette section d'\u00e9tapes pr\u00e9cises ou de d\u00e9tails d'impl\u00e9mentation qu'un nouveau d\u00e9veloppeur doit suivre pour commencer \u00e0 travailler sur le projet.</li> <li>Utilisation : parlez dans cette section de m\u00e9thodes utiles pour un d\u00e9veloppeur qui a d\u00e9j\u00e0 pris en main le projet. Voyez cette partie comme un livre de recettes de cuisine.</li> <li>R\u00e9f\u00e9rence : parlez dans cette section des d\u00e9tails d'impl\u00e9mentation du projet. En r\u00e9alit\u00e9, vous n'aurez pas besoin de beaucoup vous pencher dessus, puisque cette partie est compos\u00e9e presque uniquement des docstrings pr\u00e9sents dans le code.</li> </ol> <p>Pour plus de d\u00e9tails, lisez directement la documentation de Diataxis, qui expose ces concepts de mani\u00e8re beaucoup plus compl\u00e8te.</p>"},{"location":"explanation/conventions/#style","title":"Style","text":"<p>Votre markdown doit \u00eatre compos\u00e9 de lignes courtes ; \u00e0 partir de 88 caract\u00e8res, c'est trop long. Si une phrase est trop longue pour tenir sur une ligne, vous pouvez l'\u00e9crire sur plusieurs.</p> <p>Une ligne ne peut pas contenir plus d'une seule phrase. Dit autrement, quand vous finissez une phrase, faites syst\u00e9matiquement un saut de ligne.</p> Bien \u2714\ufe0fPas bien \u274c <pre><code>First shalt thou take out the Holy Pin,\nthen shalt thou count to three, no more, no less.\nThree shalt be the number thou shalt count,\nand the number of the counting shalt be three.\nFour shalt thou not count, neither count thou two,\nexcepting that thou then proceed to three.\nFive is right out.\nOnce the number three, being the third number, be reached,\nthen lobbest thou thy Holy Hand Grenade of Antioch towards thou foe,\nwho being naughty in My sight, shall snuff it.\n</code></pre> <pre><code>First shalt thou take out the Holy Pin, then shalt thou count to three, no more, no less. Three shalt be the number thou shalt count, and the number of the counting shalt be three. Four shalt thou not count, neither count thou two, excepting that thou then proceed to three. Five is right out. Once the number three, being the third number, be reached, then lobbest thou thy Holy Hand Grenade of Antioch towards thou foe, who being naughty in My sight, shall snuff it.\n</code></pre> <p>\u00c0 noter que ces deux exemples donnent le m\u00eame r\u00e9sultat dans la documentation g\u00e9n\u00e9r\u00e9e. Mais la version avec de courtes lignes est beaucoup plus facile \u00e0 modifier et \u00e0 versioner.</p> <p>Grammaire et orthographe</p> <p>Ca peut paraitre \u00e9vident dit comme \u00e7a, mais c'est toujours bon \u00e0 rappeler : \u00e9vitez de faire des fautes de fran\u00e7ais. Relisez vous quand vous avez fini d'\u00e9crire.</p>"},{"location":"explanation/conventions/#docstrings","title":"Docstrings","text":"<p>Les docstrings sont \u00e9crits en suivant la norme Google et les fonctionnalit\u00e9s de Griffe.</p> <p>Ils doivent \u00eatre explicites sur ce que la fonction accomplit, mais ne pas parler de comment elle le fait. Un bon docstring est celui qui dit exactement ce qu'il faut pour qu'on puisse savoir comment utiliser la fonction ou la classe document\u00e9e sans avoir \u00e0 lire son code.</p> <p>Tout comme les p\u00e9dales d'une voiture : pour pouvoir conduire, vous avez juste besoin de savoir ce qui se passe quand vous appuyez dessus. La connaissance de la m\u00e9canique interne est inutile dans ce cadre.</p> <p>N'h\u00e9sitez pas \u00e0 mettre des examples dans vos docstrings.</p>"},{"location":"explanation/conventions/#pourquoi-une-partie-du-projet-ne-respecte-pas-ces-conventions","title":"Pourquoi une partie du projet ne respecte pas ces conventions ?","text":"<p>Parce que le projet est vieux. Le commit initial date du 18 novembre 2015. C'\u00e9tait il y a presque dix ans au moment o\u00f9 ces lignes sont \u00e9crites. Au d\u00e9but, on ne se posait pas forc\u00e9ment ce genre de questions. Puis le projet a grandi, de mani\u00e8re s\u00e9dimentaire, fonctionnalit\u00e9 apr\u00e8s fonctionnalit\u00e9, d\u00e9velopp\u00e9 par des personnes n'ayant pas toutes la m\u00eame esth\u00e9tique.</p> <p>On retrouve dans le code ces inspirations diverses de personnes vari\u00e9es \u00e0 travers une d\u00e9cennie. Au bout d'un moment, il est bon de se poser et de normaliser les choses.</p> <p>De ce c\u00f4t\u00e9-l\u00e0, une premi\u00e8re pierre a \u00e9t\u00e9 pos\u00e9e en novembre 2018, avec l'utilisation d'un formateur. Il convient de poursuivre ce travail d'unification.</p> <p>Cependant, l\u00e0 o\u00f9 on peut reformater automatiquement du code, il faut y aller \u00e0 la main pour retravailler un style de code. C'est un travail de fourmi qui prendra du temps.</p>"},{"location":"explanation/technos/","title":"Technologies utilis\u00e9es","text":"<p>Bien choisir ses technologies est crucial puisqu'une fois que le projet est suffisamment avanc\u00e9, il est tr\u00e8s difficile voir impossible de revenir en arri\u00e8re.</p> <p>En novembre 2015, plusieurs choix se pr\u00e9sentaient :</p> <ul> <li>Continuer avec du PHP</li> <li>S'orienter vers un langage web plus moderne et \u00e0 la mode comme le Python ou le Ruby</li> <li>Baser le site sur un framework Javascript</li> </ul> <p>Le PHP 5, bient\u00f4t 7, de l'\u00e9poque \u00e9tant assez discutable comme cet article le montre, et l'ancien site ayant laiss\u00e9 un go\u00fbt amer \u00e0 certains d\u00e9veloppeurs, celui-ci a \u00e9t\u00e9 mis de c\u00f4t\u00e9.</p> <p>L'\u00e9cosyst\u00e8me Javascript \u00e9tant \u00e0 peine naissant et les frameworks allant et venant en seulement quelques mois, il \u00e9tait impossible de pr\u00e9dire avec certitude si ceux-ci passeraient l'\u00e9preuve du temps, il \u00e9tait inconcevable de tout parier l\u00e0-dessus.</p> <p>Ne restait plus que le Python et le Ruby avec les frameworks Django et Ruby On Rails. Ruby ayant une r\u00e9putation d'\u00eatre tr\u00e8s \"cutting edge\", c'est Python, un langage bien implant\u00e9 et ayant fait ses preuves, qui a \u00e9t\u00e9 retenu.</p> <p>Il est \u00e0 noter que r\u00e9\u00e9crire le site avec un framework PHP comme Laravel ou Symphony eut aussi \u00e9t\u00e9 possible, ces deux technologies \u00e9tant assez matures et robustes au moment o\u00f9 le d\u00e9veloppement a commenc\u00e9. Cependant, il aurait \u00e9t\u00e9 potentiellement fastidieux de maintenir en parall\u00e8le deux versions de PHP sur le serveur durant toute la dur\u00e9e du d\u00e9veloppement. Il faut aussi prendre en compte que nous \u00e9tions \u00e0 ce moment d\u00e9go\u00fbt\u00e9s du PHP.</p>"},{"location":"explanation/technos/#backend","title":"Backend","text":""},{"location":"explanation/technos/#python-3","title":"Python 3","text":"<p>Site officiel</p> <p>Le python est un langage de programmation interpr\u00e9t\u00e9 multi paradigme sorti en 1991. Il est tr\u00e8s populaire dans de nombreux domaines pour sa simplicit\u00e9 d'utilisation, sa polyvalence, sa s\u00e9curit\u00e9 ainsi que sa grande communaut\u00e9 de d\u00e9veloppeur. Sa version 3, non r\u00e9tro compatible avec sa version 2, a \u00e9t\u00e9 publi\u00e9e en 2008.</p> <p>Note</p> <p>Puisque toutes les d\u00e9pendances du backend sont des packages Python, elles sont toutes ajout\u00e9es directement dans le fichier pyproject.toml, \u00e0 la racine du projet.</p>"},{"location":"explanation/technos/#django","title":"Django","text":"<p>Site officiel</p> <p>Documentation</p> <p>Django est un framework web pour Python apparu en 2005. Il fournit un grand nombre de fonctionnalit\u00e9s pour d\u00e9velopper un site rapidement et simplement. Cela inclut entre autre un serveur Web de d\u00e9veloppement, un parseur d'URLs pour le routage, un ORM (Object-Relational Mapper) pour la gestion de la base de donn\u00e9e, un cadre pour l'\u00e9criture et l'ex\u00e9cution des tests, de nombreux utilitaires pour l'internationalisation, les zones horaires et autres, une interface web d'administration ais\u00e9ment configurable, un moteur de templates pour le rendu HTML...</p> <p>Django propose une version LTS (Long Term Support) qui reste stable et est maintenu sur des cycles plus longs. Ce sont ces versions qui sont utilis\u00e9es.</p>"},{"location":"explanation/technos/#postgresql-sqlite3","title":"PostgreSQL / SQLite3","text":"<p>Site officiel PostgreSQL</p> <p>Site officiel SQLite</p> <p>Comme la majorit\u00e9 des sites internet, le Sith de l'AE enregistre ses donn\u00e9es dans une base de donn\u00e9es. Nous utilisons une base de donn\u00e9e relationnelle puisque c'est la mani\u00e8re typique d'utiliser Django et c'est ce qu'utilise son ORM.</p> <p>Le principal \u00e0 retenir ici est :</p> <ul> <li>Sur la version de production, nous utilisons PostgreSQL, c'est cette version qui doit fonctionner en priorit\u00e9. C'est un syst\u00e8me de gestion de base de donn\u00e9es fiable, puissant, activement maintenu, et particuli\u00e8rement bien support\u00e9 par Django.</li> <li>Sur les versions de d\u00e9veloppement, pour faciliter l'installation du projet, nous utilisons la technologie SQLite3 qui ne requiert aucune installation sp\u00e9cifique (la librairie <code>sqlite</code> est incluse dans les librairies par d\u00e9faut de Python). Certaines instructions ne sont pas support\u00e9es par cette technologie et il est parfois n\u00e9cessaire d'installer PostgreSQL pour le d\u00e9veloppement de certaines parties du site (cependant, ces parties sont rares, et vous pourriez m\u00eame ne jamais en rencontrer une).</li> </ul> <p>PostgreSQL est-ce avec quoi le site doit fonctionner. Cependant, pour permettre aux d\u00e9veloppeurs de travailler en installant le moins de d\u00e9pendances possible sur leur ordinateur, il est \u00e9galement d\u00e9sirable de chercher la compatibilit\u00e9 avec SQLite. Aussi, dans la mesure du possible, nous cherchons \u00e0 ce que les interactions avec la base de donn\u00e9es fonctionnent avec l'un comme avec l'autre.</p> <p>Heureusement, et gr\u00e2ce \u00e0 l'ORM de Django, cette double compatibilit\u00e9 est presque toujours possible.</p>"},{"location":"explanation/technos/#frontend","title":"Frontend","text":""},{"location":"explanation/technos/#jinja2","title":"Jinja2","text":"<p>Site officiel</p> <p>Jinja2 est un moteur de template \u00e9crit en Python qui s'inspire fortement de la syntaxe des templates de Django. Ce moteur apporte toutefois son lot d'am\u00e9liorations non n\u00e9gligeables. Il permet par exemple l'ajout de macros, sortes de fonctions \u00e9crivant du HTML.</p> <p>Un moteur de templates permet de g\u00e9n\u00e9rer du contenu textuel de mani\u00e8re proc\u00e9dural en fonction des donn\u00e9es \u00e0 afficher, cela permet de pouvoir inclure du code proche du Python dans la syntaxe au milieu d'un document contenant principalement du HTML. On peut facilement faire des boucles ou des conditions ainsi m\u00eame que de l'h\u00e9ritage de templates.</p> <p>Note</p> <p>le rendu est fait c\u00f4t\u00e9 serveur, si on souhaite faire des modifications c\u00f4t\u00e9 client, il faut utiliser du Javascript, rien ne change \u00e0 ce niveau-l\u00e0.</p>"},{"location":"explanation/technos/#jquery","title":"jQuery","text":"<p>Site officiel</p> <p>jQuery est une biblioth\u00e8que JavaScript libre et multiplateforme cr\u00e9\u00e9e pour faciliter l'\u00e9criture de scripts c\u00f4t\u00e9 client dans le code HTML des pages web. La premi\u00e8re version est lanc\u00e9e en janvier 2006 par John Resig.</p> <p>C'est une vieille technologie et certains feront remarquer \u00e0 juste titre que le Javascript moderne permet d'utiliser assez simplement la majorit\u00e9 de ce que fournit jQuery sans rien avoir \u00e0 installer. Cependant, de nombreuses d\u00e9pendances du projet utilisent encore jQuery qui est toujours tr\u00e8s implant\u00e9 aujourd'hui. Le sucre syntaxique qu'offre cette librairie reste tr\u00e8s agr\u00e9able \u00e0 utiliser et \u00e9conomise parfois beaucoup de temps. \u00c7a fonctionne et \u00e7a fonctionne tr\u00e8s bien. C'est maintenu et pratique.</p>"},{"location":"explanation/technos/#alpinejs","title":"AlpineJS","text":"<p>Site officiel</p> <p>AlpineJS est une librairie l\u00e9g\u00e8re et minimaliste permettant le rendu dynamique d'\u00e9l\u00e9ments sur une page web, code de mani\u00e8re d\u00e9clarative. La librairie est d\u00e9crite par ses cr\u00e9ateurs comme : \"un outil robuste et minimal pour composer un comportement directement dans vos balises\".</p> <p>Alpine permet d'accomplir la plupart du temps le m\u00eame r\u00e9sultat qu'un usage des fonctionnalit\u00e9s de base des plus gros frameworks Javascript, mais est beaucoup plus l\u00e9ger, un peu plus facile \u00e0 prendre en main et ne s'embarrasse pas d'un DOM virtuel. Gr\u00e2ce \u00e0 son architecture, il est extr\u00eamement bien adapt\u00e9 pour un usage dans un site multipage. C'est une technologie simple et puissante qui se veut comme le jQuery du web moderne.</p>"},{"location":"explanation/technos/#sass","title":"Sass","text":"<p>Site officiel</p> <p>Sass (Syntactically Awesome Stylesheets) est un langage dynamique de g\u00e9n\u00e9ration de feuilles CSS apparu en 2006. C'est un langage de CSS \"am\u00e9lior\u00e9\" qui permet l'ajout de variables (\u00e0 une \u00e9poque o\u00f9 le CSS ne les supportait pas), de fonctions, mixins ainsi qu'une syntaxe pour imbriquer plus facilement et proprement les r\u00e8gles sur certains \u00e9l\u00e9ments. Le Sass est traduit en CSS directement c\u00f4t\u00e9 serveur et le client ne re\u00e7oit que du CSS.</p> <p>C'est une technologie stable, mature et pratique qui ne n\u00e9cessite pas \u00e9norm\u00e9ment d'apprentissage.</p>"},{"location":"explanation/technos/#fontawesome","title":"Fontawesome","text":"<p>Site officiel</p> <p>Fontawesome regroupe tout un ensemble d'ic\u00f4nes libres de droits utilisables facilement sur n'importe quelle page web. Ils sont simples \u00e0 modifier puisque modifiables via le CSS et pr\u00e9sentent l'avantage de fonctionner sur tous les navigateurs contrairement \u00e0 un simple ic\u00f4ne unicode qui s'affiche lui diff\u00e9remment selon la plate-forme.</p> <p>Note</p> <p>C'est une d\u00e9pendance capricieuse qui \u00e9volue tr\u00e8s vite et qu'il faut tr\u00e8s souvent mettre \u00e0 jour.</p> <p>Warning</p> <p>Il a \u00e9t\u00e9 d\u00e9cid\u00e9 de ne pas utiliser de CDN puisque le site ralentissait r\u00e9guli\u00e8rement. Il est pr\u00e9f\u00e9rable de fournir cette d\u00e9pendance avec le site.</p>"},{"location":"explanation/technos/#workflow","title":"Workflow","text":""},{"location":"explanation/technos/#git","title":"Git","text":"<p>Site officiel</p> <p>Git est un logiciel de gestion de versions \u00e9crit par Linus Torvalds pour les besoins du noyau linux en 2005. C'est ce logiciel qui remplace svn anciennement utilis\u00e9 pour g\u00e9rer les sources du projet (rappelez vous, l'ancien site date d'avant 2005). Git est plus complexe \u00e0 utiliser, mais est bien plus puissant, permet de g\u00e9rer plusieurs versions en parall\u00e8le et g\u00e9n\u00e8re des codebases vraiment plus l\u00e9g\u00e8res puisque seules les modifications sont enregistr\u00e9es (contrairement \u00e0 svn qui garde une copie de la codebase par version).</p> <p>Git s'\u00e9tant impos\u00e9 comme le principal outil de gestion de versions, sa communaut\u00e9 est tr\u00e8s grande et sa documentation tr\u00e8s fournie. Il est \u00e9galement ais\u00e9 de trouver des outils avec une interface graphique, qui simplifient grandement son usage.</p>"},{"location":"explanation/technos/#github","title":"GitHub","text":"<p>Site officiel</p> <p>Page github du P\u00f4le Informatique de l'AE</p> <p>Github est un service web d'h\u00e9bergement et de gestion de d\u00e9veloppement de logiciel. C'est une plate-forme avec interface web permettant de d\u00e9poser du code g\u00e9r\u00e9 avec Git offrant \u00e9galement de l'int\u00e9gration continue et du d\u00e9ploiement automatique. C'est au travers de cette plate-forme que le Sith de l'AE est g\u00e9r\u00e9.</p>"},{"location":"explanation/technos/#sentry","title":"Sentry","text":"<p>Site officiel</p> <p>Instance de l'AE</p> <p>Sentry est une plate-forme libre qui permet de se tenir inform\u00e9 des bugs qui ont lieu sur le site. \u00c0 chaque crash du logiciel (erreur 500), une erreur est envoy\u00e9e sur la plate-forme et il est indiqu\u00e9 pr\u00e9cis\u00e9ment \u00e0 quelle ligne de code celle-ci a eu lieu, \u00e0 quelle heure, combien de fois, avec quel navigateur la page a \u00e9t\u00e9 visit\u00e9e et m\u00eame \u00e9ventuellement un commentaire de l'utilisateur qui a rencontr\u00e9 le bug.</p> <p>C'est un outil incroyablement pratique pour savoir tout ce qui ne fonctionne pas, et surtout pour r\u00e9colter toutes les informations n\u00e9cessaires \u00e0 la r\u00e9paration des bugs.</p>"},{"location":"explanation/technos/#poetry","title":"Poetry","text":"<p>Utiliser Poetry</p> <p>Poetry est un utilitaire qui permet de cr\u00e9er et g\u00e9rer des environnements Python de mani\u00e8re simple et intuitive. Il permet \u00e9galement de g\u00e9rer et mettre \u00e0 jour le fichier de d\u00e9pendances.</p> <p>L'avantage d'utiliser poetry (et les environnements virtuels en g\u00e9n\u00e9ral) est de pouvoir g\u00e9rer plusieurs projets diff\u00e9rents en parall\u00e8le puisqu'il permet d'avoir sur sa machine plusieurs environnements diff\u00e9rents et donc plusieurs versions d'une m\u00eame d\u00e9pendance dans plusieurs projets diff\u00e9rents sans impacter le syst\u00e8me sur lequel le tout est install\u00e9.</p> <p>Poetry poss\u00e8de \u00e9galement l'avantage par rapport \u00e0 un simple venv que les versions exactes de toutes les d\u00e9pendances, y compris celles utilis\u00e9es par d'autres d\u00e9pendances, sont consign\u00e9es dans un fichier <code>.lock</code>. On est donc s\u00fbr et certain que deux environnements virtuels configur\u00e9s avec le m\u00eame fichier lock utiliseront exactement les m\u00eames versions des m\u00eames d\u00e9pendances, y compris si celles-ci ne sont pas indiqu\u00e9es explicitement.</p> <p>Les d\u00e9pendances utilis\u00e9es par poetry sont d\u00e9clar\u00e9es dans le fichier <code>pyproject.toml</code>, situ\u00e9 \u00e0 la racine du projet.</p>"},{"location":"explanation/technos/#ruff","title":"Ruff","text":"<p>Site officiel</p> <p>Pour faciliter la lecture du code, il est toujours appr\u00e9ciable d'avoir une norme d'\u00e9criture coh\u00e9rente. C'est g\u00e9n\u00e9ralement \u00e0 l'\u00e9tape de relecture des modifications par les autres contributeurs que sont rep\u00e9r\u00e9es ces fautes de normes qui se doivent d'\u00eatre corrig\u00e9es pour le bien commun.</p> <p>Imposer une norme est tr\u00e8s fastidieux, que ce soit pour ceux qui relisent ou pour ceux qui \u00e9crivent. C'est pour cela que nous utilisons Ruff, qui est un formateur et linter automatique de code. Une fois l'outil lanc\u00e9, il parcourt la codebase pour y rep\u00e9rer les fautes de norme et les erreurs de logique courantes et les corrige automatiquement (quand c'est possible) sans que l'utilisateur ait \u00e0 s'en soucier. Bien install\u00e9, il peut effectuer ce travail \u00e0 chaque sauvegarde d'un fichier dans son \u00e9diteur, ce qui est tr\u00e8s agr\u00e9able pour travailler.</p>"},{"location":"explanation/technos/#biome","title":"Biome","text":"<p>Site officiel</p> <p>Puisque Ruff ne fonctionne malheureusement que pour le Python, nous utilisons Biome pour le javascript.</p> <p>Biome est \u00e9galement capable d'analyser et formater les fichiers json et css.</p> <p>Tout comme Ruff, Biome fait office de formateur et de linter.</p>"},{"location":"explanation/technos/#djhtml","title":"DjHTML","text":"<p>Site officiel</p> <p>Ruff permet de formater les fichiers Python et Biome les fichiers js, mais ils ne formattent pas les templates et les feuilles de style. Pour \u00e7a, il faut un autre outil, ais\u00e9ment int\u00e9grable dans la CI : <code>djHTML</code>.</p> <p>En utilisant conjointement Ruff, Biome et djHTML, on arrive donc \u00e0 la fois \u00e0 formater les fichiers Python et les fichiers relatifs au frontend.</p>"},{"location":"explanation/technos/#npm","title":"Npm","text":"<p>Utiliser npm</p> <p>Npm est un gestionnaire de paquets pour Node.js. C'est l'un des gestionnaires les plus r\u00e9pandus sur le march\u00e9 et est tr\u00e8s complet et utilis\u00e9.</p> <p>Npm poss\u00e8de, tout comme Poetry, la capacit\u00e9 de locker les d\u00e9pendances au moyen d'un fichier <code>.lock</code>. Il a \u00e9galement l'avantage de presque toujours \u00eatre facilement disponible \u00e0 l'installation.</p> <p>Nous l'utilisons ici pour g\u00e9rer les d\u00e9pendances JavaScript. Celle-ci sont d\u00e9clar\u00e9es dans le fichier <code>package.json</code> situ\u00e9 \u00e0 la racine du projet.</p>"},{"location":"explanation/technos/#webpack","title":"Webpack","text":"<p>Utiliser webpack</p> <p>Webpack est un bundler de fichiers static. Il nous sert ici \u00e0 mettre \u00e0 disposition les d\u00e9pendances frontend g\u00e9r\u00e9es par npm.</p> <p>Il sert \u00e9galement \u00e0 int\u00e9grer les autres outils JavaScript au workflow du Sith de mani\u00e8re transparente.</p> <p>Webpack a \u00e9t\u00e9 choisi pour sa versatilit\u00e9 et sa popularit\u00e9. C'est un des plus anciens bundler et il est l\u00e0 pour rester.</p> <p>Le logiciel se configure au moyen du fichier <code>webpack.config.js</code> \u00e0 la racine du projet.</p>"},{"location":"explanation/technos/#babel","title":"Babel","text":"<p>Babel</p> <p>Babel est un outil qui offre la promesse de convertir le code JavaScript moderne en code JavaScript plus ancien sans action de la part du d\u00e9veloppeur. Il permet de ne pas se soucier de la compatibilit\u00e9 avec les navigateurs et de coder comme si on \u00e9tait toujours sur la derni\u00e8re version du langage.</p> <p>Babel est int\u00e9gr\u00e9 dans Webpack et tout code bundl\u00e9 par celui-ci est automatiquement converti.</p> <p>Le logiciel se configure au moyen du fichier <code>babel.config.json</code> \u00e0 la racine du projet.</p>"},{"location":"howto/direnv/","title":"Direnv","text":"<p>Pour \u00e9viter d'avoir \u00e0 sourcer l'environnement \u00e0 chaque fois qu'on rentre dans le projet, il est possible d'utiliser l'utilitaire direnv.</p> <p>Comme pour beaucoup de choses, il faut commencer par l'installer :</p> LinuxmacOS Debian/UbuntuArch Linux <pre><code>sudo apt install direnv\n</code></pre> <pre><code>sudo pacman -S direnv\n</code></pre> <pre><code>brew install direnv\n</code></pre> <p>Puis on configure :</p> bashzshnu <pre><code>echo 'eval \"$(direnv hook bash)\"' &gt;&gt; ~/.bashrc\nexit # On red\u00e9marre le terminal\n</code></pre> <pre><code>echo 'eval \"$(direnv hook zsh)\"' &gt;&gt; ~/.zshrc\nexit # On red\u00e9marre le terminal\n</code></pre> <p>D\u00e9sol\u00e9, par <code>direnv hook</code> pour <code>nu</code></p> <p>Une fois le terminal red\u00e9marr\u00e9, dans le r\u00e9pertoire du projet : <pre><code>direnv allow .\n</code></pre></p> <p>Une fois que cette configuration a \u00e9t\u00e9 appliqu\u00e9e, aller dans le dossier du site applique automatiquement l'environnement virtuel. \u00c7a peut faire gagner pas mal de temps.</p> <p>Direnv est un utilitaire tr\u00e8s puissant et qui peut s'av\u00e9rer pratique dans bien des situations, n'h\u00e9sitez pas \u00e0 aller vous renseigner plus en d\u00e9tail sur celui-ci.</p>"},{"location":"howto/js-import-paths/","title":"Ajouter un chemin d'import javascript","text":"<p>Vous avez ajout\u00e9 une application et vous voulez y mettre du javascript ?</p> <p>Vous voulez importer depuis cette nouvelle application dans votre script g\u00e9r\u00e9 par webpack ?</p> <p>Eh bien il faut manuellement enregistrer dans node o\u00f9 les trouver et c'est tr\u00e8s simple.</p> <p>D'abord, il faut ajouter dans node via <code>package.json</code>:</p> <pre><code>{\n // ...\n \"imports\": {\n // ...\n \"#mon_app:*\": \"./mon_app/static/webpack/*\"\n }\n // ...\n}\n</code></pre> <p>Ensuite, pour faire fonctionne l'auto-compl\u00e9tion, il faut configurer <code>tsconfig.json</code>:</p> <pre><code>{\n \"compilerOptions\": {\n // ...\n \"paths\": {\n // ...\n \"#mon_app:*\": [\"./mon_app/static/webpack/*\"]\n }\n }\n}\n</code></pre> <p>Et c'est tout !</p> <p>Note</p> <p>Il se peut qu'il soit n\u00e9cessaire de red\u00e9marrer <code>./manage.py runserver</code> pour que les changements prennent effet.</p>"},{"location":"howto/logo/","title":"Ajouter un logo de promo","text":""},{"location":"howto/logo/#les-logos-de-promo","title":"Les logos de promo","text":"<p>Une fois par an, il est g\u00e9n\u00e9ralement n\u00e9cessaire d'ajouter le nouveau logo d'une promo. C'est un processus manuel.</p> <p>Automatisation</p> <p>Cr\u00e9er une interface automatique sur le site serait compliqu\u00e9 et long \u00e0 faire. Les erreurs de format des utilisateurs sont g\u00e9n\u00e9ralement nombreuses et on se retrouverais souvent avec des logos ratatin\u00e9s. Il est donc plus simple et plus fiable de faire cette op\u00e9ration manuellement, \u00e7a prend quelques minutes et on est certain de la qualit\u00e9 \u00e0 la fin.</p> <p>Les logos de promo sont \u00e0 manuellement ajouter dans le projet. Ils se situent dans le dossier <code>core/static/core/img/</code>.</p> <p>Leur format est le suivant:</p> <ul> <li>PNG \u00e0 fond transparent</li> <li>Taille 120x120 px</li> <li>Nom <code>promo_xx.png</code></li> </ul>"},{"location":"howto/migrations/","title":"G\u00e9rer les migrations","text":""},{"location":"howto/migrations/#quest-ce-quune-migration","title":"Qu'est-ce qu'une migration ?","text":"<p>Une migration est un fichier Python qui contient des instructions pour modifier la base de donn\u00e9es. Une base de donn\u00e9es \u00e9volue au cours du temps, et les migrations permettent de garder une trace de ces modifications.</p> <p>Gr\u00e2ce \u00e0 elles, on peut \u00e9galement apporter des modifications \u00e0 la base de donn\u00e9es sans \u00eatre oblig\u00e9es de la recr\u00e9er. On applique seulement les modifications n\u00e9cessaires.</p>"},{"location":"howto/migrations/#appliquer-les-migrations","title":"Appliquer les migrations","text":"<p>Pour appliquer les migrations, ex\u00e9cutez la commande suivante :</p> <pre><code>python ./manage.py migrate\n</code></pre> <p>Vous remarquerez peut-\u00eatre que cette commande a \u00e9t\u00e9 utilis\u00e9e dans la section Installation. En effet, en partant d'une base de donn\u00e9es vierge et en appliquant toutes les migrations, on arrive \u00e0 l'\u00e9tat actuel de la base de donn\u00e9es. Logique.</p> <p>Si vous utilisez cette commande sur une base de donn\u00e9es sur laquelle toutes les migrations ont \u00e9t\u00e9 appliqu\u00e9es, elle ne fera rien.</p> <p>Si vous utilisez cette commande sur une base de donn\u00e9es sur laquelle seule une partie des migrations ont \u00e9t\u00e9 appliqu\u00e9es, seules les migrations manquantes seront appliqu\u00e9es.</p>"},{"location":"howto/migrations/#creer-une-migration","title":"Cr\u00e9er une migration","text":"<p>Pour cr\u00e9er une migration, ex\u00e9cutez la commande suivante :</p> <pre><code>python ./manage.py makemigrations\n</code></pre> <p>Cette commande comparera automatiquement le contenu des classes de mod\u00e8les et le comparera avec les migrations d\u00e9j\u00e0 appliqu\u00e9es. A partir de cette comparaison, elle g\u00e9n\u00e9rera automatiquement une nouvelle migration.</p> <p>Note</p> <p>La commande <code>makemigrations</code> ne fait que g\u00e9n\u00e9rer les fichiers de migration. Elle ne modifie pas la base de donn\u00e9es. Pour appliquer la migration, n'oubliez pas la commande <code>migrate</code>.</p> <p>Un fichier de migration ressemble \u00e0 \u00e7a : </p> <pre><code>from django.db import migrations\n\n\nclass Migration(migrations.Migration):\n\n dependencies = [\n # liste des autres migrations \u00e0 appliquer avant celle-ci\n ]\n\n operations = [\n # liste des op\u00e9rations \u00e0 appliquer sur la db\n ]\n</code></pre> <p>Gr\u00e2ce \u00e0 la liste des d\u00e9pendances, Django sait dans quel ordre les migrations doivent \u00eatre appliqu\u00e9es. Gr\u00e2ce \u00e0 la liste des op\u00e9rations, Django sait quelles sont les op\u00e9rations \u00e0 appliquer durant cette migration.</p>"},{"location":"howto/migrations/#revenir-a-une-migration-anterieure","title":"Revenir \u00e0 une migration ant\u00e9rieure","text":"<p>Lorsque vous d\u00e9veloppez, il peut arriver que vous vouliez revenir \u00e0 une migration ant\u00e9rieure. Pour cela, il suffit d'appliquer la commande <code>migrate</code> en sp\u00e9cifiant le nom de la migration \u00e0 laquelle vous voulez revenir :</p> <pre><code>python ./manage.py migrate &lt;application&gt; &lt;num\u00e9ro de la migration&gt;\n</code></pre> <p>Par exemple, si vous voulez revenir \u00e0 la migration <code>0001_initial</code> de l'application <code>customer</code>, vous pouvez ex\u00e9cuter la commande suivante :</p> <pre><code>python ./manage.py migrate customer 0001\n</code></pre>"},{"location":"howto/migrations/#customiser-une-migration","title":"Customiser une migration","text":"<p>Il peut arriver que vous ayez besoin de modifier le fichier de migration g\u00e9n\u00e9r\u00e9 par Django. Par exemple, si vous voulez ex\u00e9cuter un script Python lors de l'application de la migration.</p> <p>Dans ce cas, vous pouvez trouver les fichiers de migration dans le dossier <code>migrations</code> de chaque application. Vous pouvez modifier le fichier Python correspondant \u00e0 la migration que vous voulez modifier.</p> <p>Ajoutez l'op\u00e9ration que vous voulez effectuer dans l'attribut <code>operations</code> de la classe <code>Migration</code>.</p> <p>Par exemple :</p> <pre><code>from django.db import migrations\n\ndef forwards_func(apps, schema_editor):\n print(\"Appplication de la migration\")\n\ndef reverse_func(apps, schema_editor):\n print(\"Annulation de la migration\")\n\nclass Migration(migrations.Migration):\n\n dependencies = []\n\n operations = [\n migrations.RunPython(forwards_func, reverse_func),\n ]\n</code></pre> <p>Script d'annulation de la migration</p> <p>Lorsque vous incluez un script Python dans une migration, incluez toujours aussi un script d'annulation, sinon Django ne pourra pas annuler la migration apr\u00e8s son application.</p> <p>Vous ne pourrez donc pas revenir \u00e0 un \u00e9tat ant\u00e9rieur de la db, \u00e0 moins de la recr\u00e9er de z\u00e9ro.</p>"},{"location":"howto/migrations/#fusionner-des-migrations","title":"Fusionner des migrations","text":"<p>Quand on travaille sur une fonctionnalit\u00e9 qui n\u00e9cessite une modification de la base de donn\u00e9es, les fichiers de migration sont comme toute chose : on peut se rendre compte que les changements apport\u00e9s pourraient \u00eatre meilleurs.</p> <p>Par exemple, supposons que nous voulons cr\u00e9er un mod\u00e8le repr\u00e9sentant une UE suivie par un \u00e9tudiant (ne demandez pas pourquoi on voudrait faire \u00e7a, c'est juste pour l'exemple). Un tel mod\u00e8le aurait besoin des informations suivantes :</p> <ul> <li>l'utilisateur</li> <li>le code de l'UE</li> </ul> <p>On \u00e9crirait donc, dans l'application <code>pedagogy</code> : <pre><code>from django.db import models\nfrom core.models import User\n\nclass UserUe(models.Model):\n user = models.ForeignKey(User, on_delete=models.CASCADE)\n ue = models.CharField(max_length=10)\n</code></pre></p> <p>Et nous aurions le fichier de migration suivant : <pre><code>from django.db import migrations, models\nfrom django.conf import settings\n\nclass Migration(migrations.Migration):\n dependencies = [\n migrations.swappable_dependency(settings.AUTH_USER_MODEL),\n (\"pedagogy\", \"0003_alter_uv_language\"),\n ]\n\n operations = [\n migrations.CreateModel(\n name=\"UserUe\",\n fields=[\n (\n \"id\",\n models.AutoField(\n auto_created=True,\n primary_key=True,\n serialize=False,\n verbose_name=\"ID\",\n ),\n ),\n (\"ue\", models.CharField(max_length=10)),\n (\n \"user\",\n models.ForeignKey(\n on_delete=models.deletion.CASCADE,\n to=settings.AUTH_USER_MODEL,\n ),\n ),\n ],\n ),\n ]\n</code></pre></p> <p>On finit son travail, on soumet la PR. Mais l\u00e0, quelqu'un fait remarquer qu'il existe d\u00e9j\u00e0 un mod\u00e8le pour repr\u00e9senter une UE. On modifie donc le mod\u00e8le :</p> <pre><code>from django.db import models\nfrom core.models import User\nfrom pedagogy.models import UV\n\nclass UserUe(models.Model):\n user = models.ForeignKey(User, on_delete=models.CASCADE)\n ue = models.ForeignKey(UV, on_delete=models.CASCADE)\n</code></pre> <p>On refait la commande <code>makemigrations</code> et on obtient : <pre><code>from django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n dependencies = [\n (\"pedagogy\", \"0004_userue\"),\n ]\n\n operations = [\n migrations.AlterField(\n model_name=\"userue\",\n name=\"ue\",\n field=models.ForeignKey(\n on_delete=models.deletion.CASCADE, to=\"pedagogy.uv\"\n ),\n ),\n ]\n</code></pre></p> <p>Sauf que maintenant, nous avons deux fichiers de migration, alors qu'en r\u00e9alit\u00e9, on ne souhaite faire qu'une seule migration, une fois qu'on aura exp\u00e9di\u00e9 le code en prod. Certes, \u00e7a fonctionnerait d'appliquer les deux, mais \u00e7a pose un probl\u00e8me d'encombrement.</p> <p>Plus il y a de fichiers de migrations, plus il y a de migrations \u00e0 r\u00e9soudre au moment de l'installation du projet chez quelqu'un, plus c'est emb\u00eatant \u00e0 g\u00e9rer et plus Django prendra du temps \u00e0 r\u00e9soudre les migrations.</p> <p>C'est pourquoi il est bon de respecter le principe : une PR = un fichier de migration maximum par application.</p> <p>Nous voulons donc fusionner les deux, pour n'en garder qu'une. Pour \u00e7a, deux mani\u00e8res de proc\u00e9der :</p> <ul> <li>le faire \u00e0 la main</li> <li>utiliser la commande squashmigrations</li> </ul> <p>Pour la m\u00e9thode manuelle, on ne pourrait pas vous dire exhaustivement comment faire. Mais ne vous inqui\u00e9tez pas, ce n'est pas tr\u00e8s dur. Regardez bien quelles sont les instructions utilis\u00e9es par django pour les op\u00e9rations de migrations, et avec un peu d'astuce et quelques copier-coller, vous vous en sortirez comme des chefs.</p> <p>Pour la m\u00e9thode <code>squashmigrations</code>, ex\u00e9cutez la commande </p> <pre><code>python ./manage.py squasmigrations &lt;app&gt; &lt;migration de d\u00e9but (incluse)&gt; &lt;migration de fin (incluse)&gt; \n</code></pre> <p>Par exemple, dans notre cas, \u00e7a donnera :</p> <pre><code>python ./manage.py squasmigrations pedagogy 0004 0005 \n</code></pre> <p>La commande vous donnera ceci : <pre><code>from django.conf import settings\nfrom django.db import migrations, models\n\nclass Migration(migrations.Migration):\n replaces = [(\"pedagogy\", \"0004_userue\"), (\"pedagogy\", \"0005_alter_userue_ue\")]\n\n dependencies = [\n (\"pedagogy\", \"0003_alter_uv_language\"),\n migrations.swappable_dependency(settings.AUTH_USER_MODEL),\n ]\n\n operations = [\n migrations.CreateModel(\n name=\"UserUe\",\n fields=[\n (\n \"id\",\n models.AutoField(\n auto_created=True,\n primary_key=True,\n serialize=False,\n verbose_name=\"ID\",\n ),\n ),\n (\n \"ue\",\n models.ForeignKey(\n on_delete=models.deletion.CASCADE, to=\"pedagogy.uv\"\n ),\n ),\n (\n \"user\",\n models.ForeignKey(\n on_delete=models.deletion.CASCADE,\n to=settings.AUTH_USER_MODEL,\n ),\n ),\n ],\n ),\n ]\n</code></pre></p> <p>Vous pouvez alors supprimer les deux autres fichiers.</p> <p>Vous remarquerez peut-\u00eatre la pr\u00e9sence de la ligne suivante : <pre><code>replaces = [(\"pedagogy\", \"0004_userue\"), (\"pedagogy\", \"0005_alter_userue_ue\")]\n</code></pre></p> <p>Cela sert \u00e0 dire que cette migration doit \u00eatre appliqu\u00e9e \u00e0 la place des deux autres. Une fois que vous aurez supprim\u00e9 les deux fichiers, supprimez \u00e9galement cette ligne.</p> <p>Warning</p> <p>Django sait quelles migrations ont \u00e9t\u00e9 appliqu\u00e9es, en les stockant dans une table de la db. Si une migration est enregistr\u00e9e en db, sans que le fichier de migration correspondant existe, la commande <code>migrate</code> \u00e9choue.</p> <p>Quand vous faites un <code>squashmigrations</code>, pensez donc \u00e0 appliquer la commande <code>migrate</code> juste apr\u00e8s (mais avant la suppression des anciens fichiers), pour que Django supprime de la base de donn\u00e9es les migrations devenues inutiles.</p>"},{"location":"howto/prod/","title":"Configurer pour la production","text":""},{"location":"howto/prod/#configurer-sentry","title":"Configurer Sentry","text":"<p>Pour connecter l'application \u00e0 une instance de sentry (ex: https://sentry.io), il est n\u00e9cessaire de configurer la variable <code>SENTRY_DSN</code> dans le fichier <code>settings_custom.py</code>. Cette variable est compos\u00e9e d'un lien complet vers votre projet sentry.</p>"},{"location":"howto/prod/#recuperer-les-statiques","title":"R\u00e9cup\u00e9rer les statiques","text":"<p>Nous utilisons du SCSS dans le projet. En environnement de d\u00e9veloppement (<code>DEBUG=True</code>), le SCSS est compil\u00e9 \u00e0 chaque fois que le fichier est demand\u00e9. Pour la production, le projet consid\u00e8re que chacun des fichiers est d\u00e9j\u00e0 compil\u00e9. C'est pourquoi le SCSS est automatiquement compil\u00e9 lors de la r\u00e9cup\u00e9ration des fichiers statiques. Les fichiers JS sont \u00e9galement automatiquement minifi\u00e9s.</p> <p>Il peut \u00eatre judicieux de supprimer les anciens fichiers statiques avant de collecter les nouveaux. Pour \u00e7a, ajoutez le flag <code>--clear</code> \u00e0 la commande <code>collectstatic</code> :</p> <pre><code>python ./manage.py collectstatic --clear\n</code></pre> <p>Tip</p> <p>Le dossier o\u00f9 seront enregistr\u00e9s ces fichiers statiques peut \u00eatre chang\u00e9 en modifiant la variable <code>STATIC_ROOT</code> dans les param\u00e8tres.</p>"},{"location":"howto/querysets/","title":"L'ORM de Django","text":"<p>L'ORM de Django est puissant, tr\u00e8s puissant, non par parce qu'il est performant (apr\u00e8s tout, ce n'est qu'une interface, le gros du boulot, c'est la db qui le fait), mais parce qu'il permet d'\u00e9crire de mani\u00e8re relativement simple un grand panel de requ\u00eates.</p> <p>De mani\u00e8re g\u00e9n\u00e9rale, puisqu'un ORM est un syst\u00e8me consistant \u00e0 manipuler avec un code orient\u00e9-objet une db relationnelle (c'est-\u00e0-dire deux paradigmes qui ne fonctionnent absolument pas pareil), on rencontre un des deux probl\u00e8mes suivants :</p> <ul> <li>soit l'ORM n'offre pas assez d'abstraction, auquel cas, quand on veut faire des requ\u00eates plus complexes qu'un <code>select</code> avec un <code>where</code>, on s'emm\u00eale les pinceaux et on se dit que \u00e7a aurait \u00e9t\u00e9 plus simple de le faire directement en SQL.</li> <li>soit l'ORM offre trop d'abstraction, auquel cas, on a tendance \u00e0 ne pas pr\u00eater assez attention aux requ\u00eates envoy\u00e9es en base de donn\u00e9es et on finit par se rendre compte que les temps d'attente explosent parce qu'on envoie trop de requ\u00eates.</li> </ul> <p>Django est dans ce deuxi\u00e8me cas.</p> <p>C'est pourquoi nous ne parlerons pas ici de son fonctionnement exact ni de toutes les fonctions que l'on peut utiliser (la doc officielle fait d\u00e9j\u00e0 \u00e7a mieux que nous), mais plut\u00f4t des pi\u00e8ges courants et des astuces pour les \u00e9viter.</p>"},{"location":"howto/querysets/#les-n1-queries","title":"Les <code>N+1 queries</code>","text":""},{"location":"howto/querysets/#le-probleme","title":"Le probl\u00e8me","text":"<p>Normalement, quand on veut r\u00e9cup\u00e9rer une liste, on fait une requ\u00eate et c'est fini. Mais des fois, \u00e7a n'est pas si simple. Par exemple, supposons que nous voulons r\u00e9cup\u00e9rer les 100 utilisateurs les plus riches, avec leurs informations client :</p> <pre><code>from core.models import User\n\nfor user in User.objects.order_by(\"-customer__amount\")[:100]:\n print(user.customer.amount)\n</code></pre> <p>Combien de requ\u00eates le bout de code suivant effectue-t-il ? 101. En deux pauvres lignes de code, nous avons demand\u00e9 \u00e0 la base de donn\u00e9es d'effectuer 101 requ\u00eates. Une requ\u00eate toute seule n'est d\u00e9j\u00e0 une op\u00e9ration anodine, alors je vous laisse imaginer ce que \u00e7a donne pour 101.</p> <p>Si vous ne comprenez pourquoi ce nombre, c'est tr\u00e8s simple :</p> <ul> <li>Une requ\u00eate pour s\u00e9lectionner nos 100 utilisateurs</li> <li>Une requ\u00eate suppl\u00e9mentaire pour r\u00e9cup\u00e9rer les informations client de chaque utilisateur, soit 100 requ\u00eates.</li> </ul> <p>En effet, les informations client sont stock\u00e9es dans une autre table, mais le fait d'\u00e9tablir un lien de clef \u00e9trang\u00e8re permet de manipuler <code>customer</code> comme si c'\u00e9tait un membre \u00e0 part enti\u00e8re de <code>User</code>.</p> <p>Il est \u00e0 noter cependant, que Django n'effectue une requ\u00eate que pour le premier acc\u00e8s \u00e0 un membre d'une relation de clef \u00e9trang\u00e8re. Toutes les fois suivantes, l'objet est d\u00e9j\u00e0 l\u00e0, et django le r\u00e9cup\u00e8re :</p> <pre><code>from core.models import User\n\n# l'utilisateur le plus riche\nuser = User.objects.order_by(\"-customer__amount\").first() # &lt;-- requ\u00eate db\nprint(user.customer.amount) # &lt;-- requ\u00eate db\nprint(user.customer.account_id) # on a d\u00e9j\u00e0 r\u00e9cup\u00e9r\u00e9 `customer`, donc pas de requ\u00eate\n</code></pre> <p>Ce n'est donc pas gravissime si vous faites cette erreur quand vous manipulez un seul objet. En revanche, quand vous en manipulez plusieurs, il faut r\u00e9gler le probl\u00e8me. Pour \u00e7a, il y a plusieurs m\u00e9thodes, en fonction de votre cas.</p>"},{"location":"howto/querysets/#select_related","title":"<code>select_related</code>","text":"<p>La m\u00e9thode la plus basique consiste \u00e0 annoter le queryset, avec la m\u00e9thode <code>select_related()</code>. En faisant \u00e7a, Django fera une jointure sur l'autre table et demandera des informations en plus \u00e0 la db lors de la requ\u00eate.</p> <p>De la sorte, lorsque vous appellerez le membre reli\u00e9, les informations seront d\u00e9j\u00e0 l\u00e0.</p> <pre><code>from core.models import User\n\nrichest = User.objects.order_by(\"-customer__amount\")\nfor user in richest.select_related(\"customer\")[:100]:\n print(user.customer)\n</code></pre> <p>Le code ci-dessus effectue une seule requ\u00eate. Chaque fois qu'on veut acc\u00e9der \u00e0 <code>customer</code>, c'est bon, \u00e7a a d\u00e9j\u00e0 \u00e9t\u00e9 r\u00e9cup\u00e9r\u00e9 \u00e0 travers le <code>select_related</code>.</p>"},{"location":"howto/querysets/#prefetch_related","title":"<code>prefetch_related</code>","text":"<p>Maintenant, un cas plus compliqu\u00e9. Supposons que vous ne vouliez pas r\u00e9cup\u00e9rer des informations reli\u00e9es par une relation One-to-One, mais par une relation One-to-Many.</p> <p>Par exemple, un utilisateur a un seul compte client, mais il peut avoir plusieurs cotisations \u00e0 son actif. Et dans ces cas-l\u00e0, <code>annotate</code> ne marche plus. En effet, s'il peut exister plusieurs cotisations, comment savoir laquelle on veut ?</p> <p>Il faut alors utiliser un <code>prefetch_related</code>. C'est un m\u00e9canisme un peu diff\u00e9rent : au lieu de faire une jointure et d'ajouter les informations voulues dans la m\u00eame requ\u00eate, Django va effectuer une deuxi\u00e8me requ\u00eate pour r\u00e9cup\u00e9rer les \u00e9l\u00e9ments de l'autre table, puis, \u00e0 partir de ces \u00e9l\u00e9ments, peupler la relation de son c\u00f4t\u00e9.</p> <p>C'est un m\u00e9canisme qui peut \u00eatre un peu co\u00fbteux en m\u00e9moire et qui demande une deuxi\u00e8me requ\u00eate, mais qui reste quand m\u00eame largement pr\u00e9f\u00e9rable \u00e0 faire N requ\u00eates en plus.</p> <pre><code>from core.models import User\n\nfor user in User.objects.prefetch_related(\"subscriptions\")[:100]:\n # c'est bon, la m\u00e9thode prefetch a r\u00e9cup\u00e9r\u00e9 en avance les `subscriptions`\n print(user.subscriptions.all())\n</code></pre> <p>Danger</p> <p>La m\u00e9thode <code>prefetch_related</code> ne marche que si vous utilisez la m\u00e9thode <code>all()</code> pour acc\u00e9der au membre. Si vous utilisez une autre m\u00e9thode (comme <code>filter</code> ou <code>annotate</code>), alors Django effectuera une nouvelle requ\u00eate, et vous retomberez dans le probl\u00e8me initial.</p> <pre><code>from core.models import User\nfrom django.db.models import Count\n\nfor user in User.objects.prefetch_related(\"subscriptions\")[:100]:\n # Le prefetch_related ne marche plus !\n print(user.subscriptions.annotate(count=Count(\"*\")))\n</code></pre>"},{"location":"howto/querysets/#recuperer-ce-dont-vous-avez-besoin","title":"R\u00e9cup\u00e9rer ce dont vous avez besoin","text":"<p>Des fois (souvent, m\u00eame), penser explicitement \u00e0 la jointure est le meilleur choix.</p> <p>En effet, vous remarquerez que dans tous les exemples pr\u00e9c\u00e9dents, nous n'utilisions qu'une partie des informations (par exemple, nous ne r\u00e9cup\u00e9rions que la somme d'argent sur les comptes, et \u00e9ventuellement le num\u00e9ro de compte).</p> <p>Nous pouvons utiliser la m\u00e9thode <code>annotate</code> pour sp\u00e9cifier explicitement les donn\u00e9es que l'on veut joindre \u00e0 notre requ\u00eate.</p> <p>Quand nous voulions r\u00e9cup\u00e9rer les informations utilisateur, nous aurions tout aussi bien pu \u00e9crire :</p> <pre><code>from core.models import User\nfrom django.db.models import F\n\nrichest = User.objects.order_by(\"-customer__amount\")\nfor user in richest.annotate(amount=F(\"customer__amount\"))[:100]:\n print(user.amount)\n</code></pre> <p>On aurait m\u00eame pu r\u00e9organiser \u00e7a : <pre><code>from core.models import User\nfrom django.db.models import F\n\nrichest = User.objects.annotate(amount=F(\"customer__amount\")).order_by(\"-amount\")\nfor user in richest[:100]:\n print(user.amount)\n</code></pre></p> <p>\u00c7a peut sembler moins bien qu'un <code>select_related</code>, comme \u00e7a. Des fois, c'est en effet moins bien, et des fois c'est mieux. La comparaison est plus \u00e9vidente avec le <code>prefetch_related</code>.</p> <p>En effet, quand nous voulions r\u00e9cup\u00e9rer le nombre de cotisations des utilisateurs, le <code>prefetch_related</code> ne marchait plus. Pourtant, nous voulions r\u00e9cup\u00e9rer une seule information.</p> <p>Il aurait donc \u00e9t\u00e9 suffisant d'\u00e9crire : <pre><code>from core.models import User\nfrom django.db.models import Count\n\nfor user in User.objects.annotate(nb_subscriptions=Count(\"subscriptions\"))[:100]:\n # Et l\u00e0 \u00e7a marche, en une seule requ\u00eate.\n print(user.nb_subscriptions)\n</code></pre></p> <p>Faire une jointure, c'est normal en SQL. Et pourtant avec Django on les oublie trop facilement. Posez-vous toujours la question des donn\u00e9es que vous pourriez avoir besoin d'annoter, et vous \u00e9viterez beaucoup d'ennuis.</p>"},{"location":"howto/querysets/#les-aggregations-manquees","title":"Les aggr\u00e9gations manqu\u00e9es","text":"<p>Il arrive souvent que l'on veuille une information qui porte sur un ensemble d'objets de notre db.</p> <p>Imaginons par exemple que nous voulons connaitre la somme totale des ventes faites \u00e0 un comptoir.</p> <p>Nous avons tous suivi nos cours de programmation, nous \u00e9crivons donc instinctivement :</p> <pre><code>from counter.models import Counter\n\nfoyer = Counter.objects.get(name=\"Foyer\")\ntotal_amount = sum(\n sale.amount * sale.unit_price\n for sale in foyer.sellings.all()\n)\n</code></pre> <p>On pourrait penser qu'il n'y a pas de probl\u00e8me. Apr\u00e8s tout, on ne fait qu'une seule requ\u00eate. Eh bien si, il y a un probl\u00e8me : on fait beaucoup de choses en trop.</p> <p>Concr\u00e8tement, on demande \u00e0 la base de donn\u00e9es de renvoyer toutes les informations, ce qui rallonge inutilement la dur\u00e9e de l'\u00e9change entre le serveur et la db, puis on perd du temps \u00e0 convertir ces informations en objets Python (op\u00e9ration qui a un co\u00fbt \u00e9galement), et enfin on reperd du temps \u00e0 calculer en Python quelque chose que la db aurait pu calculer \u00e0 notre plus bien plus vite.</p> <p>Nous aurions d\u00fb aggr\u00e9ger la requ\u00eate, avec la m\u00e9thode <code>aggregate</code> :</p> <pre><code>from counter.models import Counter\nfrom django.db.models import Sum, F\n\nfoyer = Counter.objects.get(name=\"Foyer\")\ntotal_amount = (\n foyer.sellings.aggregate(amount=Sum(F(\"amount\") * F(\"unit_price\"), default=0))\n)[\"amount__sum\"]\n</code></pre> <p>En effectuant cette requ\u00eate, la base de donn\u00e9es nous renverra exactement l'information dont nous avons besoin. Et de notre c\u00f4t\u00e9, nous n'aurons pas \u00e0 faire de traitement en plus.</p>"},{"location":"howto/querysets/#benchmark","title":"Benchmark","text":""},{"location":"howto/querysets/#ce-quil-faut-mesurer","title":"Ce qu'il faut mesurer","text":"<p>Quand on parle d'interaction avec une base de donn\u00e9es, la question de la performance est cruciale. Et quand on parle de performance, on en vient forc\u00e9ment \u00e0 parler d'optimisation.</p> <p>Or, pour optimiser, il faut savoir quoi optimiser. C'est-\u00e0-dire qu'il nous faut un benchmark pour \u00e9tudier les performances r\u00e9elles de notre code. En ce qui concerne des requ\u00eates \u00e0 une base de donn\u00e9es, deux aspects sont \u00e9tudiables :</p> <ul> <li>le nombre de requ\u00eates qu'une vue ou une fonction effectue pour son fonctionnement.</li> <li>le temps d'ex\u00e9cution individuel des requ\u00eates les plus longues.</li> </ul> <p>Le premier aspect est celui qui nous int\u00e9resse le plus, puisqu'il est reli\u00e9 au probl\u00e8me le plus fr\u00e9quent et le plus facile \u00e0 mesurer. Le second aspect, au contraire, est bien moins fr\u00e9quent (dans 99% des cas, une requ\u00eate complexe prendra moins de temps que deux requ\u00eates, m\u00eame simples) et bien plus dur \u00e0 mesurer (il faut r\u00e9ussir \u00e0 faire des mesures fiables, dans un environnement proche de celui de la prod, avec les donn\u00e9es de la prod).</p> <p>Nous consid\u00e9rerons donc que dans la quasi-totalit\u00e9 des cas, le probl\u00e8me vient du nombre de requ\u00eates, pas du temps d'ex\u00e9cution d'une requ\u00eate en particulier. Partez du principe que moins vous faites de requ\u00eates, mieux c'est, sans pr\u00eater attention au temps d'ex\u00e9cution des requ\u00eates.</p> <p>Pour quantifier de mani\u00e8re fiables les requ\u00eates effectu\u00e9es, il y a quelques outils.</p>"},{"location":"howto/querysets/#django-debug-toolbar","title":"<code>django-debug-toolbar</code>","text":"<p>La <code>django-debug-toolbar</code> est une interface disponible sur toutes les pages quand vous \u00eates en mode debug. Elle s'affiche \u00e0 droite et vous permet de voir toutes sortes d'informations, parmi lesquelles le nombre de requ\u00eates effectu\u00e9es.</p> <p>Cette interface est tr\u00e8s pratique, puisqu'elle va plus loin que simplement compter les requ\u00eates, elle vous donne \u00e9galement le SQL qui a \u00e9t\u00e9 utilis\u00e9, l'endroit du code, avec fichier et num\u00e9ro de ligne, o\u00f9 cette requ\u00eate a \u00e9t\u00e9 faite et, encore mieux, elle vous indique quelles requ\u00eates semblent dupliqu\u00e9es.</p> <p>Quand <code>django-debug-toolbar</code> vous indique qu'une requ\u00eate a \u00e9t\u00e9 dupliqu\u00e9e quatre fois, cinq fois, ou m\u00eame deux cent fois (le chiffre peut sembler \u00e9norme, mais c'est d\u00e9j\u00e0 arriv\u00e9), vous pouvez \u00eatre s\u00fbr qu'il y a l\u00e0 quelque chose \u00e0 optimiser.</p> <p>Warning</p> <p>Le widget de <code>django-debug-toolbar</code> ne s'affiche que sur les pages html. Si vous voulez \u00e9tudier autre chose, comme une simple fonction, ou bien comme une vue retournant du JSON, vous n'aurez donc pas <code>django-debug-toolbar</code>.</p>"},{"location":"howto/querysets/#connectionqueries","title":"<code>connection.queries</code>","text":"<p>Quand vous voulez examiner les requ\u00eates d'un bout de code en particulier, Django met \u00e0 disposition un m\u00e9canisme permettant d'examiner toutes les requ\u00eates qui sont faites : <code>connection.queries</code></p> <p>C'est un historique de toutes les requ\u00eates effectu\u00e9es, qui est assez simple \u00e0 utiliser :</p> <pre><code>from django.db import connection\nfrom core.models import User\n\nprint(len(connection.queries)) # 0\n\nnb_users = User.objects.count()\n\nprint(len(connection.queries)) # 1\nprint(connection.queries) # affiche toutes les requ\u00eates effectu\u00e9es\n</code></pre>"},{"location":"howto/querysets/#assertnumqueries","title":"<code>assertNumQueries</code>","text":"<p>Quand on a mis en place une fonctionnalit\u00e9, ou qu'on en a am\u00e9lior\u00e9 les performances, on veut absolument \u00e9viter la r\u00e9gression.</p> <p>Or, une r\u00e9gression ne se manifeste pas forc\u00e9ment dans l'apparition d'un bug : \u00e7a peut aussi \u00eatre une augmentation du temps d'ex\u00e9cution, possiblement caus\u00e9 par une augmentation du nombre de requ\u00eates.</p> <p>C'est pour \u00e7a que django met \u00e0 disposition un moyen de tester automatiquement le nombre de requ\u00eates : <code>assertNumQueries</code>.</p> <p>Il s'agit d'un gestionnaire de contexte accessible dans les tests, qui teste le nombre de requ\u00eates effectu\u00e9es en son sein.</p> <p>Par exemple :</p> <pre><code>from django.test import TestCase\nfrom django.shortcuts import reverse\n\n\nclass FooTest(TestCase):\n def test_nb_queries(self):\n \"\"\"Test that the number of db queries is stable.\"\"\"\n with self.assertNumQueries(6):\n self.client.get(reverse(\"foo:bar\"))\n</code></pre> <p>Si l'ex\u00e9cution de la route n\u00e9cessite plus ou moins de six requ\u00eates, alors le test \u00e9choue. S'il y a eu moins que le nombre de requ\u00eate attendu, alors tant mieux, modifiez le test pour coller au nouveau nombre (sous r\u00e9serve que tous les autres tests passent, bien s\u00fbr). Si par contre il y a eu plus, alors d\u00e9sol\u00e9, vous avez sans doute introduit une r\u00e9gression.</p>"},{"location":"howto/statics/","title":"G\u00e9rer les statics","text":""},{"location":"howto/statics/#cest-quoi-les-fichiers-statics","title":"C'est quoi les fichiers statics ?","text":"<p>Les fichiers statics sont tous les fichiers qui ne sont pas g\u00e9n\u00e9r\u00e9s par le backend Django et qui sont t\u00e9l\u00e9charg\u00e9s par le navigateur. Cela comprend les fichiers css, javascript, images et autre.</p> <p>La documentation officielle est tr\u00e8s compr\u00e9hensive.</p> <p>Pour faire court, dans chaque module d'application il existe un dossier <code>static</code> o\u00f9 mettre tous ces fichiers. Django se d\u00e9brouille ensuite pour aller chercher ce qu'il faut \u00e0 l'int\u00e9rieur.</p> <p>Pour acc\u00e9der \u00e0 un fichier static dans un template Jinja il suffit d'utiliser la fonction <code>static</code>.</p> <pre><code> {# Exemple pour ajouter sith/core/static/core/base.css #}\n &lt;link rel=\"stylesheet\" href=\"{{ static('core/base.css') }}\"&gt;\n</code></pre>"},{"location":"howto/statics/#lintegration-des-scss","title":"L'int\u00e9gration des scss","text":"<p>Les scss sont \u00e0 mettre dans le dossier static comme le reste. Il n'y a aucune diff\u00e9rence avec le reste pour les inclure, le syst\u00e8me se d\u00e9brouille automatiquement pour les transformer en <code>.css</code></p> <pre><code> {# Exemple pour ajouter sith/core/static/core/base.scss #}\n &lt;link rel=\"stylesheet\" href=\"{{ static('core/style.scss') }}\"&gt;\n</code></pre>"},{"location":"howto/statics/#lintegration-webpack","title":"L'int\u00e9gration webpack","text":"<p>Webpack est int\u00e9gr\u00e9 un peu diff\u00e9rement. Le principe est tr\u00e8s similaire mais les fichiers sont \u00e0 mettre dans un dossier <code>static/webpack</code> de l'application \u00e0 la place.</p> <p>Pour acc\u00e9der au fichier, il faut utiliser <code>static</code> comme pour le reste mais en ajouter <code>webpack/</code> comme prefix.</p> <pre><code> {# Example pour ajouter sith/core/webpack/alpine-index.js #}\n &lt;script src=\"{{ static('webpack/alpine-index.js') }}\" defer&gt;&lt;/script&gt;\n &lt;script src=\"{{ static('webpack/other-index.ts') }}\" defer&gt;&lt;/script&gt;\n</code></pre> <p>Note</p> <p>Seuls les fichiers se terminant par <code>index.js</code> sont export\u00e9s par webpack. Les autres fichiers sont disponibles \u00e0 l'import dans le JavaScript comme si ils \u00e9taient tous au m\u00eame niveau.</p>"},{"location":"howto/statics/#les-imports-au-sein-des-fichiers-de-webpack","title":"Les imports au sein des fichiers de webpack","text":"<p>Pour importer au sein de webpack, il faut pr\u00e9fixer ses imports de <code>#app:</code>.</p> <p>Exemple:</p> <pre><code>import { paginated } from \"#core:utils/api\";\n</code></pre>"},{"location":"howto/statics/#comment-ca-fonctionne-le-post-processing","title":"Comment \u00e7a fonctionne le post processing ?","text":"<p>Le post processing est g\u00e9r\u00e9 par le module <code>staticfiles</code>. Les fichiers sont compil\u00e9s \u00e0 la vol\u00e9e en mode d\u00e9veloppement.</p> <p>Pour la production, ils sont compil\u00e9s uniquement lors du <code>./manage.py collectstatic</code>. Les fichiers g\u00e9n\u00e9r\u00e9s sont ajout\u00e9s dans le dossier <code>staticfiles/generated</code>. Celui-ci est ensuite enregistr\u00e9 comme dossier suppl\u00e9mentaire \u00e0 collecter dans Django.</p>"},{"location":"howto/subscriptions/","title":"Ajouter une cotisation","text":""},{"location":"howto/subscriptions/#ajouter-une-nouvelle-cotisation","title":"Ajouter une nouvelle cotisation","text":"<p>Il peut arriver que le type de cotisation propos\u00e9 varie en prix et en dur\u00e9e. Ces param\u00e8tres sont configurables directement dans les param\u00e8tres du projet.</p> <p>Pour modifier les cotisations disponibles, tout se g\u00e8re dans la configuration avec la variable <code>SITH_SUBSCRIPTIONS</code>.</p> <p>Par exemple, si nous voulons ajouter une nouvelle cotisation d'un mois, voici ce que nous ajouterons :</p> settings.py<pre><code>from django.utils.translation import gettext_lazy as _\n\nSITH_SUBSCRIPTIONS = {\n # Voici un \u00e9chantillon de la v\u00e9ritable configuration \u00e0 l'heure de l'\u00e9criture.\n # Celle-ci est donn\u00e9e \u00e0 titre d'exemple pour mieux comprendre comment cela fonctionne.\n \"un-semestre\": {\"name\": _(\"One semester\"), \"price\": 15, \"duration\": 1},\n \"deux-semestres\": {\"name\": _(\"Two semesters\"), \"price\": 28, \"duration\": 2},\n \"cursus-tronc-commun\": {\n \"name\": _(\"Common core cursus\"),\n \"price\": 45,\n \"duration\": 4,\n },\n \"cursus-branche\": {\"name\": _(\"Branch cursus\"), \"price\": 45, \"duration\": 6},\n \"cursus-alternant\": {\"name\": _(\"Alternating cursus\"), \"price\": 30, \"duration\": 6},\n \"membre-honoraire\": {\"name\": _(\"Honorary member\"), \"price\": 0, \"duration\": 666},\n \"un-jour\": {\"name\": _(\"One day\"), \"price\": 0, \"duration\": 0.00555333},\n\n # On rajoute ici notre cotisation\n # Elle se nomme \"Un mois\"\n # Co\u00fbte 6\u20ac\n # Dure 1 mois (on raisonne en semestre, ici, c'est 1/6 de semestre)\n \"un-mois\": {\"name\": _(\"One month\"), \"price\": 6, \"duration\": 0.166}\n}\n</code></pre> <p>Une fois ceci fait, il faut cr\u00e9er une nouvelle migration :</p> <pre><code>python ./manage.py makemigrations subscription\npython ./manage.py migrate\n</code></pre> <p>N'oubliez pas non plus les traductions (cf. ici)</p>"},{"location":"howto/terminal/","title":"Terminal","text":""},{"location":"howto/terminal/#quel-terminal-utiliser","title":"Quel terminal utiliser ?","text":"<p>Quel que soit votre configuration, si vous avez r\u00e9ussi \u00e0 installer le projet, il y a de fortes chances que bash existe sur votre ordinateur. Certains d'entre vous utilisent peut-\u00eatre un autre shell, comme <code>zsh</code>.</p> <p>En effet, <code>bash</code> est bien, il fait le taff ; mais son ergonomie finit par montrer ses limites. C'est pourquoi il existe des shells plus avanc\u00e9s, qui peuvent am\u00e9liorer l'ergonomie, la compl\u00e9tion des commandes, et l'apparence. C'est le cas de <code>zsh</code>. Certains vont m\u00eame plus loin et refont carr\u00e9ment la syntaxe. C'est le cas de <code>nu</code>.</p> <p>Pour choisir un terminal, demandez-vous juste quel est votre usage du terminal :</p> <ul> <li>Si c'est juste quelques commandes basiques et que vous ne voulez pas vous emb\u00eater \u00e0 changer votre configuration, <code>bash</code> convient parfaitement.</li> <li>Si vous commencez \u00e0 utilisez le terminal de mani\u00e8re plus intensive, \u00e0 varier les commandes que vous utilisez et/ou que vous voulez customiser un peu votre exp\u00e9rience, <code>zsh</code> est parfait pour vous.</li> <li>Si vous aimez la programmation fonctionnelle, que vous adorez les pipes et que vous voulez faire des scripts complets mais qui restent lisibles, <code>nu</code> vous plaira \u00e0 coup s\u00fbr.</li> </ul> <p>Note</p> <p>Ce ne sont que des suggestions. Le meilleur choix restera toujours celui avec lequel vous \u00eates le plus confortable.</p>"},{"location":"howto/terminal/#commandes-utiles","title":"Commandes utiles","text":""},{"location":"howto/terminal/#compter-le-nombre-de-lignes-du-projet","title":"Compter le nombre de lignes du projet","text":"bash/zshnu <p><pre><code>sudo apt install cloc\ncloc --exclude-dir=doc,env .\n</code></pre> Ok, c'est de la triche, on installe un package externe. Mais bon, \u00e7a marche, et l'\u00e9quivalent pur bash serait carr\u00e9ment plus moche.</p> <p>Nombre de lignes, group\u00e9 par fichier : <pre><code>ls **/*.py | insert linecount { get name | open | lines | length }\n</code></pre></p> <p>Nombre de lignes total : <pre><code>ls **/*.py | insert linecount { get name | open | lines | length } | math sum\n</code></pre></p> <p>Vous pouvez aussi exlure les lignes vides et les les lignes de commentaire : <pre><code>ls **/*.py |\ninsert linecount {\n get name |\n open |\n lines |\n each { str trim } |\n filter { |l| not ($l | str starts-with \"#\") } | # commentaires\n filter { |l| ($l | str length) &gt; 0 } | # lignes vides\n length\n} |\nmath sum\n</code></pre></p>"},{"location":"howto/translation/","title":"G\u00e9rer les traductions","text":"<p>Le code du site est enti\u00e8rement \u00e9crit en anglais, le texte affich\u00e9 aux utilisateurs l'est \u00e9galement. La traduction en fran\u00e7ais se fait ult\u00e9rieurement avec un fichier de traduction. Voici un petit guide rapide pour apprendre \u00e0 s'en servir.</p>"},{"location":"howto/translation/#dans-le-code-du-logiciel","title":"Dans le code du logiciel","text":"<p>Imaginons que nous souhaitons afficher \"Hello\" et le traduire en fran\u00e7ais. Voici comment signaler que ce mot doit \u00eatre traduit.</p> <p>Si le mot est dans le code Python :</p> <pre><code>from django.utils.translation import gettext as _\n\nhelp_text=_(\"Hello\")\n</code></pre> <p>Si le mot appara\u00eet dans le template Jinja :</p> <pre><code>{% trans %}Hello{% endtrans %}\n</code></pre> <p>Si on est dans un fichier javascript ou typescript :</p> <pre><code>gettext(\"Hello\");\n</code></pre>"},{"location":"howto/translation/#generer-le-fichier-djangopo","title":"G\u00e9n\u00e9rer le fichier django.po","text":"<p>La traduction se fait en trois \u00e9tapes. Il faut d'abord g\u00e9n\u00e9rer un fichier de traductions, l'\u00e9diter et enfin le compiler au format binaire pour qu'il soit lu par le serveur.</p> <pre><code>./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules # Pour le backend\n./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules # Pour le frontend\n</code></pre>"},{"location":"howto/translation/#editer-le-fichier-djangopo","title":"\u00c9diter le fichier django.po","text":"<pre><code># locale/fr/LC_MESSAGES/django.po\n\n# ...\nmsgid \"Hello\"\nmsgstr \"\" # Ligne \u00e0 modifier\n\n# ...\n</code></pre> <p>Note</p> <p>Si les commentaires suivants apparaissent, pensez \u00e0 les supprimer. Ils peuvent g\u00eaner votre traduction.</p> <pre><code>#, fuzzy\n#| msgid \"Bonjour\"\n</code></pre>"},{"location":"howto/translation/#generer-le-fichier-djangomo","title":"G\u00e9n\u00e9rer le fichier django.mo","text":"<p>Il s'agit de la derni\u00e8re \u00e9tape. Un fichier binaire est g\u00e9n\u00e9r\u00e9 \u00e0 partir du fichier django.mo.</p> <pre><code>./manage.py compilemessages\n</code></pre> <p>Tip</p> <p>Pensez \u00e0 red\u00e9marrer le serveur si les traductions ne s'affichent pas</p>"},{"location":"howto/weekmail/","title":"Modifier le weekmail","text":"<p>Le site est capable de g\u00e9n\u00e9rer des mails automatiques contenant l\u2019agr\u00e9gation d'articles \u00e9crits par les administrateurs de clubs. Le contenu est ins\u00e9r\u00e9 dans un template standardis\u00e9 et contr\u00f4l\u00e9 directement dans le code. Il arrive r\u00e9guli\u00e8rement que l'\u00e9quipe communication souhaite modifier ce template. Que ce soient les couleurs, l'agencement ou encore la banni\u00e8re ou le footer, voici tout ce qu'il y a \u00e0 savoir sur le fonctionnement du weekmail en commen\u00e7ant par la classe qui le contr\u00f4le.</p>"},{"location":"howto/weekmail/#modifier-la-banniere-et-le-footer","title":"Modifier la banni\u00e8re et le footer","text":"<p>Ces \u00e9l\u00e9ments sont contr\u00f4l\u00e9s par les m\u00e9thodes <code>get_banner</code> et <code>get_footer</code> de la classe <code>Weekmail</code>. Les modifier est donc tr\u00e8s simple, il suffit de modifier le contenu de la fonction et de rajouter les nouvelles images dans les statics.</p> <p>Les images sont \u00e0 ajouter dans <code>core/static/com/img</code> et sont \u00e0 nommer selon le type (banner ou footer), le semestre (Automne ou Printemps) et l'ann\u00e9e. Exemple : <code>weekmail_bannerA18.jpg</code> pour la banni\u00e8re de l'automne 2018.</p> <pre><code>from django.conf import settings\nfrom django.templatetags.static import static\n\n# S\u00e9lectionnez le fichier de banni\u00e8re pour le weekmail de l'automne 2018\n\ndef get_banner(self):\n return \"http://\" + settings.SITH_URL + static(\"com/img/weekmail_bannerA18.jpg\")\n</code></pre> <p>Note</p> <p>Penser \u00e0 prendre les images au format jpg et \u00e0 les compresser un peu, pour qu'elles soient le plus l\u00e9ger possible, c'est bien mieux pour l'utilisateur final.</p> <p>Warning</p> <p>Pensez \u00e0 laisser les anciennes images dans le dossier pour que les anciens weekmails ne soient pas affect\u00e9s par les changements.</p>"},{"location":"howto/weekmail/#modifier-le-template","title":"Modifier le template","text":"<p>Il existe deux templates diff\u00e9rents :</p> <ul> <li>Un en texte pur, qui sert pour le rendu d\u00e9grad\u00e9 des lecteurs de mails ne supportant pas le HTML</li> <li>un qui fait du rendu html.</li> </ul> <p>Ces deux templates sont respectivement accessibles aux emplacements suivants :</p> <ul> <li><code>com/templates/com/weekmail_renderer_html.jinja</code></li> <li><code>com/templates/com/weekmail_renderer_text.jinja</code></li> </ul> <p>Note</p> <p>Pour le rendu HTML, pensez \u00e0 utiliser le CSS et le javascript le plus simple possible pour que le rendu se fasse correctement dans les clients mails qui sont souvent capricieux.</p> <p>Note</p> <p>Le CSS est inclus statiquement pour que toute modification ult\u00e9rieure de celui-ci n'affecte pas les versions pr\u00e9c\u00e9demment envoy\u00e9es.</p> <p>Warning</p> <p>Si vous souhaitez ajouter du contenu, n'oubliez pas de bien inclure ce contenu dans les deux templates.</p>"},{"location":"reference/accounting/models/","title":"Models","text":""},{"location":"reference/accounting/models/#accounting.models.CurrencyField","title":"<code>CurrencyField(*args, **kwargs)</code>","text":"<p> Bases: <code>DecimalField</code></p> <p>Custom database field used for currency.</p> Source code in <code>accounting/models.py</code> <pre><code>def __init__(self, *args, **kwargs):\n kwargs[\"max_digits\"] = 12\n kwargs[\"decimal_places\"] = 2\n super().__init__(*args, **kwargs)\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.Company","title":"<code>Company</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/accounting/models/#accounting.models.Company.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.Company.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n return user.memberships.filter(\n end_date=None, club__role=settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n ).exists()\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.Company.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Check if that object can be viewed by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Check if that object can be viewed by the given user.\"\"\"\n return user.memberships.filter(\n end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n ).exists()\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.BankAccount","title":"<code>BankAccount</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/accounting/models/#accounting.models.BankAccount.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n m = self.club.get_membership_for(user)\n return m is not None and m.role &gt;= settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.ClubAccount","title":"<code>ClubAccount</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/accounting/models/#accounting.models.ClubAccount.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.ClubAccount.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n m = self.club.get_membership_for(user)\n return m and m.role == settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.ClubAccount.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Check if that object can be viewed by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Check if that object can be viewed by the given user.\"\"\"\n m = self.club.get_membership_for(user)\n return m and m.role &gt;= settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.GeneralJournal","title":"<code>GeneralJournal</code>","text":"<p> Bases: <code>Model</code></p> <p>Class storing all the operations for a period of time.</p>"},{"location":"reference/accounting/models/#accounting.models.GeneralJournal.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n return self.club_account.can_be_edited_by(user)\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.GeneralJournal.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n return self.club_account.can_be_edited_by(user)\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.Operation","title":"<code>Operation</code>","text":"<p> Bases: <code>Model</code></p> <p>An operation is a line in the journal, a debit or a credit.</p>"},{"location":"reference/accounting/models/#accounting.models.Operation.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n if self.journal.closed:\n return False\n m = self.journal.club_account.club.get_membership_for(user)\n return m is not None and m.role &gt;= settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.Operation.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n if self.journal.closed:\n return False\n m = self.journal.club_account.club.get_membership_for(user)\n return m is not None and m.role == settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.AccountingType","title":"<code>AccountingType</code>","text":"<p> Bases: <code>Model</code></p> <p>Accounting types.</p> <p>Those are numbers used in accounting to classify operations</p>"},{"location":"reference/accounting/models/#accounting.models.AccountingType.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/accounting/models/#accounting.models.SimplifiedAccountingType","title":"<code>SimplifiedAccountingType</code>","text":"<p> Bases: <code>Model</code></p> <p>Simplified version of <code>AccountingType</code>.</p>"},{"location":"reference/accounting/models/#accounting.models.Label","title":"<code>Label</code>","text":"<p> Bases: <code>Model</code></p> <p>Label allow a club to sort its operations.</p>"},{"location":"reference/accounting/views/","title":"Views","text":""},{"location":"reference/accounting/views/#accounting.views.AccountingType","title":"<code>AccountingType</code>","text":"<p> Bases: <code>Model</code></p> <p>Accounting types.</p> <p>Those are numbers used in accounting to classify operations</p>"},{"location":"reference/accounting/views/#accounting.views.AccountingType.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.BankAccount","title":"<code>BankAccount</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/accounting/views/#accounting.views.BankAccount.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n m = self.club.get_membership_for(user)\n return m is not None and m.role &gt;= settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.ClubAccount","title":"<code>ClubAccount</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/accounting/views/#accounting.views.ClubAccount.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.ClubAccount.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n m = self.club.get_membership_for(user)\n return m and m.role == settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.ClubAccount.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Check if that object can be viewed by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Check if that object can be viewed by the given user.\"\"\"\n m = self.club.get_membership_for(user)\n return m and m.role &gt;= settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.Company","title":"<code>Company</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/accounting/views/#accounting.views.Company.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.Company.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n return user.memberships.filter(\n end_date=None, club__role=settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n ).exists()\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.Company.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Check if that object can be viewed by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Check if that object can be viewed by the given user.\"\"\"\n return user.memberships.filter(\n end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n ).exists()\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.GeneralJournal","title":"<code>GeneralJournal</code>","text":"<p> Bases: <code>Model</code></p> <p>Class storing all the operations for a period of time.</p>"},{"location":"reference/accounting/views/#accounting.views.GeneralJournal.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n return self.club_account.can_be_edited_by(user)\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.GeneralJournal.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n return self.club_account.can_be_edited_by(user)\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.Label","title":"<code>Label</code>","text":"<p> Bases: <code>Model</code></p> <p>Label allow a club to sort its operations.</p>"},{"location":"reference/accounting/views/#accounting.views.Operation","title":"<code>Operation</code>","text":"<p> Bases: <code>Model</code></p> <p>An operation is a line in the journal, a debit or a credit.</p>"},{"location":"reference/accounting/views/#accounting.views.Operation.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n if self.journal.closed:\n return False\n m = self.journal.club_account.club.get_membership_for(user)\n return m is not None and m.role &gt;= settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.Operation.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>accounting/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):\n return True\n if self.journal.closed:\n return False\n m = self.journal.club_account.club.get_membership_for(user)\n return m is not None and m.role == settings.SITH_CLUB_ROLES_ID[\"Treasurer\"]\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.SimplifiedAccountingType","title":"<code>SimplifiedAccountingType</code>","text":"<p> Bases: <code>Model</code></p> <p>Simplified version of <code>AccountingType</code>.</p>"},{"location":"reference/accounting/views/#accounting.views.BankAccountListView","title":"<code>BankAccountListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.SimplifiedAccountingTypeListView","title":"<code>SimplifiedAccountingTypeListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.SimplifiedAccountingTypeEditView","title":"<code>SimplifiedAccountingTypeEditView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>UpdateView</code></p> <p>An edit view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.SimplifiedAccountingTypeCreateView","title":"<code>SimplifiedAccountingTypeCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create an accounting type (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.AccountingTypeListView","title":"<code>AccountingTypeListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.AccountingTypeEditView","title":"<code>AccountingTypeEditView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>UpdateView</code></p> <p>An edit view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.AccountingTypeCreateView","title":"<code>AccountingTypeCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create an accounting type (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.BankAccountEditView","title":"<code>BankAccountEditView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>UpdateView</code></p> <p>An edit view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.BankAccountDetailView","title":"<code>BankAccountDetailView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>A detail view, listing every club account.</p>"},{"location":"reference/accounting/views/#accounting.views.BankAccountCreateView","title":"<code>BankAccountCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a bank account (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.BankAccountDeleteView","title":"<code>BankAccountDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Delete a bank account (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.ClubAccountEditView","title":"<code>ClubAccountEditView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>UpdateView</code></p> <p>An edit view for the admins.</p>"},{"location":"reference/accounting/views/#accounting.views.ClubAccountDetailView","title":"<code>ClubAccountDetailView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>A detail view, listing every journal.</p>"},{"location":"reference/accounting/views/#accounting.views.ClubAccountCreateView","title":"<code>ClubAccountCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a club account (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.ClubAccountDeleteView","title":"<code>ClubAccountDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Delete a club account (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.JournalTabsMixin","title":"<code>JournalTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/accounting/views/#accounting.views.JournalCreateView","title":"<code>JournalCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a general journal.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalDetailView","title":"<code>JournalDetailView</code>","text":"<p> Bases: <code>JournalTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>A detail view, listing every operation.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalEditView","title":"<code>JournalEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p> <p>Update a general journal.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalDeleteView","title":"<code>JournalDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Delete a club account (for the admins).</p>"},{"location":"reference/accounting/views/#accounting.views.OperationForm","title":"<code>OperationForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>accounting/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n club_account = kwargs.pop(\"club_account\", None)\n super().__init__(*args, **kwargs)\n if club_account:\n self.fields[\"label\"].queryset = club_account.labels.order_by(\"name\").all()\n if self.instance.target_type == \"USER\":\n self.fields[\"user\"].initial = self.instance.target_id\n elif self.instance.target_type == \"ACCOUNT\":\n self.fields[\"club_account\"].initial = self.instance.target_id\n elif self.instance.target_type == \"CLUB\":\n self.fields[\"club\"].initial = self.instance.target_id\n elif self.instance.target_type == \"COMPANY\":\n self.fields[\"company\"].initial = self.instance.target_id\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.OperationCreateView","title":"<code>OperationCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create an operation.</p>"},{"location":"reference/accounting/views/#accounting.views.OperationCreateView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add journal to the context.</p> Source code in <code>accounting/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add journal to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n if self.journal:\n kwargs[\"object\"] = self.journal\n return kwargs\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.OperationEditView","title":"<code>OperationEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p> <p>An edit view, working as detail for the moment.</p>"},{"location":"reference/accounting/views/#accounting.views.OperationEditView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add journal to the context.</p> Source code in <code>accounting/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add journal to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"object\"] = self.object.journal\n return kwargs\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.OperationPDFView","title":"<code>OperationPDFView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display the PDF of a given operation.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalNatureStatementView","title":"<code>JournalNatureStatementView</code>","text":"<p> Bases: <code>JournalTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display a statement sorted by labels.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalNatureStatementView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add infos to the context.</p> Source code in <code>accounting/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add infos to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"statement\"] = self.big_statement()\n return kwargs\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.JournalPersonStatementView","title":"<code>JournalPersonStatementView</code>","text":"<p> Bases: <code>JournalTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Calculate a dictionary with operation target and sum of operations.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalPersonStatementView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add journal to the context.</p> Source code in <code>accounting/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add journal to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"credit_statement\"] = self.statement(\"CREDIT\")\n kwargs[\"debit_statement\"] = self.statement(\"DEBIT\")\n kwargs[\"total_credit\"] = self.total(\"CREDIT\")\n kwargs[\"total_debit\"] = self.total(\"DEBIT\")\n return kwargs\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.JournalAccountingStatementView","title":"<code>JournalAccountingStatementView</code>","text":"<p> Bases: <code>JournalTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Calculate a dictionary with operation type and sum of operations.</p>"},{"location":"reference/accounting/views/#accounting.views.JournalAccountingStatementView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add journal to the context.</p> Source code in <code>accounting/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add journal to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"statement\"] = self.statement()\n return kwargs\n</code></pre>"},{"location":"reference/accounting/views/#accounting.views.CompanyListView","title":"<code>CompanyListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p>"},{"location":"reference/accounting/views/#accounting.views.CompanyCreateView","title":"<code>CompanyCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a company.</p>"},{"location":"reference/accounting/views/#accounting.views.CompanyEditView","title":"<code>CompanyEditView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>UpdateView</code></p> <p>Edit a company.</p>"},{"location":"reference/accounting/views/#accounting.views.LabelListView","title":"<code>LabelListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/accounting/views/#accounting.views.LabelCreateView","title":"<code>LabelCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/accounting/views/#accounting.views.LabelEditView","title":"<code>LabelEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/accounting/views/#accounting.views.LabelDeleteView","title":"<code>LabelDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/accounting/views/#accounting.views.CloseCustomerAccountForm","title":"<code>CloseCustomerAccountForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/accounting/views/#accounting.views.RefoundAccountView","title":"<code>RefoundAccountView</code>","text":"<p> Bases: <code>FormView</code></p> <p>Create a selling with the same amount than the current user money.</p>"},{"location":"reference/antispam/forms/","title":"Forms","text":""},{"location":"reference/antispam/forms/#antispam.forms.ToxicDomain","title":"<code>ToxicDomain</code>","text":"<p> Bases: <code>Model</code></p> <p>Domain marked as spam in public databases</p>"},{"location":"reference/antispam/forms/#antispam.forms.AntiSpamEmailField","title":"<code>AntiSpamEmailField</code>","text":"<p> Bases: <code>EmailField</code></p> <p>An email field that email addresses with a known toxic domain.</p>"},{"location":"reference/antispam/models/","title":"Models","text":""},{"location":"reference/antispam/models/#antispam.models.ToxicDomain","title":"<code>ToxicDomain</code>","text":"<p> Bases: <code>Model</code></p> <p>Domain marked as spam in public databases</p>"},{"location":"reference/club/models/","title":"Models","text":""},{"location":"reference/club/models/#club.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/club/models/#club.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/club/models/#club.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/club/models/#club.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/club/models/#club.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/club/models/#club.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/club/models/#club.models.MembershipQuerySet","title":"<code>MembershipQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/club/models/#club.models.MembershipQuerySet.ongoing","title":"<code>ongoing()</code>","text":"<p>Filter all memberships which are not finished yet.</p> Source code in <code>club/models.py</code> <pre><code>def ongoing(self) -&gt; Self:\n \"\"\"Filter all memberships which are not finished yet.\"\"\"\n return self.filter(Q(end_date=None) | Q(end_date__gt=localdate()))\n</code></pre>"},{"location":"reference/club/models/#club.models.MembershipQuerySet.board","title":"<code>board()</code>","text":"<p>Filter all memberships where the user is/was in the board.</p> <p>Be aware that users who were in the board in the past are included, even if there are no more members.</p> <p>If you want to get the users who are currently in the board, mind combining this with the :meth:<code>ongoing</code> queryset method</p> Source code in <code>club/models.py</code> <pre><code>def board(self) -&gt; Self:\n \"\"\"Filter all memberships where the user is/was in the board.\n\n Be aware that users who were in the board in the past\n are included, even if there are no more members.\n\n If you want to get the users who are currently in the board,\n mind combining this with the :meth:`ongoing` queryset method\n \"\"\"\n return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)\n</code></pre>"},{"location":"reference/club/models/#club.models.MembershipQuerySet.update","title":"<code>update(**kwargs)</code>","text":"<p>Refresh the cache for the elements of the queryset.</p> <p>Besides that, does the same job as a regular update method.</p> <p>Be aware that this adds a db query to retrieve the updated objects</p> Source code in <code>club/models.py</code> <pre><code>def update(self, **kwargs):\n \"\"\"Refresh the cache for the elements of the queryset.\n\n Besides that, does the same job as a regular update method.\n\n Be aware that this adds a db query to retrieve the updated objects\n \"\"\"\n nb_rows = super().update(**kwargs)\n if nb_rows &gt; 0:\n # if at least a row was affected, refresh the cache\n for membership in self.all():\n if membership.end_date is not None:\n cache.set(\n f\"membership_{membership.club_id}_{membership.user_id}\",\n \"not_member\",\n )\n else:\n cache.set(\n f\"membership_{membership.club_id}_{membership.user_id}\",\n membership,\n )\n</code></pre>"},{"location":"reference/club/models/#club.models.MembershipQuerySet.delete","title":"<code>delete()</code>","text":"<p>Work just like the default Django's delete() method, but add a cache invalidation for the elements of the queryset before the deletion.</p> <p>Be aware that this adds a db query to retrieve the deleted element. As this first query take place before the deletion operation, it will be performed even if the deletion fails.</p> Source code in <code>club/models.py</code> <pre><code>def delete(self):\n \"\"\"Work just like the default Django's delete() method,\n but add a cache invalidation for the elements of the queryset\n before the deletion.\n\n Be aware that this adds a db query to retrieve the deleted element.\n As this first query take place before the deletion operation,\n it will be performed even if the deletion fails.\n \"\"\"\n ids = list(self.values_list(\"club_id\", \"user_id\"))\n nb_rows, _ = super().delete()\n if nb_rows &gt; 0:\n for club_id, user_id in ids:\n cache.set(f\"membership_{club_id}_{user_id}\", \"not_member\")\n</code></pre>"},{"location":"reference/club/models/#club.models.Membership","title":"<code>Membership</code>","text":"<p> Bases: <code>Model</code></p> <p>The Membership class makes the connection between User and Clubs.</p> Both Users and Clubs can have many Membership objects <ul> <li>a user can be a member of many clubs at a time</li> <li>a club can have many members at a time too</li> </ul> <p>A User is currently member of all the Clubs where its Membership has an end_date set to null/None. Otherwise, it's a past membership kept because it can be very useful to see who was in which Club in the past.</p>"},{"location":"reference/club/models/#club.models.Membership.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/club/models/#club.models.Membership.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user: User) -&gt; bool:\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_root or user.is_board_member:\n return True\n membership = self.club.get_membership_for(user)\n return membership is not None and membership.role &gt;= self.role\n</code></pre>"},{"location":"reference/club/models/#club.models.Mailing","title":"<code>Mailing</code>","text":"<p> Bases: <code>Model</code></p> <p>A Mailing list for a club.</p> Warning <p>Remember that mailing lists should be validated by UTBM.</p>"},{"location":"reference/club/models/#club.models.MailingSubscription","title":"<code>MailingSubscription</code>","text":"<p> Bases: <code>Model</code></p> <p>Link between user and mailing list.</p>"},{"location":"reference/club/models/#club.models.get_default_owner_group","title":"<code>get_default_owner_group()</code>","text":"Source code in <code>club/models.py</code> <pre><code>def get_default_owner_group():\n return settings.SITH_GROUP_ROOT_ID\n</code></pre>"},{"location":"reference/club/views/","title":"Views","text":""},{"location":"reference/club/views/#club.views.ClubEditForm","title":"<code>ClubEditForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>club/forms.py</code> <pre><code>def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"short_description\"].widget = forms.Textarea()\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubMemberForm","title":"<code>ClubMemberForm(*args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> <p>Form handling the members of a club.</p> Source code in <code>club/forms.py</code> <pre><code>def __init__(self, *args, **kwargs):\n self.club = kwargs.pop(\"club\")\n self.request_user = kwargs.pop(\"request_user\")\n self.club_members = kwargs.pop(\"club_members\", None)\n if not self.club_members:\n self.club_members = (\n self.club.members.filter(end_date=None).order_by(\"-role\").all()\n )\n self.request_user_membership = self.club.get_membership_for(self.request_user)\n super().__init__(*args, **kwargs)\n\n # Using a ModelForm binds too much the form with the model and we don't want that\n # We want the view to process the model creation since they are multiple users\n # We also want the form to handle bulk deletion\n self.fields.update(\n forms.fields_for_model(\n Membership,\n fields=(\"role\", \"start_date\", \"description\"),\n widgets={\"start_date\": SelectDate},\n )\n )\n\n # Role is required only if users is specified\n self.fields[\"role\"].required = False\n\n # Start date and description are never really required\n self.fields[\"start_date\"].required = False\n self.fields[\"description\"].required = False\n\n self.fields[\"users_old\"] = forms.ModelMultipleChoiceField(\n User.objects.filter(\n id__in=[\n ms.user.id\n for ms in self.club_members\n if ms.can_be_edited_by(self.request_user)\n ]\n ).all(),\n label=_(\"Mark as old\"),\n required=False,\n widget=forms.CheckboxSelectMultiple,\n )\n if not self.request_user.is_root:\n self.fields.pop(\"start_date\")\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubMemberForm.clean_users","title":"<code>clean_users()</code>","text":"<p>Check that the user is not trying to add an user already in the club.</p> <p>Also check that the user is valid and has a valid subscription.</p> Source code in <code>club/forms.py</code> <pre><code>def clean_users(self):\n \"\"\"Check that the user is not trying to add an user already in the club.\n\n Also check that the user is valid and has a valid subscription.\n \"\"\"\n cleaned_data = super().clean()\n users = []\n for user in cleaned_data[\"users\"]:\n if not user.is_subscribed:\n raise forms.ValidationError(\n _(\"User must be subscriber to take part to a club\"), code=\"invalid\"\n )\n if self.club.get_membership_for(user):\n raise forms.ValidationError(\n _(\"You can not add the same user twice\"), code=\"invalid\"\n )\n users.append(user)\n return users\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubMemberForm.clean","title":"<code>clean()</code>","text":"<p>Check user rights for adding an user.</p> Source code in <code>club/forms.py</code> <pre><code>def clean(self):\n \"\"\"Check user rights for adding an user.\"\"\"\n cleaned_data = super().clean()\n\n if \"start_date\" in cleaned_data and not cleaned_data[\"start_date\"]:\n # Drop start_date if allowed to edition but not specified\n cleaned_data.pop(\"start_date\")\n\n if not cleaned_data.get(\"users\"):\n # No user to add equals no check needed\n return cleaned_data\n\n if cleaned_data.get(\"role\", \"\") == \"\":\n # Role is required if users exists\n self.add_error(\"role\", _(\"You should specify a role\"))\n return cleaned_data\n\n request_user = self.request_user\n membership = self.request_user_membership\n if not (\n cleaned_data[\"role\"] &lt;= settings.SITH_MAXIMUM_FREE_ROLE\n or (membership is not None and membership.role &gt;= cleaned_data[\"role\"])\n or request_user.is_board_member\n or request_user.is_root\n ):\n raise forms.ValidationError(_(\"You do not have the permission to do that\"))\n return cleaned_data\n</code></pre>"},{"location":"reference/club/views/#club.views.MailingForm","title":"<code>MailingForm(club_id, user_id, mailings, *args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> <p>Form handling mailing lists right.</p> Source code in <code>club/forms.py</code> <pre><code>def __init__(self, club_id, user_id, mailings, *args, **kwargs):\n super().__init__(*args, **kwargs)\n\n self.fields[\"action\"] = forms.TypedChoiceField(\n choices=(\n (self.ACTION_NEW_MAILING, _(\"New Mailing\")),\n (self.ACTION_NEW_SUBSCRIPTION, _(\"Subscribe\")),\n (self.ACTION_REMOVE_SUBSCRIPTION, _(\"Remove\")),\n ),\n coerce=int,\n label=_(\"Action\"),\n initial=1,\n required=True,\n widget=forms.HiddenInput(),\n )\n\n # Generate bulk removal forms, they are never required\n for mailing in mailings:\n self.fields[\"removal_\" + str(mailing.id)] = forms.ModelMultipleChoiceField(\n mailing.subscriptions.all(),\n label=_(\"Remove\"),\n required=False,\n widget=forms.CheckboxSelectMultiple,\n )\n\n # Include fields for handling mailing creation\n mailing_fields = (\"email\",)\n self.fields.update(forms.fields_for_model(Mailing, fields=mailing_fields))\n for field in mailing_fields:\n self.fields[\"mailing_\" + field] = self.fields.pop(field)\n self.fields[\"mailing_\" + field].required = False\n\n # Include fields for handling subscription creation\n subscription_fields = (\"mailing\", \"email\")\n self.fields.update(\n forms.fields_for_model(MailingSubscription, fields=subscription_fields)\n )\n for field in subscription_fields:\n self.fields[\"subscription_\" + field] = self.fields.pop(field)\n self.fields[\"subscription_\" + field].required = False\n\n self.fields[\"subscription_mailing\"].queryset = Mailing.objects.filter(\n club__id=club_id, is_moderated=True\n )\n</code></pre>"},{"location":"reference/club/views/#club.views.MailingForm.check_required","title":"<code>check_required(cleaned_data, field)</code>","text":"<p>If the given field doesn't exist or has no value, add a required error on it.</p> Source code in <code>club/forms.py</code> <pre><code>def check_required(self, cleaned_data, field):\n \"\"\"If the given field doesn't exist or has no value, add a required error on it.\"\"\"\n if not cleaned_data.get(field, None):\n self.add_error(field, _(\"This field is required\"))\n</code></pre>"},{"location":"reference/club/views/#club.views.MailingForm.clean_subscription_users","title":"<code>clean_subscription_users()</code>","text":"<p>Convert given users into real users and check their validity.</p> Source code in <code>club/forms.py</code> <pre><code>def clean_subscription_users(self):\n \"\"\"Convert given users into real users and check their validity.\"\"\"\n cleaned_data = super().clean()\n users = []\n for user in cleaned_data[\"subscription_users\"]:\n if not user.email:\n raise forms.ValidationError(\n _(\"One of the selected users doesn't have an email address\"),\n code=\"invalid\",\n )\n users.append(user)\n return users\n</code></pre>"},{"location":"reference/club/views/#club.views.SellingsForm","title":"<code>SellingsForm(club, *args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> Source code in <code>club/forms.py</code> <pre><code>def __init__(self, club, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"products\"] = forms.ModelMultipleChoiceField(\n club.products.order_by(\"name\").filter(archived=False).all(),\n label=_(\"Products\"),\n required=False,\n )\n self.fields[\"archived_products\"] = forms.ModelMultipleChoiceField(\n club.products.order_by(\"name\").filter(archived=True).all(),\n label=_(\"Archived products\"),\n required=False,\n )\n</code></pre>"},{"location":"reference/club/views/#club.views.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/club/views/#club.views.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/club/views/#club.views.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/club/views/#club.views.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/club/views/#club.views.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/club/views/#club.views.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/club/views/#club.views.Mailing","title":"<code>Mailing</code>","text":"<p> Bases: <code>Model</code></p> <p>A Mailing list for a club.</p> Warning <p>Remember that mailing lists should be validated by UTBM.</p>"},{"location":"reference/club/views/#club.views.MailingSubscription","title":"<code>MailingSubscription</code>","text":"<p> Bases: <code>Model</code></p> <p>Link between user and mailing list.</p>"},{"location":"reference/club/views/#club.views.Membership","title":"<code>Membership</code>","text":"<p> Bases: <code>Model</code></p> <p>The Membership class makes the connection between User and Clubs.</p> Both Users and Clubs can have many Membership objects <ul> <li>a user can be a member of many clubs at a time</li> <li>a club can have many members at a time too</li> </ul> <p>A User is currently member of all the Clubs where its Membership has an end_date set to null/None. Otherwise, it's a past membership kept because it can be very useful to see who was in which Club in the past.</p>"},{"location":"reference/club/views/#club.views.Membership.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/club/views/#club.views.Membership.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Check if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user: User) -&gt; bool:\n \"\"\"Check if that object can be edited by the given user.\"\"\"\n if user.is_root or user.is_board_member:\n return True\n membership = self.club.get_membership_for(user)\n return membership is not None and membership.role &gt;= self.role\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubTabsMixin","title":"<code>ClubTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/club/views/#club.views.ClubListView","title":"<code>ClubListView</code>","text":"<p> Bases: <code>ListView</code></p> <p>List the Clubs.</p>"},{"location":"reference/club/views/#club.views.ClubView","title":"<code>ClubView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>DetailView</code></p> <p>Front page of a Club.</p>"},{"location":"reference/club/views/#club.views.ClubRevView","title":"<code>ClubRevView</code>","text":"<p> Bases: <code>ClubView</code></p> <p>Display a specific page revision.</p>"},{"location":"reference/club/views/#club.views.ClubPageEditView","title":"<code>ClubPageEditView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>PageEditViewBase</code></p>"},{"location":"reference/club/views/#club.views.ClubPageHistView","title":"<code>ClubPageHistView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Modification hostory of the page.</p>"},{"location":"reference/club/views/#club.views.ClubToolsView","title":"<code>ClubToolsView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanEditMixin</code>, <code>DetailView</code></p> <p>Tools page of a Club.</p>"},{"location":"reference/club/views/#club.views.ClubMembersView","title":"<code>ClubMembersView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailFormView</code></p> <p>View of a club's members.</p>"},{"location":"reference/club/views/#club.views.ClubMembersView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>Check user rights.</p> Source code in <code>club/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"Check user rights.\"\"\"\n resp = super().form_valid(form)\n\n data = form.clean()\n users = data.pop(\"users\", [])\n users_old = data.pop(\"users_old\", [])\n for user in users:\n Membership(club=self.get_object(), user=user, **data).save()\n for user in users_old:\n membership = self.get_object().get_membership_for(user)\n membership.end_date = timezone.now()\n membership.save()\n return resp\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubOldMembersView","title":"<code>ClubOldMembersView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Old members of a club.</p>"},{"location":"reference/club/views/#club.views.ClubSellingView","title":"<code>ClubSellingView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanEditMixin</code>, <code>DetailFormView</code></p> <p>Sellings of a club.</p>"},{"location":"reference/club/views/#club.views.ClubSellingCSVView","title":"<code>ClubSellingCSVView</code>","text":"<p> Bases: <code>ClubSellingView</code></p> <p>Generate sellings in csv for a given period.</p>"},{"location":"reference/club/views/#club.views.ClubSellingCSVView.StreamWriter","title":"<code>StreamWriter</code>","text":"<p>Implements a file-like interface for streaming the CSV.</p>"},{"location":"reference/club/views/#club.views.ClubSellingCSVView.StreamWriter.write","title":"<code>write(value)</code>","text":"<p>Write the value by returning it, instead of storing in a buffer.</p> Source code in <code>club/views.py</code> <pre><code>def write(self, value):\n \"\"\"Write the value by returning it, instead of storing in a buffer.\"\"\"\n return value\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubEditView","title":"<code>ClubEditView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanEditMixin</code>, <code>UpdateView</code></p> <p>Edit a Club's main informations (for the club's members).</p>"},{"location":"reference/club/views/#club.views.ClubEditPropView","title":"<code>ClubEditPropView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Edit the properties of a Club object (for the Sith admins).</p>"},{"location":"reference/club/views/#club.views.ClubCreateView","title":"<code>ClubCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a club (for the Sith admin).</p>"},{"location":"reference/club/views/#club.views.MembershipSetOldView","title":"<code>MembershipSetOldView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DetailView</code></p> <p>Set a membership as beeing old.</p>"},{"location":"reference/club/views/#club.views.MembershipDeleteView","title":"<code>MembershipDeleteView</code>","text":"<p> Bases: <code>UserIsRootMixin</code>, <code>DeleteView</code></p> <p>Delete a membership (for admins only).</p>"},{"location":"reference/club/views/#club.views.ClubStatView","title":"<code>ClubStatView</code>","text":"<p> Bases: <code>TemplateView</code></p>"},{"location":"reference/club/views/#club.views.ClubMailingView","title":"<code>ClubMailingView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>CanEditMixin</code>, <code>DetailFormView</code></p> <p>A list of mailing for a given club.</p>"},{"location":"reference/club/views/#club.views.ClubMailingView.add_new_mailing","title":"<code>add_new_mailing(cleaned_data)</code>","text":"<p>Create a new mailing list from the form.</p> Source code in <code>club/views.py</code> <pre><code>def add_new_mailing(self, cleaned_data) -&gt; ValidationError | None:\n \"\"\"Create a new mailing list from the form.\"\"\"\n mailing = Mailing(\n club=self.get_object(),\n email=cleaned_data[\"mailing_email\"],\n moderator=self.request.user,\n is_moderated=False,\n )\n try:\n mailing.clean()\n except ValidationError as validation_error:\n return validation_error\n mailing.save()\n return None\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubMailingView.add_new_subscription","title":"<code>add_new_subscription(cleaned_data)</code>","text":"<p>Add mailing subscriptions for each user given and/or for the specified email in form.</p> Source code in <code>club/views.py</code> <pre><code>def add_new_subscription(self, cleaned_data) -&gt; ValidationError | None:\n \"\"\"Add mailing subscriptions for each user given and/or for the specified email in form.\"\"\"\n users_to_save = []\n\n for user in cleaned_data[\"subscription_users\"]:\n sub = MailingSubscription(\n mailing=cleaned_data[\"subscription_mailing\"], user=user\n )\n try:\n sub.clean()\n except ValidationError as validation_error:\n return validation_error\n\n sub.save()\n users_to_save.append(sub)\n\n if cleaned_data[\"subscription_email\"]:\n sub = MailingSubscription(\n mailing=cleaned_data[\"subscription_mailing\"],\n email=cleaned_data[\"subscription_email\"],\n )\n\n try:\n sub.clean()\n except ValidationError as validation_error:\n return validation_error\n sub.save()\n\n # Save users after we are sure there is no error\n for user in users_to_save:\n user.save()\n\n return None\n</code></pre>"},{"location":"reference/club/views/#club.views.ClubMailingView.remove_subscription","title":"<code>remove_subscription(cleaned_data)</code>","text":"<p>Remove specified users from a mailing list.</p> Source code in <code>club/views.py</code> <pre><code>def remove_subscription(self, cleaned_data):\n \"\"\"Remove specified users from a mailing list.\"\"\"\n fields = [\n val for key, val in cleaned_data.items() if key.startswith(\"removal_\")\n ]\n for field in fields:\n for sub in field:\n sub.delete()\n</code></pre>"},{"location":"reference/club/views/#club.views.MailingDeleteView","title":"<code>MailingDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/club/views/#club.views.MailingSubscriptionDeleteView","title":"<code>MailingSubscriptionDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/club/views/#club.views.MailingAutoGenerationView","title":"<code>MailingAutoGenerationView</code>","text":"<p> Bases: <code>View</code></p>"},{"location":"reference/club/views/#club.views.PosterListView","title":"<code>PosterListView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>PosterListBaseView</code>, <code>CanViewMixin</code></p> <p>List communication posters.</p>"},{"location":"reference/club/views/#club.views.PosterCreateView","title":"<code>PosterCreateView</code>","text":"<p> Bases: <code>PosterCreateBaseView</code>, <code>CanCreateMixin</code></p> <p>Create communication poster.</p>"},{"location":"reference/club/views/#club.views.PosterEditView","title":"<code>PosterEditView</code>","text":"<p> Bases: <code>ClubTabsMixin</code>, <code>PosterEditBaseView</code>, <code>CanEditMixin</code></p> <p>Edit communication poster.</p>"},{"location":"reference/club/views/#club.views.PosterDeleteView","title":"<code>PosterDeleteView</code>","text":"<p> Bases: <code>PosterDeleteBaseView</code>, <code>ClubTabsMixin</code>, <code>CanEditMixin</code></p> <p>Delete communication poster.</p>"},{"location":"reference/com/models/","title":"Models","text":""},{"location":"reference/com/models/#com.models.NEWS_TYPES","title":"<code>NEWS_TYPES = [('NOTICE', _('Notice')), ('EVENT', _('Event')), ('WEEKLY', _('Weekly')), ('CALL', _('Call'))]</code> <code>module-attribute</code>","text":""},{"location":"reference/com/models/#com.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/com/models/#com.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/com/models/#com.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/com/models/#com.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/com/models/#com.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/com/models/#com.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/com/models/#com.models.Sith","title":"<code>Sith</code>","text":"<p> Bases: <code>Model</code></p> <p>A one instance class storing all the modifiable infos.</p>"},{"location":"reference/com/models/#com.models.News","title":"<code>News</code>","text":"<p> Bases: <code>Model</code></p> <p>The news class.</p>"},{"location":"reference/com/models/#com.models.NewsDate","title":"<code>NewsDate</code>","text":"<p> Bases: <code>Model</code></p> <p>A date class, useful for weekly events, or for events that just have no date.</p> <p>This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since we don't have to make copies</p>"},{"location":"reference/com/models/#com.models.Weekmail","title":"<code>Weekmail</code>","text":"<p> Bases: <code>Model</code></p> <p>The weekmail class.</p> <p>:ivar title: Title of the weekmail :ivar intro: Introduction of the weekmail :ivar joke: Joke of the week :ivar protip: Tip of the week :ivar conclusion: Conclusion of the weekmail :ivar sent: Track if the weekmail has been sent</p>"},{"location":"reference/com/models/#com.models.Weekmail.send","title":"<code>send()</code>","text":"<p>Send the weekmail to all users with the receive weekmail option opt-in.</p> <p>Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.</p> Source code in <code>com/models.py</code> <pre><code>def send(self):\n \"\"\"Send the weekmail to all users with the receive weekmail option opt-in.\n\n Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.\n \"\"\"\n dest = [\n i[0]\n for i in Preferences.objects.filter(receive_weekmail=True).values_list(\n \"user__email\"\n )\n ]\n with transaction.atomic():\n email = EmailMultiAlternatives(\n subject=self.title,\n body=self.render_text(),\n from_email=settings.SITH_COM_EMAIL,\n to=Sith.objects.first().weekmail_destinations.split(\" \"),\n bcc=dest,\n )\n email.attach_alternative(self.render_html(), \"text/html\")\n email.send()\n self.sent = True\n self.save()\n Weekmail().save()\n</code></pre>"},{"location":"reference/com/models/#com.models.Weekmail.render_text","title":"<code>render_text()</code>","text":"<p>Renders a pure text version of the mail for readers without HTML support.</p> Source code in <code>com/models.py</code> <pre><code>def render_text(self):\n \"\"\"Renders a pure text version of the mail for readers without HTML support.\"\"\"\n return render(\n None, \"com/weekmail_renderer_text.jinja\", context={\"weekmail\": self}\n ).content.decode(\"utf-8\")\n</code></pre>"},{"location":"reference/com/models/#com.models.Weekmail.render_html","title":"<code>render_html()</code>","text":"<p>Renders an HTML version of the mail with images and fancy CSS.</p> Source code in <code>com/models.py</code> <pre><code>def render_html(self):\n \"\"\"Renders an HTML version of the mail with images and fancy CSS.\"\"\"\n return render(\n None, \"com/weekmail_renderer_html.jinja\", context={\"weekmail\": self}\n ).content.decode(\"utf-8\")\n</code></pre>"},{"location":"reference/com/models/#com.models.Weekmail.get_banner","title":"<code>get_banner()</code>","text":"<p>Return an absolute link to the banner.</p> Source code in <code>com/models.py</code> <pre><code>def get_banner(self):\n \"\"\"Return an absolute link to the banner.\"\"\"\n return (\n \"http://\" + settings.SITH_URL + static(\"com/img/weekmail_bannerV2P22.png\")\n )\n</code></pre>"},{"location":"reference/com/models/#com.models.Weekmail.get_footer","title":"<code>get_footer()</code>","text":"<p>Return an absolute link to the footer.</p> Source code in <code>com/models.py</code> <pre><code>def get_footer(self):\n \"\"\"Return an absolute link to the footer.\"\"\"\n return \"http://\" + settings.SITH_URL + static(\"com/img/weekmail_footerP22.png\")\n</code></pre>"},{"location":"reference/com/models/#com.models.WeekmailArticle","title":"<code>WeekmailArticle</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/com/models/#com.models.Screen","title":"<code>Screen</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/com/models/#com.models.Poster","title":"<code>Poster</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/com/models/#com.models.news_notification_callback","title":"<code>news_notification_callback(notif)</code>","text":"Source code in <code>com/models.py</code> <pre><code>def news_notification_callback(notif):\n count = (\n News.objects.filter(\n Q(dates__start_date__gt=timezone.now(), is_moderated=False)\n | Q(type=\"NOTICE\", is_moderated=False)\n )\n .distinct()\n .count()\n )\n if count:\n notif.viewed = False\n notif.param = \"%s\" % count\n notif.date = timezone.now()\n else:\n notif.viewed = True\n</code></pre>"},{"location":"reference/com/views/","title":"Views","text":""},{"location":"reference/com/views/#com.views.sith","title":"<code>sith = Sith.objects.first</code> <code>module-attribute</code>","text":""},{"location":"reference/com/views/#com.views.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/com/views/#com.views.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/com/views/#com.views.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/com/views/#com.views.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/com/views/#com.views.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/com/views/#com.views.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/com/views/#com.views.Mailing","title":"<code>Mailing</code>","text":"<p> Bases: <code>Model</code></p> <p>A Mailing list for a club.</p> Warning <p>Remember that mailing lists should be validated by UTBM.</p>"},{"location":"reference/com/views/#com.views.News","title":"<code>News</code>","text":"<p> Bases: <code>Model</code></p> <p>The news class.</p>"},{"location":"reference/com/views/#com.views.NewsDate","title":"<code>NewsDate</code>","text":"<p> Bases: <code>Model</code></p> <p>A date class, useful for weekly events, or for events that just have no date.</p> <p>This class allows more flexibilty managing the dates related to a news, particularly when this news is weekly, since we don't have to make copies</p>"},{"location":"reference/com/views/#com.views.Poster","title":"<code>Poster</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/com/views/#com.views.Screen","title":"<code>Screen</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/com/views/#com.views.Sith","title":"<code>Sith</code>","text":"<p> Bases: <code>Model</code></p> <p>A one instance class storing all the modifiable infos.</p>"},{"location":"reference/com/views/#com.views.Weekmail","title":"<code>Weekmail</code>","text":"<p> Bases: <code>Model</code></p> <p>The weekmail class.</p> <p>:ivar title: Title of the weekmail :ivar intro: Introduction of the weekmail :ivar joke: Joke of the week :ivar protip: Tip of the week :ivar conclusion: Conclusion of the weekmail :ivar sent: Track if the weekmail has been sent</p>"},{"location":"reference/com/views/#com.views.Weekmail.send","title":"<code>send()</code>","text":"<p>Send the weekmail to all users with the receive weekmail option opt-in.</p> <p>Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.</p> Source code in <code>com/models.py</code> <pre><code>def send(self):\n \"\"\"Send the weekmail to all users with the receive weekmail option opt-in.\n\n Also send the weekmail to the mailing list in settings.SITH_COM_EMAIL.\n \"\"\"\n dest = [\n i[0]\n for i in Preferences.objects.filter(receive_weekmail=True).values_list(\n \"user__email\"\n )\n ]\n with transaction.atomic():\n email = EmailMultiAlternatives(\n subject=self.title,\n body=self.render_text(),\n from_email=settings.SITH_COM_EMAIL,\n to=Sith.objects.first().weekmail_destinations.split(\" \"),\n bcc=dest,\n )\n email.attach_alternative(self.render_html(), \"text/html\")\n email.send()\n self.sent = True\n self.save()\n Weekmail().save()\n</code></pre>"},{"location":"reference/com/views/#com.views.Weekmail.render_text","title":"<code>render_text()</code>","text":"<p>Renders a pure text version of the mail for readers without HTML support.</p> Source code in <code>com/models.py</code> <pre><code>def render_text(self):\n \"\"\"Renders a pure text version of the mail for readers without HTML support.\"\"\"\n return render(\n None, \"com/weekmail_renderer_text.jinja\", context={\"weekmail\": self}\n ).content.decode(\"utf-8\")\n</code></pre>"},{"location":"reference/com/views/#com.views.Weekmail.render_html","title":"<code>render_html()</code>","text":"<p>Renders an HTML version of the mail with images and fancy CSS.</p> Source code in <code>com/models.py</code> <pre><code>def render_html(self):\n \"\"\"Renders an HTML version of the mail with images and fancy CSS.\"\"\"\n return render(\n None, \"com/weekmail_renderer_html.jinja\", context={\"weekmail\": self}\n ).content.decode(\"utf-8\")\n</code></pre>"},{"location":"reference/com/views/#com.views.Weekmail.get_banner","title":"<code>get_banner()</code>","text":"<p>Return an absolute link to the banner.</p> Source code in <code>com/models.py</code> <pre><code>def get_banner(self):\n \"\"\"Return an absolute link to the banner.\"\"\"\n return (\n \"http://\" + settings.SITH_URL + static(\"com/img/weekmail_bannerV2P22.png\")\n )\n</code></pre>"},{"location":"reference/com/views/#com.views.Weekmail.get_footer","title":"<code>get_footer()</code>","text":"<p>Return an absolute link to the footer.</p> Source code in <code>com/models.py</code> <pre><code>def get_footer(self):\n \"\"\"Return an absolute link to the footer.\"\"\"\n return \"http://\" + settings.SITH_URL + static(\"com/img/weekmail_footerP22.png\")\n</code></pre>"},{"location":"reference/com/views/#com.views.WeekmailArticle","title":"<code>WeekmailArticle</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/com/views/#com.views.PosterForm","title":"<code>PosterForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>com/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n self.user = kwargs.pop(\"user\", None)\n super().__init__(*args, **kwargs)\n if self.user and not self.user.is_com_admin:\n self.fields[\"club\"].queryset = Club.objects.filter(\n id__in=self.user.clubs_with_rights\n )\n self.fields.pop(\"display_time\")\n</code></pre>"},{"location":"reference/com/views/#com.views.ComTabsMixin","title":"<code>ComTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/com/views/#com.views.IsComAdminMixin","title":"<code>IsComAdminMixin</code>","text":"<p> Bases: <code>View</code></p>"},{"location":"reference/com/views/#com.views.ComEditView","title":"<code>ComEditView</code>","text":"<p> Bases: <code>ComTabsMixin</code>, <code>CanEditPropMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/com/views/#com.views.AlertMsgEditView","title":"<code>AlertMsgEditView</code>","text":"<p> Bases: <code>ComEditView</code></p>"},{"location":"reference/com/views/#com.views.InfoMsgEditView","title":"<code>InfoMsgEditView</code>","text":"<p> Bases: <code>ComEditView</code></p>"},{"location":"reference/com/views/#com.views.WeekmailDestinationEditView","title":"<code>WeekmailDestinationEditView</code>","text":"<p> Bases: <code>ComEditView</code></p>"},{"location":"reference/com/views/#com.views.NewsForm","title":"<code>NewsForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/com/views/#com.views.NewsEditView","title":"<code>NewsEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/com/views/#com.views.NewsCreateView","title":"<code>NewsCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/com/views/#com.views.NewsDeleteView","title":"<code>NewsDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/com/views/#com.views.NewsModerateView","title":"<code>NewsModerateView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>SingleObjectMixin</code></p>"},{"location":"reference/com/views/#com.views.NewsAdminListView","title":"<code>NewsAdminListView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>ListView</code></p>"},{"location":"reference/com/views/#com.views.NewsListView","title":"<code>NewsListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p>"},{"location":"reference/com/views/#com.views.NewsDetailView","title":"<code>NewsDetailView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/com/views/#com.views.WeekmailPreviewView","title":"<code>WeekmailPreviewView</code>","text":"<p> Bases: <code>ComTabsMixin</code>, <code>QuickNotifMixin</code>, <code>CanEditPropMixin</code>, <code>DetailView</code></p>"},{"location":"reference/com/views/#com.views.WeekmailPreviewView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add rendered weekmail.</p> Source code in <code>com/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add rendered weekmail.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"weekmail_rendered\"] = self.object.render_html()\n kwargs[\"bad_recipients\"] = self.bad_recipients\n return kwargs\n</code></pre>"},{"location":"reference/com/views/#com.views.WeekmailEditView","title":"<code>WeekmailEditView</code>","text":"<p> Bases: <code>ComTabsMixin</code>, <code>QuickNotifMixin</code>, <code>CanEditPropMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/com/views/#com.views.WeekmailEditView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add orphan articles.</p> Source code in <code>com/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add orphan articles.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"orphans\"] = WeekmailArticle.objects.filter(weekmail=None)\n return kwargs\n</code></pre>"},{"location":"reference/com/views/#com.views.WeekmailArticleEditView","title":"<code>WeekmailArticleEditView</code>","text":"<p> Bases: <code>ComTabsMixin</code>, <code>QuickNotifMixin</code>, <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Edit an article.</p>"},{"location":"reference/com/views/#com.views.WeekmailArticleCreateView","title":"<code>WeekmailArticleCreateView</code>","text":"<p> Bases: <code>QuickNotifMixin</code>, <code>CreateView</code></p> <p>Post an article.</p>"},{"location":"reference/com/views/#com.views.WeekmailArticleDeleteView","title":"<code>WeekmailArticleDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Delete an article.</p>"},{"location":"reference/com/views/#com.views.MailingListAdminView","title":"<code>MailingListAdminView</code>","text":"<p> Bases: <code>ComTabsMixin</code>, <code>ListView</code></p>"},{"location":"reference/com/views/#com.views.MailingModerateView","title":"<code>MailingModerateView</code>","text":"<p> Bases: <code>View</code></p>"},{"location":"reference/com/views/#com.views.PosterListBaseView","title":"<code>PosterListBaseView</code>","text":"<p> Bases: <code>ListView</code></p> <p>List communication posters.</p>"},{"location":"reference/com/views/#com.views.PosterCreateBaseView","title":"<code>PosterCreateBaseView</code>","text":"<p> Bases: <code>CreateView</code></p> <p>Create communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterEditBaseView","title":"<code>PosterEditBaseView</code>","text":"<p> Bases: <code>UpdateView</code></p> <p>Edit communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterDeleteBaseView","title":"<code>PosterDeleteBaseView</code>","text":"<p> Bases: <code>DeleteView</code></p> <p>Edit communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterListView","title":"<code>PosterListView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>PosterListBaseView</code></p> <p>List communication posters.</p>"},{"location":"reference/com/views/#com.views.PosterCreateView","title":"<code>PosterCreateView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>PosterCreateBaseView</code></p> <p>Create communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterEditView","title":"<code>PosterEditView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>PosterEditBaseView</code></p> <p>Edit communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterDeleteView","title":"<code>PosterDeleteView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>PosterDeleteBaseView</code></p> <p>Delete communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterModerateListView","title":"<code>PosterModerateListView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>ListView</code></p> <p>Moderate list communication poster.</p>"},{"location":"reference/com/views/#com.views.PosterModerateView","title":"<code>PosterModerateView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>View</code></p> <p>Moderate communication poster.</p>"},{"location":"reference/com/views/#com.views.ScreenListView","title":"<code>ScreenListView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>ListView</code></p> <p>List communication screens.</p>"},{"location":"reference/com/views/#com.views.ScreenSlideshowView","title":"<code>ScreenSlideshowView</code>","text":"<p> Bases: <code>DetailView</code></p> <p>Slideshow of actives posters.</p>"},{"location":"reference/com/views/#com.views.ScreenCreateView","title":"<code>ScreenCreateView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>CreateView</code></p> <p>Create communication screen.</p>"},{"location":"reference/com/views/#com.views.ScreenEditView","title":"<code>ScreenEditView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>UpdateView</code></p> <p>Edit communication screen.</p>"},{"location":"reference/com/views/#com.views.ScreenDeleteView","title":"<code>ScreenDeleteView</code>","text":"<p> Bases: <code>IsComAdminMixin</code>, <code>ComTabsMixin</code>, <code>DeleteView</code></p> <p>Delete communication screen.</p>"},{"location":"reference/core/api_permissions/","title":"Api permissions","text":"<p>Permission classes to be used within ninja-extra controllers.</p> <p>Some permissions are global (like <code>IsInGroup</code> or <code>IsRoot</code>), and some others are per-object (like <code>CanView</code> or <code>CanEdit</code>).</p> <p>Examples:</p>"},{"location":"reference/core/api_permissions/#core.api_permissions--restrict-all-the-routes-of-this-controller","title":"restrict all the routes of this controller","text":""},{"location":"reference/core/api_permissions/#core.api_permissions--to-subscribed-users","title":"to subscribed users","text":"<p>@api_controller(\"/foo\", permissions=[IsSubscriber]) class FooController(ControllerBase): @route.get(\"/bar\") def bar_get(self): # This route inherits the permissions of the controller # ...</p> <pre><code>@route.bar(\"/bar/{bar_id}\", permissions=[CanView])\ndef bar_get_one(self, bar_id: int):\n # per-object permission resolution happens\n # when calling either the `get_object_or_exception`\n # or `get_object_or_none` method.\n bar = self.get_object_or_exception(Counter, pk=bar_id)\n\n # you can also call the `check_object_permission` manually\n other_bar = Counter.objects.first()\n self.check_object_permissions(other_bar)\n\n # ...\n\n# This route is restricted to counter admins and root users\n@route.delete(\n \"/bar/{bar_id}\",\n permissions=[IsRoot | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID)\n]\ndef bar_delete(self, bar_id: int):\n # ...\n</code></pre>"},{"location":"reference/core/api_permissions/#core.api_permissions.CanAccessLookup","title":"<code>CanAccessLookup = IsOldSubscriber | IsRoot | IsLoggedInCounter</code> <code>module-attribute</code>","text":""},{"location":"reference/core/api_permissions/#core.api_permissions.IsInGroup","title":"<code>IsInGroup(group_pk)</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that the user is in the group whose primary key is given.</p> Source code in <code>core/api_permissions.py</code> <pre><code>def __init__(self, group_pk: int):\n self._group_pk = group_pk\n</code></pre>"},{"location":"reference/core/api_permissions/#core.api_permissions.IsRoot","title":"<code>IsRoot</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that the user is root.</p>"},{"location":"reference/core/api_permissions/#core.api_permissions.IsSubscriber","title":"<code>IsSubscriber</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that the user is currently subscribed.</p>"},{"location":"reference/core/api_permissions/#core.api_permissions.IsOldSubscriber","title":"<code>IsOldSubscriber</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that the user has at least one subscription in its history.</p>"},{"location":"reference/core/api_permissions/#core.api_permissions.CanView","title":"<code>CanView</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that this user has the permission to view the object of this route.</p> <p>Wrap the <code>user.can_view(obj)</code> method. To see an example, look at the example in the module docstring.</p>"},{"location":"reference/core/api_permissions/#core.api_permissions.CanEdit","title":"<code>CanEdit</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that this user has the permission to edit the object of this route.</p> <p>Wrap the <code>user.can_edit(obj)</code> method. To see an example, look at the example in the module docstring.</p>"},{"location":"reference/core/api_permissions/#core.api_permissions.IsOwner","title":"<code>IsOwner</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that this user owns the object of this route.</p> <p>Wrap the <code>user.is_owner(obj)</code> method. To see an example, look at the example in the module docstring.</p>"},{"location":"reference/core/api_permissions/#core.api_permissions.IsLoggedInCounter","title":"<code>IsLoggedInCounter</code>","text":"<p> Bases: <code>BasePermission</code></p> <p>Check that a user is logged in a counter.</p>"},{"location":"reference/core/model_fields/","title":"Champs de mod\u00e8le","text":""},{"location":"reference/core/model_fields/#core.fields.ResizedImageFieldFile","title":"<code>ResizedImageFieldFile</code>","text":"<p> Bases: <code>ImageFieldFile</code></p>"},{"location":"reference/core/model_fields/#core.fields.ResizedImageFieldFile.get_resized_dimensions","title":"<code>get_resized_dimensions(image)</code>","text":"<p>Get the dimensions of the resized image.</p> <p>If the width and height are given, they are used. If only one is given, the other is calculated to keep the same ratio.</p> <p>Returns:</p> Type Description <code>tuple[int, int]</code> <p>Tuple of width and height</p> Source code in <code>core/fields.py</code> <pre><code>def get_resized_dimensions(self, image: Image.Image) -&gt; tuple[int, int]:\n \"\"\"Get the dimensions of the resized image.\n\n If the width and height are given, they are used.\n If only one is given, the other is calculated to keep the same ratio.\n\n Returns:\n Tuple of width and height\n \"\"\"\n width = self.field.width\n height = self.field.height\n if width is not None and height is not None:\n return self.field.width, self.field.height\n if width is None:\n width = int(image.width * height / image.height)\n elif height is None:\n height = int(image.height * width / image.width)\n return width, height\n</code></pre>"},{"location":"reference/core/model_fields/#core.fields.ResizedImageFieldFile.get_name","title":"<code>get_name()</code>","text":"<p>Get the name of the resized image.</p> <p>If the field has a force_format attribute, the extension of the file will be changed to match it. Otherwise, the name is left unchanged.</p> <p>Raises:</p> Type Description <code>ValueError</code> <p>If the image format is unknown</p> Source code in <code>core/fields.py</code> <pre><code>def get_name(self) -&gt; str:\n \"\"\"Get the name of the resized image.\n\n If the field has a force_format attribute,\n the extension of the file will be changed to match it.\n Otherwise, the name is left unchanged.\n\n Raises:\n ValueError: If the image format is unknown\n \"\"\"\n if not self.field.force_format:\n return self.name\n formats = {val: key for key, val in Image.registered_extensions().items()}\n new_format = self.field.force_format\n if new_format in formats:\n extension = formats[new_format]\n else:\n raise ValueError(f\"Unknown format {new_format}\")\n return str(Path(self.file.name).with_suffix(extension))\n</code></pre>"},{"location":"reference/core/model_fields/#core.fields.ResizedImageField","title":"<code>ResizedImageField(width=None, height=None, force_format=None, **kwargs)</code>","text":"<p> Bases: <code>ImageField</code></p> <p>A field that automatically resizes images to a given size.</p> <p>This field is useful for profile pictures or product icons, for example.</p> <p>The final size of the image is determined by the width and height parameters :</p> <ul> <li>If both are given, the image will be resized to fit in a rectangle of width x height</li> <li>If only one is given, the other will be calculated to keep the same ratio</li> </ul> <p>If the force_format parameter is given, the image will be converted to this format.</p> <p>Examples:</p> <p>To resize an image with a height of 100px, without changing the ratio, and a format of WEBP :</p> <pre><code>class Product(models.Model):\n icon = ResizedImageField(height=100, force_format=\"WEBP\")\n</code></pre> <p>To explicitly resize an image to 100x100px (but possibly change the ratio) :</p> <pre><code>class Product(models.Model):\n icon = ResizedImageField(width=100, height=100)\n</code></pre> <p>Raises:</p> Type Description <code>FieldError</code> <p>If neither width nor height is given</p> <p>Parameters:</p> Name Type Description Default <code>width</code> <code>int | None</code> <p>If given, the width of the resized image</p> <code>None</code> <code>height</code> <code>int | None</code> <p>If given, the height of the resized image</p> <code>None</code> <code>force_format</code> <code>str | None</code> <p>If given, the image will be converted to this format</p> <code>None</code> Source code in <code>core/fields.py</code> <pre><code>def __init__(\n self,\n width: int | None = None,\n height: int | None = None,\n force_format: str | None = None,\n **kwargs,\n):\n if width is None and height is None:\n raise FieldError(\n f\"{self.__class__.__name__} requires \"\n \"width, height or both, but got neither\"\n )\n self.width = width\n self.height = height\n self.force_format = force_format\n super().__init__(**kwargs)\n</code></pre>"},{"location":"reference/core/models/","title":"Models","text":""},{"location":"reference/core/models/#core.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/core/models/#core.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/core/models/#core.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/core/models/#core.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/core/models/#core.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/core/models/#core.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/core/models/#core.models.RealGroupManager","title":"<code>RealGroupManager</code>","text":"<p> Bases: <code>GroupManager</code></p>"},{"location":"reference/core/models/#core.models.MetaGroupManager","title":"<code>MetaGroupManager</code>","text":"<p> Bases: <code>GroupManager</code></p>"},{"location":"reference/core/models/#core.models.Group","title":"<code>Group</code>","text":"<p> Bases: <code>Group</code></p> <p>Implement both RealGroups and Meta groups.</p> <p>Groups are sorted by their is_meta property</p>"},{"location":"reference/core/models/#core.models.MetaGroup","title":"<code>MetaGroup(*args, **kwargs)</code>","text":"<p> Bases: <code>Group</code></p> <p>MetaGroups are dynamically created groups.</p> <p>Generally used with clubs where creating a club creates two groups:</p> <ul> <li>club-SITH_BOARD_SUFFIX</li> <li>club-SITH_MEMBER_SUFFIX</li> </ul> Source code in <code>core/models.py</code> <pre><code>def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.is_meta = True\n</code></pre>"},{"location":"reference/core/models/#core.models.MetaGroup.associated_club","title":"<code>associated_club()</code>","text":"<p>Return the group associated with this meta group.</p> <p>The result of this function is cached</p> <p>Returns:</p> Type Description <code>Club | None</code> <p>The associated club if it exists, else None</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef associated_club(self) -&gt; Club | None:\n \"\"\"Return the group associated with this meta group.\n\n The result of this function is cached\n\n\n Returns:\n The associated club if it exists, else None\n \"\"\"\n from club.models import Club\n\n if self.name.endswith(settings.SITH_BOARD_SUFFIX):\n # replace this with str.removesuffix as soon as Python\n # is upgraded to 3.10\n club_name = self.name[: -len(settings.SITH_BOARD_SUFFIX)]\n elif self.name.endswith(settings.SITH_MEMBER_SUFFIX):\n club_name = self.name[: -len(settings.SITH_MEMBER_SUFFIX)]\n else:\n return None\n club = cache.get(f\"sith_club_{club_name}\")\n if club is None:\n club = Club.objects.filter(unix_name=club_name).first()\n cache.set(f\"sith_club_{club_name}\", club)\n return club\n</code></pre>"},{"location":"reference/core/models/#core.models.RealGroup","title":"<code>RealGroup</code>","text":"<p> Bases: <code>Group</code></p> <p>RealGroups are created by the developer.</p> <p>Most of the time they match a number in settings to be easily used for permissions.</p>"},{"location":"reference/core/models/#core.models.UserQuerySet","title":"<code>UserQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/core/models/#core.models.CustomUserManager","title":"<code>CustomUserManager</code>","text":"<p> Bases: <code>from_queryset(UserQuerySet)</code></p>"},{"location":"reference/core/models/#core.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/core/models/#core.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/core/models/#core.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/core/models/#core.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/core/models/#core.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/core/models/#core.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/core/models/#core.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/core/models/#core.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/core/models/#core.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/core/models/#core.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/core/models/#core.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/core/models/#core.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/core/models/#core.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/core/models/#core.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/core/models/#core.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/core/models/#core.models.AnonymousUser","title":"<code>AnonymousUser()</code>","text":"<p> Bases: <code>AnonymousUser</code></p> Source code in <code>core/models.py</code> <pre><code>def __init__(self):\n super().__init__()\n</code></pre>"},{"location":"reference/core/models/#core.models.AnonymousUser.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>The anonymous user is only in the public group.</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"The anonymous user is only in the public group.\"\"\"\n allowed_id = settings.SITH_GROUP_PUBLIC_ID\n if pk is not None:\n return pk == allowed_id\n elif name is not None:\n group = get_group(name=name)\n return group is not None and group.id == allowed_id\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n</code></pre>"},{"location":"reference/core/models/#core.models.Preferences","title":"<code>Preferences</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/models/#core.models.SithFile","title":"<code>SithFile</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/models/#core.models.SithFile.clean","title":"<code>clean()</code>","text":"<p>Cleans up the file.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up the file.\"\"\"\n super().clean()\n if \"/\" in self.name:\n raise ValidationError(_(\"Character '/' not authorized in name\"))\n if self == self.parent:\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self == self.parent or (\n self.parent is not None and self in self.get_parent_list()\n ):\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self.parent and self.parent.is_file:\n raise ValidationError(\n _(\"You can not make a file be a children of a non folder file\")\n )\n if (\n self.parent is None\n and SithFile.objects.exclude(id=self.id)\n .filter(parent=None, name=self.name)\n .exists()\n ) or (\n self.parent\n and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()\n ):\n raise ValidationError(_(\"Duplicate file\"), code=\"duplicate\")\n if self.is_folder:\n if self.file:\n try:\n import imghdr\n\n if imghdr.what(None, self.file.read()) not in [\n \"gif\",\n \"png\",\n \"jpeg\",\n ]:\n self.file.delete()\n self.file = None\n except: # noqa E722 I don't know the exception that can be raised\n self.file = None\n self.mime_type = \"inode/directory\"\n if self.is_file and (self.file is None or self.file == \"\"):\n raise ValidationError(_(\"You must provide a file\"))\n</code></pre>"},{"location":"reference/core/models/#core.models.SithFile.apply_rights_recursively","title":"<code>apply_rights_recursively(*, only_folders=False)</code>","text":"<p>Apply the rights of this file to all children recursively.</p> <p>Parameters:</p> Name Type Description Default <code>only_folders</code> <code>bool</code> <p>If True, only apply the rights to SithFiles that are folders.</p> <code>False</code> Source code in <code>core/models.py</code> <pre><code>def apply_rights_recursively(self, *, only_folders: bool = False) -&gt; None:\n \"\"\"Apply the rights of this file to all children recursively.\n\n Args:\n only_folders: If True, only apply the rights to SithFiles that are folders.\n \"\"\"\n file_ids = []\n explored_ids = [self.id]\n while len(explored_ids) &gt; 0: # find all children recursively\n file_ids.extend(explored_ids)\n next_level = SithFile.objects.filter(parent_id__in=explored_ids)\n if only_folders:\n next_level = next_level.filter(is_folder=True)\n explored_ids = list(next_level.values_list(\"id\", flat=True))\n for through in (SithFile.view_groups.through, SithFile.edit_groups.through):\n # force evaluation. Without this, the iterator yields nothing\n groups = list(\n through.objects.filter(sithfile_id=self.id).values_list(\n \"group_id\", flat=True\n )\n )\n # delete previous rights\n through.objects.filter(sithfile_id__in=file_ids).delete()\n through.objects.bulk_create( # create new rights\n [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]\n )\n</code></pre>"},{"location":"reference/core/models/#core.models.SithFile.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>core/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n</code></pre>"},{"location":"reference/core/models/#core.models.SithFile.move_to","title":"<code>move_to(parent)</code>","text":"<p>Move a file to a new parent. <code>parent</code> must be a SithFile with the <code>is_folder=True</code> property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.</p> Source code in <code>core/models.py</code> <pre><code>def move_to(self, parent):\n \"\"\"Move a file to a new parent.\n `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change\n anything.\n This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify\n SithFiles recursively, so it stays efficient even with top-level folders.\n \"\"\"\n if not parent.is_folder:\n return\n self.parent = parent\n self.clean()\n self.save()\n</code></pre>"},{"location":"reference/core/models/#core.models.LockError","title":"<code>LockError</code>","text":"<p> Bases: <code>Exception</code></p> <p>There was a lock error on the object.</p>"},{"location":"reference/core/models/#core.models.AlreadyLocked","title":"<code>AlreadyLocked</code>","text":"<p> Bases: <code>LockError</code></p> <p>The object is already locked.</p>"},{"location":"reference/core/models/#core.models.NotLocked","title":"<code>NotLocked</code>","text":"<p> Bases: <code>LockError</code></p> <p>The object is not locked.</p>"},{"location":"reference/core/models/#core.models.Page","title":"<code>Page</code>","text":"<p> Bases: <code>Model</code></p> <p>The page class to build a Wiki Each page may have a parent and it's URL is of the form my.site/page/// It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is awkward! Prefere querying pages with Page.get_page_by_full_name(). <p>Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when playing with a Page object, use get_full_name() instead!</p>"},{"location":"reference/core/models/#core.models.Page.save","title":"<code>save(*args, **kwargs)</code>","text":"<p>Performs some needed actions before and after saving a page in database.</p> Source code in <code>core/models.py</code> <pre><code>def save(self, *args, **kwargs):\n \"\"\"Performs some needed actions before and after saving a page in database.\"\"\"\n locked = kwargs.pop(\"force_lock\", False)\n if not locked:\n locked = self.is_locked()\n if not locked:\n raise NotLocked(\"The page is not locked and thus can not be saved\")\n self.full_clean()\n if not self.id:\n super().save(\n *args, **kwargs\n ) # Save a first time to correctly set _full_name\n # This reset the _full_name just before saving to maintain a coherent field quicker for queries than the\n # recursive method\n # It also update all the children to maintain correct names\n self._full_name = self.get_full_name()\n for c in self.children.all():\n c.save()\n super().save(*args, **kwargs)\n self.unset_lock()\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.get_page_by_full_name","title":"<code>get_page_by_full_name(name)</code> <code>staticmethod</code>","text":"<p>Quicker to get a page with that method rather than building the request every time.</p> Source code in <code>core/models.py</code> <pre><code>@staticmethod\ndef get_page_by_full_name(name):\n \"\"\"Quicker to get a page with that method rather than building the request every time.\"\"\"\n return Page.objects.filter(_full_name=name).first()\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.clean","title":"<code>clean()</code>","text":"<p>Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.\"\"\"\n if \"/\" in self.name:\n self.name = self.name.split(\"/\")[-1]\n if (\n Page.objects.exclude(pk=self.pk)\n .filter(_full_name=self.get_full_name())\n .exists()\n ):\n raise ValidationError(_(\"Duplicate page\"), code=\"duplicate\")\n super().clean()\n if self.parent is not None and self in self.get_parent_list():\n raise ValidationError(_(\"Loop in page tree\"), code=\"loop\")\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.is_locked","title":"<code>is_locked()</code>","text":"<p>Is True if the page is locked, False otherwise.</p> <p>This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this function will return False.</p> Source code in <code>core/models.py</code> <pre><code>def is_locked(self):\n \"\"\"Is True if the page is locked, False otherwise.\n\n This is where the timeout is handled,\n so a locked page for which the timeout is reach will be unlocked and this\n function will return False.\n \"\"\"\n if self.lock_timeout and (\n timezone.now() - self.lock_timeout &gt; timedelta(minutes=5)\n ):\n self.unset_lock()\n return (\n self.lock_user\n and self.lock_timeout\n and (timezone.now() - self.lock_timeout &lt; timedelta(minutes=5))\n )\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.set_lock","title":"<code>set_lock(user)</code>","text":"<p>Sets a lock on the current page or raise an AlreadyLocked exception.</p> Source code in <code>core/models.py</code> <pre><code>def set_lock(self, user):\n \"\"\"Sets a lock on the current page or raise an AlreadyLocked exception.\"\"\"\n if self.is_locked() and self.get_lock() != user:\n raise AlreadyLocked(\"The page is already locked by someone else\")\n self.lock_user = user\n self.lock_timeout = timezone.now()\n super().save()\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.set_lock_recursive","title":"<code>set_lock_recursive(user)</code>","text":"<p>Locks recursively all the child pages for editing properties.</p> Source code in <code>core/models.py</code> <pre><code>def set_lock_recursive(self, user):\n \"\"\"Locks recursively all the child pages for editing properties.\"\"\"\n for p in self.children.all():\n p.set_lock_recursive(user)\n self.set_lock(user)\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.unset_lock_recursive","title":"<code>unset_lock_recursive()</code>","text":"<p>Unlocks recursively all the child pages.</p> Source code in <code>core/models.py</code> <pre><code>def unset_lock_recursive(self):\n \"\"\"Unlocks recursively all the child pages.\"\"\"\n for p in self.children.all():\n p.unset_lock_recursive()\n self.unset_lock()\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.unset_lock","title":"<code>unset_lock()</code>","text":"<p>Always try to unlock, even if there is no lock.</p> Source code in <code>core/models.py</code> <pre><code>def unset_lock(self):\n \"\"\"Always try to unlock, even if there is no lock.\"\"\"\n self.lock_user = None\n self.lock_timeout = None\n super().save()\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.get_lock","title":"<code>get_lock()</code>","text":"<p>Returns the page's mutex containing the time and the user in a dict.</p> Source code in <code>core/models.py</code> <pre><code>def get_lock(self):\n \"\"\"Returns the page's mutex containing the time and the user in a dict.\"\"\"\n if self.lock_user:\n return self.lock_user\n raise NotLocked(\"The page is not locked and thus can not return its user\")\n</code></pre>"},{"location":"reference/core/models/#core.models.Page.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Computes the real full_name of the page based on its name and its parent's name You can and must rely on this function when working on a page object that is not freshly fetched from the DB (For example when treating a Page object coming from a form).</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Computes the real full_name of the page based on its name and its parent's name\n You can and must rely on this function when working on a page object that is not freshly fetched from the DB\n (For example when treating a Page object coming from a form).\n \"\"\"\n if self.parent is None:\n return self.name\n return f\"{self.parent.get_full_name()}/{self.name}\"\n</code></pre>"},{"location":"reference/core/models/#core.models.PageRev","title":"<code>PageRev</code>","text":"<p> Bases: <code>Model</code></p> <p>True content of the page.</p> <p>Each page object has a revisions field that is a list of PageRev, ordered by date. my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus, is the real content of the page. The content is in PageRev.title and PageRev.content .</p>"},{"location":"reference/core/models/#core.models.Notification","title":"<code>Notification</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/models/#core.models.Gift","title":"<code>Gift</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/models/#core.models.OperationLog","title":"<code>OperationLog</code>","text":"<p> Bases: <code>Model</code></p> <p>General purpose log object to register operations.</p>"},{"location":"reference/core/models/#core.models.validate_promo","title":"<code>validate_promo(value)</code>","text":"Source code in <code>core/models.py</code> <pre><code>def validate_promo(value: int) -&gt; None:\n start_year = settings.SITH_SCHOOL_START_YEAR\n delta = (localdate() + timedelta(days=180)).year - start_year\n if value &lt; 0 or delta &lt; value:\n raise ValidationError(\n _(\"%(value)s is not a valid promo (between 0 and %(end)s)\"),\n params={\"value\": value, \"end\": delta},\n )\n</code></pre>"},{"location":"reference/core/models/#core.models.get_group","title":"<code>get_group(*, pk=None, name=None)</code>","text":"<p>Search for a group by its primary key or its name. Either one of the two must be set.</p> <p>The result is cached for the default duration (should be 5 minutes).</p> <p>Parameters:</p> Name Type Description Default <code>pk</code> <code>int | None</code> <p>The primary key of the group</p> <code>None</code> <code>name</code> <code>str | None</code> <p>The name of the group</p> <code>None</code> <p>Returns:</p> Type Description <code>Group | None</code> <p>The group if it exists, else None</p> <p>Raises:</p> Type Description <code>ValueError</code> <p>If no group matches the criteria</p> Source code in <code>core/models.py</code> <pre><code>def get_group(*, pk: int | None = None, name: str | None = None) -&gt; Group | None:\n \"\"\"Search for a group by its primary key or its name.\n Either one of the two must be set.\n\n The result is cached for the default duration (should be 5 minutes).\n\n Args:\n pk: The primary key of the group\n name: The name of the group\n\n Returns:\n The group if it exists, else None\n\n Raises:\n ValueError: If no group matches the criteria\n \"\"\"\n if pk is None and name is None:\n raise ValueError(\"Either pk or name must be set\")\n\n # replace space characters to hide warnings with memcached backend\n pk_or_name: str | int = pk if pk is not None else name.replace(\" \", \"_\")\n group = cache.get(f\"sith_group_{pk_or_name}\")\n\n if group == \"not_found\":\n # Using None as a cache value is a little bit tricky,\n # so we use a special string to represent None\n return None\n elif group is not None:\n return group\n # if this point is reached, the group is not in cache\n if pk is not None:\n group = Group.objects.filter(pk=pk).first()\n else:\n group = Group.objects.filter(name=name).first()\n if group is not None:\n cache.set(f\"sith_group_{group.id}\", group)\n cache.set(f\"sith_group_{group.name.replace(' ', '_')}\", group)\n else:\n cache.set(f\"sith_group_{pk_or_name}\", \"not_found\")\n return group\n</code></pre>"},{"location":"reference/core/models/#core.models.get_directory","title":"<code>get_directory(instance, filename)</code>","text":"Source code in <code>core/models.py</code> <pre><code>def get_directory(instance, filename):\n return \".{0}/{1}\".format(instance.get_parent_path(), filename)\n</code></pre>"},{"location":"reference/core/models/#core.models.get_compressed_directory","title":"<code>get_compressed_directory(instance, filename)</code>","text":"Source code in <code>core/models.py</code> <pre><code>def get_compressed_directory(instance, filename):\n return \"./.compressed/{0}/{1}\".format(instance.get_parent_path(), filename)\n</code></pre>"},{"location":"reference/core/models/#core.models.get_thumbnail_directory","title":"<code>get_thumbnail_directory(instance, filename)</code>","text":"Source code in <code>core/models.py</code> <pre><code>def get_thumbnail_directory(instance, filename):\n return \"./.thumbnails/{0}/{1}\".format(instance.get_parent_path(), filename)\n</code></pre>"},{"location":"reference/core/models/#core.models.get_default_owner_group","title":"<code>get_default_owner_group()</code>","text":"Source code in <code>core/models.py</code> <pre><code>def get_default_owner_group():\n return settings.SITH_GROUP_ROOT_ID\n</code></pre>"},{"location":"reference/core/schemas/","title":"Schemas","text":""},{"location":"reference/core/schemas/#core.schemas.Group","title":"<code>Group</code>","text":"<p> Bases: <code>Group</code></p> <p>Implement both RealGroups and Meta groups.</p> <p>Groups are sorted by their is_meta property</p>"},{"location":"reference/core/schemas/#core.schemas.SithFile","title":"<code>SithFile</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/schemas/#core.schemas.SithFile.clean","title":"<code>clean()</code>","text":"<p>Cleans up the file.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up the file.\"\"\"\n super().clean()\n if \"/\" in self.name:\n raise ValidationError(_(\"Character '/' not authorized in name\"))\n if self == self.parent:\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self == self.parent or (\n self.parent is not None and self in self.get_parent_list()\n ):\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self.parent and self.parent.is_file:\n raise ValidationError(\n _(\"You can not make a file be a children of a non folder file\")\n )\n if (\n self.parent is None\n and SithFile.objects.exclude(id=self.id)\n .filter(parent=None, name=self.name)\n .exists()\n ) or (\n self.parent\n and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()\n ):\n raise ValidationError(_(\"Duplicate file\"), code=\"duplicate\")\n if self.is_folder:\n if self.file:\n try:\n import imghdr\n\n if imghdr.what(None, self.file.read()) not in [\n \"gif\",\n \"png\",\n \"jpeg\",\n ]:\n self.file.delete()\n self.file = None\n except: # noqa E722 I don't know the exception that can be raised\n self.file = None\n self.mime_type = \"inode/directory\"\n if self.is_file and (self.file is None or self.file == \"\"):\n raise ValidationError(_(\"You must provide a file\"))\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.SithFile.apply_rights_recursively","title":"<code>apply_rights_recursively(*, only_folders=False)</code>","text":"<p>Apply the rights of this file to all children recursively.</p> <p>Parameters:</p> Name Type Description Default <code>only_folders</code> <code>bool</code> <p>If True, only apply the rights to SithFiles that are folders.</p> <code>False</code> Source code in <code>core/models.py</code> <pre><code>def apply_rights_recursively(self, *, only_folders: bool = False) -&gt; None:\n \"\"\"Apply the rights of this file to all children recursively.\n\n Args:\n only_folders: If True, only apply the rights to SithFiles that are folders.\n \"\"\"\n file_ids = []\n explored_ids = [self.id]\n while len(explored_ids) &gt; 0: # find all children recursively\n file_ids.extend(explored_ids)\n next_level = SithFile.objects.filter(parent_id__in=explored_ids)\n if only_folders:\n next_level = next_level.filter(is_folder=True)\n explored_ids = list(next_level.values_list(\"id\", flat=True))\n for through in (SithFile.view_groups.through, SithFile.edit_groups.through):\n # force evaluation. Without this, the iterator yields nothing\n groups = list(\n through.objects.filter(sithfile_id=self.id).values_list(\n \"group_id\", flat=True\n )\n )\n # delete previous rights\n through.objects.filter(sithfile_id__in=file_ids).delete()\n through.objects.bulk_create( # create new rights\n [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]\n )\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.SithFile.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>core/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.SithFile.move_to","title":"<code>move_to(parent)</code>","text":"<p>Move a file to a new parent. <code>parent</code> must be a SithFile with the <code>is_folder=True</code> property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.</p> Source code in <code>core/models.py</code> <pre><code>def move_to(self, parent):\n \"\"\"Move a file to a new parent.\n `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change\n anything.\n This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify\n SithFiles recursively, so it stays efficient even with top-level folders.\n \"\"\"\n if not parent.is_folder:\n return\n self.parent = parent\n self.clean()\n self.save()\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/core/schemas/#core.schemas.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/core/schemas/#core.schemas.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/core/schemas/#core.schemas.SimpleUserSchema","title":"<code>SimpleUserSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>A schema with the minimum amount of information to represent a user.</p>"},{"location":"reference/core/schemas/#core.schemas.UserProfileSchema","title":"<code>UserProfileSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>The necessary information to show a user profile</p>"},{"location":"reference/core/schemas/#core.schemas.SithFileSchema","title":"<code>SithFileSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/core/schemas/#core.schemas.GroupSchema","title":"<code>GroupSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/core/schemas/#core.schemas.UserFilterSchema","title":"<code>UserFilterSchema</code>","text":"<p> Bases: <code>FilterSchema</code></p>"},{"location":"reference/core/schemas/#core.schemas.MarkdownSchema","title":"<code>MarkdownSchema</code>","text":"<p> Bases: <code>Schema</code></p>"},{"location":"reference/core/schemas/#core.schemas.FamilyGodfatherSchema","title":"<code>FamilyGodfatherSchema</code>","text":"<p> Bases: <code>Schema</code></p>"},{"location":"reference/core/schemas/#core.schemas.UserFamilySchema","title":"<code>UserFamilySchema</code>","text":"<p> Bases: <code>Schema</code></p> <p>Represent a graph of a user's family</p>"},{"location":"reference/core/views/","title":"Views","text":""},{"location":"reference/core/views/#core.views.GenericContentPermissionMixinBuilder","title":"<code>GenericContentPermissionMixinBuilder</code>","text":"<p> Bases: <code>View</code></p> <p>Used to build permission mixins.</p> <p>This view protect any child view that would be showing an object that is restricted based on two properties.</p> <p>Attributes:</p> Name Type Description <code>raised_error</code> <p>permission to be raised</p>"},{"location":"reference/core/views/#core.views.GenericContentPermissionMixinBuilder.permission_function","title":"<code>permission_function(obj, user)</code> <code>staticmethod</code>","text":"<p>Function to test permission with.</p> Source code in <code>core/views/__init__.py</code> <pre><code>@staticmethod\ndef permission_function(obj: Any, user: User) -&gt; bool:\n \"\"\"Function to test permission with.\"\"\"\n return False\n</code></pre>"},{"location":"reference/core/views/#core.views.CanCreateMixin","title":"<code>CanCreateMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect any child view that would create an object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user has not the necessary permission to create the object of the view.</p>"},{"location":"reference/core/views/#core.views.CanEditPropMixin","title":"<code>CanEditPropMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has owner permissions on the child view object.</p> <p>In other word, you can make a view with this view as parent, and it will be retricted to the users that are in the object's owner_group or that pass the <code>obj.can_be_viewed_by</code> test.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user cannot see the object</p>"},{"location":"reference/core/views/#core.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/core/views/#core.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/core/views/#core.views.UserIsRootMixin","title":"<code>UserIsRootMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Allow only root admins.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user isn't root</p>"},{"location":"reference/core/views/#core.views.FormerSubscriberMixin","title":"<code>FormerSubscriberMixin</code>","text":"<p> Bases: <code>AccessMixin</code></p> <p>Check if the user was at least an old subscriber.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user never subscribed.</p>"},{"location":"reference/core/views/#core.views.SubscriberMixin","title":"<code>SubscriberMixin</code>","text":"<p> Bases: <code>AccessMixin</code></p>"},{"location":"reference/core/views/#core.views.TabedViewMixin","title":"<code>TabedViewMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Basic functions for displaying tabs in the template.</p>"},{"location":"reference/core/views/#core.views.QuickNotifMixin","title":"<code>QuickNotifMixin</code>","text":""},{"location":"reference/core/views/#core.views.QuickNotifMixin.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add quick notifications to context.</p> Source code in <code>core/views/__init__.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add quick notifications to context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"quick_notifs\"] = []\n for n in self.quick_notif_list:\n kwargs[\"quick_notifs\"].append(settings.SITH_QUICK_NOTIF[n])\n for key, val in settings.SITH_QUICK_NOTIF.items():\n for gk in self.request.GET:\n if key == gk:\n kwargs[\"quick_notifs\"].append(val)\n return kwargs\n</code></pre>"},{"location":"reference/core/views/#core.views.DetailFormView","title":"<code>DetailFormView</code>","text":"<p> Bases: <code>SingleObjectMixin</code>, <code>FormView</code></p> <p>Class that allow both a detail view and a form view.</p>"},{"location":"reference/core/views/#core.views.DetailFormView.get_object","title":"<code>get_object()</code>","text":"<p>Get current group from id in url.</p> Source code in <code>core/views/__init__.py</code> <pre><code>def get_object(self):\n \"\"\"Get current group from id in url.\"\"\"\n return self.cached_object\n</code></pre>"},{"location":"reference/core/views/#core.views.DetailFormView.cached_object","title":"<code>cached_object()</code>","text":"<p>Optimisation on group retrieval.</p> Source code in <code>core/views/__init__.py</code> <pre><code>@cached_property\ndef cached_object(self):\n \"\"\"Optimisation on group retrieval.\"\"\"\n return super().get_object()\n</code></pre>"},{"location":"reference/core/views/#core.views.SithFile","title":"<code>SithFile</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/views/#core.views.SithFile.clean","title":"<code>clean()</code>","text":"<p>Cleans up the file.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up the file.\"\"\"\n super().clean()\n if \"/\" in self.name:\n raise ValidationError(_(\"Character '/' not authorized in name\"))\n if self == self.parent:\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self == self.parent or (\n self.parent is not None and self in self.get_parent_list()\n ):\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self.parent and self.parent.is_file:\n raise ValidationError(\n _(\"You can not make a file be a children of a non folder file\")\n )\n if (\n self.parent is None\n and SithFile.objects.exclude(id=self.id)\n .filter(parent=None, name=self.name)\n .exists()\n ) or (\n self.parent\n and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()\n ):\n raise ValidationError(_(\"Duplicate file\"), code=\"duplicate\")\n if self.is_folder:\n if self.file:\n try:\n import imghdr\n\n if imghdr.what(None, self.file.read()) not in [\n \"gif\",\n \"png\",\n \"jpeg\",\n ]:\n self.file.delete()\n self.file = None\n except: # noqa E722 I don't know the exception that can be raised\n self.file = None\n self.mime_type = \"inode/directory\"\n if self.is_file and (self.file is None or self.file == \"\"):\n raise ValidationError(_(\"You must provide a file\"))\n</code></pre>"},{"location":"reference/core/views/#core.views.SithFile.apply_rights_recursively","title":"<code>apply_rights_recursively(*, only_folders=False)</code>","text":"<p>Apply the rights of this file to all children recursively.</p> <p>Parameters:</p> Name Type Description Default <code>only_folders</code> <code>bool</code> <p>If True, only apply the rights to SithFiles that are folders.</p> <code>False</code> Source code in <code>core/models.py</code> <pre><code>def apply_rights_recursively(self, *, only_folders: bool = False) -&gt; None:\n \"\"\"Apply the rights of this file to all children recursively.\n\n Args:\n only_folders: If True, only apply the rights to SithFiles that are folders.\n \"\"\"\n file_ids = []\n explored_ids = [self.id]\n while len(explored_ids) &gt; 0: # find all children recursively\n file_ids.extend(explored_ids)\n next_level = SithFile.objects.filter(parent_id__in=explored_ids)\n if only_folders:\n next_level = next_level.filter(is_folder=True)\n explored_ids = list(next_level.values_list(\"id\", flat=True))\n for through in (SithFile.view_groups.through, SithFile.edit_groups.through):\n # force evaluation. Without this, the iterator yields nothing\n groups = list(\n through.objects.filter(sithfile_id=self.id).values_list(\n \"group_id\", flat=True\n )\n )\n # delete previous rights\n through.objects.filter(sithfile_id__in=file_ids).delete()\n through.objects.bulk_create( # create new rights\n [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]\n )\n</code></pre>"},{"location":"reference/core/views/#core.views.SithFile.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>core/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n</code></pre>"},{"location":"reference/core/views/#core.views.SithFile.move_to","title":"<code>move_to(parent)</code>","text":"<p>Move a file to a new parent. <code>parent</code> must be a SithFile with the <code>is_folder=True</code> property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.</p> Source code in <code>core/models.py</code> <pre><code>def move_to(self, parent):\n \"\"\"Move a file to a new parent.\n `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change\n anything.\n This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify\n SithFiles recursively, so it stays efficient even with top-level folders.\n \"\"\"\n if not parent.is_folder:\n return\n self.parent = parent\n self.clean()\n self.save()\n</code></pre>"},{"location":"reference/core/views/#core.views.MultipleFileInput","title":"<code>MultipleFileInput</code>","text":"<p> Bases: <code>ClearableFileInput</code></p>"},{"location":"reference/core/views/#core.views.MultipleFileField","title":"<code>MultipleFileField(*args, **kwargs)</code>","text":"<p> Bases: <code>_MultipleFieldMixin</code>, <code>FileField</code></p> Source code in <code>core/views/files.py</code> <pre><code>def __init__(self, *args, **kwargs):\n kwargs.setdefault(\"widget\", MultipleFileInput())\n super().__init__(*args, **kwargs)\n</code></pre>"},{"location":"reference/core/views/#core.views.MultipleImageField","title":"<code>MultipleImageField(*args, **kwargs)</code>","text":"<p> Bases: <code>_MultipleFieldMixin</code>, <code>ImageField</code></p> Source code in <code>core/views/files.py</code> <pre><code>def __init__(self, *args, **kwargs):\n kwargs.setdefault(\"widget\", MultipleFileInput())\n super().__init__(*args, **kwargs)\n</code></pre>"},{"location":"reference/core/views/#core.views.AddFilesForm","title":"<code>AddFilesForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/core/views/#core.views.FileListView","title":"<code>FileListView</code>","text":"<p> Bases: <code>ListView</code></p>"},{"location":"reference/core/views/#core.views.FileEditView","title":"<code>FileEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/core/views/#core.views.FileEditPropForm","title":"<code>FileEditPropForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/core/views/#core.views.FileEditPropView","title":"<code>FileEditPropView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/core/views/#core.views.FileView","title":"<code>FileView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code>, <code>FormMixin</code></p> <p>Handle the upload of new files into a folder.</p>"},{"location":"reference/core/views/#core.views.FileView.handle_clipboard","title":"<code>handle_clipboard(request, obj)</code> <code>staticmethod</code>","text":"<p>Handle the clipboard in the view.</p> <p>This method can fail, since it does not catch the exceptions coming from below, allowing proper handling in the calling view. Use this method like this:</p> <pre><code>FileView.handle_clipboard(request, self.object)\n</code></pre> <p><code>request</code> is usually the self.request obj in your view <code>obj</code> is the SithFile object you want to put in the clipboard, or where you want to paste the clipboard</p> Source code in <code>core/views/files.py</code> <pre><code>@staticmethod\ndef handle_clipboard(request, obj):\n \"\"\"Handle the clipboard in the view.\n\n This method can fail, since it does not catch the exceptions coming from\n below, allowing proper handling in the calling view.\n Use this method like this:\n\n FileView.handle_clipboard(request, self.object)\n\n `request` is usually the self.request obj in your view\n `obj` is the SithFile object you want to put in the clipboard, or\n where you want to paste the clipboard\n \"\"\"\n if \"delete\" in request.POST:\n for f_id in request.POST.getlist(\"file_list\"):\n file = SithFile.objects.filter(id=f_id).first()\n if file:\n file.delete()\n if \"clear\" in request.POST:\n request.session[\"clipboard\"] = []\n if \"cut\" in request.POST:\n for f_id_str in request.POST.getlist(\"file_list\"):\n f_id = int(f_id_str)\n if (\n f_id in [c.id for c in obj.children.all()]\n and f_id not in request.session[\"clipboard\"]\n ):\n request.session[\"clipboard\"].append(f_id)\n if \"paste\" in request.POST:\n for f_id in request.session[\"clipboard\"]:\n file = SithFile.objects.filter(id=f_id).first()\n if file:\n file.move_to(obj)\n request.session[\"clipboard\"] = []\n request.session.modified = True\n</code></pre>"},{"location":"reference/core/views/#core.views.FileDeleteView","title":"<code>FileDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/core/views/#core.views.FileModerationView","title":"<code>FileModerationView</code>","text":"<p> Bases: <code>TemplateView</code></p>"},{"location":"reference/core/views/#core.views.FileModerateView","title":"<code>FileModerateView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>SingleObjectMixin</code></p>"},{"location":"reference/core/views/#core.views.RealGroup","title":"<code>RealGroup</code>","text":"<p> Bases: <code>Group</code></p> <p>RealGroups are created by the developer.</p> <p>Most of the time they match a number in settings to be easily used for permissions.</p>"},{"location":"reference/core/views/#core.views.EditMembersForm","title":"<code>EditMembersForm(*args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> <p>Add and remove members from a Group.</p> Source code in <code>core/views/group.py</code> <pre><code>def __init__(self, *args, **kwargs):\n self.current_users = kwargs.pop(\"users\", [])\n super().__init__(*args, **kwargs)\n\n self.fields[\"users_added\"] = forms.ModelMultipleChoiceField(\n label=_(\"Users to add to group\"),\n help_text=_(\"Search users to add (one or more).\"),\n required=False,\n widget=AutoCompleteSelectMultipleUser,\n queryset=User.objects.exclude(id__in=self.current_users).all(),\n )\n\n self.fields[\"users_removed\"] = forms.ModelMultipleChoiceField(\n User.objects.filter(id__in=self.current_users).all(),\n label=_(\"Users to remove from group\"),\n required=False,\n widget=forms.CheckboxSelectMultiple,\n )\n</code></pre>"},{"location":"reference/core/views/#core.views.GroupListView","title":"<code>GroupListView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>ListView</code></p> <p>Displays the Group list.</p>"},{"location":"reference/core/views/#core.views.GroupEditView","title":"<code>GroupEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p> <p>Edit infos of a Group.</p>"},{"location":"reference/core/views/#core.views.GroupCreateView","title":"<code>GroupCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Add a new Group.</p>"},{"location":"reference/core/views/#core.views.GroupTemplateView","title":"<code>GroupTemplateView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DetailFormView</code></p> <p>Display all users in a given Group Allow adding and removing users from it.</p>"},{"location":"reference/core/views/#core.views.GroupDeleteView","title":"<code>GroupDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p> <p>Delete a Group.</p>"},{"location":"reference/core/views/#core.views.LockError","title":"<code>LockError</code>","text":"<p> Bases: <code>Exception</code></p> <p>There was a lock error on the object.</p>"},{"location":"reference/core/views/#core.views.Page","title":"<code>Page</code>","text":"<p> Bases: <code>Model</code></p> <p>The page class to build a Wiki Each page may have a parent and it's URL is of the form my.site/page/// It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is awkward! Prefere querying pages with Page.get_page_by_full_name(). <p>Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when playing with a Page object, use get_full_name() instead!</p>"},{"location":"reference/core/views/#core.views.Page.save","title":"<code>save(*args, **kwargs)</code>","text":"<p>Performs some needed actions before and after saving a page in database.</p> Source code in <code>core/models.py</code> <pre><code>def save(self, *args, **kwargs):\n \"\"\"Performs some needed actions before and after saving a page in database.\"\"\"\n locked = kwargs.pop(\"force_lock\", False)\n if not locked:\n locked = self.is_locked()\n if not locked:\n raise NotLocked(\"The page is not locked and thus can not be saved\")\n self.full_clean()\n if not self.id:\n super().save(\n *args, **kwargs\n ) # Save a first time to correctly set _full_name\n # This reset the _full_name just before saving to maintain a coherent field quicker for queries than the\n # recursive method\n # It also update all the children to maintain correct names\n self._full_name = self.get_full_name()\n for c in self.children.all():\n c.save()\n super().save(*args, **kwargs)\n self.unset_lock()\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.get_page_by_full_name","title":"<code>get_page_by_full_name(name)</code> <code>staticmethod</code>","text":"<p>Quicker to get a page with that method rather than building the request every time.</p> Source code in <code>core/models.py</code> <pre><code>@staticmethod\ndef get_page_by_full_name(name):\n \"\"\"Quicker to get a page with that method rather than building the request every time.\"\"\"\n return Page.objects.filter(_full_name=name).first()\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.clean","title":"<code>clean()</code>","text":"<p>Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.\"\"\"\n if \"/\" in self.name:\n self.name = self.name.split(\"/\")[-1]\n if (\n Page.objects.exclude(pk=self.pk)\n .filter(_full_name=self.get_full_name())\n .exists()\n ):\n raise ValidationError(_(\"Duplicate page\"), code=\"duplicate\")\n super().clean()\n if self.parent is not None and self in self.get_parent_list():\n raise ValidationError(_(\"Loop in page tree\"), code=\"loop\")\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.is_locked","title":"<code>is_locked()</code>","text":"<p>Is True if the page is locked, False otherwise.</p> <p>This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this function will return False.</p> Source code in <code>core/models.py</code> <pre><code>def is_locked(self):\n \"\"\"Is True if the page is locked, False otherwise.\n\n This is where the timeout is handled,\n so a locked page for which the timeout is reach will be unlocked and this\n function will return False.\n \"\"\"\n if self.lock_timeout and (\n timezone.now() - self.lock_timeout &gt; timedelta(minutes=5)\n ):\n self.unset_lock()\n return (\n self.lock_user\n and self.lock_timeout\n and (timezone.now() - self.lock_timeout &lt; timedelta(minutes=5))\n )\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.set_lock","title":"<code>set_lock(user)</code>","text":"<p>Sets a lock on the current page or raise an AlreadyLocked exception.</p> Source code in <code>core/models.py</code> <pre><code>def set_lock(self, user):\n \"\"\"Sets a lock on the current page or raise an AlreadyLocked exception.\"\"\"\n if self.is_locked() and self.get_lock() != user:\n raise AlreadyLocked(\"The page is already locked by someone else\")\n self.lock_user = user\n self.lock_timeout = timezone.now()\n super().save()\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.set_lock_recursive","title":"<code>set_lock_recursive(user)</code>","text":"<p>Locks recursively all the child pages for editing properties.</p> Source code in <code>core/models.py</code> <pre><code>def set_lock_recursive(self, user):\n \"\"\"Locks recursively all the child pages for editing properties.\"\"\"\n for p in self.children.all():\n p.set_lock_recursive(user)\n self.set_lock(user)\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.unset_lock_recursive","title":"<code>unset_lock_recursive()</code>","text":"<p>Unlocks recursively all the child pages.</p> Source code in <code>core/models.py</code> <pre><code>def unset_lock_recursive(self):\n \"\"\"Unlocks recursively all the child pages.\"\"\"\n for p in self.children.all():\n p.unset_lock_recursive()\n self.unset_lock()\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.unset_lock","title":"<code>unset_lock()</code>","text":"<p>Always try to unlock, even if there is no lock.</p> Source code in <code>core/models.py</code> <pre><code>def unset_lock(self):\n \"\"\"Always try to unlock, even if there is no lock.\"\"\"\n self.lock_user = None\n self.lock_timeout = None\n super().save()\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.get_lock","title":"<code>get_lock()</code>","text":"<p>Returns the page's mutex containing the time and the user in a dict.</p> Source code in <code>core/models.py</code> <pre><code>def get_lock(self):\n \"\"\"Returns the page's mutex containing the time and the user in a dict.\"\"\"\n if self.lock_user:\n return self.lock_user\n raise NotLocked(\"The page is not locked and thus can not return its user\")\n</code></pre>"},{"location":"reference/core/views/#core.views.Page.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Computes the real full_name of the page based on its name and its parent's name You can and must rely on this function when working on a page object that is not freshly fetched from the DB (For example when treating a Page object coming from a form).</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Computes the real full_name of the page based on its name and its parent's name\n You can and must rely on this function when working on a page object that is not freshly fetched from the DB\n (For example when treating a Page object coming from a form).\n \"\"\"\n if self.parent is None:\n return self.name\n return f\"{self.parent.get_full_name()}/{self.name}\"\n</code></pre>"},{"location":"reference/core/views/#core.views.PageRev","title":"<code>PageRev</code>","text":"<p> Bases: <code>Model</code></p> <p>True content of the page.</p> <p>Each page object has a revisions field that is a list of PageRev, ordered by date. my_page.revisions.last() gives the PageRev object that is the most up-to-date, and thus, is the real content of the page. The content is in PageRev.title and PageRev.content .</p>"},{"location":"reference/core/views/#core.views.CanEditPagePropMixin","title":"<code>CanEditPagePropMixin</code>","text":"<p> Bases: <code>CanEditPropMixin</code></p>"},{"location":"reference/core/views/#core.views.PageListView","title":"<code>PageListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p>"},{"location":"reference/core/views/#core.views.PageView","title":"<code>PageView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/core/views/#core.views.PageHistView","title":"<code>PageHistView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/core/views/#core.views.PageRevView","title":"<code>PageRevView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/core/views/#core.views.PageCreateView","title":"<code>PageCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/core/views/#core.views.PagePropView","title":"<code>PagePropView</code>","text":"<p> Bases: <code>CanEditPagePropMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/core/views/#core.views.PageEditViewBase","title":"<code>PageEditViewBase</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/core/views/#core.views.PageEditView","title":"<code>PageEditView</code>","text":"<p> Bases: <code>PageEditViewBase</code></p>"},{"location":"reference/core/views/#core.views.PageDeleteView","title":"<code>PageDeleteView</code>","text":"<p> Bases: <code>CanEditPagePropMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/core/views/#core.views.Notification","title":"<code>Notification</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/views/#core.views.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/core/views/#core.views.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/core/views/#core.views.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/core/views/#core.views.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/core/views/#core.views.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/core/views/#core.views.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/core/views/#core.views.NotificationList","title":"<code>NotificationList</code>","text":"<p> Bases: <code>ListView</code></p>"},{"location":"reference/core/views/#core.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/core/views/#core.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/core/views/#core.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/core/views/#core.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/core/views/#core.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/core/views/#core.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/core/views/#core.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/core/views/#core.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/core/views/#core.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/core/views/#core.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/core/views/#core.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/core/views/#core.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/core/views/#core.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/core/views/#core.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/core/views/#core.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/core/views/#core.views.Gift","title":"<code>Gift</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/views/#core.views.Preferences","title":"<code>Preferences</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/core/views/#core.views.SithLoginView","title":"<code>SithLoginView</code>","text":"<p> Bases: <code>LoginView</code></p> <p>The login View.</p>"},{"location":"reference/core/views/#core.views.SithPasswordChangeView","title":"<code>SithPasswordChangeView</code>","text":"<p> Bases: <code>PasswordChangeView</code></p> <p>Allows a user to change its password.</p>"},{"location":"reference/core/views/#core.views.SithPasswordChangeDoneView","title":"<code>SithPasswordChangeDoneView</code>","text":"<p> Bases: <code>PasswordChangeDoneView</code></p> <p>Allows a user to change its password.</p>"},{"location":"reference/core/views/#core.views.SithPasswordResetView","title":"<code>SithPasswordResetView</code>","text":"<p> Bases: <code>PasswordResetView</code></p> <p>Allows someone to enter an email address for resetting password.</p>"},{"location":"reference/core/views/#core.views.SithPasswordResetDoneView","title":"<code>SithPasswordResetDoneView</code>","text":"<p> Bases: <code>PasswordResetDoneView</code></p> <p>Confirm that the reset email has been sent.</p>"},{"location":"reference/core/views/#core.views.SithPasswordResetConfirmView","title":"<code>SithPasswordResetConfirmView</code>","text":"<p> Bases: <code>PasswordResetConfirmView</code></p> <p>Provide a reset password form.</p>"},{"location":"reference/core/views/#core.views.SithPasswordResetCompleteView","title":"<code>SithPasswordResetCompleteView</code>","text":"<p> Bases: <code>PasswordResetCompleteView</code></p> <p>Confirm the password has successfully been reset.</p>"},{"location":"reference/core/views/#core.views.UserCreationView","title":"<code>UserCreationView</code>","text":"<p> Bases: <code>FormView</code></p>"},{"location":"reference/core/views/#core.views.UserTabsMixin","title":"<code>UserTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/core/views/#core.views.UserView","title":"<code>UserView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display a user's profile.</p>"},{"location":"reference/core/views/#core.views.UserPicturesView","title":"<code>UserPicturesView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display a user's pictures.</p>"},{"location":"reference/core/views/#core.views.UserGodfathersView","title":"<code>UserGodfathersView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code>, <code>FormView</code></p> <p>Display a user's godfathers.</p>"},{"location":"reference/core/views/#core.views.UserGodfathersTreeView","title":"<code>UserGodfathersTreeView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display a user's family tree.</p>"},{"location":"reference/core/views/#core.views.UserStatsView","title":"<code>UserStatsView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display a user's stats.</p>"},{"location":"reference/core/views/#core.views.UserMiniView","title":"<code>UserMiniView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display a user's profile.</p>"},{"location":"reference/core/views/#core.views.UserListView","title":"<code>UserListView</code>","text":"<p> Bases: <code>ListView</code>, <code>CanEditPropMixin</code></p> <p>Displays the user list.</p>"},{"location":"reference/core/views/#core.views.UserUpdateProfileView","title":"<code>UserUpdateProfileView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanEditMixin</code>, <code>UpdateView</code></p> <p>Edit a user's profile.</p>"},{"location":"reference/core/views/#core.views.UserUpdateProfileView.remove_restricted_fields","title":"<code>remove_restricted_fields(request)</code>","text":"<p>Removes edit_once and board_only fields.</p> Source code in <code>core/views/user.py</code> <pre><code>def remove_restricted_fields(self, request):\n \"\"\"Removes edit_once and board_only fields.\"\"\"\n for i in self.edit_once:\n if getattr(self.form.instance, i) and not (\n request.user.is_board_member or request.user.is_root\n ):\n self.form.fields.pop(i, None)\n for i in self.board_only:\n if not (request.user.is_board_member or request.user.is_root):\n self.form.fields.pop(i, None)\n</code></pre>"},{"location":"reference/core/views/#core.views.UserClubView","title":"<code>UserClubView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display the user's club(s).</p>"},{"location":"reference/core/views/#core.views.UserPreferencesView","title":"<code>UserPreferencesView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanEditMixin</code>, <code>UpdateView</code></p> <p>Edit a user's preferences.</p>"},{"location":"reference/core/views/#core.views.UserUpdateGroupView","title":"<code>UserUpdateGroupView</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Edit a user's groups.</p>"},{"location":"reference/core/views/#core.views.UserToolsView","title":"<code>UserToolsView</code>","text":"<p> Bases: <code>LoginRequiredMixin</code>, <code>QuickNotifMixin</code>, <code>UserTabsMixin</code>, <code>TemplateView</code></p> <p>Displays the logged user's tools.</p>"},{"location":"reference/core/views/#core.views.UserAccountBase","title":"<code>UserAccountBase</code>","text":"<p> Bases: <code>UserTabsMixin</code>, <code>DetailView</code></p> <p>Base class for UserAccount.</p>"},{"location":"reference/core/views/#core.views.UserAccountView","title":"<code>UserAccountView</code>","text":"<p> Bases: <code>UserAccountBase</code></p> <p>Display a user's account.</p>"},{"location":"reference/core/views/#core.views.UserAccountDetailView","title":"<code>UserAccountDetailView</code>","text":"<p> Bases: <code>UserAccountBase</code>, <code>YearMixin</code>, <code>MonthMixin</code></p> <p>Display a user's account for month.</p>"},{"location":"reference/core/views/#core.views.GiftCreateView","title":"<code>GiftCreateView</code>","text":"<p> Bases: <code>CreateView</code></p>"},{"location":"reference/core/views/#core.views.GiftDeleteView","title":"<code>GiftDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/core/views/#core.views.forbidden","title":"<code>forbidden(request, exception)</code>","text":"Source code in <code>core/views/__init__.py</code> <pre><code>def forbidden(request, exception):\n context = {\"next\": request.path, \"form\": LoginForm()}\n if popup := request.resolver_match.kwargs.get(\"popup\"):\n context[\"popup\"] = popup\n return HttpResponseForbidden(render(request, \"core/403.jinja\", context=context))\n</code></pre>"},{"location":"reference/core/views/#core.views.not_found","title":"<code>not_found(request, exception)</code>","text":"Source code in <code>core/views/__init__.py</code> <pre><code>def not_found(request, exception):\n return HttpResponseNotFound(\n render(request, \"core/404.jinja\", context={\"exception\": exception})\n )\n</code></pre>"},{"location":"reference/core/views/#core.views.internal_servor_error","title":"<code>internal_servor_error(request)</code>","text":"Source code in <code>core/views/__init__.py</code> <pre><code>def internal_servor_error(request):\n request.sentry_last_event_id = last_event_id\n return HttpResponseServerError(render(request, \"core/500.jinja\"))\n</code></pre>"},{"location":"reference/core/views/#core.views.can_edit_prop","title":"<code>can_edit_prop(obj, user)</code>","text":"<p>Can the user edit the properties of the object.</p> <p>Parameters:</p> Name Type Description Default <code>obj</code> <code>Any</code> <p>Object to test for permission</p> required <code>user</code> <code>User</code> <p>core.models.User to test permissions against</p> required <p>Returns:</p> Type Description <code>bool</code> <p>True if user is authorized to edit object properties else False</p> <p>Examples:</p> <pre><code>if not can_edit_prop(self.object ,request.user):\n raise PermissionDenied\n</code></pre> Source code in <code>core/views/__init__.py</code> <pre><code>def can_edit_prop(obj: Any, user: User) -&gt; bool:\n \"\"\"Can the user edit the properties of the object.\n\n Args:\n obj: Object to test for permission\n user: core.models.User to test permissions against\n\n Returns:\n True if user is authorized to edit object properties else False\n\n Examples:\n ```python\n if not can_edit_prop(self.object ,request.user):\n raise PermissionDenied\n ```\n \"\"\"\n return obj is None or user.is_owner(obj)\n</code></pre>"},{"location":"reference/core/views/#core.views.can_edit","title":"<code>can_edit(obj, user)</code>","text":"<p>Can the user edit the object.</p> <p>Parameters:</p> Name Type Description Default <code>obj</code> <code>Any</code> <p>Object to test for permission</p> required <code>user</code> <code>User</code> <p>core.models.User to test permissions against</p> required <p>Returns:</p> Type Description <code>bool</code> <p>True if user is authorized to edit object else False</p> <p>Examples:</p> <pre><code>if not can_edit(self.object, request.user):\n raise PermissionDenied\n</code></pre> Source code in <code>core/views/__init__.py</code> <pre><code>def can_edit(obj: Any, user: User) -&gt; bool:\n \"\"\"Can the user edit the object.\n\n Args:\n obj: Object to test for permission\n user: core.models.User to test permissions against\n\n Returns:\n True if user is authorized to edit object else False\n\n Examples:\n ```python\n if not can_edit(self.object, request.user):\n raise PermissionDenied\n ```\n \"\"\"\n if obj is None or user.can_edit(obj):\n return True\n return can_edit_prop(obj, user)\n</code></pre>"},{"location":"reference/core/views/#core.views.can_view","title":"<code>can_view(obj, user)</code>","text":"<p>Can the user see the object.</p> <p>Parameters:</p> Name Type Description Default <code>obj</code> <code>Any</code> <p>Object to test for permission</p> required <code>user</code> <code>User</code> <p>core.models.User to test permissions against</p> required <p>Returns:</p> Type Description <code>bool</code> <p>True if user is authorized to see object else False</p> <p>Examples:</p> <pre><code>if not can_view(self.object ,request.user):\n raise PermissionDenied\n</code></pre> Source code in <code>core/views/__init__.py</code> <pre><code>def can_view(obj: Any, user: User) -&gt; bool:\n \"\"\"Can the user see the object.\n\n Args:\n obj: Object to test for permission\n user: core.models.User to test permissions against\n\n Returns:\n True if user is authorized to see object else False\n\n Examples:\n ```python\n if not can_view(self.object ,request.user):\n raise PermissionDenied\n ```\n \"\"\"\n if obj is None or user.can_view(obj):\n return True\n return can_edit(obj, user)\n</code></pre>"},{"location":"reference/core/views/#core.views.send_file","title":"<code>send_file(request, file_id, file_class=SithFile, file_attr='file')</code>","text":"<p>Send a protected file, if the user can see it.</p> <p>In prod, the server won't handle the download itself, but set the appropriate headers in the response to make the reverse-proxy deal with it. In debug mode, the server will directly send the file.</p> Source code in <code>core/views/files.py</code> <pre><code>def send_file(\n request: HttpRequest,\n file_id: int,\n file_class: type[SithFile] = SithFile,\n file_attr: str = \"file\",\n) -&gt; HttpResponse:\n \"\"\"Send a protected file, if the user can see it.\n\n In prod, the server won't handle the download itself,\n but set the appropriate headers in the response to make the reverse-proxy\n deal with it.\n In debug mode, the server will directly send the file.\n \"\"\"\n f = get_object_or_404(file_class, id=file_id)\n if not can_view(f, request.user) and not is_logged_in_counter(request):\n raise PermissionDenied\n name = getattr(f, file_attr).name\n\n response = HttpResponse(\n headers={\"Content-Disposition\": f'inline; filename=\"{quote(name)}\"'}\n )\n if not settings.DEBUG:\n # When receiving a response with the Accel-Redirect header,\n # the reverse proxy will automatically handle the file sending.\n # This is really hard to test (thus isn't tested)\n # so please do not mess with this.\n response[\"Content-Type\"] = \"\" # automatically set by nginx\n response[\"X-Accel-Redirect\"] = quote(urljoin(settings.MEDIA_URL, name))\n return response\n\n filepath = settings.MEDIA_ROOT / name\n # check if file exists on disk\n if not filepath.exists():\n raise Http404\n with open(filepath, \"rb\") as filename:\n response.content = FileWrapper(filename)\n response[\"Content-Type\"] = mimetypes.guess_type(filepath)[0]\n response[\"Last-Modified\"] = http_date(f.date.timestamp())\n response[\"Content-Length\"] = filepath.stat().st_size\n return response\n</code></pre>"},{"location":"reference/core/views/#core.views.index","title":"<code>index(request, context=None)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>def index(request, context=None):\n from com.views import NewsListView\n\n return NewsListView.as_view()(request)\n</code></pre>"},{"location":"reference/core/views/#core.views.notification","title":"<code>notification(request, notif_id)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>def notification(request, notif_id):\n notif = Notification.objects.filter(id=notif_id).first()\n if notif:\n if notif.type not in settings.SITH_PERMANENT_NOTIFICATIONS:\n notif.viewed = True\n else:\n notif.callback()\n notif.save()\n return redirect(notif.url)\n return redirect(\"/\")\n</code></pre>"},{"location":"reference/core/views/#core.views.search_user","title":"<code>search_user(query)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>def search_user(query):\n try:\n # slugify turns everything into ascii and every whitespace into -\n # it ends by removing duplicate - (so ' - ' will turn into '-')\n # replace('-', ' ') because search is whitespace based\n query = slugify(query).replace(\"-\", \" \")\n # TODO: is this necessary?\n query = html.escape(query)\n res = (\n SearchQuerySet()\n .models(User)\n .autocomplete(auto=query)\n .order_by(\"-last_login\")\n .load_all()[:20]\n )\n return [r.object for r in res]\n except TypeError:\n return []\n</code></pre>"},{"location":"reference/core/views/#core.views.search_club","title":"<code>search_club(query, *, as_json=False)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>def search_club(query, *, as_json=False):\n clubs = []\n if query:\n clubs = Club.objects.filter(name__icontains=query).all()\n clubs = clubs[:5]\n if as_json:\n # Re-loads json to avoid double encoding by JsonResponse, but still benefit from serializers\n clubs = json.loads(serializers.serialize(\"json\", clubs, fields=(\"name\")))\n else:\n clubs = list(clubs)\n return clubs\n</code></pre>"},{"location":"reference/core/views/#core.views.search_view","title":"<code>search_view(request)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>@login_required\ndef search_view(request):\n result = {\n \"users\": search_user(request.GET.get(\"query\", \"\")),\n \"clubs\": search_club(request.GET.get(\"query\", \"\")),\n }\n return render(request, \"core/search.jinja\", context={\"result\": result})\n</code></pre>"},{"location":"reference/core/views/#core.views.search_user_json","title":"<code>search_user_json(request)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>@login_required\ndef search_user_json(request):\n result = {\"users\": search_user(request.GET.get(\"query\", \"\"))}\n return JsonResponse(result)\n</code></pre>"},{"location":"reference/core/views/#core.views.search_json","title":"<code>search_json(request)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>@login_required\ndef search_json(request):\n result = {\n \"users\": search_user(request.GET.get(\"query\", \"\")),\n \"clubs\": search_club(request.GET.get(\"query\", \"\"), as_json=True),\n }\n return JsonResponse(result)\n</code></pre>"},{"location":"reference/core/views/#core.views.logout","title":"<code>logout(request)</code>","text":"<p>The logout view.</p> Source code in <code>core/views/user.py</code> <pre><code>def logout(request):\n \"\"\"The logout view.\"\"\"\n return views.logout_then_login(request)\n</code></pre>"},{"location":"reference/core/views/#core.views.password_root_change","title":"<code>password_root_change(request, user_id)</code>","text":"<p>Allows a root user to change someone's password.</p> Source code in <code>core/views/user.py</code> <pre><code>def password_root_change(request, user_id):\n \"\"\"Allows a root user to change someone's password.\"\"\"\n if not request.user.is_root:\n raise PermissionDenied\n user = User.objects.filter(id=user_id).first()\n if not user:\n raise Http404(\"User not found\")\n if request.method == \"POST\":\n form = views.SetPasswordForm(user=user, data=request.POST)\n if form.is_valid():\n form.save()\n return redirect(\"core:password_change_done\")\n else:\n form = views.SetPasswordForm(user=user)\n return TemplateResponse(\n request, \"core/password_change.jinja\", {\"form\": form, \"target\": user}\n )\n</code></pre>"},{"location":"reference/core/views/#core.views.delete_user_godfather","title":"<code>delete_user_godfather(request, user_id, godfather_id, is_father)</code>","text":"Source code in <code>core/views/user.py</code> <pre><code>def delete_user_godfather(request, user_id, godfather_id, is_father):\n user_is_admin = request.user.is_root or request.user.is_board_member\n if user_id != request.user.id and not user_is_admin:\n raise PermissionDenied()\n user = get_object_or_404(User, id=user_id)\n to_remove = get_object_or_404(User, id=godfather_id)\n if is_father:\n user.godfathers.remove(to_remove)\n else:\n user.godchildren.remove(to_remove)\n return redirect(\"core:user_godfathers\", user_id=user_id)\n</code></pre>"},{"location":"reference/counter/models/","title":"Models","text":""},{"location":"reference/counter/models/#counter.models.CurrencyField","title":"<code>CurrencyField(*args, **kwargs)</code>","text":"<p> Bases: <code>DecimalField</code></p> <p>Custom database field used for currency.</p> Source code in <code>accounting/models.py</code> <pre><code>def __init__(self, *args, **kwargs):\n kwargs[\"max_digits\"] = 12\n kwargs[\"decimal_places\"] = 2\n super().__init__(*args, **kwargs)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/counter/models/#counter.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/counter/models/#counter.models.ResizedImageField","title":"<code>ResizedImageField(width=None, height=None, force_format=None, **kwargs)</code>","text":"<p> Bases: <code>ImageField</code></p> <p>A field that automatically resizes images to a given size.</p> <p>This field is useful for profile pictures or product icons, for example.</p> <p>The final size of the image is determined by the width and height parameters :</p> <ul> <li>If both are given, the image will be resized to fit in a rectangle of width x height</li> <li>If only one is given, the other will be calculated to keep the same ratio</li> </ul> <p>If the force_format parameter is given, the image will be converted to this format.</p> <p>Examples:</p> <p>To resize an image with a height of 100px, without changing the ratio, and a format of WEBP :</p> <pre><code>class Product(models.Model):\n icon = ResizedImageField(height=100, force_format=\"WEBP\")\n</code></pre> <p>To explicitly resize an image to 100x100px (but possibly change the ratio) :</p> <pre><code>class Product(models.Model):\n icon = ResizedImageField(width=100, height=100)\n</code></pre> <p>Raises:</p> Type Description <code>FieldError</code> <p>If neither width nor height is given</p> <p>Parameters:</p> Name Type Description Default <code>width</code> <code>int | None</code> <p>If given, the width of the resized image</p> <code>None</code> <code>height</code> <code>int | None</code> <p>If given, the height of the resized image</p> <code>None</code> <code>force_format</code> <code>str | None</code> <p>If given, the image will be converted to this format</p> <code>None</code> Source code in <code>core/fields.py</code> <pre><code>def __init__(\n self,\n width: int | None = None,\n height: int | None = None,\n force_format: str | None = None,\n **kwargs,\n):\n if width is None and height is None:\n raise FieldError(\n f\"{self.__class__.__name__} requires \"\n \"width, height or both, but got neither\"\n )\n self.width = width\n self.height = height\n self.force_format = force_format\n super().__init__(**kwargs)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Group","title":"<code>Group</code>","text":"<p> Bases: <code>Group</code></p> <p>Implement both RealGroups and Meta groups.</p> <p>Groups are sorted by their is_meta property</p>"},{"location":"reference/counter/models/#counter.models.Notification","title":"<code>Notification</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/models/#counter.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/counter/models/#counter.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/counter/models/#counter.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Customer","title":"<code>Customer</code>","text":"<p> Bases: <code>Model</code></p> <p>Customer data of a User.</p> <p>It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.</p>"},{"location":"reference/counter/models/#counter.models.Customer.can_buy","title":"<code>can_buy: bool</code> <code>property</code>","text":"<p>Check if whether this customer has the right to purchase any item.</p> <p>This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.</p>"},{"location":"reference/counter/models/#counter.models.Customer.save","title":"<code>save(*args, allow_negative=False, is_selling=False, **kwargs)</code>","text":"<p>is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, is_selling=False, **kwargs):\n \"\"\"is_selling : tell if the current action is a selling\n allow_negative : ignored if not a selling. Allow a selling to put the account in negative\n Those two parameters avoid blocking the save method of a customer if his account is negative.\n \"\"\"\n if self.amount &lt; 0 and (is_selling and not allow_negative):\n raise ValidationError(_(\"Not enough money\"))\n super().save(*args, **kwargs)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Customer.get_or_create","title":"<code>get_or_create(user)</code> <code>classmethod</code>","text":"<p>Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.</p> <p>If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id</p> <p>Example : ::</p> <pre><code>user = User.objects.get(pk=1)\naccount, created = Customer.get_or_create(user)\nif created:\n print(f\"created a new account with id {account.id}\")\nelse:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>@classmethod\ndef get_or_create(cls, user: User) -&gt; Tuple[Customer, bool]:\n \"\"\"Work in pretty much the same way as the usual get_or_create method,\n but with the default field replaced by some under the hood.\n\n If the user has an account, return it as is.\n Else create a new account with no money on it and a new unique account id\n\n Example : ::\n\n user = User.objects.get(pk=1)\n account, created = Customer.get_or_create(user)\n if created:\n print(f\"created a new account with id {account.id}\")\n else:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n \"\"\"\n if hasattr(user, \"customer\"):\n return user.customer, False\n\n # account_id are always a number with a letter appended\n account_id = (\n Customer.objects.order_by(Length(\"account_id\"), \"account_id\")\n .values(\"account_id\")\n .last()\n )\n if account_id is None:\n # legacy from the old site\n account = cls.objects.create(user=user, account_id=\"1504a\")\n return account, True\n\n account_id = account_id[\"account_id\"]\n account_num = int(account_id[:-1])\n while Customer.objects.filter(account_id=account_id).exists():\n # when entering the first iteration, we are using an already existing account id\n # so the loop should always execute at least one time\n account_num += 1\n account_id = f\"{account_num}{random.choice(string.ascii_lowercase)}\"\n\n account = cls.objects.create(user=user, account_id=account_id)\n return account, True\n</code></pre>"},{"location":"reference/counter/models/#counter.models.BillingInfo","title":"<code>BillingInfo</code>","text":"<p> Bases: <code>Model</code></p> <p>Represent the billing information of a user, which are required by the 3D-Secure v2 system used by the etransaction module.</p>"},{"location":"reference/counter/models/#counter.models.BillingInfo.to_3dsv2_xml","title":"<code>to_3dsv2_xml()</code>","text":"<p>Convert the data from this model into a xml usable by the online paying service of the Cr\u00e9dit Agricole bank. see : <code>https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster</code>.</p> Source code in <code>counter/models.py</code> <pre><code>def to_3dsv2_xml(self) -&gt; str:\n \"\"\"Convert the data from this model into a xml usable\n by the online paying service of the Cr\u00e9dit Agricole bank.\n see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.\n \"\"\"\n data = {\n \"Address\": {\n \"FirstName\": self.first_name,\n \"LastName\": self.last_name,\n \"Address1\": self.address_1,\n \"ZipCode\": self.zip_code,\n \"City\": self.city,\n \"CountryCode\": self.country.numeric, # ISO-3166-1 numeric code\n \"MobilePhone\": self.phone_number.as_national.replace(\" \", \"\"),\n \"CountryCodeMobilePhone\": f\"+{self.phone_number.country_code}\",\n }\n }\n if self.address_2:\n data[\"Address\"][\"Address2\"] = self.address_2\n xml = dict2xml(data, wrap=\"Billing\", newlines=False)\n return '&lt;?xml version=\"1.0\" encoding=\"UTF-8\" ?&gt;' + xml\n</code></pre>"},{"location":"reference/counter/models/#counter.models.AccountDumpQuerySet","title":"<code>AccountDumpQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/counter/models/#counter.models.AccountDumpQuerySet.ongoing","title":"<code>ongoing()</code>","text":"<p>Filter dump operations that are not completed yet.</p> Source code in <code>counter/models.py</code> <pre><code>def ongoing(self) -&gt; Self:\n \"\"\"Filter dump operations that are not completed yet.\"\"\"\n return self.filter(dump_operation=None)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.AccountDump","title":"<code>AccountDump</code>","text":"<p> Bases: <code>Model</code></p> <p>The process of dumping an account.</p>"},{"location":"reference/counter/models/#counter.models.ProductType","title":"<code>ProductType</code>","text":"<p> Bases: <code>Model</code></p> <p>A product type.</p> <p>Useful only for categorizing.</p>"},{"location":"reference/counter/models/#counter.models.ProductType.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Product","title":"<code>Product</code>","text":"<p> Bases: <code>Model</code></p> <p>A product, with all its related information.</p>"},{"location":"reference/counter/models/#counter.models.Product.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(\n pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID\n ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Product.can_be_sold_to","title":"<code>can_be_sold_to(user)</code>","text":"<p>Check if whether the user given in parameter has the right to buy this product or not.</p> <p>This must be not confused with the Customer.can_buy() method as the present method returns an information about the relation between a User and a Product, whereas the other tells something about a Customer (and not a user, they are not the same model).</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user can buy this product else False</p> Warning <p>This performs a db query, thus you can quickly have a N+1 queries problem if you call it in a loop. Hopefully, you can avoid that if you prefetch the buying_groups :</p> <pre><code>user = User.objects.get(username=\"foobar\")\nproducts = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n]\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>def can_be_sold_to(self, user: User) -&gt; bool:\n \"\"\"Check if whether the user given in parameter has the right to buy\n this product or not.\n\n This must be not confused with the Customer.can_buy()\n method as the present method returns an information\n about the relation between a User and a Product,\n whereas the other tells something about a Customer\n (and not a user, they are not the same model).\n\n Returns:\n True if the user can buy this product else False\n\n Warning:\n This performs a db query, thus you can quickly have\n a N+1 queries problem if you call it in a loop.\n Hopefully, you can avoid that if you prefetch the buying_groups :\n\n ```python\n user = User.objects.get(username=\"foobar\")\n products = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n ]\n ```\n \"\"\"\n buying_groups = list(self.buying_groups.all())\n if not buying_groups:\n return True\n return any(user.is_in_group(pk=group.id) for group in buying_groups)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.CounterQuerySet","title":"<code>CounterQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/counter/models/#counter.models.CounterQuerySet.annotate_has_barman","title":"<code>annotate_has_barman(user)</code>","text":"<p>Annotate the queryset with the <code>user_is_barman</code> field.</p> <p>For each counter, this field has value True if the user is a barman of this counter, else False.</p> <p>Parameters:</p> Name Type Description Default <code>user</code> <code>User</code> <p>the user we want to check if he is a barman</p> required <p>Examples:</p> <pre><code>sli = User.objects.get(username=\"sli\")\ncounters = (\n Counter.objects\n .annotate_has_barman(sli) # add the user_has_barman boolean field\n .filter(has_annotated_barman=True) # keep only counters where this user is barman\n)\nprint(\"Sli est barman dans les comptoirs suivants :\")\nfor counter in counters:\n print(f\"- {counter.name}\")\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>def annotate_has_barman(self, user: User) -&gt; Self:\n \"\"\"Annotate the queryset with the `user_is_barman` field.\n\n For each counter, this field has value True if the user\n is a barman of this counter, else False.\n\n Args:\n user: the user we want to check if he is a barman\n\n Examples:\n ```python\n sli = User.objects.get(username=\"sli\")\n counters = (\n Counter.objects\n .annotate_has_barman(sli) # add the user_has_barman boolean field\n .filter(has_annotated_barman=True) # keep only counters where this user is barman\n )\n print(\"Sli est barman dans les comptoirs suivants :\")\n for counter in counters:\n print(f\"- {counter.name}\")\n ```\n \"\"\"\n subquery = user.counters.filter(pk=OuterRef(\"pk\"))\n return self.annotate(has_annotated_barman=Exists(subquery))\n</code></pre>"},{"location":"reference/counter/models/#counter.models.CounterQuerySet.annotate_is_open","title":"<code>annotate_is_open()</code>","text":"<p>Annotate tue queryset with the <code>is_open</code> field.</p> <p>For each counter, if <code>is_open=True</code>, then the counter is currently opened. Else the counter is closed.</p> Source code in <code>counter/models.py</code> <pre><code>def annotate_is_open(self) -&gt; Self:\n \"\"\"Annotate tue queryset with the `is_open` field.\n\n For each counter, if `is_open=True`, then the counter is currently opened.\n Else the counter is closed.\n \"\"\"\n return self.annotate(\n is_open=Exists(\n Permanency.objects.filter(counter_id=OuterRef(\"pk\"), end=None)\n )\n )\n</code></pre>"},{"location":"reference/counter/models/#counter.models.CounterQuerySet.handle_timeout","title":"<code>handle_timeout()</code>","text":"<p>Disconnect the barmen who are inactive in the given counters.</p> <p>Returns:</p> Type Description <code>int</code> <p>The number of affected rows (ie, the number of timeouted permanences)</p> Source code in <code>counter/models.py</code> <pre><code>def handle_timeout(self) -&gt; int:\n \"\"\"Disconnect the barmen who are inactive in the given counters.\n\n Returns:\n The number of affected rows (ie, the number of timeouted permanences)\n \"\"\"\n timeout = timezone.now() - timedelta(minutes=settings.SITH_BARMAN_TIMEOUT)\n return Permanency.objects.filter(\n counter__in=self, end=None, activity__lt=timeout\n ).update(end=F(\"activity\"))\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/models/#counter.models.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/counter/models/#counter.models.RefillingQuerySet","title":"<code>RefillingQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/counter/models/#counter.models.RefillingQuerySet.annotate_total","title":"<code>annotate_total()</code>","text":"<p>Annotate the Queryset with the total amount.</p> <p>The total is just the sum of the amounts for each row. If no grouping is involved (like in most queries), this is just the same as doing nothing and fetching the <code>amount</code> attribute.</p> <p>However, it may be useful when there is a <code>group by</code> clause in the query, or when other models are queried and having a common interface is helpful (e.g. <code>Selling.objects.annotate_total()</code> and <code>Refilling.objects.annotate_total()</code> will both have the <code>total</code> field).</p> Source code in <code>counter/models.py</code> <pre><code>def annotate_total(self) -&gt; Self:\n \"\"\"Annotate the Queryset with the total amount.\n\n The total is just the sum of the amounts for each row.\n If no grouping is involved (like in most queries),\n this is just the same as doing nothing and fetching the\n `amount` attribute.\n\n However, it may be useful when there is a `group by` clause\n in the query, or when other models are queried and having\n a common interface is helpful (e.g. `Selling.objects.annotate_total()`\n and `Refilling.objects.annotate_total()` will both have the `total` field).\n \"\"\"\n return self.annotate(total=Sum(\"amount\"))\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Refilling","title":"<code>Refilling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the refilling.</p>"},{"location":"reference/counter/models/#counter.models.SellingQuerySet","title":"<code>SellingQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/counter/models/#counter.models.SellingQuerySet.annotate_total","title":"<code>annotate_total()</code>","text":"<p>Annotate the Queryset with the total amount of the sales.</p> <p>The total is considered as the sum of (unit_price * quantity).</p> Source code in <code>counter/models.py</code> <pre><code>def annotate_total(self) -&gt; Self:\n \"\"\"Annotate the Queryset with the total amount of the sales.\n\n The total is considered as the sum of (unit_price * quantity).\n \"\"\"\n return self.annotate(total=Sum(F(\"unit_price\") * F(\"quantity\")))\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Selling","title":"<code>Selling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the sellings.</p>"},{"location":"reference/counter/models/#counter.models.Selling.save","title":"<code>save(*args, allow_negative=False, **kwargs)</code>","text":"<p>allow_negative : Allow this selling to use more money than available for this user.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, **kwargs):\n \"\"\"allow_negative : Allow this selling to use more money than available for this user.\"\"\"\n if not self.date:\n self.date = timezone.now()\n self.full_clean()\n if not self.is_validated:\n self.customer.amount -= self.quantity * self.unit_price\n self.customer.save(allow_negative=allow_negative, is_selling=True)\n self.is_validated = True\n user = self.customer.user\n if user.was_subscribed:\n if (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"un-semestre\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n elif (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"deux-semestres\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n if user.preferences.notify_on_click:\n Notification(\n user=user,\n url=reverse(\n \"core:user_account_detail\",\n kwargs={\n \"user_id\": user.id,\n \"year\": self.date.year,\n \"month\": self.date.month,\n },\n ),\n param=\"%d x %s\" % (self.quantity, self.label),\n type=\"SELLING\",\n ).save()\n super().save(*args, **kwargs)\n if hasattr(self.product, \"eticket\"):\n self.send_mail_customer()\n</code></pre>"},{"location":"reference/counter/models/#counter.models.Permanency","title":"<code>Permanency</code>","text":"<p> Bases: <code>Model</code></p> <p>A permanency of a barman, on a counter.</p> <p>This aims at storing a traceability of who was barman where and when. Mainly for dick size contest establishing the top 10 barmen of the semester.</p>"},{"location":"reference/counter/models/#counter.models.CashRegisterSummary","title":"<code>CashRegisterSummary</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/models/#counter.models.CashRegisterSummary.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.CashRegisterSummaryItem","title":"<code>CashRegisterSummaryItem</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/models/#counter.models.Eticket","title":"<code>Eticket</code>","text":"<p> Bases: <code>Model</code></p> <p>Eticket can be linked to a product an allows PDF generation.</p>"},{"location":"reference/counter/models/#counter.models.Eticket.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/models/#counter.models.StudentCard","title":"<code>StudentCard</code>","text":"<p> Bases: <code>Model</code></p> <p>Alternative way to connect a customer into a counter.</p> <p>We are using Mifare DESFire EV1 specs since it's used for izly cards https://www.nxp.com/docs/en/application-note/AN10927.pdf UID is 7 byte long that means 14 hexa characters.</p>"},{"location":"reference/counter/models/#counter.models.get_start_of_semester","title":"<code>get_start_of_semester(today=None)</code>","text":"<p>Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester.</p> <p>The current semester is computed as follows:</p> <ul> <li>If the date is between 15/08 and 31/12 =&gt; Autumn semester.</li> <li>If the date is between 01/01 and 15/02 =&gt; Autumn semester of the previous year.</li> <li>If the date is between 15/02 and 15/08 =&gt; Spring semester</li> </ul> <p>Parameters:</p> Name Type Description Default <code>today</code> <code>Optional[date]</code> <p>the date to use to compute the semester. If None, use today's date.</p> <code>None</code> <p>Returns:</p> Type Description <code>date</code> <p>the date of the start of the semester</p> Source code in <code>core/utils.py</code> <pre><code>def get_start_of_semester(today: Optional[date] = None) -&gt; date:\n \"\"\"Return the date of the start of the semester of the given date.\n If no date is given, return the start date of the current semester.\n\n The current semester is computed as follows:\n\n - If the date is between 15/08 and 31/12 =&gt; Autumn semester.\n - If the date is between 01/01 and 15/02 =&gt; Autumn semester of the previous year.\n - If the date is between 15/02 and 15/08 =&gt; Spring semester\n\n Args:\n today: the date to use to compute the semester. If None, use today's date.\n\n Returns:\n the date of the start of the semester\n \"\"\"\n if today is None:\n today = localdate()\n\n autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)\n spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)\n\n if today &gt;= autumn: # between 15/08 (included) and 31/12 -&gt; autumn semester\n return autumn\n if today &gt;= spring: # between 15/02 (included) and 15/08 -&gt; spring semester\n return spring\n # between 01/01 and 15/02 -&gt; autumn semester of the previous year\n return autumn.replace(year=autumn.year - 1)\n</code></pre>"},{"location":"reference/counter/schemas/","title":"Schemas","text":""},{"location":"reference/counter/schemas/#counter.schemas.SimpleUserSchema","title":"<code>SimpleUserSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>A schema with the minimum amount of information to represent a user.</p>"},{"location":"reference/counter/schemas/#counter.schemas.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Product","title":"<code>Product</code>","text":"<p> Bases: <code>Model</code></p> <p>A product, with all its related information.</p>"},{"location":"reference/counter/schemas/#counter.schemas.Product.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(\n pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID\n ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.Product.can_be_sold_to","title":"<code>can_be_sold_to(user)</code>","text":"<p>Check if whether the user given in parameter has the right to buy this product or not.</p> <p>This must be not confused with the Customer.can_buy() method as the present method returns an information about the relation between a User and a Product, whereas the other tells something about a Customer (and not a user, they are not the same model).</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user can buy this product else False</p> Warning <p>This performs a db query, thus you can quickly have a N+1 queries problem if you call it in a loop. Hopefully, you can avoid that if you prefetch the buying_groups :</p> <pre><code>user = User.objects.get(username=\"foobar\")\nproducts = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n]\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>def can_be_sold_to(self, user: User) -&gt; bool:\n \"\"\"Check if whether the user given in parameter has the right to buy\n this product or not.\n\n This must be not confused with the Customer.can_buy()\n method as the present method returns an information\n about the relation between a User and a Product,\n whereas the other tells something about a Customer\n (and not a user, they are not the same model).\n\n Returns:\n True if the user can buy this product else False\n\n Warning:\n This performs a db query, thus you can quickly have\n a N+1 queries problem if you call it in a loop.\n Hopefully, you can avoid that if you prefetch the buying_groups :\n\n ```python\n user = User.objects.get(username=\"foobar\")\n products = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n ]\n ```\n \"\"\"\n buying_groups = list(self.buying_groups.all())\n if not buying_groups:\n return True\n return any(user.is_in_group(pk=group.id) for group in buying_groups)\n</code></pre>"},{"location":"reference/counter/schemas/#counter.schemas.CounterSchema","title":"<code>CounterSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/counter/schemas/#counter.schemas.CounterFilterSchema","title":"<code>CounterFilterSchema</code>","text":"<p> Bases: <code>FilterSchema</code></p>"},{"location":"reference/counter/schemas/#counter.schemas.SimplifiedCounterSchema","title":"<code>SimplifiedCounterSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/counter/schemas/#counter.schemas.ProductSchema","title":"<code>ProductSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/counter/views/","title":"Views","text":""},{"location":"reference/counter/views/#counter.views.CurrencyField","title":"<code>CurrencyField(*args, **kwargs)</code>","text":"<p> Bases: <code>DecimalField</code></p> <p>Custom database field used for currency.</p> Source code in <code>accounting/models.py</code> <pre><code>def __init__(self, *args, **kwargs):\n kwargs[\"max_digits\"] = 12\n kwargs[\"decimal_places\"] = 2\n super().__init__(*args, **kwargs)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/counter/views/#counter.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/counter/views/#counter.views.TabedViewMixin","title":"<code>TabedViewMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Basic functions for displaying tabs in the template.</p>"},{"location":"reference/counter/views/#counter.views.CashSummaryFormBase","title":"<code>CashSummaryFormBase</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/counter/views/#counter.views.CounterEditForm","title":"<code>CounterEditForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/counter/views/#counter.views.EticketForm","title":"<code>EticketForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/counter/views/#counter.views.GetUserForm","title":"<code>GetUserForm</code>","text":"<p> Bases: <code>Form</code></p> <p>The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, reverse function, or any other use.</p> <p>The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with some nickname, first name, or last name (TODO)</p>"},{"location":"reference/counter/views/#counter.views.NFCCardForm","title":"<code>NFCCardForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/counter/views/#counter.views.ProductEditForm","title":"<code>ProductEditForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>counter/forms.py</code> <pre><code>def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n if self.instance.id:\n self.fields[\"counters\"].initial = self.instance.counters.all()\n</code></pre>"},{"location":"reference/counter/views/#counter.views.RefillForm","title":"<code>RefillForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/counter/views/#counter.views.StudentCardForm","title":"<code>StudentCardForm</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form for adding student cards Only used for user profile since CounterClick is to complicated.</p>"},{"location":"reference/counter/views/#counter.views.CashRegisterSummary","title":"<code>CashRegisterSummary</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/views/#counter.views.CashRegisterSummary.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CashRegisterSummaryItem","title":"<code>CashRegisterSummaryItem</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/views/#counter.views.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/counter/views/#counter.views.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Customer","title":"<code>Customer</code>","text":"<p> Bases: <code>Model</code></p> <p>Customer data of a User.</p> <p>It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.</p>"},{"location":"reference/counter/views/#counter.views.Customer.can_buy","title":"<code>can_buy: bool</code> <code>property</code>","text":"<p>Check if whether this customer has the right to purchase any item.</p> <p>This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.</p>"},{"location":"reference/counter/views/#counter.views.Customer.save","title":"<code>save(*args, allow_negative=False, is_selling=False, **kwargs)</code>","text":"<p>is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, is_selling=False, **kwargs):\n \"\"\"is_selling : tell if the current action is a selling\n allow_negative : ignored if not a selling. Allow a selling to put the account in negative\n Those two parameters avoid blocking the save method of a customer if his account is negative.\n \"\"\"\n if self.amount &lt; 0 and (is_selling and not allow_negative):\n raise ValidationError(_(\"Not enough money\"))\n super().save(*args, **kwargs)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Customer.get_or_create","title":"<code>get_or_create(user)</code> <code>classmethod</code>","text":"<p>Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.</p> <p>If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id</p> <p>Example : ::</p> <pre><code>user = User.objects.get(pk=1)\naccount, created = Customer.get_or_create(user)\nif created:\n print(f\"created a new account with id {account.id}\")\nelse:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>@classmethod\ndef get_or_create(cls, user: User) -&gt; Tuple[Customer, bool]:\n \"\"\"Work in pretty much the same way as the usual get_or_create method,\n but with the default field replaced by some under the hood.\n\n If the user has an account, return it as is.\n Else create a new account with no money on it and a new unique account id\n\n Example : ::\n\n user = User.objects.get(pk=1)\n account, created = Customer.get_or_create(user)\n if created:\n print(f\"created a new account with id {account.id}\")\n else:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n \"\"\"\n if hasattr(user, \"customer\"):\n return user.customer, False\n\n # account_id are always a number with a letter appended\n account_id = (\n Customer.objects.order_by(Length(\"account_id\"), \"account_id\")\n .values(\"account_id\")\n .last()\n )\n if account_id is None:\n # legacy from the old site\n account = cls.objects.create(user=user, account_id=\"1504a\")\n return account, True\n\n account_id = account_id[\"account_id\"]\n account_num = int(account_id[:-1])\n while Customer.objects.filter(account_id=account_id).exists():\n # when entering the first iteration, we are using an already existing account id\n # so the loop should always execute at least one time\n account_num += 1\n account_id = f\"{account_num}{random.choice(string.ascii_lowercase)}\"\n\n account = cls.objects.create(user=user, account_id=account_id)\n return account, True\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Eticket","title":"<code>Eticket</code>","text":"<p> Bases: <code>Model</code></p> <p>Eticket can be linked to a product an allows PDF generation.</p>"},{"location":"reference/counter/views/#counter.views.Eticket.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Permanency","title":"<code>Permanency</code>","text":"<p> Bases: <code>Model</code></p> <p>A permanency of a barman, on a counter.</p> <p>This aims at storing a traceability of who was barman where and when. Mainly for dick size contest establishing the top 10 barmen of the semester.</p>"},{"location":"reference/counter/views/#counter.views.Product","title":"<code>Product</code>","text":"<p> Bases: <code>Model</code></p> <p>A product, with all its related information.</p>"},{"location":"reference/counter/views/#counter.views.Product.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(\n pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID\n ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Product.can_be_sold_to","title":"<code>can_be_sold_to(user)</code>","text":"<p>Check if whether the user given in parameter has the right to buy this product or not.</p> <p>This must be not confused with the Customer.can_buy() method as the present method returns an information about the relation between a User and a Product, whereas the other tells something about a Customer (and not a user, they are not the same model).</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user can buy this product else False</p> Warning <p>This performs a db query, thus you can quickly have a N+1 queries problem if you call it in a loop. Hopefully, you can avoid that if you prefetch the buying_groups :</p> <pre><code>user = User.objects.get(username=\"foobar\")\nproducts = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n]\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>def can_be_sold_to(self, user: User) -&gt; bool:\n \"\"\"Check if whether the user given in parameter has the right to buy\n this product or not.\n\n This must be not confused with the Customer.can_buy()\n method as the present method returns an information\n about the relation between a User and a Product,\n whereas the other tells something about a Customer\n (and not a user, they are not the same model).\n\n Returns:\n True if the user can buy this product else False\n\n Warning:\n This performs a db query, thus you can quickly have\n a N+1 queries problem if you call it in a loop.\n Hopefully, you can avoid that if you prefetch the buying_groups :\n\n ```python\n user = User.objects.get(username=\"foobar\")\n products = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n ]\n ```\n \"\"\"\n buying_groups = list(self.buying_groups.all())\n if not buying_groups:\n return True\n return any(user.is_in_group(pk=group.id) for group in buying_groups)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.ProductType","title":"<code>ProductType</code>","text":"<p> Bases: <code>Model</code></p> <p>A product type.</p> <p>Useful only for categorizing.</p>"},{"location":"reference/counter/views/#counter.views.ProductType.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.Refilling","title":"<code>Refilling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the refilling.</p>"},{"location":"reference/counter/views/#counter.views.Selling","title":"<code>Selling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the sellings.</p>"},{"location":"reference/counter/views/#counter.views.Selling.save","title":"<code>save(*args, allow_negative=False, **kwargs)</code>","text":"<p>allow_negative : Allow this selling to use more money than available for this user.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, **kwargs):\n \"\"\"allow_negative : Allow this selling to use more money than available for this user.\"\"\"\n if not self.date:\n self.date = timezone.now()\n self.full_clean()\n if not self.is_validated:\n self.customer.amount -= self.quantity * self.unit_price\n self.customer.save(allow_negative=allow_negative, is_selling=True)\n self.is_validated = True\n user = self.customer.user\n if user.was_subscribed:\n if (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"un-semestre\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n elif (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"deux-semestres\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n if user.preferences.notify_on_click:\n Notification(\n user=user,\n url=reverse(\n \"core:user_account_detail\",\n kwargs={\n \"user_id\": user.id,\n \"year\": self.date.year,\n \"month\": self.date.month,\n },\n ),\n param=\"%d x %s\" % (self.quantity, self.label),\n type=\"SELLING\",\n ).save()\n super().save(*args, **kwargs)\n if hasattr(self.product, \"eticket\"):\n self.send_mail_customer()\n</code></pre>"},{"location":"reference/counter/views/#counter.views.StudentCard","title":"<code>StudentCard</code>","text":"<p> Bases: <code>Model</code></p> <p>Alternative way to connect a customer into a counter.</p> <p>We are using Mifare DESFire EV1 specs since it's used for izly cards https://www.nxp.com/docs/en/application-note/AN10927.pdf UID is 7 byte long that means 14 hexa characters.</p>"},{"location":"reference/counter/views/#counter.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/counter/views/#counter.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/counter/views/#counter.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterAdminMixin","title":"<code>CounterAdminMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect counter admin section.</p>"},{"location":"reference/counter/views/#counter.views.StudentCardDeleteView","title":"<code>StudentCardDeleteView</code>","text":"<p> Bases: <code>DeleteView</code>, <code>CanEditMixin</code></p> <p>View used to delete a card from a user.</p>"},{"location":"reference/counter/views/#counter.views.CounterTabsMixin","title":"<code>CounterTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/counter/views/#counter.views.CounterMain","title":"<code>CounterMain</code>","text":"<p> Bases: <code>CounterTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code>, <code>ProcessFormView</code>, <code>FormMixin</code></p> <p>The public (barman) view.</p>"},{"location":"reference/counter/views/#counter.views.CounterMain.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>We handle here the login form for the barman.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"We handle here the login form for the barman.\"\"\"\n if self.request.method == \"POST\":\n self.object = self.get_object()\n self.object.update_activity()\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"login_form\"] = LoginForm()\n kwargs[\"login_form\"].fields[\"username\"].widget.attrs[\"autofocus\"] = True\n kwargs[\n \"login_form\"\n ].cleaned_data = {} # add_error fails if there are no cleaned_data\n if \"credentials\" in self.request.GET:\n kwargs[\"login_form\"].add_error(None, _(\"Bad credentials\"))\n if \"sellers\" in self.request.GET:\n kwargs[\"login_form\"].add_error(None, _(\"User is not barman\"))\n kwargs[\"form\"] = self.get_form()\n kwargs[\"form\"].cleaned_data = {} # same as above\n if \"bad_location\" in self.request.GET:\n kwargs[\"form\"].add_error(\n None, _(\"Bad location, someone is already logged in somewhere else\")\n )\n if self.object.type == \"BAR\":\n kwargs[\"barmen\"] = self.object.barmen_list\n elif self.request.user.is_authenticated:\n kwargs[\"barmen\"] = [self.request.user]\n if \"last_basket\" in self.request.session:\n kwargs[\"last_basket\"] = self.request.session.pop(\"last_basket\")\n kwargs[\"last_customer\"] = self.request.session.pop(\"last_customer\")\n kwargs[\"last_total\"] = self.request.session.pop(\"last_total\")\n kwargs[\"new_customer_amount\"] = self.request.session.pop(\n \"new_customer_amount\"\n )\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterMain.form_valid","title":"<code>form_valid(form)</code>","text":"<p>We handle here the redirection, passing the user id of the asked customer.</p> Source code in <code>counter/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"We handle here the redirection, passing the user id of the asked customer.\"\"\"\n self.kwargs[\"user_id\"] = form.cleaned_data[\"user_id\"]\n return super().form_valid(form)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick","title":"<code>CounterClick</code>","text":"<p> Bases: <code>CounterTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>The click view This is a detail view not to have to worry about loading the counter Everything is made by hand in the post method.</p>"},{"location":"reference/counter/views/#counter.views.CounterClick.get","title":"<code>get(request, *args, **kwargs)</code>","text":"<p>Simple get view.</p> Source code in <code>counter/views.py</code> <pre><code>def get(self, request, *args, **kwargs):\n \"\"\"Simple get view.\"\"\"\n if \"basket\" not in request.session: # Init the basket session entry\n request.session[\"basket\"] = {}\n request.session[\"basket_total\"] = 0\n request.session[\"not_enough\"] = False # Reset every variable\n request.session[\"too_young\"] = False\n request.session[\"not_allowed\"] = False\n request.session[\"no_age\"] = False\n self.refill_form = None\n ret = super().get(request, *args, **kwargs)\n if (self.object.type != \"BAR\" and not request.user.is_authenticated) or (\n self.object.type == \"BAR\" and len(self.object.barmen_list) == 0\n ): # Check that at least one barman is logged in\n ret = self.cancel(request) # Otherwise, go to main view\n return ret\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.post","title":"<code>post(request, *args, **kwargs)</code>","text":"<p>Handle the many possibilities of the post request.</p> Source code in <code>counter/views.py</code> <pre><code>def post(self, request, *args, **kwargs):\n \"\"\"Handle the many possibilities of the post request.\"\"\"\n self.object = self.get_object()\n self.refill_form = None\n if (self.object.type != \"BAR\" and not request.user.is_authenticated) or (\n self.object.type == \"BAR\" and len(self.object.barmen_list) &lt; 1\n ): # Check that at least one barman is logged in\n return self.cancel(request)\n if self.object.type == \"BAR\" and not (\n \"counter_token\" in self.request.session\n and self.request.session[\"counter_token\"] == self.object.token\n ): # Also check the token to avoid the bar to be stolen\n return HttpResponseRedirect(\n reverse_lazy(\n \"counter:details\",\n args=self.args,\n kwargs={\"counter_id\": self.object.id},\n )\n + \"?bad_location\"\n )\n if \"basket\" not in request.session:\n request.session[\"basket\"] = {}\n request.session[\"basket_total\"] = 0\n request.session[\"not_enough\"] = False # Reset every variable\n request.session[\"too_young\"] = False\n request.session[\"not_allowed\"] = False\n request.session[\"no_age\"] = False\n request.session[\"not_valid_student_card_uid\"] = False\n if self.object.type != \"BAR\":\n self.operator = request.user\n elif self.customer_is_barman():\n self.operator = self.customer.user\n else:\n self.operator = self.object.get_random_barman()\n action = self.request.POST.get(\"action\", None)\n if action is None:\n action = parse_qs(request.body.decode()).get(\"action\", [\"\"])[0]\n if action == \"add_product\":\n self.add_product(request)\n elif action == \"add_student_card\":\n self.add_student_card(request)\n elif action == \"del_product\":\n self.del_product(request)\n elif action == \"refill\":\n self.refill(request)\n elif action == \"code\":\n return self.parse_code(request)\n elif action == \"cancel\":\n return self.cancel(request)\n elif action == \"finish\":\n return self.finish(request)\n context = self.get_context_data(object=self.object)\n return self.render_to_response(context)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.add_product","title":"<code>add_product(request, q=1, p=None)</code>","text":"<p>Add a product to the basket q is the quantity passed as integer p is the product id, passed as an integer.</p> Source code in <code>counter/views.py</code> <pre><code>def add_product(self, request, q=1, p=None):\n \"\"\"Add a product to the basket\n q is the quantity passed as integer\n p is the product id, passed as an integer.\n \"\"\"\n pid = p or parse_qs(request.body.decode())[\"product_id\"][0]\n pid = str(pid)\n price = self.get_price(pid)\n total = self.sum_basket(request)\n product: Product = self.get_product(pid)\n user: User = self.customer.user\n buying_groups = list(product.buying_groups.values_list(\"pk\", flat=True))\n can_buy = len(buying_groups) == 0 or any(\n user.is_in_group(pk=group_id) for group_id in buying_groups\n )\n if not can_buy:\n request.session[\"not_allowed\"] = True\n return False\n bq = 0 # Bonus quantity, for trays\n if (\n product.tray\n ): # Handle the tray to adjust the quantity q to add and the bonus quantity bq\n total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6\n bq = int((total_qty_mod_6 + q) / 6) # Integer division\n q -= bq\n if self.customer.amount &lt; (\n total + round(q * float(price), 2)\n ): # Check for enough money\n request.session[\"not_enough\"] = True\n return False\n if product.is_unrecord_product and not self.is_record_product_ok(\n request, product\n ):\n request.session[\"not_allowed\"] = True\n return False\n if product.limit_age &gt;= 18 and not user.date_of_birth:\n request.session[\"no_age\"] = True\n return False\n if product.limit_age &gt;= 18 and user.is_banned_alcohol:\n request.session[\"not_allowed\"] = True\n return False\n if user.is_banned_counter:\n request.session[\"not_allowed\"] = True\n return False\n if (\n user.date_of_birth and self.customer.user.get_age() &lt; product.limit_age\n ): # Check if affordable\n request.session[\"too_young\"] = True\n return False\n if pid in request.session[\"basket\"]: # Add if already in basket\n request.session[\"basket\"][pid][\"qty\"] += q\n request.session[\"basket\"][pid][\"bonus_qty\"] += bq\n else: # or create if not\n request.session[\"basket\"][pid] = {\n \"qty\": q,\n \"price\": int(price * 100),\n \"bonus_qty\": bq,\n }\n request.session.modified = True\n return True\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.add_student_card","title":"<code>add_student_card(request)</code>","text":"<p>Add a new student card on the customer account.</p> Source code in <code>counter/views.py</code> <pre><code>def add_student_card(self, request):\n \"\"\"Add a new student card on the customer account.\"\"\"\n uid = str(request.POST[\"student_card_uid\"])\n if not StudentCard.is_valid(uid):\n request.session[\"not_valid_student_card_uid\"] = True\n return False\n\n if not (\n self.object.type == \"BAR\"\n and \"counter_token\" in request.session\n and request.session[\"counter_token\"] == self.object.token\n and self.object.is_open\n ):\n raise PermissionDenied\n StudentCard(customer=self.customer, uid=uid).save()\n return True\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.del_product","title":"<code>del_product(request)</code>","text":"<p>Delete a product from the basket.</p> Source code in <code>counter/views.py</code> <pre><code>def del_product(self, request):\n \"\"\"Delete a product from the basket.\"\"\"\n pid = parse_qs(request.body.decode())[\"product_id\"][0]\n product = self.get_product(pid)\n if pid in request.session[\"basket\"]:\n if (\n product.tray\n and (self.get_total_quantity_for_pid(request, pid) % 6 == 0)\n and request.session[\"basket\"][pid][\"bonus_qty\"]\n ):\n request.session[\"basket\"][pid][\"bonus_qty\"] -= 1\n else:\n request.session[\"basket\"][pid][\"qty\"] -= 1\n if request.session[\"basket\"][pid][\"qty\"] &lt;= 0:\n del request.session[\"basket\"][pid]\n request.session.modified = True\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.parse_code","title":"<code>parse_code(request)</code>","text":"<p>Parse the string entered by the barman.</p> This can be of two forms <ul> <li><code>&lt;str&gt;</code>, where the string is the code of the product</li> <li><code>&lt;int&gt;X&lt;str&gt;</code>, where the integer is the quantity and str the code.</li> </ul> Source code in <code>counter/views.py</code> <pre><code>def parse_code(self, request):\n \"\"\"Parse the string entered by the barman.\n\n This can be of two forms :\n - `&lt;str&gt;`, where the string is the code of the product\n - `&lt;int&gt;X&lt;str&gt;`, where the integer is the quantity and str the code.\n \"\"\"\n string = parse_qs(request.body.decode()).get(\"code\", [\"\"])[0].upper()\n if string == \"FIN\":\n return self.finish(request)\n elif string == \"ANN\":\n return self.cancel(request)\n regex = re.compile(r\"^((?P&lt;nb&gt;[0-9]+)X)?(?P&lt;code&gt;[A-Z0-9]+)$\")\n m = regex.match(string)\n if m is not None:\n nb = m.group(\"nb\")\n code = m.group(\"code\")\n nb = int(nb) if nb is not None else 1\n p = self.object.products.filter(code=code).first()\n if p is not None:\n self.add_product(request, nb, p.id)\n context = self.get_context_data(object=self.object)\n return self.render_to_response(context)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.finish","title":"<code>finish(request)</code>","text":"<p>Finish the click session, and validate the basket.</p> Source code in <code>counter/views.py</code> <pre><code>def finish(self, request):\n \"\"\"Finish the click session, and validate the basket.\"\"\"\n with transaction.atomic():\n request.session[\"last_basket\"] = []\n if self.sum_basket(request) &gt; self.customer.amount:\n raise DataError(_(\"You have not enough money to buy all the basket\"))\n\n for pid, infos in request.session[\"basket\"].items():\n # This duplicates code for DB optimization (prevent to load many times the same object)\n p = Product.objects.filter(pk=pid).first()\n if self.customer_is_barman():\n uprice = p.special_selling_price\n else:\n uprice = p.selling_price\n request.session[\"last_basket\"].append(\n \"%d x %s\" % (infos[\"qty\"] + infos[\"bonus_qty\"], p.name)\n )\n s = Selling(\n label=p.name,\n product=p,\n club=p.club,\n counter=self.object,\n unit_price=uprice,\n quantity=infos[\"qty\"],\n seller=self.operator,\n customer=self.customer,\n )\n s.save()\n if infos[\"bonus_qty\"]:\n s = Selling(\n label=p.name + \" (Plateau)\",\n product=p,\n club=p.club,\n counter=self.object,\n unit_price=0,\n quantity=infos[\"bonus_qty\"],\n seller=self.operator,\n customer=self.customer,\n )\n s.save()\n self.customer.recorded_products -= self.compute_record_product(request)\n self.customer.save()\n request.session[\"last_customer\"] = self.customer.user.get_display_name()\n request.session[\"last_total\"] = \"%0.2f\" % self.sum_basket(request)\n request.session[\"new_customer_amount\"] = str(self.customer.amount)\n del request.session[\"basket\"]\n request.session.modified = True\n kwargs = {\"counter_id\": self.object.id}\n return HttpResponseRedirect(\n reverse_lazy(\"counter:details\", args=self.args, kwargs=kwargs)\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.cancel","title":"<code>cancel(request)</code>","text":"<p>Cancel the click session.</p> Source code in <code>counter/views.py</code> <pre><code>def cancel(self, request):\n \"\"\"Cancel the click session.\"\"\"\n kwargs = {\"counter_id\": self.object.id}\n request.session.pop(\"basket\", None)\n return HttpResponseRedirect(\n reverse_lazy(\"counter:details\", args=self.args, kwargs=kwargs)\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.refill","title":"<code>refill(request)</code>","text":"<p>Refill the customer's account.</p> Source code in <code>counter/views.py</code> <pre><code>def refill(self, request):\n \"\"\"Refill the customer's account.\"\"\"\n if not self.object.can_refill():\n raise PermissionDenied\n form = RefillForm(request.POST)\n if form.is_valid():\n form.instance.counter = self.object\n form.instance.operator = self.operator\n form.instance.customer = self.customer\n form.instance.save()\n else:\n self.refill_form = form\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterClick.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add customer to the context.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add customer to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n products = self.object.products.select_related(\"product_type\")\n if self.customer_is_barman():\n products = products.annotate(price=F(\"special_selling_price\"))\n else:\n products = products.annotate(price=F(\"selling_price\"))\n kwargs[\"products\"] = products\n kwargs[\"categories\"] = {}\n for product in kwargs[\"products\"]:\n if product.product_type:\n kwargs[\"categories\"].setdefault(product.product_type, []).append(\n product\n )\n kwargs[\"customer\"] = self.customer\n kwargs[\"student_cards\"] = self.customer.student_cards.all()\n kwargs[\"student_card_input\"] = NFCCardForm()\n kwargs[\"basket_total\"] = self.sum_basket(self.request)\n kwargs[\"refill_form\"] = self.refill_form or RefillForm()\n kwargs[\"student_card_max_uid_size\"] = StudentCard.UID_SIZE\n kwargs[\"barmens_can_refill\"] = self.object.can_refill()\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterAdminTabsMixin","title":"<code>CounterAdminTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/counter/views/#counter.views.CounterListView","title":"<code>CounterListView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CanViewMixin</code>, <code>ListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.CounterEditView","title":"<code>CounterEditView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>UpdateView</code></p> <p>Edit a counter's main informations (for the counter's manager).</p>"},{"location":"reference/counter/views/#counter.views.CounterEditPropView","title":"<code>CounterEditPropView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>UpdateView</code></p> <p>Edit a counter's main informations (for the counter's admin).</p>"},{"location":"reference/counter/views/#counter.views.CounterCreateView","title":"<code>CounterCreateView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>CreateView</code></p> <p>Create a counter (for the admins).</p>"},{"location":"reference/counter/views/#counter.views.CounterDeleteView","title":"<code>CounterDeleteView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>DeleteView</code></p> <p>Delete a counter (for the admins).</p>"},{"location":"reference/counter/views/#counter.views.ProductTypeListView","title":"<code>ProductTypeListView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>ListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.ProductTypeCreateView","title":"<code>ProductTypeCreateView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>CreateView</code></p> <p>A create view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.ProductTypeEditView","title":"<code>ProductTypeEditView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>UpdateView</code></p> <p>An edit view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.ProductListView","title":"<code>ProductListView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>ListView</code></p>"},{"location":"reference/counter/views/#counter.views.ArchivedProductListView","title":"<code>ArchivedProductListView</code>","text":"<p> Bases: <code>ProductListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.ActiveProductListView","title":"<code>ActiveProductListView</code>","text":"<p> Bases: <code>ProductListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.ProductCreateView","title":"<code>ProductCreateView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>CreateView</code></p> <p>A create view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.ProductEditView","title":"<code>ProductEditView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>UpdateView</code></p> <p>An edit view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.RefillingDeleteView","title":"<code>RefillingDeleteView</code>","text":"<p> Bases: <code>DeleteView</code></p> <p>Delete a refilling (for the admins).</p>"},{"location":"reference/counter/views/#counter.views.RefillingDeleteView.dispatch","title":"<code>dispatch(request, *args, **kwargs)</code>","text":"<p>We have here a very particular right handling, we can't inherit from CanEditPropMixin.</p> Source code in <code>counter/views.py</code> <pre><code>def dispatch(self, request, *args, **kwargs):\n \"\"\"We have here a very particular right handling, we can't inherit from CanEditPropMixin.\"\"\"\n self.object = self.get_object()\n if timezone.now() - self.object.date &lt;= timedelta(\n minutes=settings.SITH_LAST_OPERATIONS_LIMIT\n ) and is_logged_in_counter(request):\n self.success_url = reverse(\n \"counter:details\", kwargs={\"counter_id\": self.object.counter.id}\n )\n return super().dispatch(request, *args, **kwargs)\n elif self.object.is_owned_by(request.user):\n self.success_url = reverse(\n \"core:user_account\", kwargs={\"user_id\": self.object.customer.user.id}\n )\n return super().dispatch(request, *args, **kwargs)\n raise PermissionDenied\n</code></pre>"},{"location":"reference/counter/views/#counter.views.SellingDeleteView","title":"<code>SellingDeleteView</code>","text":"<p> Bases: <code>DeleteView</code></p> <p>Delete a selling (for the admins).</p>"},{"location":"reference/counter/views/#counter.views.SellingDeleteView.dispatch","title":"<code>dispatch(request, *args, **kwargs)</code>","text":"<p>We have here a very particular right handling, we can't inherit from CanEditPropMixin.</p> Source code in <code>counter/views.py</code> <pre><code>def dispatch(self, request, *args, **kwargs):\n \"\"\"We have here a very particular right handling, we can't inherit from CanEditPropMixin.\"\"\"\n self.object = self.get_object()\n if timezone.now() - self.object.date &lt;= timedelta(\n minutes=settings.SITH_LAST_OPERATIONS_LIMIT\n ) and is_logged_in_counter(request):\n self.success_url = reverse(\n \"counter:details\", kwargs={\"counter_id\": self.object.counter.id}\n )\n return super().dispatch(request, *args, **kwargs)\n elif self.object.is_owned_by(request.user):\n self.success_url = reverse(\n \"core:user_account\", kwargs={\"user_id\": self.object.customer.user.id}\n )\n return super().dispatch(request, *args, **kwargs)\n raise PermissionDenied\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CashRegisterSummaryForm","title":"<code>CashRegisterSummaryForm(*args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> <p>Provide the cash summary form.</p> Source code in <code>counter/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n instance = kwargs.pop(\"instance\", None)\n super().__init__(*args, **kwargs)\n if instance:\n self.fields[\"ten_cents\"].initial = (\n instance.ten_cents.quantity if instance.ten_cents else 0\n )\n self.fields[\"twenty_cents\"].initial = (\n instance.twenty_cents.quantity if instance.twenty_cents else 0\n )\n self.fields[\"fifty_cents\"].initial = (\n instance.fifty_cents.quantity if instance.fifty_cents else 0\n )\n self.fields[\"one_euro\"].initial = (\n instance.one_euro.quantity if instance.one_euro else 0\n )\n self.fields[\"two_euros\"].initial = (\n instance.two_euros.quantity if instance.two_euros else 0\n )\n self.fields[\"five_euros\"].initial = (\n instance.five_euros.quantity if instance.five_euros else 0\n )\n self.fields[\"ten_euros\"].initial = (\n instance.ten_euros.quantity if instance.ten_euros else 0\n )\n self.fields[\"twenty_euros\"].initial = (\n instance.twenty_euros.quantity if instance.twenty_euros else 0\n )\n self.fields[\"fifty_euros\"].initial = (\n instance.fifty_euros.quantity if instance.fifty_euros else 0\n )\n self.fields[\"hundred_euros\"].initial = (\n instance.hundred_euros.quantity if instance.hundred_euros else 0\n )\n self.fields[\"check_1_quantity\"].initial = (\n instance.check_1.quantity if instance.check_1 else 0\n )\n self.fields[\"check_2_quantity\"].initial = (\n instance.check_2.quantity if instance.check_2 else 0\n )\n self.fields[\"check_3_quantity\"].initial = (\n instance.check_3.quantity if instance.check_3 else 0\n )\n self.fields[\"check_4_quantity\"].initial = (\n instance.check_4.quantity if instance.check_4 else 0\n )\n self.fields[\"check_5_quantity\"].initial = (\n instance.check_5.quantity if instance.check_5 else 0\n )\n self.fields[\"check_1_value\"].initial = (\n instance.check_1.value if instance.check_1 else 0\n )\n self.fields[\"check_2_value\"].initial = (\n instance.check_2.value if instance.check_2 else 0\n )\n self.fields[\"check_3_value\"].initial = (\n instance.check_3.value if instance.check_3 else 0\n )\n self.fields[\"check_4_value\"].initial = (\n instance.check_4.value if instance.check_4 else 0\n )\n self.fields[\"check_5_value\"].initial = (\n instance.check_5.value if instance.check_5 else 0\n )\n self.fields[\"comment\"].initial = instance.comment\n self.fields[\"emptied\"].initial = instance.emptied\n self.instance = instance\n else:\n self.instance = None\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterLastOperationsView","title":"<code>CounterLastOperationsView</code>","text":"<p> Bases: <code>CounterTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Provide the last operations to allow barmen to delete them.</p>"},{"location":"reference/counter/views/#counter.views.CounterLastOperationsView.dispatch","title":"<code>dispatch(request, *args, **kwargs)</code>","text":"<p>We have here again a very particular right handling.</p> Source code in <code>counter/views.py</code> <pre><code>def dispatch(self, request, *args, **kwargs):\n \"\"\"We have here again a very particular right handling.\"\"\"\n self.object = self.get_object()\n if is_logged_in_counter(request) and self.object.barmen_list:\n return super().dispatch(request, *args, **kwargs)\n return HttpResponseRedirect(\n reverse(\"counter:details\", kwargs={\"counter_id\": self.object.id})\n + \"?bad_location\"\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterLastOperationsView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add form to the context.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add form to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n threshold = timezone.now() - timedelta(\n minutes=settings.SITH_LAST_OPERATIONS_LIMIT\n )\n kwargs[\"last_refillings\"] = (\n self.object.refillings.filter(date__gte=threshold)\n .select_related(\"operator\", \"customer__user\")\n .order_by(\"-id\")[:20]\n )\n kwargs[\"last_sellings\"] = (\n self.object.sellings.filter(date__gte=threshold)\n .select_related(\"seller\", \"customer__user\")\n .order_by(\"-id\")[:20]\n )\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterCashSummaryView","title":"<code>CounterCashSummaryView</code>","text":"<p> Bases: <code>CounterTabsMixin</code>, <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Provide the cash summary form.</p>"},{"location":"reference/counter/views/#counter.views.CounterCashSummaryView.dispatch","title":"<code>dispatch(request, *args, **kwargs)</code>","text":"<p>We have here again a very particular right handling.</p> Source code in <code>counter/views.py</code> <pre><code>def dispatch(self, request, *args, **kwargs):\n \"\"\"We have here again a very particular right handling.\"\"\"\n self.object = self.get_object()\n if is_logged_in_counter(request) and self.object.barmen_list:\n return super().dispatch(request, *args, **kwargs)\n return HttpResponseRedirect(\n reverse(\"counter:details\", kwargs={\"counter_id\": self.object.id})\n + \"?bad_location\"\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterCashSummaryView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add form to the context.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add form to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"form\"] = self.form\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CounterActivityView","title":"<code>CounterActivityView</code>","text":"<p> Bases: <code>DetailView</code></p> <p>Show the bar activity.</p>"},{"location":"reference/counter/views/#counter.views.CounterStatView","title":"<code>CounterStatView</code>","text":"<p> Bases: <code>DetailView</code>, <code>CounterAdminMixin</code></p> <p>Show the bar stats.</p>"},{"location":"reference/counter/views/#counter.views.CounterStatView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add stats to the context.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add stats to the context.\"\"\"\n counter: Counter = self.object\n semester_start = get_start_of_semester()\n office_hours = counter.get_top_barmen()\n kwargs = super().get_context_data(**kwargs)\n kwargs.update(\n {\n \"counter\": counter,\n \"current_semester\": get_semester_code(),\n \"total_sellings\": counter.get_total_sales(since=semester_start),\n \"top_customers\": counter.get_top_customers(since=semester_start)[:100],\n \"top_barman\": office_hours[:100],\n \"top_barman_semester\": (\n office_hours.filter(start__gt=semester_start)[:100]\n ),\n }\n )\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.CashSummaryEditView","title":"<code>CashSummaryEditView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>UpdateView</code></p> <p>Edit cash summaries.</p>"},{"location":"reference/counter/views/#counter.views.CashSummaryListView","title":"<code>CashSummaryListView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>ListView</code></p> <p>Display a list of cash summaries.</p>"},{"location":"reference/counter/views/#counter.views.CashSummaryListView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add sums to the context.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add sums to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n form = CashSummaryFormBase(self.request.GET)\n kwargs[\"form\"] = form\n kwargs[\"summaries_sums\"] = {}\n kwargs[\"refilling_sums\"] = {}\n for c in Counter.objects.filter(type=\"BAR\").all():\n refillings = Refilling.objects.filter(counter=c)\n cashredistersummaries = CashRegisterSummary.objects.filter(counter=c)\n if form.is_valid() and form.cleaned_data[\"begin_date\"]:\n refillings = refillings.filter(\n date__gte=form.cleaned_data[\"begin_date\"]\n )\n cashredistersummaries = cashredistersummaries.filter(\n date__gte=form.cleaned_data[\"begin_date\"]\n )\n else:\n last_summary = (\n CashRegisterSummary.objects.filter(counter=c, emptied=True)\n .order_by(\"-date\")\n .first()\n )\n if last_summary:\n refillings = refillings.filter(date__gt=last_summary.date)\n cashredistersummaries = cashredistersummaries.filter(\n date__gt=last_summary.date\n )\n else:\n refillings = refillings.filter(\n date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)\n ) # My birth date should be old enough\n cashredistersummaries = cashredistersummaries.filter(\n date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)\n )\n if form.is_valid() and form.cleaned_data[\"end_date\"]:\n refillings = refillings.filter(date__lte=form.cleaned_data[\"end_date\"])\n cashredistersummaries = cashredistersummaries.filter(\n date__lte=form.cleaned_data[\"end_date\"]\n )\n kwargs[\"summaries_sums\"][c.name] = sum(\n [s.get_total() for s in cashredistersummaries.all()]\n )\n kwargs[\"refilling_sums\"][c.name] = sum([s.amount for s in refillings.all()])\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.InvoiceCallView","title":"<code>InvoiceCallView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>TemplateView</code></p>"},{"location":"reference/counter/views/#counter.views.InvoiceCallView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add sums to the context.</p> Source code in <code>counter/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add sums to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"months\"] = Selling.objects.datetimes(\"date\", \"month\", order=\"DESC\")\n if \"month\" in self.request.GET:\n start_date = datetime.strptime(self.request.GET[\"month\"], \"%Y-%m\")\n else:\n start_date = datetime(\n year=timezone.now().year,\n month=(timezone.now().month + 10) % 12 + 1,\n day=1,\n )\n start_date = start_date.replace(tzinfo=tz.utc)\n end_date = (start_date + timedelta(days=32)).replace(\n day=1, hour=0, minute=0, microsecond=0\n )\n from django.db.models import Case, F, Sum, When\n\n kwargs[\"sum_cb\"] = sum(\n [\n r.amount\n for r in Refilling.objects.filter(\n payment_method=\"CARD\",\n is_validated=True,\n date__gte=start_date,\n date__lte=end_date,\n )\n ]\n )\n kwargs[\"sum_cb\"] += sum(\n [\n s.quantity * s.unit_price\n for s in Selling.objects.filter(\n payment_method=\"CARD\",\n is_validated=True,\n date__gte=start_date,\n date__lte=end_date,\n )\n ]\n )\n kwargs[\"start_date\"] = start_date\n kwargs[\"sums\"] = (\n Selling.objects.values(\"club__name\")\n .annotate(\n selling_sum=Sum(\n Case(\n When(\n date__gte=start_date,\n date__lt=end_date,\n then=F(\"unit_price\") * F(\"quantity\"),\n ),\n output_field=CurrencyField(),\n )\n )\n )\n .exclude(selling_sum=None)\n .order_by(\"-selling_sum\")\n )\n return kwargs\n</code></pre>"},{"location":"reference/counter/views/#counter.views.EticketListView","title":"<code>EticketListView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>ListView</code></p> <p>A list view for the admins.</p>"},{"location":"reference/counter/views/#counter.views.EticketCreateView","title":"<code>EticketCreateView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>CreateView</code></p> <p>Create an eticket.</p>"},{"location":"reference/counter/views/#counter.views.EticketEditView","title":"<code>EticketEditView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>UpdateView</code></p> <p>Edit an eticket.</p>"},{"location":"reference/counter/views/#counter.views.EticketPDFView","title":"<code>EticketPDFView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display the PDF of an eticket.</p>"},{"location":"reference/counter/views/#counter.views.CounterRefillingListView","title":"<code>CounterRefillingListView</code>","text":"<p> Bases: <code>CounterAdminTabsMixin</code>, <code>CounterAdminMixin</code>, <code>ListView</code></p> <p>List of refillings on a counter.</p>"},{"location":"reference/counter/views/#counter.views.StudentCardFormView","title":"<code>StudentCardFormView</code>","text":"<p> Bases: <code>FormView</code></p> <p>Add a new student card.</p>"},{"location":"reference/counter/views/#counter.views.get_semester_code","title":"<code>get_semester_code(d=None)</code>","text":"<p>Return the semester code of the given date. If no date is given, return the semester code of the current semester.</p> <p>The semester code is an upper letter (A for autumn, P for spring), followed by the last two digits of the year. For example, the autumn semester of 2018 is \"A18\".</p> <p>Parameters:</p> Name Type Description Default <code>d</code> <code>Optional[date]</code> <p>the date to use to compute the semester. If None, use today's date.</p> <code>None</code> <p>Returns:</p> Type Description <code>str</code> <p>the semester code corresponding to the given date</p> Source code in <code>core/utils.py</code> <pre><code>def get_semester_code(d: Optional[date] = None) -&gt; str:\n \"\"\"Return the semester code of the given date.\n If no date is given, return the semester code of the current semester.\n\n The semester code is an upper letter (A for autumn, P for spring),\n followed by the last two digits of the year.\n For example, the autumn semester of 2018 is \"A18\".\n\n Args:\n d: the date to use to compute the semester. If None, use today's date.\n\n Returns:\n the semester code corresponding to the given date\n \"\"\"\n if d is None:\n d = localdate()\n\n start = get_start_of_semester(d)\n\n if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN:\n return \"A\" + str(start.year)[-2:]\n return \"P\" + str(start.year)[-2:]\n</code></pre>"},{"location":"reference/counter/views/#counter.views.get_start_of_semester","title":"<code>get_start_of_semester(today=None)</code>","text":"<p>Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester.</p> <p>The current semester is computed as follows:</p> <ul> <li>If the date is between 15/08 and 31/12 =&gt; Autumn semester.</li> <li>If the date is between 01/01 and 15/02 =&gt; Autumn semester of the previous year.</li> <li>If the date is between 15/02 and 15/08 =&gt; Spring semester</li> </ul> <p>Parameters:</p> Name Type Description Default <code>today</code> <code>Optional[date]</code> <p>the date to use to compute the semester. If None, use today's date.</p> <code>None</code> <p>Returns:</p> Type Description <code>date</code> <p>the date of the start of the semester</p> Source code in <code>core/utils.py</code> <pre><code>def get_start_of_semester(today: Optional[date] = None) -&gt; date:\n \"\"\"Return the date of the start of the semester of the given date.\n If no date is given, return the start date of the current semester.\n\n The current semester is computed as follows:\n\n - If the date is between 15/08 and 31/12 =&gt; Autumn semester.\n - If the date is between 01/01 and 15/02 =&gt; Autumn semester of the previous year.\n - If the date is between 15/02 and 15/08 =&gt; Spring semester\n\n Args:\n today: the date to use to compute the semester. If None, use today's date.\n\n Returns:\n the date of the start of the semester\n \"\"\"\n if today is None:\n today = localdate()\n\n autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)\n spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)\n\n if today &gt;= autumn: # between 15/08 (included) and 31/12 -&gt; autumn semester\n return autumn\n if today &gt;= spring: # between 15/02 (included) and 15/08 -&gt; spring semester\n return spring\n # between 01/01 and 15/02 -&gt; autumn semester of the previous year\n return autumn.replace(year=autumn.year - 1)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.is_logged_in_counter","title":"<code>is_logged_in_counter(request)</code>","text":"<p>Check if the request is sent from a device logged to a counter.</p> <p>The request must also be sent within the frame of a counter's activity. Trying to use this function to manage access to non-sas related resources probably won't work.</p> <p>A request is considered as coming from a logged counter if :</p> <ul> <li>Its referer comes from the counter app (eg. fetching user pictures from the click UI) or the request path belongs to the counter app (eg. the barman went back to the main by missclick and go back to the counter)</li> <li>The current session has a counter token associated with it.</li> <li>A counter with this token exists.</li> </ul> Source code in <code>counter/utils.py</code> <pre><code>def is_logged_in_counter(request: HttpRequest) -&gt; bool:\n \"\"\"Check if the request is sent from a device logged to a counter.\n\n The request must also be sent within the frame of a counter's activity.\n Trying to use this function to manage access to non-sas\n related resources probably won't work.\n\n A request is considered as coming from a logged counter if :\n\n - Its referer comes from the counter app\n (eg. fetching user pictures from the click UI)\n or the request path belongs to the counter app\n (eg. the barman went back to the main by missclick and go back\n to the counter)\n - The current session has a counter token associated with it.\n - A counter with this token exists.\n \"\"\"\n referer_ok = (\n \"HTTP_REFERER\" in request.META\n and resolve(urlparse(request.META[\"HTTP_REFERER\"]).path).app_name == \"counter\"\n )\n return (\n (referer_ok or request.resolver_match.app_name == \"counter\")\n and \"counter_token\" in request.session\n and request.session[\"counter_token\"]\n and Counter.objects.filter(token=request.session[\"counter_token\"]).exists()\n )\n</code></pre>"},{"location":"reference/counter/views/#counter.views.counter_login","title":"<code>counter_login(request, counter_id)</code>","text":"<p>Log a user in a counter.</p> <p>A successful login will result in the beginning of a counter duty for the user.</p> Source code in <code>counter/views.py</code> <pre><code>@require_POST\ndef counter_login(request: HttpRequest, counter_id: int) -&gt; HttpResponseRedirect:\n \"\"\"Log a user in a counter.\n\n A successful login will result in the beginning of a counter duty\n for the user.\n \"\"\"\n counter = get_object_or_404(Counter, pk=counter_id)\n form = LoginForm(request, data=request.POST)\n if not form.is_valid():\n return redirect(counter.get_absolute_url() + \"?credentials\")\n user = form.get_user()\n if not counter.sellers.contains(user) or user in counter.barmen_list:\n return redirect(counter.get_absolute_url() + \"?sellers\")\n if len(counter.barmen_list) == 0:\n counter.gen_token()\n request.session[\"counter_token\"] = counter.token\n counter.permanencies.create(user=user, start=timezone.now())\n return redirect(counter)\n</code></pre>"},{"location":"reference/counter/views/#counter.views.counter_logout","title":"<code>counter_logout(request, counter_id)</code>","text":"<p>End the permanency of a user in this counter.</p> Source code in <code>counter/views.py</code> <pre><code>@require_POST\ndef counter_logout(request: HttpRequest, counter_id: int) -&gt; HttpResponseRedirect:\n \"\"\"End the permanency of a user in this counter.\"\"\"\n Permanency.objects.filter(counter=counter_id, user=request.POST[\"user_id\"]).update(\n end=F(\"activity\")\n )\n return redirect(\"counter:details\", counter_id=counter_id)\n</code></pre>"},{"location":"reference/eboutic/models/","title":"Models","text":""},{"location":"reference/eboutic/models/#eboutic.models.CurrencyField","title":"<code>CurrencyField(*args, **kwargs)</code>","text":"<p> Bases: <code>DecimalField</code></p> <p>Custom database field used for currency.</p> Source code in <code>accounting/models.py</code> <pre><code>def __init__(self, *args, **kwargs):\n kwargs[\"max_digits\"] = 12\n kwargs[\"decimal_places\"] = 2\n super().__init__(*args, **kwargs)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/eboutic/models/#eboutic.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/eboutic/models/#eboutic.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.BillingInfo","title":"<code>BillingInfo</code>","text":"<p> Bases: <code>Model</code></p> <p>Represent the billing information of a user, which are required by the 3D-Secure v2 system used by the etransaction module.</p>"},{"location":"reference/eboutic/models/#eboutic.models.BillingInfo.to_3dsv2_xml","title":"<code>to_3dsv2_xml()</code>","text":"<p>Convert the data from this model into a xml usable by the online paying service of the Cr\u00e9dit Agricole bank. see : <code>https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster</code>.</p> Source code in <code>counter/models.py</code> <pre><code>def to_3dsv2_xml(self) -&gt; str:\n \"\"\"Convert the data from this model into a xml usable\n by the online paying service of the Cr\u00e9dit Agricole bank.\n see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.\n \"\"\"\n data = {\n \"Address\": {\n \"FirstName\": self.first_name,\n \"LastName\": self.last_name,\n \"Address1\": self.address_1,\n \"ZipCode\": self.zip_code,\n \"City\": self.city,\n \"CountryCode\": self.country.numeric, # ISO-3166-1 numeric code\n \"MobilePhone\": self.phone_number.as_national.replace(\" \", \"\"),\n \"CountryCodeMobilePhone\": f\"+{self.phone_number.country_code}\",\n }\n }\n if self.address_2:\n data[\"Address\"][\"Address2\"] = self.address_2\n xml = dict2xml(data, wrap=\"Billing\", newlines=False)\n return '&lt;?xml version=\"1.0\" encoding=\"UTF-8\" ?&gt;' + xml\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Customer","title":"<code>Customer</code>","text":"<p> Bases: <code>Model</code></p> <p>Customer data of a User.</p> <p>It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.</p>"},{"location":"reference/eboutic/models/#eboutic.models.Customer.can_buy","title":"<code>can_buy: bool</code> <code>property</code>","text":"<p>Check if whether this customer has the right to purchase any item.</p> <p>This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.</p>"},{"location":"reference/eboutic/models/#eboutic.models.Customer.save","title":"<code>save(*args, allow_negative=False, is_selling=False, **kwargs)</code>","text":"<p>is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, is_selling=False, **kwargs):\n \"\"\"is_selling : tell if the current action is a selling\n allow_negative : ignored if not a selling. Allow a selling to put the account in negative\n Those two parameters avoid blocking the save method of a customer if his account is negative.\n \"\"\"\n if self.amount &lt; 0 and (is_selling and not allow_negative):\n raise ValidationError(_(\"Not enough money\"))\n super().save(*args, **kwargs)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Customer.get_or_create","title":"<code>get_or_create(user)</code> <code>classmethod</code>","text":"<p>Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.</p> <p>If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id</p> <p>Example : ::</p> <pre><code>user = User.objects.get(pk=1)\naccount, created = Customer.get_or_create(user)\nif created:\n print(f\"created a new account with id {account.id}\")\nelse:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>@classmethod\ndef get_or_create(cls, user: User) -&gt; Tuple[Customer, bool]:\n \"\"\"Work in pretty much the same way as the usual get_or_create method,\n but with the default field replaced by some under the hood.\n\n If the user has an account, return it as is.\n Else create a new account with no money on it and a new unique account id\n\n Example : ::\n\n user = User.objects.get(pk=1)\n account, created = Customer.get_or_create(user)\n if created:\n print(f\"created a new account with id {account.id}\")\n else:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n \"\"\"\n if hasattr(user, \"customer\"):\n return user.customer, False\n\n # account_id are always a number with a letter appended\n account_id = (\n Customer.objects.order_by(Length(\"account_id\"), \"account_id\")\n .values(\"account_id\")\n .last()\n )\n if account_id is None:\n # legacy from the old site\n account = cls.objects.create(user=user, account_id=\"1504a\")\n return account, True\n\n account_id = account_id[\"account_id\"]\n account_num = int(account_id[:-1])\n while Customer.objects.filter(account_id=account_id).exists():\n # when entering the first iteration, we are using an already existing account id\n # so the loop should always execute at least one time\n account_num += 1\n account_id = f\"{account_num}{random.choice(string.ascii_lowercase)}\"\n\n account = cls.objects.create(user=user, account_id=account_id)\n return account, True\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Product","title":"<code>Product</code>","text":"<p> Bases: <code>Model</code></p> <p>A product, with all its related information.</p>"},{"location":"reference/eboutic/models/#eboutic.models.Product.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(\n pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID\n ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Product.can_be_sold_to","title":"<code>can_be_sold_to(user)</code>","text":"<p>Check if whether the user given in parameter has the right to buy this product or not.</p> <p>This must be not confused with the Customer.can_buy() method as the present method returns an information about the relation between a User and a Product, whereas the other tells something about a Customer (and not a user, they are not the same model).</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user can buy this product else False</p> Warning <p>This performs a db query, thus you can quickly have a N+1 queries problem if you call it in a loop. Hopefully, you can avoid that if you prefetch the buying_groups :</p> <pre><code>user = User.objects.get(username=\"foobar\")\nproducts = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n]\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>def can_be_sold_to(self, user: User) -&gt; bool:\n \"\"\"Check if whether the user given in parameter has the right to buy\n this product or not.\n\n This must be not confused with the Customer.can_buy()\n method as the present method returns an information\n about the relation between a User and a Product,\n whereas the other tells something about a Customer\n (and not a user, they are not the same model).\n\n Returns:\n True if the user can buy this product else False\n\n Warning:\n This performs a db query, thus you can quickly have\n a N+1 queries problem if you call it in a loop.\n Hopefully, you can avoid that if you prefetch the buying_groups :\n\n ```python\n user = User.objects.get(username=\"foobar\")\n products = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n ]\n ```\n \"\"\"\n buying_groups = list(self.buying_groups.all())\n if not buying_groups:\n return True\n return any(user.is_in_group(pk=group.id) for group in buying_groups)\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Refilling","title":"<code>Refilling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the refilling.</p>"},{"location":"reference/eboutic/models/#eboutic.models.Selling","title":"<code>Selling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the sellings.</p>"},{"location":"reference/eboutic/models/#eboutic.models.Selling.save","title":"<code>save(*args, allow_negative=False, **kwargs)</code>","text":"<p>allow_negative : Allow this selling to use more money than available for this user.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, **kwargs):\n \"\"\"allow_negative : Allow this selling to use more money than available for this user.\"\"\"\n if not self.date:\n self.date = timezone.now()\n self.full_clean()\n if not self.is_validated:\n self.customer.amount -= self.quantity * self.unit_price\n self.customer.save(allow_negative=allow_negative, is_selling=True)\n self.is_validated = True\n user = self.customer.user\n if user.was_subscribed:\n if (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"un-semestre\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n elif (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"deux-semestres\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n if user.preferences.notify_on_click:\n Notification(\n user=user,\n url=reverse(\n \"core:user_account_detail\",\n kwargs={\n \"user_id\": user.id,\n \"year\": self.date.year,\n \"month\": self.date.month,\n },\n ),\n param=\"%d x %s\" % (self.quantity, self.label),\n type=\"SELLING\",\n ).save()\n super().save(*args, **kwargs)\n if hasattr(self.product, \"eticket\"):\n self.send_mail_customer()\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Basket","title":"<code>Basket</code>","text":"<p> Bases: <code>Model</code></p> <p>Basket is built when the user connects to an eboutic page.</p>"},{"location":"reference/eboutic/models/#eboutic.models.Basket.from_session","title":"<code>from_session(session)</code> <code>classmethod</code>","text":"<p>The basket stored in the session object, if it exists.</p> Source code in <code>eboutic/models.py</code> <pre><code>@classmethod\ndef from_session(cls, session) -&gt; Basket | None:\n \"\"\"The basket stored in the session object, if it exists.\"\"\"\n if \"basket_id\" in session:\n return cls.objects.filter(id=session[\"basket_id\"]).first()\n return None\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Basket.generate_sales","title":"<code>generate_sales(counter, seller, payment_method)</code>","text":"<p>Generate a list of sold items corresponding to the items of this basket WITHOUT saving them NOR deleting the basket.</p> Example <pre><code>counter = Counter.objects.get(name=\"Eboutic\")\nsales = basket.generate_sales(counter, \"SITH_ACCOUNT\")\n# here the basket is in the same state as before the method call\n\nwith transaction.atomic():\n for sale in sales:\n sale.save()\n basket.delete()\n # all the basket items are deleted by the on_delete=CASCADE relation\n # thus only the sales remain\n</code></pre> Source code in <code>eboutic/models.py</code> <pre><code>def generate_sales(self, counter, seller: User, payment_method: str):\n \"\"\"Generate a list of sold items corresponding to the items\n of this basket WITHOUT saving them NOR deleting the basket.\n\n Example:\n ```python\n counter = Counter.objects.get(name=\"Eboutic\")\n sales = basket.generate_sales(counter, \"SITH_ACCOUNT\")\n # here the basket is in the same state as before the method call\n\n with transaction.atomic():\n for sale in sales:\n sale.save()\n basket.delete()\n # all the basket items are deleted by the on_delete=CASCADE relation\n # thus only the sales remain\n ```\n \"\"\"\n # I must proceed with two distinct requests instead of\n # only one with a join because the AbstractBaseItem model has been\n # poorly designed. If you refactor the model, please refactor this too.\n items = self.items.order_by(\"product_id\")\n ids = [item.product_id for item in items]\n products = Product.objects.filter(id__in=ids).order_by(\"id\")\n # items and products are sorted in the same order\n sales = []\n for item, product in zip(items, products):\n sales.append(\n Selling(\n label=product.name,\n counter=counter,\n club=product.club,\n product=product,\n seller=seller,\n customer=self.user.customer,\n unit_price=item.product_unit_price,\n quantity=item.quantity,\n payment_method=payment_method,\n )\n )\n return sales\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.InvoiceQueryset","title":"<code>InvoiceQueryset</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/eboutic/models/#eboutic.models.InvoiceQueryset.annotate_total","title":"<code>annotate_total()</code>","text":"<p>Annotate the queryset with the total amount of each invoice.</p> <p>The total amount is the sum of (product_unit_price * quantity) for all items related to the invoice.</p> Source code in <code>eboutic/models.py</code> <pre><code>def annotate_total(self) -&gt; Self:\n \"\"\"Annotate the queryset with the total amount of each invoice.\n\n The total amount is the sum of (product_unit_price * quantity)\n for all items related to the invoice.\n \"\"\"\n # aggregates within subqueries require a little bit of black magic,\n # but hopefully, django gives a comprehensive documentation for that :\n # https://docs.djangoproject.com/en/stable/ref/models/expressions/#using-aggregates-within-a-subquery-expression\n return self.annotate(\n total=Subquery(\n InvoiceItem.objects.filter(invoice_id=OuterRef(\"pk\"))\n .values(\"invoice_id\")\n .annotate(total=Sum(F(\"product_unit_price\") * F(\"quantity\")))\n .values(\"total\")\n )\n )\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.Invoice","title":"<code>Invoice</code>","text":"<p> Bases: <code>Model</code></p> <p>Invoices are generated once the payment has been validated.</p>"},{"location":"reference/eboutic/models/#eboutic.models.AbstractBaseItem","title":"<code>AbstractBaseItem</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/eboutic/models/#eboutic.models.BasketItem","title":"<code>BasketItem</code>","text":"<p> Bases: <code>AbstractBaseItem</code></p>"},{"location":"reference/eboutic/models/#eboutic.models.BasketItem.from_product","title":"<code>from_product(product, quantity, basket)</code> <code>classmethod</code>","text":"<p>Create a BasketItem with the same characteristics as the product passed in parameters, with the specified quantity.</p> Warning <p>the basket field is not filled, so you must set it yourself before saving the model.</p> Source code in <code>eboutic/models.py</code> <pre><code>@classmethod\ndef from_product(cls, product: Product, quantity: int, basket: Basket):\n \"\"\"Create a BasketItem with the same characteristics as the\n product passed in parameters, with the specified quantity.\n\n Warning:\n the basket field is not filled, so you must set\n it yourself before saving the model.\n \"\"\"\n return cls(\n basket=basket,\n product_id=product.id,\n product_name=product.name,\n type_id=product.product_type_id,\n quantity=quantity,\n product_unit_price=product.selling_price,\n )\n</code></pre>"},{"location":"reference/eboutic/models/#eboutic.models.InvoiceItem","title":"<code>InvoiceItem</code>","text":"<p> Bases: <code>AbstractBaseItem</code></p>"},{"location":"reference/eboutic/models/#eboutic.models.get_eboutic_products","title":"<code>get_eboutic_products(user)</code>","text":"Source code in <code>eboutic/models.py</code> <pre><code>def get_eboutic_products(user: User) -&gt; list[Product]:\n products = (\n Counter.objects.get(type=\"EBOUTIC\")\n .products.filter(product_type__isnull=False)\n .filter(archived=False)\n .filter(limit_age__lte=user.age)\n .annotate(priority=F(\"product_type__priority\"))\n .annotate(category=F(\"product_type__name\"))\n .annotate(category_comment=F(\"product_type__comment\"))\n .prefetch_related(\"buying_groups\") # &lt;-- used in `Product.can_be_sold_to`\n )\n return [p for p in products if p.can_be_sold_to(user)]\n</code></pre>"},{"location":"reference/eboutic/views/","title":"Views","text":""},{"location":"reference/eboutic/views/#eboutic.views.PurchaseItemList","title":"<code>PurchaseItemList = TypeAdapter(list[PurchaseItemSchema])</code> <code>module-attribute</code>","text":""},{"location":"reference/eboutic/views/#eboutic.views.BillingInfoForm","title":"<code>BillingInfoForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Customer","title":"<code>Customer</code>","text":"<p> Bases: <code>Model</code></p> <p>Customer data of a User.</p> <p>It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.</p>"},{"location":"reference/eboutic/views/#eboutic.views.Customer.can_buy","title":"<code>can_buy: bool</code> <code>property</code>","text":"<p>Check if whether this customer has the right to purchase any item.</p> <p>This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.</p>"},{"location":"reference/eboutic/views/#eboutic.views.Customer.save","title":"<code>save(*args, allow_negative=False, is_selling=False, **kwargs)</code>","text":"<p>is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, is_selling=False, **kwargs):\n \"\"\"is_selling : tell if the current action is a selling\n allow_negative : ignored if not a selling. Allow a selling to put the account in negative\n Those two parameters avoid blocking the save method of a customer if his account is negative.\n \"\"\"\n if self.amount &lt; 0 and (is_selling and not allow_negative):\n raise ValidationError(_(\"Not enough money\"))\n super().save(*args, **kwargs)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Customer.get_or_create","title":"<code>get_or_create(user)</code> <code>classmethod</code>","text":"<p>Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.</p> <p>If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id</p> <p>Example : ::</p> <pre><code>user = User.objects.get(pk=1)\naccount, created = Customer.get_or_create(user)\nif created:\n print(f\"created a new account with id {account.id}\")\nelse:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>@classmethod\ndef get_or_create(cls, user: User) -&gt; Tuple[Customer, bool]:\n \"\"\"Work in pretty much the same way as the usual get_or_create method,\n but with the default field replaced by some under the hood.\n\n If the user has an account, return it as is.\n Else create a new account with no money on it and a new unique account id\n\n Example : ::\n\n user = User.objects.get(pk=1)\n account, created = Customer.get_or_create(user)\n if created:\n print(f\"created a new account with id {account.id}\")\n else:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n \"\"\"\n if hasattr(user, \"customer\"):\n return user.customer, False\n\n # account_id are always a number with a letter appended\n account_id = (\n Customer.objects.order_by(Length(\"account_id\"), \"account_id\")\n .values(\"account_id\")\n .last()\n )\n if account_id is None:\n # legacy from the old site\n account = cls.objects.create(user=user, account_id=\"1504a\")\n return account, True\n\n account_id = account_id[\"account_id\"]\n account_num = int(account_id[:-1])\n while Customer.objects.filter(account_id=account_id).exists():\n # when entering the first iteration, we are using an already existing account id\n # so the loop should always execute at least one time\n account_num += 1\n account_id = f\"{account_num}{random.choice(string.ascii_lowercase)}\"\n\n account = cls.objects.create(user=user, account_id=account_id)\n return account, True\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Product","title":"<code>Product</code>","text":"<p> Bases: <code>Model</code></p> <p>A product, with all its related information.</p>"},{"location":"reference/eboutic/views/#eboutic.views.Product.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>counter/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_in_group(\n pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID\n ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Product.can_be_sold_to","title":"<code>can_be_sold_to(user)</code>","text":"<p>Check if whether the user given in parameter has the right to buy this product or not.</p> <p>This must be not confused with the Customer.can_buy() method as the present method returns an information about the relation between a User and a Product, whereas the other tells something about a Customer (and not a user, they are not the same model).</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user can buy this product else False</p> Warning <p>This performs a db query, thus you can quickly have a N+1 queries problem if you call it in a loop. Hopefully, you can avoid that if you prefetch the buying_groups :</p> <pre><code>user = User.objects.get(username=\"foobar\")\nproducts = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n]\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>def can_be_sold_to(self, user: User) -&gt; bool:\n \"\"\"Check if whether the user given in parameter has the right to buy\n this product or not.\n\n This must be not confused with the Customer.can_buy()\n method as the present method returns an information\n about the relation between a User and a Product,\n whereas the other tells something about a Customer\n (and not a user, they are not the same model).\n\n Returns:\n True if the user can buy this product else False\n\n Warning:\n This performs a db query, thus you can quickly have\n a N+1 queries problem if you call it in a loop.\n Hopefully, you can avoid that if you prefetch the buying_groups :\n\n ```python\n user = User.objects.get(username=\"foobar\")\n products = [\n p\n for p in Product.objects.prefetch_related(\"buying_groups\")\n if p.can_be_sold_to(user)\n ]\n ```\n \"\"\"\n buying_groups = list(self.buying_groups.all())\n if not buying_groups:\n return True\n return any(user.is_in_group(pk=group.id) for group in buying_groups)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.BasketForm","title":"<code>BasketForm(request)</code>","text":"<p>Class intended to perform checks on the request sended to the server when the user submits his basket from /eboutic/.</p> <p>Because it must check an unknown number of fields, coming from a cookie and needing some databases checks to be performed, inheriting from forms.Form or using formset would have been likely to end in a big ball of wibbly-wobbly hacky stuff. Thus this class is a pure standalone and performs its operations by its own means. However, it still tries to share some similarities with a standard django Form.</p> <p>Examples:</p> <p>::</p> <pre><code>def my_view(request):\n form = BasketForm(request)\n form.clean()\n if form.is_valid():\n # perform operations\n else:\n errors = form.get_error_messages()\n\n # return the cookie that was in the request, but with all\n # incorrects elements removed\n cookie = form.get_cleaned_cookie()\n</code></pre> <p>You can also use a little shortcut by directly calling <code>form.is_valid()</code> without calling <code>form.clean()</code>. In this case, the latter method shall be implicitly called.</p> Source code in <code>eboutic/forms.py</code> <pre><code>def __init__(self, request: HttpRequest):\n self.user = request.user\n self.cookies = request.COOKIES\n self.error_messages = set()\n self.correct_items = []\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.BasketForm.clean","title":"<code>clean()</code>","text":"<p>Perform all the checks, but return nothing. To know if the form is valid, the <code>is_valid()</code> method must be used.</p> The form shall be considered as valid if it meets all the following conditions <ul> <li>it contains a \"basket_items\" key in the cookies of the request given in the constructor</li> <li>this cookie is a list of objects formatted this way : <code>[{'id': &lt;int&gt;, 'quantity': &lt;int&gt;, 'name': &lt;str&gt;, 'unit_price': &lt;float&gt;}, ...]</code>. The order of the fields in each object does not matter</li> <li>all the ids are positive integers</li> <li>all the ids refer to products available in the EBOUTIC</li> <li>all the ids refer to products the user is allowed to buy</li> <li>all the quantities are positive integers</li> </ul> Source code in <code>eboutic/forms.py</code> <pre><code>def clean(self) -&gt; None:\n \"\"\"Perform all the checks, but return nothing.\n To know if the form is valid, the `is_valid()` method must be used.\n\n The form shall be considered as valid if it meets all the following conditions :\n - it contains a \"basket_items\" key in the cookies of the request given in the constructor\n - this cookie is a list of objects formatted this way : `[{'id': &lt;int&gt;, 'quantity': &lt;int&gt;,\n 'name': &lt;str&gt;, 'unit_price': &lt;float&gt;}, ...]`. The order of the fields in each object does not matter\n - all the ids are positive integers\n - all the ids refer to products available in the EBOUTIC\n - all the ids refer to products the user is allowed to buy\n - all the quantities are positive integers\n \"\"\"\n try:\n basket = PurchaseItemList.validate_json(\n unquote(self.cookies.get(\"basket_items\", \"[]\"))\n )\n except ValidationError:\n self.error_messages.add(_(\"The request was badly formatted.\"))\n return\n if len(basket) == 0:\n self.error_messages.add(_(\"Your basket is empty.\"))\n return\n existing_ids = {product.id for product in get_eboutic_products(self.user)}\n for item in basket:\n # check a product with this id does exist\n if item.product_id in existing_ids:\n self.correct_items.append(item)\n else:\n self.error_messages.add(\n _(\n \"%(name)s : this product does not exist or may no longer be available.\"\n )\n % {\"name\": item.name}\n )\n continue\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.BasketForm.is_valid","title":"<code>is_valid()</code>","text":"<p>Return True if the form is correct else False.</p> <p>If the <code>clean()</code> method has not been called beforehand, call it.</p> Source code in <code>eboutic/forms.py</code> <pre><code>def is_valid(self) -&gt; bool:\n \"\"\"Return True if the form is correct else False.\n\n If the `clean()` method has not been called beforehand, call it.\n \"\"\"\n if not self.error_messages and not self.correct_items:\n self.clean()\n return not self.error_messages\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Basket","title":"<code>Basket</code>","text":"<p> Bases: <code>Model</code></p> <p>Basket is built when the user connects to an eboutic page.</p>"},{"location":"reference/eboutic/views/#eboutic.views.Basket.from_session","title":"<code>from_session(session)</code> <code>classmethod</code>","text":"<p>The basket stored in the session object, if it exists.</p> Source code in <code>eboutic/models.py</code> <pre><code>@classmethod\ndef from_session(cls, session) -&gt; Basket | None:\n \"\"\"The basket stored in the session object, if it exists.\"\"\"\n if \"basket_id\" in session:\n return cls.objects.filter(id=session[\"basket_id\"]).first()\n return None\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Basket.generate_sales","title":"<code>generate_sales(counter, seller, payment_method)</code>","text":"<p>Generate a list of sold items corresponding to the items of this basket WITHOUT saving them NOR deleting the basket.</p> Example <pre><code>counter = Counter.objects.get(name=\"Eboutic\")\nsales = basket.generate_sales(counter, \"SITH_ACCOUNT\")\n# here the basket is in the same state as before the method call\n\nwith transaction.atomic():\n for sale in sales:\n sale.save()\n basket.delete()\n # all the basket items are deleted by the on_delete=CASCADE relation\n # thus only the sales remain\n</code></pre> Source code in <code>eboutic/models.py</code> <pre><code>def generate_sales(self, counter, seller: User, payment_method: str):\n \"\"\"Generate a list of sold items corresponding to the items\n of this basket WITHOUT saving them NOR deleting the basket.\n\n Example:\n ```python\n counter = Counter.objects.get(name=\"Eboutic\")\n sales = basket.generate_sales(counter, \"SITH_ACCOUNT\")\n # here the basket is in the same state as before the method call\n\n with transaction.atomic():\n for sale in sales:\n sale.save()\n basket.delete()\n # all the basket items are deleted by the on_delete=CASCADE relation\n # thus only the sales remain\n ```\n \"\"\"\n # I must proceed with two distinct requests instead of\n # only one with a join because the AbstractBaseItem model has been\n # poorly designed. If you refactor the model, please refactor this too.\n items = self.items.order_by(\"product_id\")\n ids = [item.product_id for item in items]\n products = Product.objects.filter(id__in=ids).order_by(\"id\")\n # items and products are sorted in the same order\n sales = []\n for item, product in zip(items, products):\n sales.append(\n Selling(\n label=product.name,\n counter=counter,\n club=product.club,\n product=product,\n seller=seller,\n customer=self.user.customer,\n unit_price=item.product_unit_price,\n quantity=item.quantity,\n payment_method=payment_method,\n )\n )\n return sales\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.BasketItem","title":"<code>BasketItem</code>","text":"<p> Bases: <code>AbstractBaseItem</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.BasketItem.from_product","title":"<code>from_product(product, quantity, basket)</code> <code>classmethod</code>","text":"<p>Create a BasketItem with the same characteristics as the product passed in parameters, with the specified quantity.</p> Warning <p>the basket field is not filled, so you must set it yourself before saving the model.</p> Source code in <code>eboutic/models.py</code> <pre><code>@classmethod\ndef from_product(cls, product: Product, quantity: int, basket: Basket):\n \"\"\"Create a BasketItem with the same characteristics as the\n product passed in parameters, with the specified quantity.\n\n Warning:\n the basket field is not filled, so you must set\n it yourself before saving the model.\n \"\"\"\n return cls(\n basket=basket,\n product_id=product.id,\n product_name=product.name,\n type_id=product.product_type_id,\n quantity=quantity,\n product_unit_price=product.selling_price,\n )\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.Invoice","title":"<code>Invoice</code>","text":"<p> Bases: <code>Model</code></p> <p>Invoices are generated once the payment has been validated.</p>"},{"location":"reference/eboutic/views/#eboutic.views.InvoiceItem","title":"<code>InvoiceItem</code>","text":"<p> Bases: <code>AbstractBaseItem</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.PurchaseItemSchema","title":"<code>PurchaseItemSchema</code>","text":"<p> Bases: <code>Schema</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.BillingInfoState","title":"<code>BillingInfoState</code>","text":"<p> Bases: <code>Enum</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.EbouticCommand","title":"<code>EbouticCommand</code>","text":"<p> Bases: <code>LoginRequiredMixin</code>, <code>TemplateView</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.EtransactionAutoAnswer","title":"<code>EtransactionAutoAnswer</code>","text":"<p> Bases: <code>View</code></p>"},{"location":"reference/eboutic/views/#eboutic.views.get_eboutic_products","title":"<code>get_eboutic_products(user)</code>","text":"Source code in <code>eboutic/models.py</code> <pre><code>def get_eboutic_products(user: User) -&gt; list[Product]:\n products = (\n Counter.objects.get(type=\"EBOUTIC\")\n .products.filter(product_type__isnull=False)\n .filter(archived=False)\n .filter(limit_age__lte=user.age)\n .annotate(priority=F(\"product_type__priority\"))\n .annotate(category=F(\"product_type__name\"))\n .annotate(category_comment=F(\"product_type__comment\"))\n .prefetch_related(\"buying_groups\") # &lt;-- used in `Product.can_be_sold_to`\n )\n return [p for p in products if p.can_be_sold_to(user)]\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.eboutic_main","title":"<code>eboutic_main(request)</code>","text":"<p>Main view of the eboutic application.</p> <p>Return an Http response whose content is of type text/html. The latter represents the page from which a user can see the catalogue of products that he can buy and fill his shopping cart.</p> <p>The purchasable products are those of the eboutic which belong to a category of products of a product category (orphan products are inaccessible).</p> <p>If the session contains a key-value pair that associates \"errors\" with a list of strings, this pair is removed from the session and its value displayed to the user when the page is rendered.</p> Source code in <code>eboutic/views.py</code> <pre><code>@login_required\n@require_GET\ndef eboutic_main(request: HttpRequest) -&gt; HttpResponse:\n \"\"\"Main view of the eboutic application.\n\n Return an Http response whose content is of type text/html.\n The latter represents the page from which a user can see\n the catalogue of products that he can buy and fill\n his shopping cart.\n\n The purchasable products are those of the eboutic which\n belong to a category of products of a product category\n (orphan products are inaccessible).\n\n If the session contains a key-value pair that associates \"errors\"\n with a list of strings, this pair is removed from the session\n and its value displayed to the user when the page is rendered.\n \"\"\"\n errors = request.session.pop(\"errors\", None)\n products = get_eboutic_products(request.user)\n context = {\n \"errors\": errors,\n \"products\": products,\n \"customer_amount\": request.user.account_balance,\n }\n return render(request, \"eboutic/eboutic_main.jinja\", context)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.payment_result","title":"<code>payment_result(request, result)</code>","text":"Source code in <code>eboutic/views.py</code> <pre><code>@require_GET\n@login_required\ndef payment_result(request, result: str) -&gt; HttpResponse:\n context = {\"success\": result == \"success\"}\n return render(request, \"eboutic/eboutic_payment_result.jinja\", context)\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.e_transaction_data","title":"<code>e_transaction_data(request)</code>","text":"Source code in <code>eboutic/views.py</code> <pre><code>@login_required\n@require_GET\ndef e_transaction_data(request):\n basket = Basket.from_session(request.session)\n if basket is None:\n return HttpResponse(status=404, content=json.dumps({\"data\": []}))\n data = basket.get_e_transaction_data()\n data = {\"data\": [{\"key\": key, \"value\": val} for key, val in data]}\n return HttpResponse(status=200, content=json.dumps(data))\n</code></pre>"},{"location":"reference/eboutic/views/#eboutic.views.pay_with_sith","title":"<code>pay_with_sith(request)</code>","text":"Source code in <code>eboutic/views.py</code> <pre><code>@login_required\n@require_POST\ndef pay_with_sith(request):\n basket = Basket.from_session(request.session)\n refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING\n if basket is None or basket.items.filter(type_id=refilling).exists():\n return redirect(\"eboutic:main\")\n c = Customer.objects.filter(user__id=basket.user_id).first()\n if c is None:\n return redirect(\"eboutic:main\")\n if c.amount &lt; basket.total:\n res = redirect(\"eboutic:payment_result\", \"failure\")\n res.delete_cookie(\"basket_items\", \"/eboutic\")\n return res\n eboutic = Counter.objects.get(type=\"EBOUTIC\")\n sales = basket.generate_sales(eboutic, c.user, \"SITH_ACCOUNT\")\n try:\n with transaction.atomic():\n # Selling.save has some important business logic in it.\n # Do not bulk_create this\n for sale in sales:\n sale.save()\n basket.delete()\n request.session.pop(\"basket_id\", None)\n res = redirect(\"eboutic:payment_result\", \"success\")\n except DatabaseError as e:\n with sentry_sdk.push_scope() as scope:\n scope.user = {\"username\": request.user.username}\n scope.set_extra(\"someVariable\", e.__repr__())\n sentry_sdk.capture_message(\n f\"Erreur le {datetime.now()} dans eboutic.pay_with_sith\"\n )\n res = redirect(\"eboutic:payment_result\", \"failure\")\n res.delete_cookie(\"basket_items\", \"/eboutic\")\n return res\n</code></pre>"},{"location":"reference/election/models/","title":"Models","text":""},{"location":"reference/election/models/#election.models.Group","title":"<code>Group</code>","text":"<p> Bases: <code>Group</code></p> <p>Implement both RealGroups and Meta groups.</p> <p>Groups are sorted by their is_meta property</p>"},{"location":"reference/election/models/#election.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/election/models/#election.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/election/models/#election.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/election/models/#election.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/election/models/#election.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/election/models/#election.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/election/models/#election.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/election/models/#election.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/election/models/#election.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/election/models/#election.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/election/models/#election.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/election/models/#election.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/election/models/#election.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/election/models/#election.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/election/models/#election.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/election/models/#election.models.Election","title":"<code>Election</code>","text":"<p> Bases: <code>Model</code></p> <p>This class allows to create a new election.</p>"},{"location":"reference/election/models/#election.models.Role","title":"<code>Role</code>","text":"<p> Bases: <code>OrderedModel</code></p> <p>This class allows to create a new role avaliable for a candidature.</p>"},{"location":"reference/election/models/#election.models.ElectionList","title":"<code>ElectionList</code>","text":"<p> Bases: <code>Model</code></p> <p>To allow per list vote.</p>"},{"location":"reference/election/models/#election.models.Candidature","title":"<code>Candidature</code>","text":"<p> Bases: <code>Model</code></p> <p>This class is a component of responsability.</p>"},{"location":"reference/election/models/#election.models.Vote","title":"<code>Vote</code>","text":"<p> Bases: <code>Model</code></p> <p>This class allows to vote for candidates.</p>"},{"location":"reference/election/views/","title":"Views","text":""},{"location":"reference/election/views/#election.views.CanCreateMixin","title":"<code>CanCreateMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect any child view that would create an object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user has not the necessary permission to create the object of the view.</p>"},{"location":"reference/election/views/#election.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/election/views/#election.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/election/views/#election.views.Candidature","title":"<code>Candidature</code>","text":"<p> Bases: <code>Model</code></p> <p>This class is a component of responsability.</p>"},{"location":"reference/election/views/#election.views.Election","title":"<code>Election</code>","text":"<p> Bases: <code>Model</code></p> <p>This class allows to create a new election.</p>"},{"location":"reference/election/views/#election.views.ElectionList","title":"<code>ElectionList</code>","text":"<p> Bases: <code>Model</code></p> <p>To allow per list vote.</p>"},{"location":"reference/election/views/#election.views.Role","title":"<code>Role</code>","text":"<p> Bases: <code>OrderedModel</code></p> <p>This class allows to create a new role avaliable for a candidature.</p>"},{"location":"reference/election/views/#election.views.Vote","title":"<code>Vote</code>","text":"<p> Bases: <code>Model</code></p> <p>This class allows to vote for candidates.</p>"},{"location":"reference/election/views/#election.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/election/views/#election.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/election/views/#election.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/election/views/#election.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/election/views/#election.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/election/views/#election.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/election/views/#election.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/election/views/#election.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/election/views/#election.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/election/views/#election.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/election/views/#election.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/election/views/#election.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/election/views/#election.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/election/views/#election.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/election/views/#election.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/election/views/#election.views.LimitedCheckboxField","title":"<code>LimitedCheckboxField(queryset, max_choice, **kwargs)</code>","text":"<p> Bases: <code>ModelMultipleChoiceField</code></p> <p>A <code>ModelMultipleChoiceField</code>, with a max limit of selectable inputs.</p> Source code in <code>election/views.py</code> <pre><code>def __init__(self, queryset, max_choice, **kwargs):\n self.max_choice = max_choice\n super().__init__(queryset, **kwargs)\n</code></pre>"},{"location":"reference/election/views/#election.views.CandidateForm","title":"<code>CandidateForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form to candidate.</p> Source code in <code>election/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n election_id = kwargs.pop(\"election_id\", None)\n can_edit = kwargs.pop(\"can_edit\", False)\n super().__init__(*args, **kwargs)\n if election_id:\n self.fields[\"role\"].queryset = Role.objects.filter(\n election__id=election_id\n ).all()\n self.fields[\"election_list\"].queryset = ElectionList.objects.filter(\n election__id=election_id\n ).all()\n if not can_edit:\n self.fields[\"user\"].widget = forms.HiddenInput()\n</code></pre>"},{"location":"reference/election/views/#election.views.VoteForm","title":"<code>VoteForm(election, user, *args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> Source code in <code>election/views.py</code> <pre><code>def __init__(self, election, user, *args, **kwargs):\n super().__init__(*args, **kwargs)\n if not election.has_voted(user):\n for role in election.roles.all():\n cand = role.candidatures\n if role.max_choice &gt; 1:\n self.fields[role.title] = LimitedCheckboxField(\n cand, role.max_choice, required=False\n )\n else:\n self.fields[role.title] = forms.ModelChoiceField(\n cand,\n required=False,\n widget=forms.RadioSelect(),\n empty_label=_(\"Blank vote\"),\n )\n</code></pre>"},{"location":"reference/election/views/#election.views.RoleForm","title":"<code>RoleForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form for creating a role.</p> Source code in <code>election/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n election_id = kwargs.pop(\"election_id\", None)\n super().__init__(*args, **kwargs)\n if election_id:\n self.fields[\"election\"].queryset = Election.objects.filter(\n id=election_id\n ).all()\n</code></pre>"},{"location":"reference/election/views/#election.views.ElectionListForm","title":"<code>ElectionListForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>election/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n election_id = kwargs.pop(\"election_id\", None)\n super().__init__(*args, **kwargs)\n if election_id:\n self.fields[\"election\"].queryset = Election.objects.filter(\n id=election_id\n ).all()\n</code></pre>"},{"location":"reference/election/views/#election.views.ElectionForm","title":"<code>ElectionForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/election/views/#election.views.ElectionsListView","title":"<code>ElectionsListView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p> <p>A list of all non archived elections visible.</p>"},{"location":"reference/election/views/#election.views.ElectionListArchivedView","title":"<code>ElectionListArchivedView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p> <p>A list of all archived elections visible.</p>"},{"location":"reference/election/views/#election.views.ElectionDetailView","title":"<code>ElectionDetailView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Details an election responsability by responsability.</p>"},{"location":"reference/election/views/#election.views.ElectionDetailView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add additionnal data to the template.</p> Source code in <code>election/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add additionnal data to the template.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"election_form\"] = VoteForm(self.object, self.request.user)\n kwargs[\"election_results\"] = self.object.results\n return kwargs\n</code></pre>"},{"location":"reference/election/views/#election.views.VoteFormView","title":"<code>VoteFormView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>FormView</code></p> <p>Alows users to vote.</p>"},{"location":"reference/election/views/#election.views.VoteFormView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>Verify that the user is part in a vote group.</p> Source code in <code>election/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"Verify that the user is part in a vote group.\"\"\"\n data = form.clean()\n res = super(FormView, self).form_valid(form)\n for grp_id in self.election.vote_groups.values_list(\"pk\", flat=True):\n if self.request.user.is_in_group(pk=grp_id):\n self.vote(data)\n return res\n return res\n</code></pre>"},{"location":"reference/election/views/#election.views.VoteFormView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add additionnal data to the template.</p> Source code in <code>election/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add additionnal data to the template.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"object\"] = self.election\n kwargs[\"election\"] = self.election\n kwargs[\"election_form\"] = self.get_form()\n return kwargs\n</code></pre>"},{"location":"reference/election/views/#election.views.CandidatureCreateView","title":"<code>CandidatureCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>View dedicated to a cundidature creation.</p>"},{"location":"reference/election/views/#election.views.CandidatureCreateView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>Verify that the selected user is in candidate group.</p> Source code in <code>election/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"Verify that the selected user is in candidate group.\"\"\"\n obj = form.instance\n obj.election = Election.objects.get(id=self.election.id)\n obj.user = obj.user if hasattr(obj, \"user\") else self.request.user\n if (obj.election.can_candidate(obj.user)) and (\n obj.user == self.request.user or self.can_edit\n ):\n return super(CreateView, self).form_valid(form)\n raise PermissionDenied\n</code></pre>"},{"location":"reference/election/views/#election.views.ElectionCreateView","title":"<code>ElectionCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/election/views/#election.views.ElectionCreateView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>Allow every user that had passed the dispatch to create an election.</p> Source code in <code>election/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"Allow every user that had passed the dispatch to create an election.\"\"\"\n return super(CreateView, self).form_valid(form)\n</code></pre>"},{"location":"reference/election/views/#election.views.RoleCreateView","title":"<code>RoleCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/election/views/#election.views.RoleCreateView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>Verify that the user can edit properly.</p> Source code in <code>election/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"Verify that the user can edit properly.\"\"\"\n obj: Role = form.instance\n user: User = self.request.user\n if obj.election:\n for grp_id in obj.election.edit_groups.values_list(\"pk\", flat=True):\n if user.is_in_group(pk=grp_id):\n return super(CreateView, self).form_valid(form)\n raise PermissionDenied\n</code></pre>"},{"location":"reference/election/views/#election.views.ElectionListCreateView","title":"<code>ElectionListCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/election/views/#election.views.ElectionListCreateView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>Verify that the user can vote on this election.</p> Source code in <code>election/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"Verify that the user can vote on this election.\"\"\"\n obj: ElectionList = form.instance\n user: User = self.request.user\n if obj.election:\n for grp_id in obj.election.candidature_groups.values_list(\"pk\", flat=True):\n if user.is_in_group(pk=grp_id):\n return super(CreateView, self).form_valid(form)\n for grp_id in obj.election.edit_groups.values_list(\"pk\", flat=True):\n if user.is_in_group(pk=grp_id):\n return super(CreateView, self).form_valid(form)\n raise PermissionDenied\n</code></pre>"},{"location":"reference/election/views/#election.views.ElectionUpdateView","title":"<code>ElectionUpdateView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/election/views/#election.views.CandidatureUpdateView","title":"<code>CandidatureUpdateView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/election/views/#election.views.RoleUpdateView","title":"<code>RoleUpdateView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/election/views/#election.views.ElectionDeleteView","title":"<code>ElectionDeleteView</code>","text":"<p> Bases: <code>DeleteView</code></p>"},{"location":"reference/election/views/#election.views.CandidatureDeleteView","title":"<code>CandidatureDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/election/views/#election.views.RoleDeleteView","title":"<code>RoleDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/election/views/#election.views.ElectionListDeleteView","title":"<code>ElectionListDeleteView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/forum/models/","title":"Models","text":""},{"location":"reference/forum/models/#forum.models.MESSAGE_META_ACTIONS","title":"<code>MESSAGE_META_ACTIONS = [('EDIT', _('Message edited by')), ('DELETE', _('Message deleted by')), ('UNDELETE', _('Message undeleted by'))]</code> <code>module-attribute</code>","text":""},{"location":"reference/forum/models/#forum.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/forum/models/#forum.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Group","title":"<code>Group</code>","text":"<p> Bases: <code>Group</code></p> <p>Implement both RealGroups and Meta groups.</p> <p>Groups are sorted by their is_meta property</p>"},{"location":"reference/forum/models/#forum.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/forum/models/#forum.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/forum/models/#forum.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/forum/models/#forum.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Forum","title":"<code>Forum</code>","text":"<p> Bases: <code>Model</code></p> <p>The Forum class, made as a tree to allow nice tidy organization.</p> <p>owner_club allows club members to moderate there own topics edit_groups allows to put any group as a forum admin view_groups allows some groups to view a forum</p>"},{"location":"reference/forum/models/#forum.models.Forum.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>forum/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.owner_club = self.parent.owner_club\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n self.save()\n</code></pre>"},{"location":"reference/forum/models/#forum.models.Forum.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>forum/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in forums\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/forum/models/#forum.models.ForumTopic","title":"<code>ForumTopic</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/forum/models/#forum.models.ForumMessage","title":"<code>ForumMessage</code>","text":"<p> Bases: <code>Model</code></p> <p>A message in the forum (thx Cpt. Obvious.).</p>"},{"location":"reference/forum/models/#forum.models.ForumMessageMeta","title":"<code>ForumMessageMeta</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/forum/models/#forum.models.ForumUserInfo","title":"<code>ForumUserInfo</code>","text":"<p> Bases: <code>Model</code></p> <p>The forum infos of a user.</p> <p>This currently stores only the last date a user clicked \"Mark all as read\". However, this can be extended with lot of user preferences dedicated to a user, such as the favourite topics, the signature, and so on...</p>"},{"location":"reference/forum/models/#forum.models.get_default_edit_group","title":"<code>get_default_edit_group()</code>","text":"Source code in <code>forum/models.py</code> <pre><code>def get_default_edit_group():\n return [settings.SITH_GROUP_OLD_SUBSCRIBERS_ID]\n</code></pre>"},{"location":"reference/forum/models/#forum.models.get_default_view_group","title":"<code>get_default_view_group()</code>","text":"Source code in <code>forum/models.py</code> <pre><code>def get_default_view_group():\n return [settings.SITH_GROUP_PUBLIC_ID]\n</code></pre>"},{"location":"reference/forum/views/","title":"Views","text":""},{"location":"reference/forum/views/#forum.views.CanCreateMixin","title":"<code>CanCreateMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect any child view that would create an object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user has not the necessary permission to create the object of the view.</p>"},{"location":"reference/forum/views/#forum.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/forum/views/#forum.views.CanEditPropMixin","title":"<code>CanEditPropMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has owner permissions on the child view object.</p> <p>In other word, you can make a view with this view as parent, and it will be retricted to the users that are in the object's owner_group or that pass the <code>obj.can_be_viewed_by</code> test.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user cannot see the object</p>"},{"location":"reference/forum/views/#forum.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/forum/views/#forum.views.Forum","title":"<code>Forum</code>","text":"<p> Bases: <code>Model</code></p> <p>The Forum class, made as a tree to allow nice tidy organization.</p> <p>owner_club allows club members to moderate there own topics edit_groups allows to put any group as a forum admin view_groups allows some groups to view a forum</p>"},{"location":"reference/forum/views/#forum.views.Forum.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>forum/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.owner_club = self.parent.owner_club\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n self.save()\n</code></pre>"},{"location":"reference/forum/views/#forum.views.Forum.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>forum/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in forums\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/forum/views/#forum.views.ForumMessage","title":"<code>ForumMessage</code>","text":"<p> Bases: <code>Model</code></p> <p>A message in the forum (thx Cpt. Obvious.).</p>"},{"location":"reference/forum/views/#forum.views.ForumMessageMeta","title":"<code>ForumMessageMeta</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/forum/views/#forum.views.ForumTopic","title":"<code>ForumTopic</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/forum/views/#forum.views.ForumSearchView","title":"<code>ForumSearchView</code>","text":"<p> Bases: <code>ListView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMainView","title":"<code>ForumMainView</code>","text":"<p> Bases: <code>ListView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMarkAllAsRead","title":"<code>ForumMarkAllAsRead</code>","text":"<p> Bases: <code>RedirectView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumFavoriteTopics","title":"<code>ForumFavoriteTopics</code>","text":"<p> Bases: <code>ListView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumLastUnread","title":"<code>ForumLastUnread</code>","text":"<p> Bases: <code>ListView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumNameField","title":"<code>ForumNameField</code>","text":"<p> Bases: <code>ModelChoiceField</code></p>"},{"location":"reference/forum/views/#forum.views.ForumForm","title":"<code>ForumForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/forum/views/#forum.views.ForumCreateView","title":"<code>ForumCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumEditForm","title":"<code>ForumEditForm</code>","text":"<p> Bases: <code>ForumForm</code></p>"},{"location":"reference/forum/views/#forum.views.ForumEditView","title":"<code>ForumEditView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumDeleteView","title":"<code>ForumDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumDetailView","title":"<code>ForumDetailView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/forum/views/#forum.views.TopicForm","title":"<code>TopicForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/forum/views/#forum.views.ForumTopicCreateView","title":"<code>ForumTopicCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumTopicEditView","title":"<code>ForumTopicEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumTopicSubscribeView","title":"<code>ForumTopicSubscribeView</code>","text":"<p> Bases: <code>LoginRequiredMixin</code>, <code>CanViewMixin</code>, <code>SingleObjectMixin</code>, <code>RedirectView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumTopicDetailView","title":"<code>ForumTopicDetailView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMessageView","title":"<code>ForumMessageView</code>","text":"<p> Bases: <code>SingleObjectMixin</code>, <code>RedirectView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMessageEditView","title":"<code>ForumMessageEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMessageDeleteView","title":"<code>ForumMessageDeleteView</code>","text":"<p> Bases: <code>SingleObjectMixin</code>, <code>RedirectView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMessageUndeleteView","title":"<code>ForumMessageUndeleteView</code>","text":"<p> Bases: <code>SingleObjectMixin</code>, <code>RedirectView</code></p>"},{"location":"reference/forum/views/#forum.views.ForumMessageCreateView","title":"<code>ForumMessageCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p>"},{"location":"reference/forum/views/#forum.views.can_view","title":"<code>can_view(obj, user)</code>","text":"<p>Can the user see the object.</p> <p>Parameters:</p> Name Type Description Default <code>obj</code> <code>Any</code> <p>Object to test for permission</p> required <code>user</code> <code>User</code> <p>core.models.User to test permissions against</p> required <p>Returns:</p> Type Description <code>bool</code> <p>True if user is authorized to see object else False</p> <p>Examples:</p> <pre><code>if not can_view(self.object ,request.user):\n raise PermissionDenied\n</code></pre> Source code in <code>core/views/__init__.py</code> <pre><code>def can_view(obj: Any, user: User) -&gt; bool:\n \"\"\"Can the user see the object.\n\n Args:\n obj: Object to test for permission\n user: core.models.User to test permissions against\n\n Returns:\n True if user is authorized to see object else False\n\n Examples:\n ```python\n if not can_view(self.object ,request.user):\n raise PermissionDenied\n ```\n \"\"\"\n if obj is None or user.can_view(obj):\n return True\n return can_edit(obj, user)\n</code></pre>"},{"location":"reference/galaxy/models/","title":"Models","text":""},{"location":"reference/galaxy/models/#galaxy.models.current_star","title":"<code>current_star: GalaxyStar | None</code> <code>property</code>","text":"<p>The star of this user in the :class:<code>Galaxy</code>.</p> <p>Only take into account the most recent active galaxy.</p> <p>Returns:</p> Type Description <code>GalaxyStar | None</code> <p>The star of this user if there is an active Galaxy</p> <code>GalaxyStar | None</code> <p>and this user is a citizen of it, else <code>None</code></p>"},{"location":"reference/galaxy/models/#galaxy.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/galaxy/models/#galaxy.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/galaxy/models/#galaxy.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/galaxy/models/#galaxy.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.GalaxyStar","title":"<code>GalaxyStar</code>","text":"<p> Bases: <code>Model</code></p> <p>Define a star (vertex -&gt; user) in the galaxy graph.</p> <p>Store a reference to its owner citizen.</p> <p>Stars are linked to each others through the :class:<code>GalaxyLane</code> model.</p> <p>Each GalaxyStar has a mass which push it towards the center of the galaxy. This mass is proportional to the number of pictures the owner of the star is tagged on.</p>"},{"location":"reference/galaxy/models/#galaxy.models.GalaxyLane","title":"<code>GalaxyLane</code>","text":"<p> Bases: <code>Model</code></p> <p>Define a lane (edge -&gt; link between galaxy citizen) in the galaxy map.</p> <p>Store a reference to both its ends and the distance it covers. Score details between citizen owning the stars is also stored here.</p>"},{"location":"reference/galaxy/models/#galaxy.models.StarDict","title":"<code>StarDict</code>","text":"<p> Bases: <code>TypedDict</code></p>"},{"location":"reference/galaxy/models/#galaxy.models.GalaxyDict","title":"<code>GalaxyDict</code>","text":"<p> Bases: <code>TypedDict</code></p>"},{"location":"reference/galaxy/models/#galaxy.models.RelationScore","title":"<code>RelationScore</code>","text":"<p> Bases: <code>NamedTuple</code></p>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy","title":"<code>Galaxy</code>","text":"<p> Bases: <code>Model</code></p> <p>The Galaxy, a graph linking the active users between each others.</p> <p>The distance between two users is given by a relation score which takes into account a few parameter like the number of pictures they are both tagged on, the time during which they were in the same clubs and whether they are in the same family.</p> <p>The citizens of the Galaxy are represented by :class:<code>GalaxyStar</code> and their relations by :class:<code>GalaxyLane</code>.</p> <p>Several galaxies can coexist. In this case, only the most recent active one shall usually be taken into account. This is useful to keep the current galaxy while generating a new one and swapping them only at the very end.</p> <p>Please take into account that generating the galaxy is a very expensive operation. For this reason, try not to call the :meth:<code>rule</code> method more than once a day in production.</p> <p>To quickly access to the state of a galaxy, use the :attr:<code>state</code> attribute.</p>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.compute_user_score","title":"<code>compute_user_score(user)</code> <code>classmethod</code>","text":"<p>Compute an individual score for each citizen.</p> <p>It will later be used by the graph algorithm to push higher scores towards the center of the galaxy.</p> <p>Idea: This could be added to the computation:</p> <ul> <li>Forum posts</li> <li>Picture count</li> <li>Counter consumption</li> <li>Barman time</li> <li>...</li> </ul> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_user_score(cls, user: User) -&gt; int:\n \"\"\"Compute an individual score for each citizen.\n\n It will later be used by the graph algorithm to push\n higher scores towards the center of the galaxy.\n\n Idea: This could be added to the computation:\n\n - Forum posts\n - Picture count\n - Counter consumption\n - Barman time\n - ...\n \"\"\"\n user_score = 1\n user_score += cls.query_user_score(user)\n\n # TODO:\n # Scale that value with some magic number to accommodate to typical data\n # Really active galaxy citizen after 5 years typically have a score of about XXX\n # Citizen that were seen regularly without taking much part in organizations typically have a score of about XXX\n # Citizen that only went to a few events typically score about XXX\n user_score = int(math.log2(user_score))\n\n return user_score\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.query_user_score","title":"<code>query_user_score(user)</code> <code>classmethod</code>","text":"<p>Get the individual score of the given user in the galaxy.</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef query_user_score(cls, user: User) -&gt; int:\n \"\"\"Get the individual score of the given user in the galaxy.\"\"\"\n score_query = (\n User.objects.filter(id=user.id)\n .annotate(\n godchildren_count=Count(\"godchildren\", distinct=True)\n * cls.FAMILY_LINK_POINTS,\n godfathers_count=Count(\"godfathers\", distinct=True)\n * cls.FAMILY_LINK_POINTS,\n pictures_score=Count(\"pictures\", distinct=True) * cls.PICTURE_POINTS,\n clubs_score=Count(\"memberships\", distinct=True) * cls.CLUBS_POINTS,\n )\n .aggregate(\n score=models.Sum(\n F(\"godchildren_count\")\n + F(\"godfathers_count\")\n + F(\"pictures_score\")\n + F(\"clubs_score\")\n )\n )\n )\n return score_query.get(\"score\")\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.compute_users_score","title":"<code>compute_users_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the relationship scores of the two given users.</p> <p>The computation is done with the following fields :</p> <ul> <li>family: if they have some godfather/godchild relation</li> <li>pictures: in how many pictures are both tagged</li> <li>clubs: during how many days they were members of the same clubs</li> </ul> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_score(cls, user1: User, user2: User) -&gt; RelationScore:\n \"\"\"Compute the relationship scores of the two given users.\n\n The computation is done with the following fields :\n\n - family: if they have some godfather/godchild relation\n - pictures: in how many pictures are both tagged\n - clubs: during how many days they were members of the same clubs\n \"\"\"\n family = cls.compute_users_family_score(user1, user2)\n pictures = cls.compute_users_pictures_score(user1, user2)\n clubs = cls.compute_users_clubs_score(user1, user2)\n return RelationScore(family=family, pictures=pictures, clubs=clubs)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.compute_users_family_score","title":"<code>compute_users_family_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the family score of the relation between the given users.</p> <p>This takes into account mutual godfathers.</p> <p>Returns:</p> Type Description <code>int</code> <p>366 if user1 is the godfather of user2 (or vice versa) else 0</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_family_score(cls, user1: User, user2: User) -&gt; int:\n \"\"\"Compute the family score of the relation between the given users.\n\n This takes into account mutual godfathers.\n\n Returns:\n 366 if user1 is the godfather of user2 (or vice versa) else 0\n \"\"\"\n link_count = User.objects.filter(\n Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)\n ).count()\n if link_count &gt; 0:\n cls.logger.debug(\n f\"\\t\\t- '{user1}' and '{user2}' have {link_count} direct family link\"\n )\n return link_count * cls.FAMILY_LINK_POINTS\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.compute_users_pictures_score","title":"<code>compute_users_pictures_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the pictures score of the relation between the given users.</p> <p>The pictures score is obtained by counting the number of :class:<code>Picture</code> in which they have been both identified. This score is then multiplied by 2.</p> <p>Returns:</p> Type Description <code>int</code> <p>The number of pictures both users have in common, times 2</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_pictures_score(cls, user1: User, user2: User) -&gt; int:\n \"\"\"Compute the pictures score of the relation between the given users.\n\n The pictures score is obtained by counting the number\n of :class:`Picture` in which they have been both identified.\n This score is then multiplied by 2.\n\n Returns:\n The number of pictures both users have in common, times 2\n \"\"\"\n picture_count = (\n Picture.objects.filter(people__user__in=(user1,))\n .filter(people__user__in=(user2,))\n .count()\n )\n if picture_count:\n cls.logger.debug(\n f\"\\t\\t- '{user1}' was pictured with '{user2}' {picture_count} times\"\n )\n return picture_count * cls.PICTURE_POINTS\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.compute_users_clubs_score","title":"<code>compute_users_clubs_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the clubs score of the relation between the given users.</p> <p>The club score is obtained by counting the number of days during which the memberships (see :class:<code>club.models.Membership</code>) of both users overlapped.</p> <p>For example, if user1 was a member of Unitec from 01/01/2020 to 31/12/2021 (two years) and user2 was a member of the same club from 01/01/2021 to 31/12/2022 (also two years, but with an offset of one year), then their club score is 365.</p> <p>Returns:</p> Type Description <code>int</code> <p>the number of days during which both users were in the same club</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_clubs_score(cls, user1: User, user2: User) -&gt; int:\n \"\"\"Compute the clubs score of the relation between the given users.\n\n The club score is obtained by counting the number of days\n during which the memberships (see :class:`club.models.Membership`)\n of both users overlapped.\n\n For example, if user1 was a member of Unitec from 01/01/2020 to 31/12/2021\n (two years) and user2 was a member of the same club from 01/01/2021 to\n 31/12/2022 (also two years, but with an offset of one year), then their\n club score is 365.\n\n Returns:\n the number of days during which both users were in the same club\n \"\"\"\n common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(\n members__in=user2.memberships.all()\n )\n user1_memberships = user1.memberships.filter(club__in=common_clubs)\n user2_memberships = user2.memberships.filter(club__in=common_clubs)\n\n score = 0\n for user1_membership in user1_memberships:\n if user1_membership.end_date is None:\n # user1_membership.save() is not called in this function, hence this is safe\n user1_membership.end_date = localdate()\n query = Q( # start2 &lt;= start1 &lt;= end2\n start_date__lte=user1_membership.start_date,\n end_date__gte=user1_membership.start_date,\n )\n query |= Q( # start2 &lt;= start1 &lt;= now\n start_date__lte=user1_membership.start_date, end_date=None\n )\n query |= Q( # start1 &lt;= start2 &lt;= end2\n start_date__gte=user1_membership.start_date,\n start_date__lte=user1_membership.end_date,\n )\n for user2_membership in user2_memberships.filter(\n query, club=user1_membership.club\n ):\n if user2_membership.end_date is None:\n user2_membership.end_date = localdate()\n latest_start = max(\n user1_membership.start_date, user2_membership.start_date\n )\n earliest_end = min(user1_membership.end_date, user2_membership.end_date)\n cls.logger.debug(\n \"\\t\\t- '%s' was with '%s' in %s starting on %s until %s (%s days)\"\n % (\n user1,\n user2,\n user2_membership.club,\n latest_start,\n earliest_end,\n (earliest_end - latest_start).days,\n )\n )\n score += cls.CLUBS_POINTS * (earliest_end - latest_start).days\n return score\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.scale_distance","title":"<code>scale_distance(value)</code> <code>classmethod</code>","text":"<p>Given a numeric value, return a scaled value which can be used in the Galaxy's graphical interface to set the distance between two stars.</p> <p>Returns:</p> Type Description <code>int</code> <p>the scaled value usable in the Galaxy's 3d graph</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef scale_distance(cls, value: int | float) -&gt; int:\n \"\"\"Given a numeric value, return a scaled value which can\n be used in the Galaxy's graphical interface to set the distance\n between two stars.\n\n Returns:\n the scaled value usable in the Galaxy's 3d graph\n \"\"\"\n # TODO: this will need adjustements with the real, typical data on Taiste\n if value == 0:\n return 4000 # Following calculus would give us +\u221e, we cap it to 4000\n\n cls.logger.debug(f\"\\t\\t&gt; Score: {value}\")\n # Invert score to draw close users together\n value = 1 / value # Cannot be 0\n value += 2 # We use log2 just below and need to stay above 1\n value = ( # Let's get something in the range ]0; log2(3)-1\u22480.58[ that we can multiply later\n math.log2(value) - 1\n )\n value *= ( # Scale that value with a magic number to accommodate to typical data\n # Really close galaxy citizen after 5 years typically have a score of about XXX\n # Citizen that were in the same year without being really friends typically have a score of about XXX\n # Citizen that have met once or twice only have a couple of pictures together typically score about XXX\n cls.GALAXY_SCALE_FACTOR\n )\n cls.logger.debug(f\"\\t\\t&gt; Scaled distance: {value}\")\n return int(value)\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.rule","title":"<code>rule(picture_count_threshold=10)</code>","text":"<p>Main function of the Galaxy.</p> <p>Iterate over all the rulable users to promote them to citizens. A citizen is a user who has a corresponding star in the Galaxy. Also build up the lanes, which are the links between the different citizen.</p> <p>Users who can be ruled are defined with the <code>picture_count_threshold</code>: all users who are identified in a strictly lower number of pictures won't be promoted to citizens. This does very effectively limit the quantity of computing to do and only includes users who have had a minimum of activity.</p> <p>This method still remains very expensive, so think thoroughly before you call it, especially in production.</p> <p>:param picture_count_threshold: the minimum number of picture to have to be included in the galaxy</p> Source code in <code>galaxy/models.py</code> <pre><code>def rule(self, picture_count_threshold=10) -&gt; None:\n \"\"\"Main function of the Galaxy.\n\n Iterate over all the rulable users to promote them to citizens.\n A citizen is a user who has a corresponding star in the Galaxy.\n Also build up the lanes, which are the links between the different citizen.\n\n Users who can be ruled are defined with the `picture_count_threshold`:\n all users who are identified in a strictly lower number of pictures\n won't be promoted to citizens.\n This does very effectively limit the quantity of computing to do\n and only includes users who have had a minimum of activity.\n\n This method still remains very expensive, so think thoroughly before\n you call it, especially in production.\n\n :param picture_count_threshold: the minimum number of picture to have to be\n included in the galaxy\n \"\"\"\n total_time = time.time()\n self.logger.info(\"Listing rulable citizen.\")\n rulable_users = (\n User.objects.filter(subscriptions__isnull=False)\n .annotate(pictures_count=Count(\"pictures\"))\n .filter(pictures_count__gt=picture_count_threshold)\n .distinct()\n )\n\n # force fetch of the whole query to make sure there won't\n # be any more db hits\n # this is memory expensive but prevents a lot of db hits, therefore\n # is far more time efficient\n\n rulable_users = list(rulable_users)\n rulable_users_count = len(rulable_users)\n user1_count = 0\n self.logger.info(\n f\"{rulable_users_count} citizen have been listed. Starting to rule.\"\n )\n\n stars = []\n self.logger.info(\"Creating stars for all citizen\")\n for user in rulable_users:\n star = GalaxyStar(\n owner=user, galaxy=self, mass=self.compute_user_score(user)\n )\n stars.append(star)\n GalaxyStar.objects.bulk_create(stars)\n\n stars = {}\n for star in GalaxyStar.objects.filter(galaxy=self):\n stars[star.owner.id] = star\n\n self.logger.info(\"Creating lanes between stars\")\n # Display current speed every $speed_count_frequency users\n speed_count_frequency = max(rulable_users_count // 10, 1) # ten time at most\n global_avg_speed_accumulator = 0\n global_avg_speed_count = 0\n t_global_start = time.time()\n while len(rulable_users) &gt; 0:\n user1 = rulable_users.pop()\n user1_count += 1\n rulable_users_count2 = len(rulable_users)\n\n star1 = stars[user1.id]\n\n user_avg_speed = 0\n user_avg_speed_count = 0\n\n tstart = time.time()\n lanes = []\n for user2_count, user2 in enumerate(rulable_users, start=1):\n self.logger.debug(\"\")\n self.logger.debug(\n f\"\\t&gt; Examining '{user1}' ({user1_count}/{rulable_users_count}) with '{user2}' ({user2_count}/{rulable_users_count2})\"\n )\n\n star2 = stars[user2.id]\n\n score = Galaxy.compute_users_score(user1, user2)\n distance = self.scale_distance(sum(score))\n if distance &lt; 30: # TODO: this needs tuning with real-world data\n lanes.append(\n GalaxyLane(\n star1=star1,\n star2=star2,\n distance=distance,\n family=score.family,\n pictures=score.pictures,\n clubs=score.clubs,\n )\n )\n\n if user2_count % speed_count_frequency == 0:\n tend = time.time()\n delta = tend - tstart\n speed = float(speed_count_frequency) / delta\n user_avg_speed += speed\n user_avg_speed_count += 1\n self.logger.debug(\n f\"\\tSpeed: {speed:.2f} users per second (time for last {speed_count_frequency} citizens: {delta:.2f} second)\"\n )\n tstart = time.time()\n\n GalaxyLane.objects.bulk_create(lanes)\n\n self.logger.info(\"\")\n\n t_global_end = time.time()\n global_delta = t_global_end - t_global_start\n speed = 1.0 / global_delta\n global_avg_speed_accumulator += speed\n global_avg_speed_count += 1\n global_avg_speed = global_avg_speed_accumulator / global_avg_speed_count\n\n self.logger.info(f\" Ruling of {self} \".center(60, \"#\"))\n self.logger.info(\n f\"Progression: {user1_count}/{rulable_users_count} citizen -- {rulable_users_count - user1_count} remaining\"\n )\n self.logger.info(f\"Speed: {60.0*global_avg_speed:.2f} citizen per minute\")\n\n # We can divide the computed ETA by 2 because each loop, there is one citizen less to check, and maths tell\n # us that this averages to a division by two\n eta = rulable_users_count2 / global_avg_speed / 2\n eta_hours = int(eta // 3600)\n eta_minutes = int(eta // 60 % 60)\n self.logger.info(\n f\"ETA: {eta_hours} hours {eta_minutes} minutes ({eta / 3600 / 24:.2f} days)\"\n )\n self.logger.info(\"#\" * 60)\n t_global_start = time.time()\n\n # Here, we get the IDs of the old galaxies that we'll need to delete. In normal operation, only one galaxy\n # should be returned, and we can't delete it yet, as it's the one still displayed by the Sith.\n old_galaxies_pks = list(\n Galaxy.objects.filter(state__isnull=False).values_list(\"pk\", flat=True)\n )\n self.logger.info(\n f\"These old galaxies will be deleted once the new one is ready: {old_galaxies_pks}\"\n )\n\n # Making the state sets this new galaxy as being ready. From now on, the Sith will show us to the world.\n self.make_state()\n\n # Avoid accident if there is nothing to delete\n if len(old_galaxies_pks) &gt; 0:\n # Former galaxies can now be deleted.\n Galaxy.objects.filter(pk__in=old_galaxies_pks).delete()\n\n total_time = time.time() - total_time\n total_time_hours = int(total_time // 3600)\n total_time_minutes = int(total_time // 60 % 60)\n total_time_seconds = int(total_time % 60)\n self.logger.info(\n f\"{self} ruled in {total_time:.2f} seconds ({total_time_hours} hours, {total_time_minutes} minutes, {total_time_seconds} seconds)\"\n )\n</code></pre>"},{"location":"reference/galaxy/models/#galaxy.models.Galaxy.make_state","title":"<code>make_state()</code>","text":"<p>Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/.</p> Source code in <code>galaxy/models.py</code> <pre><code>def make_state(self) -&gt; None:\n \"\"\"Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/.\"\"\"\n self.logger.info(\n \"Caching current Galaxy state for a quicker display of the Empire's power.\"\n )\n\n without_nickname = Concat(\n F(\"owner__first_name\"), Value(\" \"), F(\"owner__last_name\")\n )\n with_nickname = Concat(\n F(\"owner__first_name\"),\n Value(\" \"),\n F(\"owner__last_name\"),\n Value(\" (\"),\n F(\"owner__nick_name\"),\n Value(\")\"),\n )\n stars = (\n GalaxyStar.objects.filter(galaxy=self)\n .order_by(\n \"owner\"\n ) # This helps determinism for the tests and doesn't cost much\n .annotate(\n owner_name=Case(\n When(owner__nick_name=None, then=without_nickname),\n default=with_nickname,\n )\n )\n )\n lanes = (\n GalaxyLane.objects.filter(star1__galaxy=self)\n .order_by(\n \"star1\"\n ) # This helps determinism for the tests and doesn't cost much\n .annotate(\n star1_owner=F(\"star1__owner__id\"),\n star2_owner=F(\"star2__owner__id\"),\n )\n )\n json = GalaxyDict(\n nodes=[\n StarDict(\n id=star.owner_id,\n name=star.owner_name,\n mass=star.mass,\n )\n for star in stars\n ],\n links=[],\n )\n for path in lanes:\n json[\"links\"].append(\n {\n \"source\": path.star1_owner,\n \"target\": path.star2_owner,\n \"value\": path.distance,\n }\n )\n self.state = json\n self.save()\n self.logger.info(f\"{self} is now ready!\")\n</code></pre>"},{"location":"reference/galaxy/views/","title":"Views","text":""},{"location":"reference/galaxy/views/#galaxy.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/galaxy/views/#galaxy.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/galaxy/views/#galaxy.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/galaxy/views/#galaxy.views.FormerSubscriberMixin","title":"<code>FormerSubscriberMixin</code>","text":"<p> Bases: <code>AccessMixin</code></p> <p>Check if the user was at least an old subscriber.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user never subscribed.</p>"},{"location":"reference/galaxy/views/#galaxy.views.UserTabsMixin","title":"<code>UserTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy","title":"<code>Galaxy</code>","text":"<p> Bases: <code>Model</code></p> <p>The Galaxy, a graph linking the active users between each others.</p> <p>The distance between two users is given by a relation score which takes into account a few parameter like the number of pictures they are both tagged on, the time during which they were in the same clubs and whether they are in the same family.</p> <p>The citizens of the Galaxy are represented by :class:<code>GalaxyStar</code> and their relations by :class:<code>GalaxyLane</code>.</p> <p>Several galaxies can coexist. In this case, only the most recent active one shall usually be taken into account. This is useful to keep the current galaxy while generating a new one and swapping them only at the very end.</p> <p>Please take into account that generating the galaxy is a very expensive operation. For this reason, try not to call the :meth:<code>rule</code> method more than once a day in production.</p> <p>To quickly access to the state of a galaxy, use the :attr:<code>state</code> attribute.</p>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.compute_user_score","title":"<code>compute_user_score(user)</code> <code>classmethod</code>","text":"<p>Compute an individual score for each citizen.</p> <p>It will later be used by the graph algorithm to push higher scores towards the center of the galaxy.</p> <p>Idea: This could be added to the computation:</p> <ul> <li>Forum posts</li> <li>Picture count</li> <li>Counter consumption</li> <li>Barman time</li> <li>...</li> </ul> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_user_score(cls, user: User) -&gt; int:\n \"\"\"Compute an individual score for each citizen.\n\n It will later be used by the graph algorithm to push\n higher scores towards the center of the galaxy.\n\n Idea: This could be added to the computation:\n\n - Forum posts\n - Picture count\n - Counter consumption\n - Barman time\n - ...\n \"\"\"\n user_score = 1\n user_score += cls.query_user_score(user)\n\n # TODO:\n # Scale that value with some magic number to accommodate to typical data\n # Really active galaxy citizen after 5 years typically have a score of about XXX\n # Citizen that were seen regularly without taking much part in organizations typically have a score of about XXX\n # Citizen that only went to a few events typically score about XXX\n user_score = int(math.log2(user_score))\n\n return user_score\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.query_user_score","title":"<code>query_user_score(user)</code> <code>classmethod</code>","text":"<p>Get the individual score of the given user in the galaxy.</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef query_user_score(cls, user: User) -&gt; int:\n \"\"\"Get the individual score of the given user in the galaxy.\"\"\"\n score_query = (\n User.objects.filter(id=user.id)\n .annotate(\n godchildren_count=Count(\"godchildren\", distinct=True)\n * cls.FAMILY_LINK_POINTS,\n godfathers_count=Count(\"godfathers\", distinct=True)\n * cls.FAMILY_LINK_POINTS,\n pictures_score=Count(\"pictures\", distinct=True) * cls.PICTURE_POINTS,\n clubs_score=Count(\"memberships\", distinct=True) * cls.CLUBS_POINTS,\n )\n .aggregate(\n score=models.Sum(\n F(\"godchildren_count\")\n + F(\"godfathers_count\")\n + F(\"pictures_score\")\n + F(\"clubs_score\")\n )\n )\n )\n return score_query.get(\"score\")\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.compute_users_score","title":"<code>compute_users_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the relationship scores of the two given users.</p> <p>The computation is done with the following fields :</p> <ul> <li>family: if they have some godfather/godchild relation</li> <li>pictures: in how many pictures are both tagged</li> <li>clubs: during how many days they were members of the same clubs</li> </ul> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_score(cls, user1: User, user2: User) -&gt; RelationScore:\n \"\"\"Compute the relationship scores of the two given users.\n\n The computation is done with the following fields :\n\n - family: if they have some godfather/godchild relation\n - pictures: in how many pictures are both tagged\n - clubs: during how many days they were members of the same clubs\n \"\"\"\n family = cls.compute_users_family_score(user1, user2)\n pictures = cls.compute_users_pictures_score(user1, user2)\n clubs = cls.compute_users_clubs_score(user1, user2)\n return RelationScore(family=family, pictures=pictures, clubs=clubs)\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.compute_users_family_score","title":"<code>compute_users_family_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the family score of the relation between the given users.</p> <p>This takes into account mutual godfathers.</p> <p>Returns:</p> Type Description <code>int</code> <p>366 if user1 is the godfather of user2 (or vice versa) else 0</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_family_score(cls, user1: User, user2: User) -&gt; int:\n \"\"\"Compute the family score of the relation between the given users.\n\n This takes into account mutual godfathers.\n\n Returns:\n 366 if user1 is the godfather of user2 (or vice versa) else 0\n \"\"\"\n link_count = User.objects.filter(\n Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)\n ).count()\n if link_count &gt; 0:\n cls.logger.debug(\n f\"\\t\\t- '{user1}' and '{user2}' have {link_count} direct family link\"\n )\n return link_count * cls.FAMILY_LINK_POINTS\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.compute_users_pictures_score","title":"<code>compute_users_pictures_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the pictures score of the relation between the given users.</p> <p>The pictures score is obtained by counting the number of :class:<code>Picture</code> in which they have been both identified. This score is then multiplied by 2.</p> <p>Returns:</p> Type Description <code>int</code> <p>The number of pictures both users have in common, times 2</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_pictures_score(cls, user1: User, user2: User) -&gt; int:\n \"\"\"Compute the pictures score of the relation between the given users.\n\n The pictures score is obtained by counting the number\n of :class:`Picture` in which they have been both identified.\n This score is then multiplied by 2.\n\n Returns:\n The number of pictures both users have in common, times 2\n \"\"\"\n picture_count = (\n Picture.objects.filter(people__user__in=(user1,))\n .filter(people__user__in=(user2,))\n .count()\n )\n if picture_count:\n cls.logger.debug(\n f\"\\t\\t- '{user1}' was pictured with '{user2}' {picture_count} times\"\n )\n return picture_count * cls.PICTURE_POINTS\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.compute_users_clubs_score","title":"<code>compute_users_clubs_score(user1, user2)</code> <code>classmethod</code>","text":"<p>Compute the clubs score of the relation between the given users.</p> <p>The club score is obtained by counting the number of days during which the memberships (see :class:<code>club.models.Membership</code>) of both users overlapped.</p> <p>For example, if user1 was a member of Unitec from 01/01/2020 to 31/12/2021 (two years) and user2 was a member of the same club from 01/01/2021 to 31/12/2022 (also two years, but with an offset of one year), then their club score is 365.</p> <p>Returns:</p> Type Description <code>int</code> <p>the number of days during which both users were in the same club</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef compute_users_clubs_score(cls, user1: User, user2: User) -&gt; int:\n \"\"\"Compute the clubs score of the relation between the given users.\n\n The club score is obtained by counting the number of days\n during which the memberships (see :class:`club.models.Membership`)\n of both users overlapped.\n\n For example, if user1 was a member of Unitec from 01/01/2020 to 31/12/2021\n (two years) and user2 was a member of the same club from 01/01/2021 to\n 31/12/2022 (also two years, but with an offset of one year), then their\n club score is 365.\n\n Returns:\n the number of days during which both users were in the same club\n \"\"\"\n common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(\n members__in=user2.memberships.all()\n )\n user1_memberships = user1.memberships.filter(club__in=common_clubs)\n user2_memberships = user2.memberships.filter(club__in=common_clubs)\n\n score = 0\n for user1_membership in user1_memberships:\n if user1_membership.end_date is None:\n # user1_membership.save() is not called in this function, hence this is safe\n user1_membership.end_date = localdate()\n query = Q( # start2 &lt;= start1 &lt;= end2\n start_date__lte=user1_membership.start_date,\n end_date__gte=user1_membership.start_date,\n )\n query |= Q( # start2 &lt;= start1 &lt;= now\n start_date__lte=user1_membership.start_date, end_date=None\n )\n query |= Q( # start1 &lt;= start2 &lt;= end2\n start_date__gte=user1_membership.start_date,\n start_date__lte=user1_membership.end_date,\n )\n for user2_membership in user2_memberships.filter(\n query, club=user1_membership.club\n ):\n if user2_membership.end_date is None:\n user2_membership.end_date = localdate()\n latest_start = max(\n user1_membership.start_date, user2_membership.start_date\n )\n earliest_end = min(user1_membership.end_date, user2_membership.end_date)\n cls.logger.debug(\n \"\\t\\t- '%s' was with '%s' in %s starting on %s until %s (%s days)\"\n % (\n user1,\n user2,\n user2_membership.club,\n latest_start,\n earliest_end,\n (earliest_end - latest_start).days,\n )\n )\n score += cls.CLUBS_POINTS * (earliest_end - latest_start).days\n return score\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.scale_distance","title":"<code>scale_distance(value)</code> <code>classmethod</code>","text":"<p>Given a numeric value, return a scaled value which can be used in the Galaxy's graphical interface to set the distance between two stars.</p> <p>Returns:</p> Type Description <code>int</code> <p>the scaled value usable in the Galaxy's 3d graph</p> Source code in <code>galaxy/models.py</code> <pre><code>@classmethod\ndef scale_distance(cls, value: int | float) -&gt; int:\n \"\"\"Given a numeric value, return a scaled value which can\n be used in the Galaxy's graphical interface to set the distance\n between two stars.\n\n Returns:\n the scaled value usable in the Galaxy's 3d graph\n \"\"\"\n # TODO: this will need adjustements with the real, typical data on Taiste\n if value == 0:\n return 4000 # Following calculus would give us +\u221e, we cap it to 4000\n\n cls.logger.debug(f\"\\t\\t&gt; Score: {value}\")\n # Invert score to draw close users together\n value = 1 / value # Cannot be 0\n value += 2 # We use log2 just below and need to stay above 1\n value = ( # Let's get something in the range ]0; log2(3)-1\u22480.58[ that we can multiply later\n math.log2(value) - 1\n )\n value *= ( # Scale that value with a magic number to accommodate to typical data\n # Really close galaxy citizen after 5 years typically have a score of about XXX\n # Citizen that were in the same year without being really friends typically have a score of about XXX\n # Citizen that have met once or twice only have a couple of pictures together typically score about XXX\n cls.GALAXY_SCALE_FACTOR\n )\n cls.logger.debug(f\"\\t\\t&gt; Scaled distance: {value}\")\n return int(value)\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.rule","title":"<code>rule(picture_count_threshold=10)</code>","text":"<p>Main function of the Galaxy.</p> <p>Iterate over all the rulable users to promote them to citizens. A citizen is a user who has a corresponding star in the Galaxy. Also build up the lanes, which are the links between the different citizen.</p> <p>Users who can be ruled are defined with the <code>picture_count_threshold</code>: all users who are identified in a strictly lower number of pictures won't be promoted to citizens. This does very effectively limit the quantity of computing to do and only includes users who have had a minimum of activity.</p> <p>This method still remains very expensive, so think thoroughly before you call it, especially in production.</p> <p>:param picture_count_threshold: the minimum number of picture to have to be included in the galaxy</p> Source code in <code>galaxy/models.py</code> <pre><code>def rule(self, picture_count_threshold=10) -&gt; None:\n \"\"\"Main function of the Galaxy.\n\n Iterate over all the rulable users to promote them to citizens.\n A citizen is a user who has a corresponding star in the Galaxy.\n Also build up the lanes, which are the links between the different citizen.\n\n Users who can be ruled are defined with the `picture_count_threshold`:\n all users who are identified in a strictly lower number of pictures\n won't be promoted to citizens.\n This does very effectively limit the quantity of computing to do\n and only includes users who have had a minimum of activity.\n\n This method still remains very expensive, so think thoroughly before\n you call it, especially in production.\n\n :param picture_count_threshold: the minimum number of picture to have to be\n included in the galaxy\n \"\"\"\n total_time = time.time()\n self.logger.info(\"Listing rulable citizen.\")\n rulable_users = (\n User.objects.filter(subscriptions__isnull=False)\n .annotate(pictures_count=Count(\"pictures\"))\n .filter(pictures_count__gt=picture_count_threshold)\n .distinct()\n )\n\n # force fetch of the whole query to make sure there won't\n # be any more db hits\n # this is memory expensive but prevents a lot of db hits, therefore\n # is far more time efficient\n\n rulable_users = list(rulable_users)\n rulable_users_count = len(rulable_users)\n user1_count = 0\n self.logger.info(\n f\"{rulable_users_count} citizen have been listed. Starting to rule.\"\n )\n\n stars = []\n self.logger.info(\"Creating stars for all citizen\")\n for user in rulable_users:\n star = GalaxyStar(\n owner=user, galaxy=self, mass=self.compute_user_score(user)\n )\n stars.append(star)\n GalaxyStar.objects.bulk_create(stars)\n\n stars = {}\n for star in GalaxyStar.objects.filter(galaxy=self):\n stars[star.owner.id] = star\n\n self.logger.info(\"Creating lanes between stars\")\n # Display current speed every $speed_count_frequency users\n speed_count_frequency = max(rulable_users_count // 10, 1) # ten time at most\n global_avg_speed_accumulator = 0\n global_avg_speed_count = 0\n t_global_start = time.time()\n while len(rulable_users) &gt; 0:\n user1 = rulable_users.pop()\n user1_count += 1\n rulable_users_count2 = len(rulable_users)\n\n star1 = stars[user1.id]\n\n user_avg_speed = 0\n user_avg_speed_count = 0\n\n tstart = time.time()\n lanes = []\n for user2_count, user2 in enumerate(rulable_users, start=1):\n self.logger.debug(\"\")\n self.logger.debug(\n f\"\\t&gt; Examining '{user1}' ({user1_count}/{rulable_users_count}) with '{user2}' ({user2_count}/{rulable_users_count2})\"\n )\n\n star2 = stars[user2.id]\n\n score = Galaxy.compute_users_score(user1, user2)\n distance = self.scale_distance(sum(score))\n if distance &lt; 30: # TODO: this needs tuning with real-world data\n lanes.append(\n GalaxyLane(\n star1=star1,\n star2=star2,\n distance=distance,\n family=score.family,\n pictures=score.pictures,\n clubs=score.clubs,\n )\n )\n\n if user2_count % speed_count_frequency == 0:\n tend = time.time()\n delta = tend - tstart\n speed = float(speed_count_frequency) / delta\n user_avg_speed += speed\n user_avg_speed_count += 1\n self.logger.debug(\n f\"\\tSpeed: {speed:.2f} users per second (time for last {speed_count_frequency} citizens: {delta:.2f} second)\"\n )\n tstart = time.time()\n\n GalaxyLane.objects.bulk_create(lanes)\n\n self.logger.info(\"\")\n\n t_global_end = time.time()\n global_delta = t_global_end - t_global_start\n speed = 1.0 / global_delta\n global_avg_speed_accumulator += speed\n global_avg_speed_count += 1\n global_avg_speed = global_avg_speed_accumulator / global_avg_speed_count\n\n self.logger.info(f\" Ruling of {self} \".center(60, \"#\"))\n self.logger.info(\n f\"Progression: {user1_count}/{rulable_users_count} citizen -- {rulable_users_count - user1_count} remaining\"\n )\n self.logger.info(f\"Speed: {60.0*global_avg_speed:.2f} citizen per minute\")\n\n # We can divide the computed ETA by 2 because each loop, there is one citizen less to check, and maths tell\n # us that this averages to a division by two\n eta = rulable_users_count2 / global_avg_speed / 2\n eta_hours = int(eta // 3600)\n eta_minutes = int(eta // 60 % 60)\n self.logger.info(\n f\"ETA: {eta_hours} hours {eta_minutes} minutes ({eta / 3600 / 24:.2f} days)\"\n )\n self.logger.info(\"#\" * 60)\n t_global_start = time.time()\n\n # Here, we get the IDs of the old galaxies that we'll need to delete. In normal operation, only one galaxy\n # should be returned, and we can't delete it yet, as it's the one still displayed by the Sith.\n old_galaxies_pks = list(\n Galaxy.objects.filter(state__isnull=False).values_list(\"pk\", flat=True)\n )\n self.logger.info(\n f\"These old galaxies will be deleted once the new one is ready: {old_galaxies_pks}\"\n )\n\n # Making the state sets this new galaxy as being ready. From now on, the Sith will show us to the world.\n self.make_state()\n\n # Avoid accident if there is nothing to delete\n if len(old_galaxies_pks) &gt; 0:\n # Former galaxies can now be deleted.\n Galaxy.objects.filter(pk__in=old_galaxies_pks).delete()\n\n total_time = time.time() - total_time\n total_time_hours = int(total_time // 3600)\n total_time_minutes = int(total_time // 60 % 60)\n total_time_seconds = int(total_time % 60)\n self.logger.info(\n f\"{self} ruled in {total_time:.2f} seconds ({total_time_hours} hours, {total_time_minutes} minutes, {total_time_seconds} seconds)\"\n )\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.Galaxy.make_state","title":"<code>make_state()</code>","text":"<p>Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/.</p> Source code in <code>galaxy/models.py</code> <pre><code>def make_state(self) -&gt; None:\n \"\"\"Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/.\"\"\"\n self.logger.info(\n \"Caching current Galaxy state for a quicker display of the Empire's power.\"\n )\n\n without_nickname = Concat(\n F(\"owner__first_name\"), Value(\" \"), F(\"owner__last_name\")\n )\n with_nickname = Concat(\n F(\"owner__first_name\"),\n Value(\" \"),\n F(\"owner__last_name\"),\n Value(\" (\"),\n F(\"owner__nick_name\"),\n Value(\")\"),\n )\n stars = (\n GalaxyStar.objects.filter(galaxy=self)\n .order_by(\n \"owner\"\n ) # This helps determinism for the tests and doesn't cost much\n .annotate(\n owner_name=Case(\n When(owner__nick_name=None, then=without_nickname),\n default=with_nickname,\n )\n )\n )\n lanes = (\n GalaxyLane.objects.filter(star1__galaxy=self)\n .order_by(\n \"star1\"\n ) # This helps determinism for the tests and doesn't cost much\n .annotate(\n star1_owner=F(\"star1__owner__id\"),\n star2_owner=F(\"star2__owner__id\"),\n )\n )\n json = GalaxyDict(\n nodes=[\n StarDict(\n id=star.owner_id,\n name=star.owner_name,\n mass=star.mass,\n )\n for star in stars\n ],\n links=[],\n )\n for path in lanes:\n json[\"links\"].append(\n {\n \"source\": path.star1_owner,\n \"target\": path.star2_owner,\n \"value\": path.distance,\n }\n )\n self.state = json\n self.save()\n self.logger.info(f\"{self} is now ready!\")\n</code></pre>"},{"location":"reference/galaxy/views/#galaxy.views.GalaxyLane","title":"<code>GalaxyLane</code>","text":"<p> Bases: <code>Model</code></p> <p>Define a lane (edge -&gt; link between galaxy citizen) in the galaxy map.</p> <p>Store a reference to both its ends and the distance it covers. Score details between citizen owning the stars is also stored here.</p>"},{"location":"reference/galaxy/views/#galaxy.views.GalaxyUserView","title":"<code>GalaxyUserView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>UserTabsMixin</code>, <code>DetailView</code></p>"},{"location":"reference/galaxy/views/#galaxy.views.GalaxyDataView","title":"<code>GalaxyDataView</code>","text":"<p> Bases: <code>FormerSubscriberMixin</code>, <code>View</code></p>"},{"location":"reference/launderette/models/","title":"Models","text":""},{"location":"reference/launderette/models/#launderette.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/launderette/models/#launderette.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/launderette/models/#launderette.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/launderette/models/#launderette.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/models/#launderette.models.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Launderette","title":"<code>Launderette</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/models/#launderette.models.Launderette.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>launderette/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n launderette_club = Club.objects.filter(\n unix_name=settings.SITH_LAUNDERETTE_MANAGER[\"unix_name\"]\n ).first()\n m = launderette_club.get_membership_for(user)\n return bool(m and m.role &gt;= 9)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Machine","title":"<code>Machine</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/models/#launderette.models.Machine.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>launderette/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n launderette_club = Club.objects.filter(\n unix_name=settings.SITH_LAUNDERETTE_MANAGER[\"unix_name\"]\n ).first()\n m = launderette_club.get_membership_for(user)\n return bool(m and m.role &gt;= 9)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Token","title":"<code>Token</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/models/#launderette.models.Token.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>launderette/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n launderette_club = Club.objects.filter(\n unix_name=settings.SITH_LAUNDERETTE_MANAGER[\"unix_name\"]\n ).first()\n m = launderette_club.get_membership_for(user)\n return bool(m and m.role &gt;= 9)\n</code></pre>"},{"location":"reference/launderette/models/#launderette.models.Slot","title":"<code>Slot</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/views/","title":"Views","text":""},{"location":"reference/launderette/views/#launderette.views.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/launderette/views/#launderette.views.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page","title":"<code>Page</code>","text":"<p> Bases: <code>Model</code></p> <p>The page class to build a Wiki Each page may have a parent and it's URL is of the form my.site/page/// It has an ID field, but don't use it, since it's only there for DB part, and because compound primary key is awkward! Prefere querying pages with Page.get_page_by_full_name(). <p>Be careful with the _full_name attribute: this field may not be valid until you call save(). It's made for fast query, but don't rely on it when playing with a Page object, use get_full_name() instead!</p>"},{"location":"reference/launderette/views/#launderette.views.Page.save","title":"<code>save(*args, **kwargs)</code>","text":"<p>Performs some needed actions before and after saving a page in database.</p> Source code in <code>core/models.py</code> <pre><code>def save(self, *args, **kwargs):\n \"\"\"Performs some needed actions before and after saving a page in database.\"\"\"\n locked = kwargs.pop(\"force_lock\", False)\n if not locked:\n locked = self.is_locked()\n if not locked:\n raise NotLocked(\"The page is not locked and thus can not be saved\")\n self.full_clean()\n if not self.id:\n super().save(\n *args, **kwargs\n ) # Save a first time to correctly set _full_name\n # This reset the _full_name just before saving to maintain a coherent field quicker for queries than the\n # recursive method\n # It also update all the children to maintain correct names\n self._full_name = self.get_full_name()\n for c in self.children.all():\n c.save()\n super().save(*args, **kwargs)\n self.unset_lock()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.get_page_by_full_name","title":"<code>get_page_by_full_name(name)</code> <code>staticmethod</code>","text":"<p>Quicker to get a page with that method rather than building the request every time.</p> Source code in <code>core/models.py</code> <pre><code>@staticmethod\ndef get_page_by_full_name(name):\n \"\"\"Quicker to get a page with that method rather than building the request every time.\"\"\"\n return Page.objects.filter(_full_name=name).first()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.clean","title":"<code>clean()</code>","text":"<p>Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up only the name for the moment, but this can be used to make any treatment before saving the object.\"\"\"\n if \"/\" in self.name:\n self.name = self.name.split(\"/\")[-1]\n if (\n Page.objects.exclude(pk=self.pk)\n .filter(_full_name=self.get_full_name())\n .exists()\n ):\n raise ValidationError(_(\"Duplicate page\"), code=\"duplicate\")\n super().clean()\n if self.parent is not None and self in self.get_parent_list():\n raise ValidationError(_(\"Loop in page tree\"), code=\"loop\")\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.is_locked","title":"<code>is_locked()</code>","text":"<p>Is True if the page is locked, False otherwise.</p> <p>This is where the timeout is handled, so a locked page for which the timeout is reach will be unlocked and this function will return False.</p> Source code in <code>core/models.py</code> <pre><code>def is_locked(self):\n \"\"\"Is True if the page is locked, False otherwise.\n\n This is where the timeout is handled,\n so a locked page for which the timeout is reach will be unlocked and this\n function will return False.\n \"\"\"\n if self.lock_timeout and (\n timezone.now() - self.lock_timeout &gt; timedelta(minutes=5)\n ):\n self.unset_lock()\n return (\n self.lock_user\n and self.lock_timeout\n and (timezone.now() - self.lock_timeout &lt; timedelta(minutes=5))\n )\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.set_lock","title":"<code>set_lock(user)</code>","text":"<p>Sets a lock on the current page or raise an AlreadyLocked exception.</p> Source code in <code>core/models.py</code> <pre><code>def set_lock(self, user):\n \"\"\"Sets a lock on the current page or raise an AlreadyLocked exception.\"\"\"\n if self.is_locked() and self.get_lock() != user:\n raise AlreadyLocked(\"The page is already locked by someone else\")\n self.lock_user = user\n self.lock_timeout = timezone.now()\n super().save()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.set_lock_recursive","title":"<code>set_lock_recursive(user)</code>","text":"<p>Locks recursively all the child pages for editing properties.</p> Source code in <code>core/models.py</code> <pre><code>def set_lock_recursive(self, user):\n \"\"\"Locks recursively all the child pages for editing properties.\"\"\"\n for p in self.children.all():\n p.set_lock_recursive(user)\n self.set_lock(user)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.unset_lock_recursive","title":"<code>unset_lock_recursive()</code>","text":"<p>Unlocks recursively all the child pages.</p> Source code in <code>core/models.py</code> <pre><code>def unset_lock_recursive(self):\n \"\"\"Unlocks recursively all the child pages.\"\"\"\n for p in self.children.all():\n p.unset_lock_recursive()\n self.unset_lock()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.unset_lock","title":"<code>unset_lock()</code>","text":"<p>Always try to unlock, even if there is no lock.</p> Source code in <code>core/models.py</code> <pre><code>def unset_lock(self):\n \"\"\"Always try to unlock, even if there is no lock.\"\"\"\n self.lock_user = None\n self.lock_timeout = None\n super().save()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.get_lock","title":"<code>get_lock()</code>","text":"<p>Returns the page's mutex containing the time and the user in a dict.</p> Source code in <code>core/models.py</code> <pre><code>def get_lock(self):\n \"\"\"Returns the page's mutex containing the time and the user in a dict.\"\"\"\n if self.lock_user:\n return self.lock_user\n raise NotLocked(\"The page is not locked and thus can not return its user\")\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Page.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Computes the real full_name of the page based on its name and its parent's name You can and must rely on this function when working on a page object that is not freshly fetched from the DB (For example when treating a Page object coming from a form).</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Computes the real full_name of the page based on its name and its parent's name\n You can and must rely on this function when working on a page object that is not freshly fetched from the DB\n (For example when treating a Page object coming from a form).\n \"\"\"\n if self.parent is None:\n return self.name\n return f\"{self.parent.get_full_name()}/{self.name}\"\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/launderette/views/#launderette.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/launderette/views/#launderette.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.CanCreateMixin","title":"<code>CanCreateMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect any child view that would create an object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user has not the necessary permission to create the object of the view.</p>"},{"location":"reference/launderette/views/#launderette.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/launderette/views/#launderette.views.CanEditPropMixin","title":"<code>CanEditPropMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has owner permissions on the child view object.</p> <p>In other word, you can make a view with this view as parent, and it will be retricted to the users that are in the object's owner_group or that pass the <code>obj.can_be_viewed_by</code> test.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user cannot see the object</p>"},{"location":"reference/launderette/views/#launderette.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/launderette/views/#launderette.views.GetUserForm","title":"<code>GetUserForm</code>","text":"<p> Bases: <code>Form</code></p> <p>The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, reverse function, or any other use.</p> <p>The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with some nickname, first name, or last name (TODO)</p>"},{"location":"reference/launderette/views/#launderette.views.Counter","title":"<code>Counter</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/views/#launderette.views.Counter.gen_token","title":"<code>gen_token()</code>","text":"<p>Generate a new token for this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def gen_token(self) -&gt; None:\n \"\"\"Generate a new token for this counter.\"\"\"\n self.token = \"\".join(\n random.choice(string.ascii_letters + string.digits) for _ in range(30)\n )\n self.save()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.barmen_list","title":"<code>barmen_list()</code>","text":"<p>Returns the barman list as list of User.</p> Source code in <code>counter/models.py</code> <pre><code>@cached_property\ndef barmen_list(self) -&gt; list[User]:\n \"\"\"Returns the barman list as list of User.\"\"\"\n return [\n p.user for p in self.permanencies.filter(end=None).select_related(\"user\")\n ]\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.get_random_barman","title":"<code>get_random_barman()</code>","text":"<p>Return a random user being currently a barman.</p> Source code in <code>counter/models.py</code> <pre><code>def get_random_barman(self) -&gt; User:\n \"\"\"Return a random user being currently a barman.\"\"\"\n return random.choice(self.barmen_list)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.update_activity","title":"<code>update_activity()</code>","text":"<p>Update the barman activity to prevent timeout.</p> Source code in <code>counter/models.py</code> <pre><code>def update_activity(self) -&gt; None:\n \"\"\"Update the barman activity to prevent timeout.\"\"\"\n self.permanencies.filter(end=None).update(activity=timezone.now())\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.can_refill","title":"<code>can_refill()</code>","text":"<p>Show if the counter authorize the refilling with physic money.</p> Source code in <code>counter/models.py</code> <pre><code>def can_refill(self) -&gt; bool:\n \"\"\"Show if the counter authorize the refilling with physic money.\"\"\"\n if self.type != \"BAR\":\n return False\n if self.id in SITH_COUNTER_OFFICES:\n # If the counter is either 'AE' or 'BdF', refills are authorized\n return True\n # at least one of the barmen is in the AE board\n ae = Club.objects.get(unix_name=SITH_MAIN_CLUB[\"unix_name\"])\n return any(ae.get_membership_for(barman) for barman in self.barmen_list)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.get_top_barmen","title":"<code>get_top_barmen()</code>","text":"<p>Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.</p> Each element of the QuerySet corresponds to a barman and has the following data <ul> <li>the full name (first name + last name) of the barman</li> <li>the nickname of the barman</li> <li>the promo of the barman</li> <li>the total number of office hours the barman did attend</li> </ul> Source code in <code>counter/models.py</code> <pre><code>def get_top_barmen(self) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the office hours stats of all the barmen of all time\n of this counter, ordered by descending number of hours.\n\n Each element of the QuerySet corresponds to a barman and has the following data :\n - the full name (first name + last name) of the barman\n - the nickname of the barman\n - the promo of the barman\n - the total number of office hours the barman did attend\n \"\"\"\n return (\n self.permanencies.exclude(end=None)\n .annotate(\n name=Concat(F(\"user__first_name\"), Value(\" \"), F(\"user__last_name\"))\n )\n .annotate(nickname=F(\"user__nick_name\"))\n .annotate(promo=F(\"user__promo\"))\n .values(\"user\", \"name\", \"nickname\", \"promo\")\n .annotate(perm_sum=Sum(F(\"end\") - F(\"start\")))\n .exclude(perm_sum=None)\n .order_by(\"-perm_sum\")\n )\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.get_top_customers","title":"<code>get_top_customers(since=None)</code>","text":"<p>Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.</p> <p>Each element of the QuerySet corresponds to a customer and has the following data :</p> <ul> <li>the full name (first name + last name) of the customer</li> <li>the nickname of the customer</li> <li>the amount of money spent by the customer</li> </ul> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> Source code in <code>counter/models.py</code> <pre><code>def get_top_customers(self, since: datetime | date | None = None) -&gt; QuerySet:\n \"\"\"Return a QuerySet querying the money spent by customers of this counter\n since the specified date, ordered by descending amount of money spent.\n\n Each element of the QuerySet corresponds to a customer and has the following data :\n\n - the full name (first name + last name) of the customer\n - the nickname of the customer\n - the amount of money spent by the customer\n\n Args:\n since: timestamp from which to perform the calculation\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return (\n self.sellings.filter(date__gte=since)\n .annotate(\n name=Concat(\n F(\"customer__user__first_name\"),\n Value(\" \"),\n F(\"customer__user__last_name\"),\n )\n )\n .annotate(nickname=F(\"customer__user__nick_name\"))\n .annotate(promo=F(\"customer__user__promo\"))\n .annotate(user=F(\"customer__user\"))\n .values(\"user\", \"promo\", \"name\", \"nickname\")\n .annotate(\n selling_sum=Sum(\n F(\"unit_price\") * F(\"quantity\"), output_field=CurrencyField()\n )\n )\n .filter(selling_sum__gt=0)\n .order_by(\"-selling_sum\")\n )\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Counter.get_total_sales","title":"<code>get_total_sales(since=None)</code>","text":"<p>Compute and return the total turnover of this counter since the given date.</p> <p>By default, the date is the start of the current semester.</p> <p>Parameters:</p> Name Type Description Default <code>since</code> <code>datetime | date | None</code> <p>timestamp from which to perform the calculation</p> <code>None</code> <p>Returns:</p> Type Description <code>CurrencyField</code> <p>Total revenue earned at this counter.</p> Source code in <code>counter/models.py</code> <pre><code>def get_total_sales(self, since: datetime | date | None = None) -&gt; CurrencyField:\n \"\"\"Compute and return the total turnover of this counter since the given date.\n\n By default, the date is the start of the current semester.\n\n Args:\n since: timestamp from which to perform the calculation\n\n Returns:\n Total revenue earned at this counter.\n \"\"\"\n if since is None:\n since = get_start_of_semester()\n if isinstance(since, date):\n since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)\n return self.sellings.filter(date__gte=since).aggregate(\n total=Sum(\n F(\"quantity\") * F(\"unit_price\"),\n default=0,\n output_field=CurrencyField(),\n )\n )[\"total\"]\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Customer","title":"<code>Customer</code>","text":"<p> Bases: <code>Model</code></p> <p>Customer data of a User.</p> <p>It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.</p>"},{"location":"reference/launderette/views/#launderette.views.Customer.can_buy","title":"<code>can_buy: bool</code> <code>property</code>","text":"<p>Check if whether this customer has the right to purchase any item.</p> <p>This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.</p>"},{"location":"reference/launderette/views/#launderette.views.Customer.save","title":"<code>save(*args, allow_negative=False, is_selling=False, **kwargs)</code>","text":"<p>is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, is_selling=False, **kwargs):\n \"\"\"is_selling : tell if the current action is a selling\n allow_negative : ignored if not a selling. Allow a selling to put the account in negative\n Those two parameters avoid blocking the save method of a customer if his account is negative.\n \"\"\"\n if self.amount &lt; 0 and (is_selling and not allow_negative):\n raise ValidationError(_(\"Not enough money\"))\n super().save(*args, **kwargs)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Customer.get_or_create","title":"<code>get_or_create(user)</code> <code>classmethod</code>","text":"<p>Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.</p> <p>If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id</p> <p>Example : ::</p> <pre><code>user = User.objects.get(pk=1)\naccount, created = Customer.get_or_create(user)\nif created:\n print(f\"created a new account with id {account.id}\")\nelse:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>@classmethod\ndef get_or_create(cls, user: User) -&gt; Tuple[Customer, bool]:\n \"\"\"Work in pretty much the same way as the usual get_or_create method,\n but with the default field replaced by some under the hood.\n\n If the user has an account, return it as is.\n Else create a new account with no money on it and a new unique account id\n\n Example : ::\n\n user = User.objects.get(pk=1)\n account, created = Customer.get_or_create(user)\n if created:\n print(f\"created a new account with id {account.id}\")\n else:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n \"\"\"\n if hasattr(user, \"customer\"):\n return user.customer, False\n\n # account_id are always a number with a letter appended\n account_id = (\n Customer.objects.order_by(Length(\"account_id\"), \"account_id\")\n .values(\"account_id\")\n .last()\n )\n if account_id is None:\n # legacy from the old site\n account = cls.objects.create(user=user, account_id=\"1504a\")\n return account, True\n\n account_id = account_id[\"account_id\"]\n account_num = int(account_id[:-1])\n while Customer.objects.filter(account_id=account_id).exists():\n # when entering the first iteration, we are using an already existing account id\n # so the loop should always execute at least one time\n account_num += 1\n account_id = f\"{account_num}{random.choice(string.ascii_lowercase)}\"\n\n account = cls.objects.create(user=user, account_id=account_id)\n return account, True\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Selling","title":"<code>Selling</code>","text":"<p> Bases: <code>Model</code></p> <p>Handle the sellings.</p>"},{"location":"reference/launderette/views/#launderette.views.Selling.save","title":"<code>save(*args, allow_negative=False, **kwargs)</code>","text":"<p>allow_negative : Allow this selling to use more money than available for this user.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, **kwargs):\n \"\"\"allow_negative : Allow this selling to use more money than available for this user.\"\"\"\n if not self.date:\n self.date = timezone.now()\n self.full_clean()\n if not self.is_validated:\n self.customer.amount -= self.quantity * self.unit_price\n self.customer.save(allow_negative=allow_negative, is_selling=True)\n self.is_validated = True\n user = self.customer.user\n if user.was_subscribed:\n if (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"un-semestre\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n elif (\n self.product\n and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS\n ):\n sub = Subscription(\n member=user,\n subscription_type=\"deux-semestres\",\n payment_method=\"EBOUTIC\",\n location=\"EBOUTIC\",\n )\n sub.subscription_start = Subscription.compute_start()\n sub.subscription_start = Subscription.compute_start(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ]\n )\n sub.subscription_end = Subscription.compute_end(\n duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][\n \"duration\"\n ],\n start=sub.subscription_start,\n )\n sub.save()\n if user.preferences.notify_on_click:\n Notification(\n user=user,\n url=reverse(\n \"core:user_account_detail\",\n kwargs={\n \"user_id\": user.id,\n \"year\": self.date.year,\n \"month\": self.date.month,\n },\n ),\n param=\"%d x %s\" % (self.quantity, self.label),\n type=\"SELLING\",\n ).save()\n super().save(*args, **kwargs)\n if hasattr(self.product, \"eticket\"):\n self.send_mail_customer()\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Launderette","title":"<code>Launderette</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/views/#launderette.views.Launderette.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>launderette/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n launderette_club = Club.objects.filter(\n unix_name=settings.SITH_LAUNDERETTE_MANAGER[\"unix_name\"]\n ).first()\n m = launderette_club.get_membership_for(user)\n return bool(m and m.role &gt;= 9)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Machine","title":"<code>Machine</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/views/#launderette.views.Machine.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>launderette/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n launderette_club = Club.objects.filter(\n unix_name=settings.SITH_LAUNDERETTE_MANAGER[\"unix_name\"]\n ).first()\n m = launderette_club.get_membership_for(user)\n return bool(m and m.role &gt;= 9)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.Slot","title":"<code>Slot</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/views/#launderette.views.Token","title":"<code>Token</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/launderette/views/#launderette.views.Token.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>launderette/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n launderette_club = Club.objects.filter(\n unix_name=settings.SITH_LAUNDERETTE_MANAGER[\"unix_name\"]\n ).first()\n m = launderette_club.get_membership_for(user)\n return bool(m and m.role &gt;= 9)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteMainView","title":"<code>LaunderetteMainView</code>","text":"<p> Bases: <code>TemplateView</code></p> <p>Main presentation view.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteMainView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add page to the context.</p> Source code in <code>launderette/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add page to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"page\"] = Page.objects.filter(name=\"launderette\").first()\n return kwargs\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteBookMainView","title":"<code>LaunderetteBookMainView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>ListView</code></p> <p>Choose which launderette to book.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteBookView","title":"<code>LaunderetteBookView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p> <p>Display the launderette schedule.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteBookView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add page to the context.</p> Source code in <code>launderette/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add page to the context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"planning\"] = OrderedDict()\n kwargs[\"slot_type\"] = self.slot_type\n start_date = datetime.now().replace(\n hour=0, minute=0, second=0, microsecond=0, tzinfo=tz.utc\n )\n for date in LaunderetteBookView.date_iterator(\n start_date, start_date + timedelta(days=6), timedelta(days=1)\n ):\n kwargs[\"planning\"][date] = []\n for h in LaunderetteBookView.date_iterator(\n date, date + timedelta(days=1), timedelta(hours=1)\n ):\n free = False\n if (\n (\n self.slot_type == \"BOTH\"\n and self.check_slot(\"WASHING\", h)\n and self.check_slot(\"DRYING\", h + timedelta(hours=1))\n )\n or self.slot_type == \"WASHING\"\n and self.check_slot(\"WASHING\", h)\n or self.slot_type == \"DRYING\"\n and self.check_slot(\"DRYING\", h)\n ):\n free = True\n if free and datetime.now().replace(tzinfo=tz.utc) &lt; h:\n kwargs[\"planning\"][date].append(h)\n else:\n kwargs[\"planning\"][date].append(None)\n return kwargs\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.SlotDeleteView","title":"<code>SlotDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Delete a slot.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteListView","title":"<code>LaunderetteListView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>ListView</code></p> <p>Choose which launderette to administer.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteEditView","title":"<code>LaunderetteEditView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Edit a launderette.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteCreateView","title":"<code>LaunderetteCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a new launderette.</p>"},{"location":"reference/launderette/views/#launderette.views.ManageTokenForm","title":"<code>ManageTokenForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteAdminView","title":"<code>LaunderetteAdminView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>BaseFormView</code>, <code>DetailView</code></p> <p>The admin page of the launderette.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteAdminView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>We handle here the redirection, passing the user id of the asked customer.</p> Source code in <code>launderette/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"We handle here the redirection, passing the user id of the asked customer.\"\"\"\n form.process(self.object)\n if form.is_valid():\n return super().form_valid(form)\n else:\n return super().form_invalid(form)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteAdminView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>We handle here the login form for the barman.</p> Source code in <code>launderette/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"We handle here the login form for the barman.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n if self.request.method == \"GET\":\n kwargs[\"form\"] = self.get_form()\n return kwargs\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.GetLaunderetteUserForm","title":"<code>GetLaunderetteUserForm</code>","text":"<p> Bases: <code>GetUserForm</code></p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteMainClickView","title":"<code>LaunderetteMainClickView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>BaseFormView</code>, <code>DetailView</code></p> <p>The click page of the launderette.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteMainClickView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>We handle here the redirection, passing the user id of the asked customer.</p> Source code in <code>launderette/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"We handle here the redirection, passing the user id of the asked customer.\"\"\"\n self.kwargs[\"user_id\"] = form.cleaned_data[\"user_id\"]\n return super().form_valid(form)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteMainClickView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>We handle here the login form for the barman.</p> Source code in <code>launderette/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"We handle here the login form for the barman.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"counter\"] = self.object.counter\n kwargs[\"form\"] = self.get_form()\n kwargs[\"barmen\"] = [self.request.user]\n if \"last_basket\" in self.request.session:\n kwargs[\"last_basket\"] = self.request.session.pop(\"last_basket\", None)\n kwargs[\"last_customer\"] = self.request.session.pop(\"last_customer\", None)\n kwargs[\"last_total\"] = self.request.session.pop(\"last_total\", None)\n kwargs[\"new_customer_amount\"] = self.request.session.pop(\n \"new_customer_amount\", None\n )\n return kwargs\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.ClickTokenForm","title":"<code>ClickTokenForm</code>","text":"<p> Bases: <code>BaseForm</code></p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteClickView","title":"<code>LaunderetteClickView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>DetailView</code>, <code>BaseFormView</code></p> <p>The click page of the launderette.</p>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteClickView.get","title":"<code>get(request, *args, **kwargs)</code>","text":"<p>Simple get view.</p> Source code in <code>launderette/views.py</code> <pre><code>def get(self, request, *args, **kwargs):\n \"\"\"Simple get view.\"\"\"\n self.customer = Customer.objects.filter(user__id=self.kwargs[\"user_id\"]).first()\n self.subscriber = self.customer.user\n self.operator = request.user\n return super().get(request, *args, **kwargs)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteClickView.post","title":"<code>post(request, *args, **kwargs)</code>","text":"<p>Handle the many possibilities of the post request.</p> Source code in <code>launderette/views.py</code> <pre><code>def post(self, request, *args, **kwargs):\n \"\"\"Handle the many possibilities of the post request.\"\"\"\n self.object = self.get_object()\n self.customer = Customer.objects.filter(user__id=self.kwargs[\"user_id\"]).first()\n self.subscriber = self.customer.user\n self.operator = request.user\n return super().post(request, *args, **kwargs)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteClickView.form_valid","title":"<code>form_valid(form)</code>","text":"<p>We handle here the redirection, passing the user id of the asked customer.</p> Source code in <code>launderette/views.py</code> <pre><code>def form_valid(self, form):\n \"\"\"We handle here the redirection, passing the user id of the asked customer.\"\"\"\n self.request.session.update(form.last_basket)\n return super().form_valid(form)\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.LaunderetteClickView.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>We handle here the login form for the barman.</p> Source code in <code>launderette/views.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"We handle here the login form for the barman.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n if \"form\" not in kwargs:\n kwargs[\"form\"] = self.get_form()\n kwargs[\"counter\"] = self.object.counter\n kwargs[\"customer\"] = self.customer\n return kwargs\n</code></pre>"},{"location":"reference/launderette/views/#launderette.views.MachineEditView","title":"<code>MachineEditView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Edit a machine.</p>"},{"location":"reference/launderette/views/#launderette.views.MachineDeleteView","title":"<code>MachineDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Edit a machine.</p>"},{"location":"reference/launderette/views/#launderette.views.MachineCreateView","title":"<code>MachineCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a new machine.</p>"},{"location":"reference/matmat/models/","title":"Models","text":""},{"location":"reference/matmat/views/","title":"Views","text":""},{"location":"reference/matmat/views/#matmat.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/matmat/views/#matmat.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/matmat/views/#matmat.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.FormerSubscriberMixin","title":"<code>FormerSubscriberMixin</code>","text":"<p> Bases: <code>AccessMixin</code></p> <p>Check if the user was at least an old subscriber.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user never subscribed.</p>"},{"location":"reference/matmat/views/#matmat.views.SearchType","title":"<code>SearchType</code>","text":"<p> Bases: <code>Enum</code></p>"},{"location":"reference/matmat/views/#matmat.views.SearchForm","title":"<code>SearchForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>matmat/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n for key in self.fields:\n self.fields[key].required = False\n</code></pre>"},{"location":"reference/matmat/views/#matmat.views.SearchFormListView","title":"<code>SearchFormListView</code>","text":"<p> Bases: <code>FormerSubscriberMixin</code>, <code>SingleObjectMixin</code>, <code>ListView</code></p>"},{"location":"reference/matmat/views/#matmat.views.SearchFormView","title":"<code>SearchFormView</code>","text":"<p> Bases: <code>FormerSubscriberMixin</code>, <code>FormView</code></p> <p>Allows users to search inside the user list.</p>"},{"location":"reference/matmat/views/#matmat.views.SearchNormalFormView","title":"<code>SearchNormalFormView</code>","text":"<p> Bases: <code>SearchFormView</code></p>"},{"location":"reference/matmat/views/#matmat.views.SearchReverseFormView","title":"<code>SearchReverseFormView</code>","text":"<p> Bases: <code>SearchFormView</code></p>"},{"location":"reference/matmat/views/#matmat.views.SearchQuickFormView","title":"<code>SearchQuickFormView</code>","text":"<p> Bases: <code>SearchFormView</code></p>"},{"location":"reference/matmat/views/#matmat.views.SearchClearFormView","title":"<code>SearchClearFormView</code>","text":"<p> Bases: <code>FormerSubscriberMixin</code>, <code>View</code></p> <p>Clear SearchFormView and redirect to it.</p>"},{"location":"reference/matmat/views/#matmat.views.search_user","title":"<code>search_user(query)</code>","text":"Source code in <code>core/views/site.py</code> <pre><code>def search_user(query):\n try:\n # slugify turns everything into ascii and every whitespace into -\n # it ends by removing duplicate - (so ' - ' will turn into '-')\n # replace('-', ' ') because search is whitespace based\n query = slugify(query).replace(\"-\", \" \")\n # TODO: is this necessary?\n query = html.escape(query)\n res = (\n SearchQuerySet()\n .models(User)\n .autocomplete(auto=query)\n .order_by(\"-last_login\")\n .load_all()[:20]\n )\n return [r.object for r in res]\n except TypeError:\n return []\n</code></pre>"},{"location":"reference/pedagogy/models/","title":"Models","text":""},{"location":"reference/pedagogy/models/#pedagogy.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.UV","title":"<code>UV</code>","text":"<p> Bases: <code>Model</code></p> <p>Contains infos about an UV (course).</p>"},{"location":"reference/pedagogy/models/#pedagogy.models.UV.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Can be created by superuser, root or pedagogy admin user.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Can be created by superuser, root or pedagogy admin user.\"\"\"\n return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.UV.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Only visible by subscribers.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Only visible by subscribers.\"\"\"\n return user.is_subscribed\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.UV.has_user_already_commented","title":"<code>has_user_already_commented(user)</code>","text":"<p>Help prevent multiples comments from the same user.</p> <p>This function checks that no other comment has been posted by a specified user.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user has already posted a comment on this UV, else False.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def has_user_already_commented(self, user: User) -&gt; bool:\n \"\"\"Help prevent multiples comments from the same user.\n\n This function checks that no other comment has been posted by a specified user.\n\n Returns:\n True if the user has already posted a comment on this UV, else False.\n \"\"\"\n return self.comments.filter(author=user).exists()\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.UVComment","title":"<code>UVComment</code>","text":"<p> Bases: <code>Model</code></p> <p>A comment about an UV.</p>"},{"location":"reference/pedagogy/models/#pedagogy.models.UVComment.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Is owned by a pedagogy admin, a superuser or the author himself.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Is owned by a pedagogy admin, a superuser or the author himself.\"\"\"\n return self.author == user or user.is_owner(self.uv)\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.UVComment.is_reported","title":"<code>is_reported()</code>","text":"<p>Return True if someone reported this UV.</p> Source code in <code>pedagogy/models.py</code> <pre><code>@cached_property\ndef is_reported(self):\n \"\"\"Return True if someone reported this UV.\"\"\"\n return self.reports.exists()\n</code></pre>"},{"location":"reference/pedagogy/models/#pedagogy.models.UVResult","title":"<code>UVResult</code>","text":"<p> Bases: <code>Model</code></p> <p>Results got to an UV.</p> <p>Views will be implemented after the first release Will list every UV done by an user Linked to user uv Contains a grade settings.SITH_PEDAGOGY_UV_RESULT_GRADE a semester (P/A)20xx.</p>"},{"location":"reference/pedagogy/models/#pedagogy.models.UVCommentReport","title":"<code>UVCommentReport</code>","text":"<p> Bases: <code>Model</code></p> <p>Report an inapropriate comment.</p>"},{"location":"reference/pedagogy/models/#pedagogy.models.UVCommentReport.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Can be created by a pedagogy admin, a superuser or a subscriber.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Can be created by a pedagogy admin, a superuser or a subscriber.\"\"\"\n return user.is_subscribed or user.is_owner(self.comment.uv)\n</code></pre>"},{"location":"reference/pedagogy/schemas/","title":"Schemas","text":""},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.ShortUvList","title":"<code>ShortUvList = TypeAdapter(list[UtbmShortUvSchema])</code> <code>module-attribute</code>","text":""},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UV","title":"<code>UV</code>","text":"<p> Bases: <code>Model</code></p> <p>Contains infos about an UV (course).</p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UV.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Can be created by superuser, root or pedagogy admin user.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Can be created by superuser, root or pedagogy admin user.\"\"\"\n return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)\n</code></pre>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UV.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Only visible by subscribers.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Only visible by subscribers.\"\"\"\n return user.is_subscribed\n</code></pre>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UV.has_user_already_commented","title":"<code>has_user_already_commented(user)</code>","text":"<p>Help prevent multiples comments from the same user.</p> <p>This function checks that no other comment has been posted by a specified user.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user has already posted a comment on this UV, else False.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def has_user_already_commented(self, user: User) -&gt; bool:\n \"\"\"Help prevent multiples comments from the same user.\n\n This function checks that no other comment has been posted by a specified user.\n\n Returns:\n True if the user has already posted a comment on this UV, else False.\n \"\"\"\n return self.comments.filter(author=user).exists()\n</code></pre>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UtbmShortUvSchema","title":"<code>UtbmShortUvSchema</code>","text":"<p> Bases: <code>Schema</code></p> <p>Short representation of an UV in the UTBM API.</p> Notes <p>This schema holds only the fields we actually need. The UTBM API returns more data than that.</p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.WorkloadSchema","title":"<code>WorkloadSchema</code>","text":"<p> Bases: <code>Schema</code></p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.SemesterUvState","title":"<code>SemesterUvState</code>","text":"<p> Bases: <code>Schema</code></p> <p>The state of the UV during either autumn or spring semester</p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UtbmFullUvSchema","title":"<code>UtbmFullUvSchema</code>","text":"<p> Bases: <code>Schema</code></p> <p>Long representation of an UV in the UTBM API.</p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.SimpleUvSchema","title":"<code>SimpleUvSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>Our minimal representation of an UV.</p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UvSchema","title":"<code>UvSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>Our complete representation of an UV</p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UvFilterSchema","title":"<code>UvFilterSchema</code>","text":"<p> Bases: <code>FilterSchema</code></p>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UvFilterSchema.filter_search","title":"<code>filter_search(value)</code>","text":"<p>Special filter for the search text.</p> <p>It does a full text search if available.</p> Source code in <code>pedagogy/schemas.py</code> <pre><code>def filter_search(self, value: str | None) -&gt; Q:\n \"\"\"Special filter for the search text.\n\n It does a full text search if available.\n \"\"\"\n if not value:\n return Q()\n\n if len(value) &lt; 3 or (len(value) &lt; 5 and any(c.isdigit() for c in value)):\n # Likely to be an UV code\n return Q(code__istartswith=value)\n\n qs = list(\n SearchQuerySet()\n .models(UV)\n .autocomplete(auto=html.escape(value))\n .values_list(\"pk\", flat=True)\n )\n\n return Q(id__in=qs)\n</code></pre>"},{"location":"reference/pedagogy/schemas/#pedagogy.schemas.UvFilterSchema.filter_semester","title":"<code>filter_semester(value)</code>","text":"<p>Special filter for the semester.</p> <p>If either \"SPRING\" or \"AUTUMN\" is given, UV that are available during \"AUTUMN_AND_SPRING\" will be filtered.</p> Source code in <code>pedagogy/schemas.py</code> <pre><code>def filter_semester(self, value: set[str] | None) -&gt; Q:\n \"\"\"Special filter for the semester.\n\n If either \"SPRING\" or \"AUTUMN\" is given, UV that are available\n during \"AUTUMN_AND_SPRING\" will be filtered.\n \"\"\"\n if not value:\n return Q()\n value.add(\"AUTUMN_AND_SPRING\")\n return Q(semester__in=value)\n</code></pre>"},{"location":"reference/pedagogy/views/","title":"Views","text":""},{"location":"reference/pedagogy/views/#pedagogy.views.Notification","title":"<code>Notification</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/pedagogy/views/#pedagogy.views.RealGroup","title":"<code>RealGroup</code>","text":"<p> Bases: <code>Group</code></p> <p>RealGroups are created by the developer.</p> <p>Most of the time they match a number in settings to be easily used for permissions.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.CanCreateMixin","title":"<code>CanCreateMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect any child view that would create an object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user has not the necessary permission to create the object of the view.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.CanEditPropMixin","title":"<code>CanEditPropMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has owner permissions on the child view object.</p> <p>In other word, you can make a view with this view as parent, and it will be retricted to the users that are in the object's owner_group or that pass the <code>obj.can_be_viewed_by</code> test.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user cannot see the object</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.DetailFormView","title":"<code>DetailFormView</code>","text":"<p> Bases: <code>SingleObjectMixin</code>, <code>FormView</code></p> <p>Class that allow both a detail view and a form view.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.DetailFormView.get_object","title":"<code>get_object()</code>","text":"<p>Get current group from id in url.</p> Source code in <code>core/views/__init__.py</code> <pre><code>def get_object(self):\n \"\"\"Get current group from id in url.\"\"\"\n return self.cached_object\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.DetailFormView.cached_object","title":"<code>cached_object()</code>","text":"<p>Optimisation on group retrieval.</p> Source code in <code>core/views/__init__.py</code> <pre><code>@cached_property\ndef cached_object(self):\n \"\"\"Optimisation on group retrieval.\"\"\"\n return super().get_object()\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.FormerSubscriberMixin","title":"<code>FormerSubscriberMixin</code>","text":"<p> Bases: <code>AccessMixin</code></p> <p>Check if the user was at least an old subscriber.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user never subscribed.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentForm","title":"<code>UVCommentForm(author_id, uv_id, is_creation, *args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form handeling creation and edit of an UVComment.</p> Source code in <code>pedagogy/forms.py</code> <pre><code>def __init__(self, author_id, uv_id, is_creation, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"author\"].queryset = User.objects.filter(id=author_id).all()\n self.fields[\"author\"].initial = author_id\n self.fields[\"uv\"].queryset = UV.objects.filter(id=uv_id).all()\n self.fields[\"uv\"].initial = uv_id\n self.is_creation = is_creation\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentModerationForm","title":"<code>UVCommentModerationForm</code>","text":"<p> Bases: <code>Form</code></p> <p>Form handeling bulk comment deletion.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentReportForm","title":"<code>UVCommentReportForm(reporter_id, comment_id, *args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form handeling creation and edit of an UVReport.</p> Source code in <code>pedagogy/forms.py</code> <pre><code>def __init__(self, reporter_id, comment_id, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"reporter\"].queryset = User.objects.filter(id=reporter_id).all()\n self.fields[\"reporter\"].initial = reporter_id\n self.fields[\"comment\"].queryset = UVComment.objects.filter(id=comment_id).all()\n self.fields[\"comment\"].initial = comment_id\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVForm","title":"<code>UVForm(author_id, *args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form handeling creation and edit of an UV.</p> Source code in <code>pedagogy/forms.py</code> <pre><code>def __init__(self, author_id, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"author\"].queryset = User.objects.filter(id=author_id).all()\n self.fields[\"author\"].initial = author_id\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UV","title":"<code>UV</code>","text":"<p> Bases: <code>Model</code></p> <p>Contains infos about an UV (course).</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UV.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Can be created by superuser, root or pedagogy admin user.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Can be created by superuser, root or pedagogy admin user.\"\"\"\n return user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UV.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Only visible by subscribers.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Only visible by subscribers.\"\"\"\n return user.is_subscribed\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UV.has_user_already_commented","title":"<code>has_user_already_commented(user)</code>","text":"<p>Help prevent multiples comments from the same user.</p> <p>This function checks that no other comment has been posted by a specified user.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user has already posted a comment on this UV, else False.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def has_user_already_commented(self, user: User) -&gt; bool:\n \"\"\"Help prevent multiples comments from the same user.\n\n This function checks that no other comment has been posted by a specified user.\n\n Returns:\n True if the user has already posted a comment on this UV, else False.\n \"\"\"\n return self.comments.filter(author=user).exists()\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVComment","title":"<code>UVComment</code>","text":"<p> Bases: <code>Model</code></p> <p>A comment about an UV.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVComment.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Is owned by a pedagogy admin, a superuser or the author himself.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Is owned by a pedagogy admin, a superuser or the author himself.\"\"\"\n return self.author == user or user.is_owner(self.uv)\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVComment.is_reported","title":"<code>is_reported()</code>","text":"<p>Return True if someone reported this UV.</p> Source code in <code>pedagogy/models.py</code> <pre><code>@cached_property\ndef is_reported(self):\n \"\"\"Return True if someone reported this UV.\"\"\"\n return self.reports.exists()\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentReport","title":"<code>UVCommentReport</code>","text":"<p> Bases: <code>Model</code></p> <p>Report an inapropriate comment.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentReport.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Can be created by a pedagogy admin, a superuser or a subscriber.</p> Source code in <code>pedagogy/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Can be created by a pedagogy admin, a superuser or a subscriber.\"\"\"\n return user.is_subscribed or user.is_owner(self.comment.uv)\n</code></pre>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVDetailFormView","title":"<code>UVDetailFormView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailFormView</code></p> <p>Display every comment of an UV and detailed infos about it.</p> <p>Allow to comment the UV.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentUpdateView","title":"<code>UVCommentUpdateView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Allow edit of a given comment.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentDeleteView","title":"<code>UVCommentDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Allow delete of a given comment.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVGuideView","title":"<code>UVGuideView</code>","text":"<p> Bases: <code>LoginRequiredMixin</code>, <code>FormerSubscriberMixin</code>, <code>TemplateView</code></p> <p>UV guide main page.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCommentReportCreateView","title":"<code>UVCommentReportCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a new report for an inapropriate comment.</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVModerationFormView","title":"<code>UVModerationFormView</code>","text":"<p> Bases: <code>FormView</code></p> <p>Moderation interface (Privileged).</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVCreateView","title":"<code>UVCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Add a new UV (Privileged).</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVDeleteView","title":"<code>UVDeleteView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>DeleteView</code></p> <p>Allow to delete an UV (Privileged).</p>"},{"location":"reference/pedagogy/views/#pedagogy.views.UVUpdateView","title":"<code>UVUpdateView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>UpdateView</code></p> <p>Allow to edit an UV (Privilegied).</p>"},{"location":"reference/rootplace/models/","title":"Models","text":""},{"location":"reference/rootplace/views/","title":"Views","text":""},{"location":"reference/rootplace/views/#rootplace.views.OperationLog","title":"<code>OperationLog</code>","text":"<p> Bases: <code>Model</code></p> <p>General purpose log object to register operations.</p>"},{"location":"reference/rootplace/views/#rootplace.views.SithFile","title":"<code>SithFile</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/rootplace/views/#rootplace.views.SithFile.clean","title":"<code>clean()</code>","text":"<p>Cleans up the file.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up the file.\"\"\"\n super().clean()\n if \"/\" in self.name:\n raise ValidationError(_(\"Character '/' not authorized in name\"))\n if self == self.parent:\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self == self.parent or (\n self.parent is not None and self in self.get_parent_list()\n ):\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self.parent and self.parent.is_file:\n raise ValidationError(\n _(\"You can not make a file be a children of a non folder file\")\n )\n if (\n self.parent is None\n and SithFile.objects.exclude(id=self.id)\n .filter(parent=None, name=self.name)\n .exists()\n ) or (\n self.parent\n and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()\n ):\n raise ValidationError(_(\"Duplicate file\"), code=\"duplicate\")\n if self.is_folder:\n if self.file:\n try:\n import imghdr\n\n if imghdr.what(None, self.file.read()) not in [\n \"gif\",\n \"png\",\n \"jpeg\",\n ]:\n self.file.delete()\n self.file = None\n except: # noqa E722 I don't know the exception that can be raised\n self.file = None\n self.mime_type = \"inode/directory\"\n if self.is_file and (self.file is None or self.file == \"\"):\n raise ValidationError(_(\"You must provide a file\"))\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.SithFile.apply_rights_recursively","title":"<code>apply_rights_recursively(*, only_folders=False)</code>","text":"<p>Apply the rights of this file to all children recursively.</p> <p>Parameters:</p> Name Type Description Default <code>only_folders</code> <code>bool</code> <p>If True, only apply the rights to SithFiles that are folders.</p> <code>False</code> Source code in <code>core/models.py</code> <pre><code>def apply_rights_recursively(self, *, only_folders: bool = False) -&gt; None:\n \"\"\"Apply the rights of this file to all children recursively.\n\n Args:\n only_folders: If True, only apply the rights to SithFiles that are folders.\n \"\"\"\n file_ids = []\n explored_ids = [self.id]\n while len(explored_ids) &gt; 0: # find all children recursively\n file_ids.extend(explored_ids)\n next_level = SithFile.objects.filter(parent_id__in=explored_ids)\n if only_folders:\n next_level = next_level.filter(is_folder=True)\n explored_ids = list(next_level.values_list(\"id\", flat=True))\n for through in (SithFile.view_groups.through, SithFile.edit_groups.through):\n # force evaluation. Without this, the iterator yields nothing\n groups = list(\n through.objects.filter(sithfile_id=self.id).values_list(\n \"group_id\", flat=True\n )\n )\n # delete previous rights\n through.objects.filter(sithfile_id__in=file_ids).delete()\n through.objects.bulk_create( # create new rights\n [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]\n )\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.SithFile.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>core/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.SithFile.move_to","title":"<code>move_to(parent)</code>","text":"<p>Move a file to a new parent. <code>parent</code> must be a SithFile with the <code>is_folder=True</code> property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.</p> Source code in <code>core/models.py</code> <pre><code>def move_to(self, parent):\n \"\"\"Move a file to a new parent.\n `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change\n anything.\n This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify\n SithFiles recursively, so it stays efficient even with top-level folders.\n \"\"\"\n if not parent.is_folder:\n return\n self.parent = parent\n self.clean()\n self.save()\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/rootplace/views/#rootplace.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/rootplace/views/#rootplace.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.CanEditPropMixin","title":"<code>CanEditPropMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has owner permissions on the child view object.</p> <p>In other word, you can make a view with this view as parent, and it will be retricted to the users that are in the object's owner_group or that pass the <code>obj.can_be_viewed_by</code> test.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user cannot see the object</p>"},{"location":"reference/rootplace/views/#rootplace.views.Customer","title":"<code>Customer</code>","text":"<p> Bases: <code>Model</code></p> <p>Customer data of a User.</p> <p>It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.</p>"},{"location":"reference/rootplace/views/#rootplace.views.Customer.can_buy","title":"<code>can_buy: bool</code> <code>property</code>","text":"<p>Check if whether this customer has the right to purchase any item.</p> <p>This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.</p>"},{"location":"reference/rootplace/views/#rootplace.views.Customer.save","title":"<code>save(*args, allow_negative=False, is_selling=False, **kwargs)</code>","text":"<p>is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.</p> Source code in <code>counter/models.py</code> <pre><code>def save(self, *args, allow_negative=False, is_selling=False, **kwargs):\n \"\"\"is_selling : tell if the current action is a selling\n allow_negative : ignored if not a selling. Allow a selling to put the account in negative\n Those two parameters avoid blocking the save method of a customer if his account is negative.\n \"\"\"\n if self.amount &lt; 0 and (is_selling and not allow_negative):\n raise ValidationError(_(\"Not enough money\"))\n super().save(*args, **kwargs)\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.Customer.get_or_create","title":"<code>get_or_create(user)</code> <code>classmethod</code>","text":"<p>Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.</p> <p>If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id</p> <p>Example : ::</p> <pre><code>user = User.objects.get(pk=1)\naccount, created = Customer.get_or_create(user)\nif created:\n print(f\"created a new account with id {account.id}\")\nelse:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n</code></pre> Source code in <code>counter/models.py</code> <pre><code>@classmethod\ndef get_or_create(cls, user: User) -&gt; Tuple[Customer, bool]:\n \"\"\"Work in pretty much the same way as the usual get_or_create method,\n but with the default field replaced by some under the hood.\n\n If the user has an account, return it as is.\n Else create a new account with no money on it and a new unique account id\n\n Example : ::\n\n user = User.objects.get(pk=1)\n account, created = Customer.get_or_create(user)\n if created:\n print(f\"created a new account with id {account.id}\")\n else:\n print(f\"user has already an account, with {account.id} \u20ac on it\"\n \"\"\"\n if hasattr(user, \"customer\"):\n return user.customer, False\n\n # account_id are always a number with a letter appended\n account_id = (\n Customer.objects.order_by(Length(\"account_id\"), \"account_id\")\n .values(\"account_id\")\n .last()\n )\n if account_id is None:\n # legacy from the old site\n account = cls.objects.create(user=user, account_id=\"1504a\")\n return account, True\n\n account_id = account_id[\"account_id\"]\n account_num = int(account_id[:-1])\n while Customer.objects.filter(account_id=account_id).exists():\n # when entering the first iteration, we are using an already existing account id\n # so the loop should always execute at least one time\n account_num += 1\n account_id = f\"{account_num}{random.choice(string.ascii_lowercase)}\"\n\n account = cls.objects.create(user=user, account_id=account_id)\n return account, True\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.ForumMessageMeta","title":"<code>ForumMessageMeta</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/rootplace/views/#rootplace.views.MergeForm","title":"<code>MergeForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/rootplace/views/#rootplace.views.SelectUserForm","title":"<code>SelectUserForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/rootplace/views/#rootplace.views.MergeUsersView","title":"<code>MergeUsersView</code>","text":"<p> Bases: <code>FormView</code></p>"},{"location":"reference/rootplace/views/#rootplace.views.DeleteAllForumUserMessagesView","title":"<code>DeleteAllForumUserMessagesView</code>","text":"<p> Bases: <code>FormView</code></p> <p>Delete all forum messages from an user.</p> <p>Messages are soft deleted and are still visible from admins GUI frontend to the dedicated command.</p>"},{"location":"reference/rootplace/views/#rootplace.views.OperationLogListView","title":"<code>OperationLogListView</code>","text":"<p> Bases: <code>ListView</code>, <code>CanEditPropMixin</code></p> <p>List all logs.</p>"},{"location":"reference/rootplace/views/#rootplace.views.__merge_subscriptions","title":"<code>__merge_subscriptions(u1, u2)</code>","text":"<p>Give all the subscriptions of the second user to first one.</p> <p>If some subscriptions are still active, update their end date to increase the overall subscription time of the first user.</p> <p>Some examples : - if u1 is not subscribed, his subscription end date become the one of u2 - if u1 is subscribed but not u2, nothing happen - if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months, he shall then be subscribed for 5 months</p> Source code in <code>rootplace/views.py</code> <pre><code>def __merge_subscriptions(u1: User, u2: User):\n \"\"\"Give all the subscriptions of the second user to first one.\n\n If some subscriptions are still active, update their end date\n to increase the overall subscription time of the first user.\n\n Some examples :\n - if u1 is not subscribed, his subscription end date become the one of u2\n - if u1 is subscribed but not u2, nothing happen\n - if u1 is subscribed for, let's say,\n 2 remaining months and u2 is subscribed for 3 remaining months,\n he shall then be subscribed for 5 months\n \"\"\"\n last_subscription = (\n u1.subscriptions.filter(\n subscription_start__lte=timezone.now(), subscription_end__gte=timezone.now()\n )\n .order_by(\"subscription_end\")\n .last()\n )\n if last_subscription is not None:\n subscription_end = last_subscription.subscription_end\n for subscription in u2.subscriptions.filter(\n subscription_end__gte=timezone.now()\n ):\n subscription.subscription_start = subscription_end\n if subscription.subscription_start &gt; localdate():\n remaining = subscription.subscription_end - localdate()\n else:\n remaining = (\n subscription.subscription_end - subscription.subscription_start\n )\n subscription_end += remaining\n subscription.subscription_end = subscription_end\n subscription.save()\n u2.subscriptions.all().update(member=u1)\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.__merge_pictures","title":"<code>__merge_pictures(u1, u2)</code>","text":"Source code in <code>rootplace/views.py</code> <pre><code>def __merge_pictures(u1: User, u2: User) -&gt; None:\n SithFile.objects.filter(owner=u2).update(owner=u1)\n if u1.profile_pict is None and u2.profile_pict is not None:\n u1.profile_pict, u2.profile_pict = u2.profile_pict, None\n if u1.scrub_pict is None and u2.scrub_pict is not None:\n u1.scrub_pict, u2.scrub_pict = u2.scrub_pict, None\n if u1.avatar_pict is None and u2.avatar_pict is not None:\n u1.avatar_pict, u2.avatar_pict = u2.avatar_pict, None\n u2.save()\n u1.save()\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.merge_users","title":"<code>merge_users(u1, u2)</code>","text":"<p>Merge u2 into u1.</p> <p>This means that u1 shall receive everything that belonged to u2 :</p> <pre><code>- pictures\n- refills of the sith account\n- purchases of any item bought on the eboutic or the counters\n- subscriptions\n- godfathers\n- godchildren\n</code></pre> <p>If u1 had no account id, he shall receive the one of u2. If u1 and u2 were both in the middle of a subscription, the remaining durations stack If u1 had no profile picture, he shall receive the one of u2</p> Source code in <code>rootplace/views.py</code> <pre><code>def merge_users(u1: User, u2: User) -&gt; User:\n \"\"\"Merge u2 into u1.\n\n This means that u1 shall receive everything that belonged to u2 :\n\n - pictures\n - refills of the sith account\n - purchases of any item bought on the eboutic or the counters\n - subscriptions\n - godfathers\n - godchildren\n\n If u1 had no account id, he shall receive the one of u2.\n If u1 and u2 were both in the middle of a subscription, the remaining\n durations stack\n If u1 had no profile picture, he shall receive the one of u2\n \"\"\"\n for field in u1._meta.fields:\n if not field.is_relation and not u1.__dict__[field.name]:\n u1.__dict__[field.name] = u2.__dict__[field.name]\n for group in u2.groups.all():\n u1.groups.add(group.id)\n for godfather in u2.godfathers.exclude(id=u1.id):\n u1.godfathers.add(godfather)\n for godchild in u2.godchildren.exclude(id=u1.id):\n u1.godchildren.add(godchild)\n __merge_subscriptions(u1, u2)\n __merge_pictures(u1, u2)\n u2.invoices.all().update(user=u1)\n c_src = Customer.objects.filter(user=u2).first()\n if c_src is not None:\n c_dest, created = Customer.get_or_create(u1)\n c_src.refillings.update(customer=c_dest)\n c_src.buyings.update(customer=c_dest)\n c_dest.recompute_amount()\n if created:\n # swap the account numbers, so that the user keep\n # the id he is accustomed to\n tmp_id = c_src.account_id\n # delete beforehand in order not to have a unique constraint violation\n c_src.delete()\n c_dest.account_id = tmp_id\n u1.save()\n u2.delete() # everything remaining in u2 gets deleted thanks to on_delete=CASCADE\n return u1\n</code></pre>"},{"location":"reference/rootplace/views/#rootplace.views.delete_all_forum_user_messages","title":"<code>delete_all_forum_user_messages(user, moderator, *, verbose=False)</code>","text":"<p>Soft delete all messages of a user.</p> <p>Parameters:</p> Name Type Description Default <code>user</code> <code>User</code> <p>core.models.User the user to delete messages from</p> required <code>moderator</code> <code>User</code> <p>core.models.User the one marked as the moderator.</p> required <code>verbose</code> <code>bool</code> <p>bool if True, print the deleted messages</p> <code>False</code> Source code in <code>rootplace/views.py</code> <pre><code>def delete_all_forum_user_messages(\n user: User, moderator: User, *, verbose: bool = False\n):\n \"\"\"Soft delete all messages of a user.\n\n Args:\n user: core.models.User the user to delete messages from\n moderator: core.models.User the one marked as the moderator.\n verbose: bool if True, print the deleted messages\n \"\"\"\n for message in user.forum_messages.all():\n if message.is_deleted():\n continue\n\n if verbose:\n logging.getLogger(\"django\").info(message)\n ForumMessageMeta(message=message, user=moderator, action=\"DELETE\").save()\n</code></pre>"},{"location":"reference/sas/models/","title":"Models","text":""},{"location":"reference/sas/models/#sas.models.SithFile","title":"<code>SithFile</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/sas/models/#sas.models.SithFile.clean","title":"<code>clean()</code>","text":"<p>Cleans up the file.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up the file.\"\"\"\n super().clean()\n if \"/\" in self.name:\n raise ValidationError(_(\"Character '/' not authorized in name\"))\n if self == self.parent:\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self == self.parent or (\n self.parent is not None and self in self.get_parent_list()\n ):\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self.parent and self.parent.is_file:\n raise ValidationError(\n _(\"You can not make a file be a children of a non folder file\")\n )\n if (\n self.parent is None\n and SithFile.objects.exclude(id=self.id)\n .filter(parent=None, name=self.name)\n .exists()\n ) or (\n self.parent\n and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()\n ):\n raise ValidationError(_(\"Duplicate file\"), code=\"duplicate\")\n if self.is_folder:\n if self.file:\n try:\n import imghdr\n\n if imghdr.what(None, self.file.read()) not in [\n \"gif\",\n \"png\",\n \"jpeg\",\n ]:\n self.file.delete()\n self.file = None\n except: # noqa E722 I don't know the exception that can be raised\n self.file = None\n self.mime_type = \"inode/directory\"\n if self.is_file and (self.file is None or self.file == \"\"):\n raise ValidationError(_(\"You must provide a file\"))\n</code></pre>"},{"location":"reference/sas/models/#sas.models.SithFile.apply_rights_recursively","title":"<code>apply_rights_recursively(*, only_folders=False)</code>","text":"<p>Apply the rights of this file to all children recursively.</p> <p>Parameters:</p> Name Type Description Default <code>only_folders</code> <code>bool</code> <p>If True, only apply the rights to SithFiles that are folders.</p> <code>False</code> Source code in <code>core/models.py</code> <pre><code>def apply_rights_recursively(self, *, only_folders: bool = False) -&gt; None:\n \"\"\"Apply the rights of this file to all children recursively.\n\n Args:\n only_folders: If True, only apply the rights to SithFiles that are folders.\n \"\"\"\n file_ids = []\n explored_ids = [self.id]\n while len(explored_ids) &gt; 0: # find all children recursively\n file_ids.extend(explored_ids)\n next_level = SithFile.objects.filter(parent_id__in=explored_ids)\n if only_folders:\n next_level = next_level.filter(is_folder=True)\n explored_ids = list(next_level.values_list(\"id\", flat=True))\n for through in (SithFile.view_groups.through, SithFile.edit_groups.through):\n # force evaluation. Without this, the iterator yields nothing\n groups = list(\n through.objects.filter(sithfile_id=self.id).values_list(\n \"group_id\", flat=True\n )\n )\n # delete previous rights\n through.objects.filter(sithfile_id__in=file_ids).delete()\n through.objects.bulk_create( # create new rights\n [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]\n )\n</code></pre>"},{"location":"reference/sas/models/#sas.models.SithFile.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>core/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n</code></pre>"},{"location":"reference/sas/models/#sas.models.SithFile.move_to","title":"<code>move_to(parent)</code>","text":"<p>Move a file to a new parent. <code>parent</code> must be a SithFile with the <code>is_folder=True</code> property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.</p> Source code in <code>core/models.py</code> <pre><code>def move_to(self, parent):\n \"\"\"Move a file to a new parent.\n `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change\n anything.\n This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify\n SithFiles recursively, so it stays efficient even with top-level folders.\n \"\"\"\n if not parent.is_folder:\n return\n self.parent = parent\n self.clean()\n self.save()\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/sas/models/#sas.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/sas/models/#sas.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/sas/models/#sas.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/sas/models/#sas.models.SasFile","title":"<code>SasFile</code>","text":"<p> Bases: <code>SithFile</code></p> <p>Proxy model for any file in the SAS.</p> <p>May be used to have logic that should be shared by both Picture and Album.</p>"},{"location":"reference/sas/models/#sas.models.PictureQuerySet","title":"<code>PictureQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/sas/models/#sas.models.PictureQuerySet.viewable_by","title":"<code>viewable_by(user)</code>","text":"<p>Filter the pictures that this user can view.</p> Warning <p>Calling this queryset method may add several additional requests.</p> Source code in <code>sas/models.py</code> <pre><code>def viewable_by(self, user: User) -&gt; Self:\n \"\"\"Filter the pictures that this user can view.\n\n Warning:\n Calling this queryset method may add several additional requests.\n \"\"\"\n if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):\n return self.all()\n if user.was_subscribed:\n return self.filter(is_moderated=True)\n return self.filter(people__user_id=user.id, is_moderated=True)\n</code></pre>"},{"location":"reference/sas/models/#sas.models.SASPictureManager","title":"<code>SASPictureManager</code>","text":"<p> Bases: <code>Manager</code></p>"},{"location":"reference/sas/models/#sas.models.Picture","title":"<code>Picture</code>","text":"<p> Bases: <code>SasFile</code></p>"},{"location":"reference/sas/models/#sas.models.AlbumQuerySet","title":"<code>AlbumQuerySet</code>","text":"<p> Bases: <code>QuerySet</code></p>"},{"location":"reference/sas/models/#sas.models.AlbumQuerySet.viewable_by","title":"<code>viewable_by(user)</code>","text":"<p>Filter the albums that this user can view.</p> Warning <p>Calling this queryset method may add several additional requests.</p> Source code in <code>sas/models.py</code> <pre><code>def viewable_by(self, user: User) -&gt; Self:\n \"\"\"Filter the albums that this user can view.\n\n Warning:\n Calling this queryset method may add several additional requests.\n \"\"\"\n if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):\n return self.all()\n if user.was_subscribed:\n return self.filter(is_moderated=True)\n # known bug : if all children of an album are also albums\n # then this album is excluded, even if one of the sub-albums should be visible.\n # The fs-like navigation is likely to be half-broken for non-subscribers,\n # but that's ok, since non-subscribers are expected to see only the albums\n # containing pictures on which they have been identified (hence, very few).\n # Most, if not all, of their albums will be displayed on the\n # `latest albums` section of the SAS.\n # Moreover, they will still see all of their picture in their profile.\n return self.filter(\n Exists(Picture.objects.filter(parent_id=OuterRef(\"pk\")).viewable_by(user))\n )\n</code></pre>"},{"location":"reference/sas/models/#sas.models.SASAlbumManager","title":"<code>SASAlbumManager</code>","text":"<p> Bases: <code>Manager</code></p>"},{"location":"reference/sas/models/#sas.models.Album","title":"<code>Album</code>","text":"<p> Bases: <code>SasFile</code></p>"},{"location":"reference/sas/models/#sas.models.Album.NAME_MAX_LENGTH","title":"<code>NAME_MAX_LENGTH: int = 50</code> <code>class-attribute</code>","text":"<p>Maximum length of an album's name.</p> <p>SithFile have a maximum length of 256 characters. However, this limit is too high for albums. Names longer than 50 characters are harder to read and harder to display on the SAS page.</p> <p>It is to be noted, though, that this does not add or modify any db behaviour. It's just a constant to be used in views and forms.</p>"},{"location":"reference/sas/models/#sas.models.PeoplePictureRelation","title":"<code>PeoplePictureRelation</code>","text":"<p> Bases: <code>Model</code></p> <p>The PeoplePictureRelation class makes the connection between User and Picture.</p>"},{"location":"reference/sas/models/#sas.models.PictureModerationRequest","title":"<code>PictureModerationRequest</code>","text":"<p> Bases: <code>Model</code></p> <p>A request to remove a Picture from the SAS.</p>"},{"location":"reference/sas/models/#sas.models.exif_auto_rotate","title":"<code>exif_auto_rotate(image)</code>","text":"Source code in <code>core/utils.py</code> <pre><code>def exif_auto_rotate(image):\n for orientation in ExifTags.TAGS:\n if ExifTags.TAGS[orientation] == \"Orientation\":\n break\n exif = dict(image._getexif().items())\n\n if exif[orientation] == 3:\n image = image.rotate(180, expand=True)\n elif exif[orientation] == 6:\n image = image.rotate(270, expand=True)\n elif exif[orientation] == 8:\n image = image.rotate(90, expand=True)\n\n return image\n</code></pre>"},{"location":"reference/sas/models/#sas.models.resize_image","title":"<code>resize_image(im, edge, img_format, *, optimize=True)</code>","text":"<p>Resize an image to fit the given edge length and format.</p> <p>Parameters:</p> Name Type Description Default <code>im</code> <code>Image</code> <p>the image to resize</p> required <code>edge</code> <code>int</code> <p>the length that the greater side of the resized image should have</p> required <code>img_format</code> <code>str</code> <p>the target format of the image (\"JPEG\", \"PNG\", \"WEBP\"...)</p> required <code>optimize</code> <code>bool</code> <p>Should the resized image be optimized ?</p> <code>True</code> Source code in <code>core/utils.py</code> <pre><code>def resize_image(\n im: Image, edge: int, img_format: str, *, optimize: bool = True\n) -&gt; ContentFile:\n \"\"\"Resize an image to fit the given edge length and format.\n\n Args:\n im: the image to resize\n edge: the length that the greater side of the resized image should have\n img_format: the target format of the image (\"JPEG\", \"PNG\", \"WEBP\"...)\n optimize: Should the resized image be optimized ?\n \"\"\"\n (w, h) = im.size\n ratio = edge / max(w, h)\n (width, height) = int(w * ratio), int(h * ratio)\n return resize_image_explicit(im, (width, height), img_format, optimize=optimize)\n</code></pre>"},{"location":"reference/sas/models/#sas.models.sas_notification_callback","title":"<code>sas_notification_callback(notif)</code>","text":"Source code in <code>sas/models.py</code> <pre><code>def sas_notification_callback(notif):\n count = Picture.objects.filter(is_moderated=False).count()\n if count:\n notif.viewed = False\n else:\n notif.viewed = True\n notif.param = \"%s\" % count\n notif.date = timezone.now()\n</code></pre>"},{"location":"reference/sas/schemas/","title":"Schemas","text":""},{"location":"reference/sas/schemas/#sas.schemas.SimpleUserSchema","title":"<code>SimpleUserSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>A schema with the minimum amount of information to represent a user.</p>"},{"location":"reference/sas/schemas/#sas.schemas.UserProfileSchema","title":"<code>UserProfileSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p> <p>The necessary information to show a user profile</p>"},{"location":"reference/sas/schemas/#sas.schemas.Album","title":"<code>Album</code>","text":"<p> Bases: <code>SasFile</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.Album.NAME_MAX_LENGTH","title":"<code>NAME_MAX_LENGTH: int = 50</code> <code>class-attribute</code>","text":"<p>Maximum length of an album's name.</p> <p>SithFile have a maximum length of 256 characters. However, this limit is too high for albums. Names longer than 50 characters are harder to read and harder to display on the SAS page.</p> <p>It is to be noted, though, that this does not add or modify any db behaviour. It's just a constant to be used in views and forms.</p>"},{"location":"reference/sas/schemas/#sas.schemas.Picture","title":"<code>Picture</code>","text":"<p> Bases: <code>SasFile</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.PictureModerationRequest","title":"<code>PictureModerationRequest</code>","text":"<p> Bases: <code>Model</code></p> <p>A request to remove a Picture from the SAS.</p>"},{"location":"reference/sas/schemas/#sas.schemas.AlbumSchema","title":"<code>AlbumSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.PictureFilterSchema","title":"<code>PictureFilterSchema</code>","text":"<p> Bases: <code>FilterSchema</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.PictureSchema","title":"<code>PictureSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.PictureRelationCreationSchema","title":"<code>PictureRelationCreationSchema</code>","text":"<p> Bases: <code>Schema</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.IdentifiedUserSchema","title":"<code>IdentifiedUserSchema</code>","text":"<p> Bases: <code>Schema</code></p>"},{"location":"reference/sas/schemas/#sas.schemas.ModerationRequestSchema","title":"<code>ModerationRequestSchema</code>","text":"<p> Bases: <code>ModelSchema</code></p>"},{"location":"reference/sas/views/","title":"Views","text":""},{"location":"reference/sas/views/#sas.views.SithFile","title":"<code>SithFile</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/sas/views/#sas.views.SithFile.clean","title":"<code>clean()</code>","text":"<p>Cleans up the file.</p> Source code in <code>core/models.py</code> <pre><code>def clean(self):\n \"\"\"Cleans up the file.\"\"\"\n super().clean()\n if \"/\" in self.name:\n raise ValidationError(_(\"Character '/' not authorized in name\"))\n if self == self.parent:\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self == self.parent or (\n self.parent is not None and self in self.get_parent_list()\n ):\n raise ValidationError(_(\"Loop in folder tree\"), code=\"loop\")\n if self.parent and self.parent.is_file:\n raise ValidationError(\n _(\"You can not make a file be a children of a non folder file\")\n )\n if (\n self.parent is None\n and SithFile.objects.exclude(id=self.id)\n .filter(parent=None, name=self.name)\n .exists()\n ) or (\n self.parent\n and self.parent.children.exclude(id=self.id).filter(name=self.name).exists()\n ):\n raise ValidationError(_(\"Duplicate file\"), code=\"duplicate\")\n if self.is_folder:\n if self.file:\n try:\n import imghdr\n\n if imghdr.what(None, self.file.read()) not in [\n \"gif\",\n \"png\",\n \"jpeg\",\n ]:\n self.file.delete()\n self.file = None\n except: # noqa E722 I don't know the exception that can be raised\n self.file = None\n self.mime_type = \"inode/directory\"\n if self.is_file and (self.file is None or self.file == \"\"):\n raise ValidationError(_(\"You must provide a file\"))\n</code></pre>"},{"location":"reference/sas/views/#sas.views.SithFile.apply_rights_recursively","title":"<code>apply_rights_recursively(*, only_folders=False)</code>","text":"<p>Apply the rights of this file to all children recursively.</p> <p>Parameters:</p> Name Type Description Default <code>only_folders</code> <code>bool</code> <p>If True, only apply the rights to SithFiles that are folders.</p> <code>False</code> Source code in <code>core/models.py</code> <pre><code>def apply_rights_recursively(self, *, only_folders: bool = False) -&gt; None:\n \"\"\"Apply the rights of this file to all children recursively.\n\n Args:\n only_folders: If True, only apply the rights to SithFiles that are folders.\n \"\"\"\n file_ids = []\n explored_ids = [self.id]\n while len(explored_ids) &gt; 0: # find all children recursively\n file_ids.extend(explored_ids)\n next_level = SithFile.objects.filter(parent_id__in=explored_ids)\n if only_folders:\n next_level = next_level.filter(is_folder=True)\n explored_ids = list(next_level.values_list(\"id\", flat=True))\n for through in (SithFile.view_groups.through, SithFile.edit_groups.through):\n # force evaluation. Without this, the iterator yields nothing\n groups = list(\n through.objects.filter(sithfile_id=self.id).values_list(\n \"group_id\", flat=True\n )\n )\n # delete previous rights\n through.objects.filter(sithfile_id__in=file_ids).delete()\n through.objects.bulk_create( # create new rights\n [through(sithfile_id=f, group_id=g) for f in file_ids for g in groups]\n )\n</code></pre>"},{"location":"reference/sas/views/#sas.views.SithFile.copy_rights","title":"<code>copy_rights()</code>","text":"<p>Copy, if possible, the rights of the parent folder.</p> Source code in <code>core/models.py</code> <pre><code>def copy_rights(self):\n \"\"\"Copy, if possible, the rights of the parent folder.\"\"\"\n if self.parent is not None:\n self.edit_groups.set(self.parent.edit_groups.all())\n self.view_groups.set(self.parent.view_groups.all())\n</code></pre>"},{"location":"reference/sas/views/#sas.views.SithFile.move_to","title":"<code>move_to(parent)</code>","text":"<p>Move a file to a new parent. <code>parent</code> must be a SithFile with the <code>is_folder=True</code> property. Otherwise, this function doesn't change anything. This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify SithFiles recursively, so it stays efficient even with top-level folders.</p> Source code in <code>core/models.py</code> <pre><code>def move_to(self, parent):\n \"\"\"Move a file to a new parent.\n `parent` must be a SithFile with the `is_folder=True` property. Otherwise, this function doesn't change\n anything.\n This is done only at the DB level, so that it's very fast for the user. Indeed, this function doesn't modify\n SithFiles recursively, so it stays efficient even with top-level folders.\n \"\"\"\n if not parent.is_folder:\n return\n self.parent = parent\n self.clean()\n self.save()\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/sas/views/#sas.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/sas/views/#sas.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/sas/views/#sas.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/sas/views/#sas.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/sas/views/#sas.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/sas/views/#sas.views.FileView","title":"<code>FileView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code>, <code>FormMixin</code></p> <p>Handle the upload of new files into a folder.</p>"},{"location":"reference/sas/views/#sas.views.FileView.handle_clipboard","title":"<code>handle_clipboard(request, obj)</code> <code>staticmethod</code>","text":"<p>Handle the clipboard in the view.</p> <p>This method can fail, since it does not catch the exceptions coming from below, allowing proper handling in the calling view. Use this method like this:</p> <pre><code>FileView.handle_clipboard(request, self.object)\n</code></pre> <p><code>request</code> is usually the self.request obj in your view <code>obj</code> is the SithFile object you want to put in the clipboard, or where you want to paste the clipboard</p> Source code in <code>core/views/files.py</code> <pre><code>@staticmethod\ndef handle_clipboard(request, obj):\n \"\"\"Handle the clipboard in the view.\n\n This method can fail, since it does not catch the exceptions coming from\n below, allowing proper handling in the calling view.\n Use this method like this:\n\n FileView.handle_clipboard(request, self.object)\n\n `request` is usually the self.request obj in your view\n `obj` is the SithFile object you want to put in the clipboard, or\n where you want to paste the clipboard\n \"\"\"\n if \"delete\" in request.POST:\n for f_id in request.POST.getlist(\"file_list\"):\n file = SithFile.objects.filter(id=f_id).first()\n if file:\n file.delete()\n if \"clear\" in request.POST:\n request.session[\"clipboard\"] = []\n if \"cut\" in request.POST:\n for f_id_str in request.POST.getlist(\"file_list\"):\n f_id = int(f_id_str)\n if (\n f_id in [c.id for c in obj.children.all()]\n and f_id not in request.session[\"clipboard\"]\n ):\n request.session[\"clipboard\"].append(f_id)\n if \"paste\" in request.POST:\n for f_id in request.session[\"clipboard\"]:\n file = SithFile.objects.filter(id=f_id).first()\n if file:\n file.move_to(obj)\n request.session[\"clipboard\"] = []\n request.session.modified = True\n</code></pre>"},{"location":"reference/sas/views/#sas.views.AlbumEditForm","title":"<code>AlbumEditForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/sas/views/#sas.views.PictureEditForm","title":"<code>PictureEditForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/sas/views/#sas.views.PictureModerationRequestForm","title":"<code>PictureModerationRequestForm(*args, user, picture, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> <p>Form to create a PictureModerationRequest.</p> <p>The form only manages the reason field, because the author and the picture are set in the view.</p> Source code in <code>sas/forms.py</code> <pre><code>def __init__(self, *args, user: User, picture: Picture, **kwargs):\n super().__init__(*args, **kwargs)\n self.user = user\n self.picture = picture\n</code></pre>"},{"location":"reference/sas/views/#sas.views.SASForm","title":"<code>SASForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/sas/views/#sas.views.Album","title":"<code>Album</code>","text":"<p> Bases: <code>SasFile</code></p>"},{"location":"reference/sas/views/#sas.views.Album.NAME_MAX_LENGTH","title":"<code>NAME_MAX_LENGTH: int = 50</code> <code>class-attribute</code>","text":"<p>Maximum length of an album's name.</p> <p>SithFile have a maximum length of 256 characters. However, this limit is too high for albums. Names longer than 50 characters are harder to read and harder to display on the SAS page.</p> <p>It is to be noted, though, that this does not add or modify any db behaviour. It's just a constant to be used in views and forms.</p>"},{"location":"reference/sas/views/#sas.views.Picture","title":"<code>Picture</code>","text":"<p> Bases: <code>SasFile</code></p>"},{"location":"reference/sas/views/#sas.views.SASMainView","title":"<code>SASMainView</code>","text":"<p> Bases: <code>FormView</code></p>"},{"location":"reference/sas/views/#sas.views.PictureView","title":"<code>PictureView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code></p>"},{"location":"reference/sas/views/#sas.views.AlbumUploadView","title":"<code>AlbumUploadView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code>, <code>FormMixin</code></p>"},{"location":"reference/sas/views/#sas.views.AlbumView","title":"<code>AlbumView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code>, <code>FormMixin</code></p>"},{"location":"reference/sas/views/#sas.views.ModerationView","title":"<code>ModerationView</code>","text":"<p> Bases: <code>TemplateView</code></p>"},{"location":"reference/sas/views/#sas.views.PictureEditView","title":"<code>PictureEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/sas/views/#sas.views.PictureAskRemovalView","title":"<code>PictureAskRemovalView</code>","text":"<p> Bases: <code>CanViewMixin</code>, <code>DetailView</code>, <code>FormView</code></p> <p>View to allow users to ask pictures to be removed.</p>"},{"location":"reference/sas/views/#sas.views.PictureAskRemovalView.get_form_kwargs","title":"<code>get_form_kwargs()</code>","text":"<p>Add the user and picture to the form kwargs.</p> <p>Those are required to create the PictureModerationRequest, and aren't part of the form itself (picture is a path parameter, and user is the request user).</p> Source code in <code>sas/views.py</code> <pre><code>def get_form_kwargs(self) -&gt; dict[str, Any]:\n \"\"\"Add the user and picture to the form kwargs.\n\n Those are required to create the PictureModerationRequest,\n and aren't part of the form itself\n (picture is a path parameter, and user is the request user).\n \"\"\"\n return super().get_form_kwargs() | {\n \"user\": self.request.user,\n \"picture\": self.object,\n }\n</code></pre>"},{"location":"reference/sas/views/#sas.views.PictureAskRemovalView.get_success_url","title":"<code>get_success_url()</code>","text":"<p>Return the URL to the album containing the picture.</p> Source code in <code>sas/views.py</code> <pre><code>def get_success_url(self) -&gt; str:\n \"\"\"Return the URL to the album containing the picture.\"\"\"\n album = Album.objects.filter(pk=self.object.parent_id).first()\n if not album:\n return reverse(\"sas:main\")\n return album.get_absolute_url()\n</code></pre>"},{"location":"reference/sas/views/#sas.views.AlbumEditView","title":"<code>AlbumEditView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/sas/views/#sas.views.send_file","title":"<code>send_file(request, file_id, file_class=SithFile, file_attr='file')</code>","text":"<p>Send a protected file, if the user can see it.</p> <p>In prod, the server won't handle the download itself, but set the appropriate headers in the response to make the reverse-proxy deal with it. In debug mode, the server will directly send the file.</p> Source code in <code>core/views/files.py</code> <pre><code>def send_file(\n request: HttpRequest,\n file_id: int,\n file_class: type[SithFile] = SithFile,\n file_attr: str = \"file\",\n) -&gt; HttpResponse:\n \"\"\"Send a protected file, if the user can see it.\n\n In prod, the server won't handle the download itself,\n but set the appropriate headers in the response to make the reverse-proxy\n deal with it.\n In debug mode, the server will directly send the file.\n \"\"\"\n f = get_object_or_404(file_class, id=file_id)\n if not can_view(f, request.user) and not is_logged_in_counter(request):\n raise PermissionDenied\n name = getattr(f, file_attr).name\n\n response = HttpResponse(\n headers={\"Content-Disposition\": f'inline; filename=\"{quote(name)}\"'}\n )\n if not settings.DEBUG:\n # When receiving a response with the Accel-Redirect header,\n # the reverse proxy will automatically handle the file sending.\n # This is really hard to test (thus isn't tested)\n # so please do not mess with this.\n response[\"Content-Type\"] = \"\" # automatically set by nginx\n response[\"X-Accel-Redirect\"] = quote(urljoin(settings.MEDIA_URL, name))\n return response\n\n filepath = settings.MEDIA_ROOT / name\n # check if file exists on disk\n if not filepath.exists():\n raise Http404\n with open(filepath, \"rb\") as filename:\n response.content = FileWrapper(filename)\n response[\"Content-Type\"] = mimetypes.guess_type(filepath)[0]\n response[\"Last-Modified\"] = http_date(f.date.timestamp())\n response[\"Content-Length\"] = filepath.stat().st_size\n return response\n</code></pre>"},{"location":"reference/sas/views/#sas.views.send_album","title":"<code>send_album(request, album_id)</code>","text":"Source code in <code>sas/views.py</code> <pre><code>def send_album(request, album_id):\n return send_file(request, album_id, Album)\n</code></pre>"},{"location":"reference/sas/views/#sas.views.send_pict","title":"<code>send_pict(request, picture_id)</code>","text":"Source code in <code>sas/views.py</code> <pre><code>def send_pict(request, picture_id):\n return send_file(request, picture_id, Picture)\n</code></pre>"},{"location":"reference/sas/views/#sas.views.send_compressed","title":"<code>send_compressed(request, picture_id)</code>","text":"Source code in <code>sas/views.py</code> <pre><code>def send_compressed(request, picture_id):\n return send_file(request, picture_id, Picture, \"compressed\")\n</code></pre>"},{"location":"reference/sas/views/#sas.views.send_thumb","title":"<code>send_thumb(request, picture_id)</code>","text":"Source code in <code>sas/views.py</code> <pre><code>def send_thumb(request, picture_id):\n return send_file(request, picture_id, Picture, \"thumbnail\")\n</code></pre>"},{"location":"reference/staticfiles/apps/","title":"Apps","text":"<p> Bases: <code>StaticFilesConfig</code></p> <p>Application in charge of processing statics files. It replaces the original django staticfiles It integrates scss files and webpack. It makes sure that statics are properly collected and that they are automatically when using the development server.</p>"},{"location":"reference/staticfiles/apps/#staticfiles.apps.StaticFilesConfig.ignore_patterns","title":"<code>ignore_patterns = IGNORE_PATTERNS</code> <code>class-attribute</code> <code>instance-attribute</code>","text":""},{"location":"reference/staticfiles/apps/#staticfiles.apps.StaticFilesConfig.name","title":"<code>name = 'staticfiles'</code> <code>class-attribute</code> <code>instance-attribute</code>","text":""},{"location":"reference/staticfiles/finders/","title":"Finders","text":""},{"location":"reference/staticfiles/finders/#staticfiles.finders.GENERATED_ROOT","title":"<code>GENERATED_ROOT = Path(__file__).parent.resolve() / 'generated'</code> <code>module-attribute</code>","text":""},{"location":"reference/staticfiles/finders/#staticfiles.finders.IGNORE_PATTERNS_WEBPACK","title":"<code>IGNORE_PATTERNS_WEBPACK = ['webpack/*']</code> <code>module-attribute</code>","text":""},{"location":"reference/staticfiles/finders/#staticfiles.finders.GeneratedFilesFinder","title":"<code>GeneratedFilesFinder(app_names=None, *args, **kwargs)</code>","text":"<p> Bases: <code>FileSystemFinder</code></p> <p>Find generated and regular static files</p> Source code in <code>staticfiles/finders.py</code> <pre><code>def __init__(self, app_names=None, *args, **kwargs):\n super().__init__(*args, **kwargs)\n\n # Add GENERATED_ROOT after adding everything in settings.STATICFILES_DIRS\n self.locations.append((\"\", GENERATED_ROOT))\n generated_storage = FileSystemStorage(location=GENERATED_ROOT)\n generated_storage.prefix = \"\"\n self.storages[GENERATED_ROOT] = generated_storage\n</code></pre>"},{"location":"reference/staticfiles/processors/","title":"Processors","text":""},{"location":"reference/staticfiles/processors/#staticfiles.processors.GENERATED_ROOT","title":"<code>GENERATED_ROOT = Path(__file__).parent.resolve() / 'generated'</code> <code>module-attribute</code>","text":""},{"location":"reference/staticfiles/processors/#staticfiles.processors.Webpack","title":"<code>Webpack</code>","text":""},{"location":"reference/staticfiles/processors/#staticfiles.processors.Webpack.compile","title":"<code>compile()</code> <code>staticmethod</code>","text":"<p>Bundle js files with webpack for production.</p> Source code in <code>staticfiles/processors.py</code> <pre><code>@staticmethod\ndef compile():\n \"\"\"Bundle js files with webpack for production.\"\"\"\n process = subprocess.Popen([\"npm\", \"run\", \"compile\"])\n process.wait()\n if process.returncode:\n raise RuntimeError(f\"Webpack failed with returncode {process.returncode}\")\n</code></pre>"},{"location":"reference/staticfiles/processors/#staticfiles.processors.Webpack.runserver","title":"<code>runserver()</code> <code>staticmethod</code>","text":"<p>Bundle js files automatically in background when called in debug mode.</p> Source code in <code>staticfiles/processors.py</code> <pre><code>@staticmethod\ndef runserver() -&gt; subprocess.Popen:\n \"\"\"Bundle js files automatically in background when called in debug mode.\"\"\"\n logging.getLogger(\"django\").info(\"Running webpack server\")\n return subprocess.Popen([\"npm\", \"run\", \"serve\"])\n</code></pre>"},{"location":"reference/staticfiles/processors/#staticfiles.processors.Scss","title":"<code>Scss</code>","text":""},{"location":"reference/staticfiles/processors/#staticfiles.processors.Scss.compile","title":"<code>compile(files)</code> <code>staticmethod</code>","text":"<p>Compile scss files to css files.</p> Source code in <code>staticfiles/processors.py</code> <pre><code>@staticmethod\ndef compile(files: CompileArg | Iterable[CompileArg]):\n \"\"\"Compile scss files to css files.\"\"\"\n # Generate files inside the generated folder\n # .css files respects the hierarchy in the static folder it was found\n # This converts arg.absolute -&gt; generated/{arg.relative}.scss\n # Example:\n # app/static/foo.scss -&gt; generated/foo.css\n # app/static/bar/foo.scss -&gt; generated/bar/foo.css\n # custom/location/bar/foo.scss -&gt; generated/bar/foo.css\n if isinstance(files, Scss.CompileArg):\n files = [files]\n\n base_args = {\"output_style\": \"compressed\", \"precision\": settings.SASS_PRECISION}\n\n compiled_files = {\n file.relative.with_suffix(\".css\"): sass.compile(\n filename=str(file.absolute), **base_args\n )\n for file in files\n }\n for file, content in compiled_files.items():\n dest = GENERATED_ROOT / file\n dest.parent.mkdir(exist_ok=True, parents=True)\n dest.write_text(content)\n</code></pre>"},{"location":"reference/staticfiles/processors/#staticfiles.processors.JS","title":"<code>JS</code>","text":""},{"location":"reference/staticfiles/processors/#staticfiles.processors.OpenApi","title":"<code>OpenApi</code>","text":""},{"location":"reference/staticfiles/processors/#staticfiles.processors.OpenApi.compile","title":"<code>compile()</code> <code>classmethod</code>","text":"<p>Compile a TS client for the sith API. Only generates it if it changed.</p> Source code in <code>staticfiles/processors.py</code> <pre><code>@classmethod\ndef compile(cls):\n \"\"\"Compile a TS client for the sith API. Only generates it if it changed.\"\"\"\n logging.getLogger(\"django\").info(\"Compiling open api typescript client\")\n out = cls.OPENAPI_DIR / \"schema.json\"\n cls.OPENAPI_DIR.mkdir(parents=True, exist_ok=True)\n\n old_hash = \"\"\n if out.exists():\n with open(out, \"rb\") as f:\n old_hash = sha1(f.read()).hexdigest()\n\n schema = api.get_openapi_schema()\n # Remove hash from operationIds\n # This is done for cache invalidation but this is too aggressive\n for path in schema[\"paths\"].values():\n for action, desc in path.items():\n path[action][\"operationId\"] = \"_\".join(\n desc[\"operationId\"].split(\"_\")[:-1]\n )\n schema = str(schema)\n\n if old_hash == sha1(schema.encode(\"utf-8\")).hexdigest():\n logging.getLogger(\"django\").info(\"\u2728 Api did not change, nothing to do \u2728\")\n return\n\n with open(out, \"w\") as f:\n _ = f.write(schema)\n\n subprocess.run([\"npx\", \"openapi-ts\"], check=True)\n</code></pre>"},{"location":"reference/staticfiles/storage/","title":"Storage","text":""},{"location":"reference/staticfiles/storage/#staticfiles.storage.JS","title":"<code>JS</code>","text":""},{"location":"reference/staticfiles/storage/#staticfiles.storage.Scss","title":"<code>Scss</code>","text":""},{"location":"reference/staticfiles/storage/#staticfiles.storage.Scss.compile","title":"<code>compile(files)</code> <code>staticmethod</code>","text":"<p>Compile scss files to css files.</p> Source code in <code>staticfiles/processors.py</code> <pre><code>@staticmethod\ndef compile(files: CompileArg | Iterable[CompileArg]):\n \"\"\"Compile scss files to css files.\"\"\"\n # Generate files inside the generated folder\n # .css files respects the hierarchy in the static folder it was found\n # This converts arg.absolute -&gt; generated/{arg.relative}.scss\n # Example:\n # app/static/foo.scss -&gt; generated/foo.css\n # app/static/bar/foo.scss -&gt; generated/bar/foo.css\n # custom/location/bar/foo.scss -&gt; generated/bar/foo.css\n if isinstance(files, Scss.CompileArg):\n files = [files]\n\n base_args = {\"output_style\": \"compressed\", \"precision\": settings.SASS_PRECISION}\n\n compiled_files = {\n file.relative.with_suffix(\".css\"): sass.compile(\n filename=str(file.absolute), **base_args\n )\n for file in files\n }\n for file, content in compiled_files.items():\n dest = GENERATED_ROOT / file\n dest.parent.mkdir(exist_ok=True, parents=True)\n dest.write_text(content)\n</code></pre>"},{"location":"reference/staticfiles/storage/#staticfiles.storage.ManifestPostProcessingStorage","title":"<code>ManifestPostProcessingStorage</code>","text":"<p> Bases: <code>ManifestStaticFilesStorage</code></p>"},{"location":"reference/staticfiles/storage/#staticfiles.storage.ManifestPostProcessingStorage.url","title":"<code>url(name, *, force=False)</code>","text":"<p>Get the URL for a file, convert .scss calls to .css ones and .ts to .js</p> Source code in <code>staticfiles/storage.py</code> <pre><code>def url(self, name: str, *, force: bool = False) -&gt; str:\n \"\"\"Get the URL for a file, convert .scss calls to .css ones and .ts to .js\"\"\"\n # This name swap has to be done here\n # Otherwise, the manifest isn't aware of the file and can't work properly\n path = Path(name)\n if path.suffix == \".scss\":\n # Compile scss files automatically in debug mode\n if settings.DEBUG:\n Scss.compile(\n [\n Scss.CompileArg(absolute=Path(p), relative=Path(name))\n for p in find(name, all=True)\n ]\n )\n name = str(path.with_suffix(\".css\"))\n\n elif path.suffix == \".ts\":\n name = str(path.with_suffix(\".js\"))\n\n return super().url(name, force=force)\n</code></pre>"},{"location":"reference/subscription/models/","title":"Models","text":""},{"location":"reference/subscription/models/#subscription.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/subscription/models/#subscription.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/subscription/models/#subscription.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.Subscription","title":"<code>Subscription</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/subscription/models/#subscription.models.Subscription.compute_start","title":"<code>compute_start(d=None, duration=1, user=None)</code> <code>staticmethod</code>","text":"<p>Computes the start date of the subscription.</p> <p>The computation is done with respect to the given date (default is today) and the start date given in settings.SITH_SEMESTER_START_AUTUMN. It takes the nearest past start date. Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15) Today -&gt; Start date 2015-03-17 -&gt; 2015-02-15 2015-01-11 -&gt; 2014-08-15.</p> Source code in <code>subscription/models.py</code> <pre><code>@staticmethod\ndef compute_start(\n d: date | None = None, duration: int = 1, user: User | None = None\n) -&gt; date:\n \"\"\"Computes the start date of the subscription.\n\n The computation is done with respect to the given date (default is today)\n and the start date given in settings.SITH_SEMESTER_START_AUTUMN.\n It takes the nearest past start date.\n Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)\n Today -&gt; Start date\n 2015-03-17 -&gt; 2015-02-15\n 2015-01-11 -&gt; 2014-08-15.\n \"\"\"\n if not d:\n d = date.today()\n if user is not None and user.subscriptions.exists():\n last = user.subscriptions.last()\n if last.is_valid_now():\n d = last.subscription_end\n if duration &lt;= 2: # Sliding subscriptions for 1 or 2 semesters\n return d\n return get_start_of_semester(d)\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.Subscription.compute_end","title":"<code>compute_end(duration, start=None, user=None)</code> <code>staticmethod</code>","text":"<p>Compute the end date of the subscription.</p> <p>Parameters:</p> Name Type Description Default <code>duration</code> <code>int</code> <p>the duration of the subscription, in semester (for example, 2 =&gt; 2 semesters =&gt; 1 year)</p> required <code>start</code> <code>date | None</code> <p>The start date of the subscription</p> <code>None</code> <code>user</code> <code>User | None</code> <p>the user which is (or will be) subscribed</p> <code>None</code> Exemples <p>Start - Duration -&gt; End date 2015-09-18 - 1 -&gt; 2016-03-18 2015-09-18 - 2 -&gt; 2016-09-18 2015-09-18 - 3 -&gt; 2017-03-18 2015-09-18 - 4 -&gt; 2017-09-18.</p> Source code in <code>subscription/models.py</code> <pre><code>@staticmethod\ndef compute_end(\n duration: int, start: date | None = None, user: User | None = None\n) -&gt; date:\n \"\"\"Compute the end date of the subscription.\n\n Args:\n duration:\n the duration of the subscription, in semester\n (for example, 2 =&gt; 2 semesters =&gt; 1 year)\n start: The start date of the subscription\n user: the user which is (or will be) subscribed\n\n Exemples:\n Start - Duration -&gt; End date\n 2015-09-18 - 1 -&gt; 2016-03-18\n 2015-09-18 - 2 -&gt; 2016-09-18\n 2015-09-18 - 3 -&gt; 2017-03-18\n 2015-09-18 - 4 -&gt; 2017-09-18.\n \"\"\"\n if start is None:\n start = Subscription.compute_start(duration=duration, user=user)\n\n return start + relativedelta(\n months=round(6 * duration),\n days=math.ceil((6 * duration - round(6 * duration)) * 30),\n )\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.get_start_of_semester","title":"<code>get_start_of_semester(today=None)</code>","text":"<p>Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester.</p> <p>The current semester is computed as follows:</p> <ul> <li>If the date is between 15/08 and 31/12 =&gt; Autumn semester.</li> <li>If the date is between 01/01 and 15/02 =&gt; Autumn semester of the previous year.</li> <li>If the date is between 15/02 and 15/08 =&gt; Spring semester</li> </ul> <p>Parameters:</p> Name Type Description Default <code>today</code> <code>Optional[date]</code> <p>the date to use to compute the semester. If None, use today's date.</p> <code>None</code> <p>Returns:</p> Type Description <code>date</code> <p>the date of the start of the semester</p> Source code in <code>core/utils.py</code> <pre><code>def get_start_of_semester(today: Optional[date] = None) -&gt; date:\n \"\"\"Return the date of the start of the semester of the given date.\n If no date is given, return the start date of the current semester.\n\n The current semester is computed as follows:\n\n - If the date is between 15/08 and 31/12 =&gt; Autumn semester.\n - If the date is between 01/01 and 15/02 =&gt; Autumn semester of the previous year.\n - If the date is between 15/02 and 15/08 =&gt; Spring semester\n\n Args:\n today: the date to use to compute the semester. If None, use today's date.\n\n Returns:\n the date of the start of the semester\n \"\"\"\n if today is None:\n today = localdate()\n\n autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)\n spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)\n\n if today &gt;= autumn: # between 15/08 (included) and 31/12 -&gt; autumn semester\n return autumn\n if today &gt;= spring: # between 15/02 (included) and 15/08 -&gt; spring semester\n return spring\n # between 01/01 and 15/02 -&gt; autumn semester of the previous year\n return autumn.replace(year=autumn.year - 1)\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.validate_type","title":"<code>validate_type(value)</code>","text":"Source code in <code>subscription/models.py</code> <pre><code>def validate_type(value):\n if value not in settings.SITH_SUBSCRIPTIONS:\n raise ValidationError(_(\"Bad subscription type\"))\n</code></pre>"},{"location":"reference/subscription/models/#subscription.models.validate_payment","title":"<code>validate_payment(value)</code>","text":"Source code in <code>subscription/models.py</code> <pre><code>def validate_payment(value):\n if value not in settings.SITH_SUBSCRIPTION_PAYMENT_METHOD:\n raise ValidationError(_(\"Bad payment method\"))\n</code></pre>"},{"location":"reference/subscription/views/","title":"Views","text":""},{"location":"reference/subscription/views/#subscription.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/subscription/views/#subscription.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/subscription/views/#subscription.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.Subscription","title":"<code>Subscription</code>","text":"<p> Bases: <code>Model</code></p>"},{"location":"reference/subscription/views/#subscription.views.Subscription.compute_start","title":"<code>compute_start(d=None, duration=1, user=None)</code> <code>staticmethod</code>","text":"<p>Computes the start date of the subscription.</p> <p>The computation is done with respect to the given date (default is today) and the start date given in settings.SITH_SEMESTER_START_AUTUMN. It takes the nearest past start date. Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15) Today -&gt; Start date 2015-03-17 -&gt; 2015-02-15 2015-01-11 -&gt; 2014-08-15.</p> Source code in <code>subscription/models.py</code> <pre><code>@staticmethod\ndef compute_start(\n d: date | None = None, duration: int = 1, user: User | None = None\n) -&gt; date:\n \"\"\"Computes the start date of the subscription.\n\n The computation is done with respect to the given date (default is today)\n and the start date given in settings.SITH_SEMESTER_START_AUTUMN.\n It takes the nearest past start date.\n Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)\n Today -&gt; Start date\n 2015-03-17 -&gt; 2015-02-15\n 2015-01-11 -&gt; 2014-08-15.\n \"\"\"\n if not d:\n d = date.today()\n if user is not None and user.subscriptions.exists():\n last = user.subscriptions.last()\n if last.is_valid_now():\n d = last.subscription_end\n if duration &lt;= 2: # Sliding subscriptions for 1 or 2 semesters\n return d\n return get_start_of_semester(d)\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.Subscription.compute_end","title":"<code>compute_end(duration, start=None, user=None)</code> <code>staticmethod</code>","text":"<p>Compute the end date of the subscription.</p> <p>Parameters:</p> Name Type Description Default <code>duration</code> <code>int</code> <p>the duration of the subscription, in semester (for example, 2 =&gt; 2 semesters =&gt; 1 year)</p> required <code>start</code> <code>date | None</code> <p>The start date of the subscription</p> <code>None</code> <code>user</code> <code>User | None</code> <p>the user which is (or will be) subscribed</p> <code>None</code> Exemples <p>Start - Duration -&gt; End date 2015-09-18 - 1 -&gt; 2016-03-18 2015-09-18 - 2 -&gt; 2016-09-18 2015-09-18 - 3 -&gt; 2017-03-18 2015-09-18 - 4 -&gt; 2017-09-18.</p> Source code in <code>subscription/models.py</code> <pre><code>@staticmethod\ndef compute_end(\n duration: int, start: date | None = None, user: User | None = None\n) -&gt; date:\n \"\"\"Compute the end date of the subscription.\n\n Args:\n duration:\n the duration of the subscription, in semester\n (for example, 2 =&gt; 2 semesters =&gt; 1 year)\n start: The start date of the subscription\n user: the user which is (or will be) subscribed\n\n Exemples:\n Start - Duration -&gt; End date\n 2015-09-18 - 1 -&gt; 2016-03-18\n 2015-09-18 - 2 -&gt; 2016-09-18\n 2015-09-18 - 3 -&gt; 2017-03-18\n 2015-09-18 - 4 -&gt; 2017-09-18.\n \"\"\"\n if start is None:\n start = Subscription.compute_start(duration=duration, user=user)\n\n return start + relativedelta(\n months=round(6 * duration),\n days=math.ceil((6 * duration - round(6 * duration)) * 30),\n )\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.SelectionDateForm","title":"<code>SelectionDateForm(*args, **kwargs)</code>","text":"<p> Bases: <code>Form</code></p> Source code in <code>subscription/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"start_date\"] = forms.DateTimeField(\n label=_(\"Start date\"), widget=SelectDateTime, required=True\n )\n self.fields[\"end_date\"] = forms.DateTimeField(\n label=_(\"End date\"), widget=SelectDateTime, required=True\n )\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.SubscriptionForm","title":"<code>SubscriptionForm(*args, **kwargs)</code>","text":"<p> Bases: <code>ModelForm</code></p> Source code in <code>subscription/views.py</code> <pre><code>def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.fields[\"member\"].required = False\n self.fields |= forms.fields_for_model(\n User,\n fields=[\"first_name\", \"last_name\", \"email\", \"date_of_birth\"],\n widgets={\"date_of_birth\": SelectDate},\n )\n</code></pre>"},{"location":"reference/subscription/views/#subscription.views.NewSubscription","title":"<code>NewSubscription</code>","text":"<p> Bases: <code>CreateView</code></p>"},{"location":"reference/subscription/views/#subscription.views.SubscriptionsStatsView","title":"<code>SubscriptionsStatsView</code>","text":"<p> Bases: <code>FormView</code></p>"},{"location":"reference/trombi/models/","title":"Models","text":""},{"location":"reference/trombi/models/#trombi.models.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/trombi/models/#trombi.models.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/trombi/models/#trombi.models.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/trombi/models/#trombi.models.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/trombi/models/#trombi.models.TrombiManager","title":"<code>TrombiManager</code>","text":"<p> Bases: <code>Manager</code></p>"},{"location":"reference/trombi/models/#trombi.models.AvailableTrombiManager","title":"<code>AvailableTrombiManager</code>","text":"<p> Bases: <code>Manager</code></p>"},{"location":"reference/trombi/models/#trombi.models.Trombi","title":"<code>Trombi</code>","text":"<p> Bases: <code>Model</code></p> <p>Main class of the trombi, the Trombi itself.</p> <p>It contains the deadlines for the users, and the link to the club that makes its Trombi.</p>"},{"location":"reference/trombi/models/#trombi.models.TrombiUser","title":"<code>TrombiUser</code>","text":"<p> Bases: <code>Model</code></p> <p>Bound between a <code>User</code> and a <code>Trombi</code>.</p> <p>This class is here to avoid cross-references between the core, club, and trombi modules. It also adds the pictures to the profile without needing all the security like the other SithFiles.</p>"},{"location":"reference/trombi/models/#trombi.models.TrombiComment","title":"<code>TrombiComment</code>","text":"<p> Bases: <code>Model</code></p> <p>A comment given by someone to someone else in the same Trombi instance.</p>"},{"location":"reference/trombi/models/#trombi.models.TrombiClubMembership","title":"<code>TrombiClubMembership</code>","text":"<p> Bases: <code>Model</code></p> <p>A membership in a club.</p>"},{"location":"reference/trombi/models/#trombi.models.get_semester_code","title":"<code>get_semester_code(d=None)</code>","text":"<p>Return the semester code of the given date. If no date is given, return the semester code of the current semester.</p> <p>The semester code is an upper letter (A for autumn, P for spring), followed by the last two digits of the year. For example, the autumn semester of 2018 is \"A18\".</p> <p>Parameters:</p> Name Type Description Default <code>d</code> <code>Optional[date]</code> <p>the date to use to compute the semester. If None, use today's date.</p> <code>None</code> <p>Returns:</p> Type Description <code>str</code> <p>the semester code corresponding to the given date</p> Source code in <code>core/utils.py</code> <pre><code>def get_semester_code(d: Optional[date] = None) -&gt; str:\n \"\"\"Return the semester code of the given date.\n If no date is given, return the semester code of the current semester.\n\n The semester code is an upper letter (A for autumn, P for spring),\n followed by the last two digits of the year.\n For example, the autumn semester of 2018 is \"A18\".\n\n Args:\n d: the date to use to compute the semester. If None, use today's date.\n\n Returns:\n the semester code corresponding to the given date\n \"\"\"\n if d is None:\n d = localdate()\n\n start = get_start_of_semester(d)\n\n if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN:\n return \"A\" + str(start.year)[-2:]\n return \"P\" + str(start.year)[-2:]\n</code></pre>"},{"location":"reference/trombi/views/","title":"Views","text":""},{"location":"reference/trombi/views/#trombi.views.Club","title":"<code>Club</code>","text":"<p> Bases: <code>Model</code></p> <p>The Club class, made as a tree to allow nice tidy organization.</p>"},{"location":"reference/trombi/views/#trombi.views.Club.check_loop","title":"<code>check_loop()</code>","text":"<p>Raise a validation error when a loop is found within the parent list.</p> Source code in <code>club/models.py</code> <pre><code>def check_loop(self):\n \"\"\"Raise a validation error when a loop is found within the parent list.\"\"\"\n objs = []\n cur = self\n while cur.parent is not None:\n if cur in objs:\n raise ValidationError(_(\"You can not make loops in clubs\"))\n objs.append(cur)\n cur = cur.parent\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.Club.is_owned_by","title":"<code>is_owned_by(user)</code>","text":"<p>Method to see if that object can be super edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def is_owned_by(self, user):\n \"\"\"Method to see if that object can be super edited by the given user.\"\"\"\n if user.is_anonymous:\n return False\n return user.is_board_member\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.Club.can_be_edited_by","title":"<code>can_be_edited_by(user)</code>","text":"<p>Method to see if that object can be edited by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_edited_by(self, user):\n \"\"\"Method to see if that object can be edited by the given user.\"\"\"\n return self.has_rights_in_club(user)\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.Club.can_be_viewed_by","title":"<code>can_be_viewed_by(user)</code>","text":"<p>Method to see if that object can be seen by the given user.</p> Source code in <code>club/models.py</code> <pre><code>def can_be_viewed_by(self, user):\n \"\"\"Method to see if that object can be seen by the given user.\"\"\"\n sub = User.objects.filter(pk=user.pk).first()\n if sub is None:\n return False\n return sub.was_subscribed\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.Club.get_membership_for","title":"<code>get_membership_for(user)</code>","text":"<p>Return the current membership the given user.</p> Note <p>The result is cached.</p> Source code in <code>club/models.py</code> <pre><code>def get_membership_for(self, user: User) -&gt; Membership | None:\n \"\"\"Return the current membership the given user.\n\n Note:\n The result is cached.\n \"\"\"\n if user.is_anonymous:\n return None\n membership = cache.get(f\"membership_{self.id}_{user.id}\")\n if membership == \"not_member\":\n return None\n if membership is None:\n membership = self.members.filter(user=user, end_date=None).first()\n if membership is None:\n cache.set(f\"membership_{self.id}_{user.id}\", \"not_member\")\n else:\n cache.set(f\"membership_{self.id}_{user.id}\", membership)\n return membership\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User","title":"<code>User</code>","text":"<p> Bases: <code>AbstractBaseUser</code></p> <p>Defines the base user class, useable in every app.</p> <p>This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).</p> <p>Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth</p>"},{"location":"reference/trombi/views/#trombi.views.User.cached_groups","title":"<code>cached_groups: list[Group]</code> <code>property</code>","text":"<p>Get the list of groups this user is in.</p> <p>The result is cached for the default duration (should be 5 minutes)</p> <p>Returns: A list of all the groups this user is in.</p>"},{"location":"reference/trombi/views/#trombi.views.User.is_in_group","title":"<code>is_in_group(*, pk=None, name=None)</code>","text":"<p>Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.</p> <p>The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.</p> <p>Returns:</p> Type Description <code>bool</code> <p>True if the user is the group, else False</p> Source code in <code>core/models.py</code> <pre><code>def is_in_group(self, *, pk: int | None = None, name: str | None = None) -&gt; bool:\n \"\"\"Check if this user is in the given group.\n Either a group id or a group name must be provided.\n If both are passed, only the id will be considered.\n\n The group will be fetched using the given parameter.\n If no group is found, return False.\n If a group is found, check if this user is in the latter.\n\n Returns:\n True if the user is the group, else False\n \"\"\"\n if pk is not None:\n group: Optional[Group] = get_group(pk=pk)\n elif name is not None:\n group: Optional[Group] = get_group(name=name)\n else:\n raise ValueError(\"You must either provide the id or the name of the group\")\n if group is None:\n return False\n if group.id == settings.SITH_GROUP_PUBLIC_ID:\n return True\n if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:\n return self.is_subscribed\n if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:\n return self.was_subscribed\n if group.id == settings.SITH_GROUP_ROOT_ID:\n return self.is_root\n if group.is_meta:\n # check if this group is associated with a club\n group.__class__ = MetaGroup\n club = group.associated_club\n if club is None:\n return False\n membership = club.get_membership_for(self)\n if membership is None:\n return False\n if group.name.endswith(settings.SITH_MEMBER_SUFFIX):\n return True\n return membership.role &gt; settings.SITH_MAXIMUM_FREE_ROLE\n return group in self.cached_groups\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.age","title":"<code>age()</code>","text":"<p>Return the age this user has the day the method is called. If the user has not filled his age, return 0.</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef age(self) -&gt; int:\n \"\"\"Return the age this user has the day the method is called.\n If the user has not filled his age, return 0.\n \"\"\"\n if self.date_of_birth is None:\n return 0\n today = timezone.now()\n age = today.year - self.date_of_birth.year\n # remove a year if this year's birthday is yet to come\n age -= (today.month, today.day) &lt; (\n self.date_of_birth.month,\n self.date_of_birth.day,\n )\n return age\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.get_full_name","title":"<code>get_full_name()</code>","text":"<p>Returns the first_name plus the last_name, with a space in between.</p> Source code in <code>core/models.py</code> <pre><code>def get_full_name(self):\n \"\"\"Returns the first_name plus the last_name, with a space in between.\"\"\"\n full_name = \"%s %s\" % (self.first_name, self.last_name)\n return full_name.strip()\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.get_short_name","title":"<code>get_short_name()</code>","text":"<p>Returns the short name for the user.</p> Source code in <code>core/models.py</code> <pre><code>def get_short_name(self):\n \"\"\"Returns the short name for the user.\"\"\"\n if self.nick_name:\n return self.nick_name\n return self.first_name + \" \" + self.last_name\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.get_display_name","title":"<code>get_display_name()</code>","text":"<p>Returns the display name of the user.</p> <p>A nickname if possible, otherwise, the full name.</p> Source code in <code>core/models.py</code> <pre><code>def get_display_name(self) -&gt; str:\n \"\"\"Returns the display name of the user.\n\n A nickname if possible, otherwise, the full name.\n \"\"\"\n if self.nick_name:\n return \"%s (%s)\" % (self.get_full_name(), self.nick_name)\n return self.get_full_name()\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.get_age","title":"<code>get_age()</code>","text":"<p>Returns the age.</p> Source code in <code>core/models.py</code> <pre><code>def get_age(self):\n \"\"\"Returns the age.\"\"\"\n today = timezone.now()\n born = self.date_of_birth\n return (\n today.year - born.year - ((today.month, today.day) &lt; (born.month, born.day))\n )\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.get_family","title":"<code>get_family(godfathers_depth=4, godchildren_depth=4)</code>","text":"<p>Get the family of the user, with the given depth.</p> <p>Parameters:</p> Name Type Description Default <code>godfathers_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godfathers to fetch</p> <code>4</code> <code>godchildren_depth</code> <code>NonNegativeInt</code> <p>The number of generations of godchildren to fetch</p> <code>4</code> <p>Returns:</p> Type Description <code>set[through]</code> <p>A list of family relationships in this user's family</p> Source code in <code>core/models.py</code> <pre><code>def get_family(\n self,\n godfathers_depth: NonNegativeInt = 4,\n godchildren_depth: NonNegativeInt = 4,\n) -&gt; set[User.godfathers.through]:\n \"\"\"Get the family of the user, with the given depth.\n\n Args:\n godfathers_depth: The number of generations of godfathers to fetch\n godchildren_depth: The number of generations of godchildren to fetch\n\n Returns:\n A list of family relationships in this user's family\n \"\"\"\n res = []\n for depth, key, reverse_key in [\n (godfathers_depth, \"from_user_id\", \"to_user_id\"),\n (godchildren_depth, \"to_user_id\", \"from_user_id\"),\n ]:\n if depth == 0:\n continue\n links = list(User.godfathers.through.objects.filter(**{key: self.id}))\n res.extend(links)\n for _ in range(1, depth): # noqa: F402 we don't care about gettext here\n ids = [getattr(c, reverse_key) for c in links]\n links = list(\n User.godfathers.through.objects.filter(\n **{f\"{key}__in\": ids}\n ).exclude(id__in=[r.id for r in res])\n )\n if not links:\n break\n res.extend(links)\n return set(res)\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.email_user","title":"<code>email_user(subject, message, from_email=None, **kwargs)</code>","text":"<p>Sends an email to this User.</p> Source code in <code>core/models.py</code> <pre><code>def email_user(self, subject, message, from_email=None, **kwargs):\n \"\"\"Sends an email to this User.\"\"\"\n if from_email is None:\n from_email = settings.DEFAULT_FROM_EMAIL\n send_mail(subject, message, from_email, [self.email], **kwargs)\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.generate_username","title":"<code>generate_username()</code>","text":"<p>Generates a unique username based on the first and last names.</p> <p>For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.</p> <p>Returns:</p> Type Description <code>str</code> <p>The generated username.</p> Source code in <code>core/models.py</code> <pre><code>def generate_username(self) -&gt; str:\n \"\"\"Generates a unique username based on the first and last names.\n\n For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.\n\n Returns:\n The generated username.\n \"\"\"\n\n def remove_accents(data):\n return \"\".join(\n x\n for x in unicodedata.normalize(\"NFKD\", data)\n if unicodedata.category(x)[0] == \"L\"\n ).lower()\n\n user_name = (\n remove_accents(self.first_name[0] + self.last_name)\n .encode(\"ascii\", \"ignore\")\n .decode(\"utf-8\")\n )\n un_set = [u.username for u in User.objects.all()]\n if user_name in un_set:\n i = 1\n while user_name + str(i) in un_set:\n i += 1\n user_name += str(i)\n self.username = user_name\n return user_name\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.is_owner","title":"<code>is_owner(obj)</code>","text":"<p>Determine if the object is owned by the user.</p> Source code in <code>core/models.py</code> <pre><code>def is_owner(self, obj):\n \"\"\"Determine if the object is owned by the user.\"\"\"\n if hasattr(obj, \"is_owned_by\") and obj.is_owned_by(self):\n return True\n if hasattr(obj, \"owner_group\") and self.is_in_group(pk=obj.owner_group.id):\n return True\n return self.is_root\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.can_edit","title":"<code>can_edit(obj)</code>","text":"<p>Determine if the object can be edited by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_edit(self, obj):\n \"\"\"Determine if the object can be edited by the user.\"\"\"\n if hasattr(obj, \"can_be_edited_by\") and obj.can_be_edited_by(self):\n return True\n if hasattr(obj, \"edit_groups\"):\n for pk in obj.edit_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n if isinstance(obj, User) and obj == self:\n return True\n return self.is_owner(obj)\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.can_view","title":"<code>can_view(obj)</code>","text":"<p>Determine if the object can be viewed by the user.</p> Source code in <code>core/models.py</code> <pre><code>def can_view(self, obj):\n \"\"\"Determine if the object can be viewed by the user.\"\"\"\n if hasattr(obj, \"can_be_viewed_by\") and obj.can_be_viewed_by(self):\n return True\n if hasattr(obj, \"view_groups\"):\n for pk in obj.view_groups.values_list(\"pk\", flat=True):\n if self.is_in_group(pk=pk):\n return True\n return self.can_edit(obj)\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.User.clubs_with_rights","title":"<code>clubs_with_rights()</code>","text":"<p>The list of clubs where the user has rights</p> Source code in <code>core/models.py</code> <pre><code>@cached_property\ndef clubs_with_rights(self) -&gt; list[Club]:\n \"\"\"The list of clubs where the user has rights\"\"\"\n memberships = self.memberships.ongoing().board().select_related(\"club\")\n return [m.club for m in memberships]\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.CanCreateMixin","title":"<code>CanCreateMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Protect any child view that would create an object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user has not the necessary permission to create the object of the view.</p>"},{"location":"reference/trombi/views/#trombi.views.CanEditMixin","title":"<code>CanEditMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to edit this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/trombi/views/#trombi.views.CanEditPropMixin","title":"<code>CanEditPropMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has owner permissions on the child view object.</p> <p>In other word, you can make a view with this view as parent, and it will be retricted to the users that are in the object's owner_group or that pass the <code>obj.can_be_viewed_by</code> test.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>If the user cannot see the object</p>"},{"location":"reference/trombi/views/#trombi.views.CanViewMixin","title":"<code>CanViewMixin</code>","text":"<p> Bases: <code>GenericContentPermissionMixinBuilder</code></p> <p>Ensure the user has permission to view this view's object.</p> <p>Raises:</p> Type Description <code>PermissionDenied</code> <p>if the user cannot edit this view's object.</p>"},{"location":"reference/trombi/views/#trombi.views.QuickNotifMixin","title":"<code>QuickNotifMixin</code>","text":""},{"location":"reference/trombi/views/#trombi.views.QuickNotifMixin.get_context_data","title":"<code>get_context_data(**kwargs)</code>","text":"<p>Add quick notifications to context.</p> Source code in <code>core/views/__init__.py</code> <pre><code>def get_context_data(self, **kwargs):\n \"\"\"Add quick notifications to context.\"\"\"\n kwargs = super().get_context_data(**kwargs)\n kwargs[\"quick_notifs\"] = []\n for n in self.quick_notif_list:\n kwargs[\"quick_notifs\"].append(settings.SITH_QUICK_NOTIF[n])\n for key, val in settings.SITH_QUICK_NOTIF.items():\n for gk in self.request.GET:\n if key == gk:\n kwargs[\"quick_notifs\"].append(val)\n return kwargs\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.TabedViewMixin","title":"<code>TabedViewMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Basic functions for displaying tabs in the template.</p>"},{"location":"reference/trombi/views/#trombi.views.Trombi","title":"<code>Trombi</code>","text":"<p> Bases: <code>Model</code></p> <p>Main class of the trombi, the Trombi itself.</p> <p>It contains the deadlines for the users, and the link to the club that makes its Trombi.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiClubMembership","title":"<code>TrombiClubMembership</code>","text":"<p> Bases: <code>Model</code></p> <p>A membership in a club.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiComment","title":"<code>TrombiComment</code>","text":"<p> Bases: <code>Model</code></p> <p>A comment given by someone to someone else in the same Trombi instance.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiUser","title":"<code>TrombiUser</code>","text":"<p> Bases: <code>Model</code></p> <p>Bound between a <code>User</code> and a <code>Trombi</code>.</p> <p>This class is here to avoid cross-references between the core, club, and trombi modules. It also adds the pictures to the profile without needing all the security like the other SithFiles.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiTabsMixin","title":"<code>TrombiTabsMixin</code>","text":"<p> Bases: <code>TabedViewMixin</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserIsInATrombiMixin","title":"<code>UserIsInATrombiMixin</code>","text":"<p> Bases: <code>View</code></p> <p>Check if the requested user has a trombi_user attribute.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiForm","title":"<code>TrombiForm</code>","text":"<p> Bases: <code>ModelForm</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiCreateView","title":"<code>TrombiCreateView</code>","text":"<p> Bases: <code>CanCreateMixin</code>, <code>CreateView</code></p> <p>Create a trombi for a club.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiCreateView.post","title":"<code>post(request, *args, **kwargs)</code>","text":"<p>Affect club.</p> Source code in <code>trombi/views.py</code> <pre><code>def post(self, request, *args, **kwargs):\n \"\"\"Affect club.\"\"\"\n form = self.get_form()\n if form.is_valid():\n club = get_object_or_404(Club, id=self.kwargs[\"club_id\"])\n form.instance.club = club\n ret = self.form_valid(form)\n return ret\n else:\n return self.form_invalid(form)\n</code></pre>"},{"location":"reference/trombi/views/#trombi.views.TrombiEditView","title":"<code>TrombiEditView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>TrombiTabsMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/trombi/views/#trombi.views.AddUserForm","title":"<code>AddUserForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiDetailView","title":"<code>TrombiDetailView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>QuickNotifMixin</code>, <code>TrombiTabsMixin</code>, <code>DetailView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiExportView","title":"<code>TrombiExportView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>TrombiTabsMixin</code>, <code>DetailView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiDeleteUserView","title":"<code>TrombiDeleteUserView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>TrombiTabsMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiModerateCommentsView","title":"<code>TrombiModerateCommentsView</code>","text":"<p> Bases: <code>CanEditPropMixin</code>, <code>QuickNotifMixin</code>, <code>TrombiTabsMixin</code>, <code>DetailView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiModerateForm","title":"<code>TrombiModerateForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiModerateCommentView","title":"<code>TrombiModerateCommentView</code>","text":"<p> Bases: <code>DetailView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiModelChoiceField","title":"<code>TrombiModelChoiceField</code>","text":"<p> Bases: <code>ModelChoiceField</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiForm","title":"<code>UserTrombiForm</code>","text":"<p> Bases: <code>Form</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiToolsView","title":"<code>UserTrombiToolsView</code>","text":"<p> Bases: <code>LoginRequiredMixin</code>, <code>QuickNotifMixin</code>, <code>TrombiTabsMixin</code>, <code>TemplateView</code></p> <p>Display a user's trombi tools.</p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiEditPicturesView","title":"<code>UserTrombiEditPicturesView</code>","text":"<p> Bases: <code>TrombiTabsMixin</code>, <code>UserIsInATrombiMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiEditProfileView","title":"<code>UserTrombiEditProfileView</code>","text":"<p> Bases: <code>QuickNotifMixin</code>, <code>TrombiTabsMixin</code>, <code>UserIsInATrombiMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiResetClubMembershipsView","title":"<code>UserTrombiResetClubMembershipsView</code>","text":"<p> Bases: <code>UserIsInATrombiMixin</code>, <code>RedirectView</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiDeleteMembershipView","title":"<code>UserTrombiDeleteMembershipView</code>","text":"<p> Bases: <code>TrombiTabsMixin</code>, <code>CanEditMixin</code>, <code>DeleteView</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiAddMembershipView","title":"<code>UserTrombiAddMembershipView</code>","text":"<p> Bases: <code>TrombiTabsMixin</code>, <code>CreateView</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiEditMembershipView","title":"<code>UserTrombiEditMembershipView</code>","text":"<p> Bases: <code>CanEditMixin</code>, <code>TrombiTabsMixin</code>, <code>UpdateView</code></p>"},{"location":"reference/trombi/views/#trombi.views.UserTrombiProfileView","title":"<code>UserTrombiProfileView</code>","text":"<p> Bases: <code>TrombiTabsMixin</code>, <code>DetailView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiCommentFormView","title":"<code>TrombiCommentFormView</code>","text":"<p> Bases: <code>LoginRequiredMixin</code>, <code>View</code></p> <p>Create/edit a trombi comment.</p>"},{"location":"reference/trombi/views/#trombi.views.TrombiCommentCreateView","title":"<code>TrombiCommentCreateView</code>","text":"<p> Bases: <code>TrombiCommentFormView</code>, <code>CreateView</code></p>"},{"location":"reference/trombi/views/#trombi.views.TrombiCommentEditView","title":"<code>TrombiCommentEditView</code>","text":"<p> Bases: <code>TrombiCommentFormView</code>, <code>CanViewMixin</code>, <code>UpdateView</code></p>"},{"location":"tutorial/devtools/","title":"Configurer son \u00e9diteur","text":"<p>Le projet n'est en aucun cas li\u00e9 \u00e0 un quelconque environnement de d\u00e9veloppement. Il est possible pour chacun de travailler avec les outils dont il a envie et d'utiliser l'\u00e9diteur de code avec lequel il est le plus \u00e0 l'aise.</p> <p>Pour donner une id\u00e9e, Skia a \u00e9crit une \u00e9norme partie de projet avec l'\u00e9diteur Vim sur du GNU/Linux alors que Sli a utilis\u00e9 Sublime Text sur MacOS et que Mar\u00e9chal travaille avec PyCharm sur Windows muni de WSL Arch Linux btw.</p>"},{"location":"tutorial/devtools/#configurer-les-pre-commit-hooks","title":"Configurer les pre-commit hooks","text":"<p>La proc\u00e9dure habituelle pour contribuer au projet consiste \u00e0 commit des modifications, puis \u00e0 les push sur le d\u00e9p\u00f4t distant et \u00e0 ouvrir une pull request. Cette PR va faire tourner les outils de v\u00e9rification de la qualit\u00e9 de code. Si la v\u00e9rification \u00e9choue, la PR est bloqu\u00e9e, et il faut r\u00e9parer le probl\u00e8me (ce qui implique de push un micro-commit ou de push force sur la branche).</p> <p>Dans l'id\u00e9al, on aimerait donc qu'il soit impossible d'oublier de faire tourner ces v\u00e9rifications. Pour \u00e7a, il existe un m\u00e9canisme : les pre-commits hooks. Ce sont des actions qui tournent automatiquement lorsque vous effectuez un <code>git commit</code>. Ces derni\u00e8res vont analyser et \u00e9ventuellement modifier le code, avant que Git n'ajoute effectivement le commit sur l'arbre git. Voyez \u00e7a comme une micro-CI qui tourne en local.</p> <p>Les git hooks sont une fonctionnalit\u00e9 par d\u00e9faut de Git. Cependant, leur configuration peut-\u00eatre un peu emb\u00eatante si vous le faites manuellement. Pour g\u00e9rer \u00e7a plus simplement, nous utilisons le logiciel python pre-commit qui permet de contr\u00f4ler leur installation via un seul fichier de configuration, plac\u00e9 \u00e0 la racine du projet (plus pr\u00e9cis\u00e9ment, il s'agit du fichier <code>.pre-commit-config.yaml</code>).</p> <p>Note</p> <p>Les pre-commits sont \u00e9galement utilis\u00e9s dans la CI. Si ces derniers fonctionnent localement, vous avez la garantie que la pipeline ne sera pas fach\u00e9e. ;)</p> <p>C'est une fonctionnalit\u00e9 de git lui-m\u00eame, mais c'est assez emb\u00eatant \u00e0 g\u00e9rer manuellement. Pour g\u00e9rer \u00e7a plus simplement, nous utilisons le logiciel python pre-commit qui permet de contr\u00f4ller leur installation via un fichier yaml.</p> <p>Le logiciel est install\u00e9 par d\u00e9faut par poetry. Il suffit ensuite de lancer :</p> <p><pre><code>pre-commit install\n</code></pre> Une fois que vous avez fait cette commande, pre-commit tournera automatiquement chaque fois que vous ferez un nouveau commit.</p> <p>Il est \u00e9galement possible d'appeler soi-m\u00eame les pre-commits :</p> <pre><code>pre-commit run --all-files\n</code></pre>"},{"location":"tutorial/devtools/#configurer-ruff-pour-son-editeur","title":"Configurer Ruff pour son \u00e9diteur","text":"<p>Note</p> <p>Ruff est inclus dans les d\u00e9pendances du projet. Si vous avez r\u00e9ussi \u00e0 terminer l'installation, vous n'avez donc pas de configuration suppl\u00e9mentaire \u00e0 effectuer.</p> <p>Pour utiliser Ruff, placez-vous \u00e0 la racine du projet et lancez la commande suivante :</p> <pre><code>ruff format # pour formatter le code\nruff check # pour linter le code\n</code></pre> <p>Ruff va alors faire son travail sur l'ensemble du projet puis vous dire si des documents ont \u00e9t\u00e9 reformat\u00e9s (si vous avez fait <code>ruff format</code>) ou bien s'il y a des erreurs \u00e0 r\u00e9parer (si vous avez faire <code>ruff check</code>).</p> <p>Appeler Ruff en ligne de commandes avant de pousser votre code sur Github est une technique qui marche tr\u00e8s bien. Cependant, vous risquez de souvent l'oublier. Or, lorsque le code ne respecte pas les standards de qualit\u00e9, la pipeline bloque les PR sur les branches prot\u00e9g\u00e9es.</p> <p>Pour \u00e9viter de vous faire r\u00e9guli\u00e8rement avoir, vous pouvez configurer votre \u00e9diteur pour que Ruff fasse son travail automatiquement \u00e0 chaque \u00e9dition d'un fichier. Nous tenterons de vous faire ici un r\u00e9sum\u00e9 pour deux \u00e9diteurs de textes populaires que sont VsCode et Sublime Text.</p> VsCodeSublime Text <p>Installez l'extension Ruff pour VsCode. Ensuite, ajoutez ceci dans votre configuration :</p> <pre><code>{\n \"[python]\": {\n \"editor.formatOnSave\": true,\n \"editor.defaultFormatter\": \"charliermarsh.ruff\"\n }\n}\n</code></pre> <p>Vous devez installer le plugin LSP-ruff. Suivez ensuite les instructions donn\u00e9es dans la description du plugin.</p> <p>Dans la configuration de votre projet, ajoutez ceci:</p> <pre><code>{\n \"settings\": {\n \"lsp_format_on_save\": true, // Commun \u00e0 ruff et biome\n \"LSP\": { \n \"LSP-ruff\": {\n \"enabled\": true,\n }\n }\n }\n}\n</code></pre> <p>Si vous utilisez le plugin anaconda, pensez \u00e0 modifier les param\u00e8tres du linter pep8 pour \u00e9viter de recevoir des warnings dans le formatage de ruff comme ceci :</p> <pre><code>{\n \"pep8_ignore\": [\n \"E203\",\n \"E266\",\n \"E501\",\n \"W503\"\n ]\n}\n</code></pre>"},{"location":"tutorial/devtools/#configurer-biome-pour-son-editeur","title":"Configurer Biome pour son \u00e9diteur","text":"<p>Note</p> <p>Biome est inclus dans les d\u00e9pendances du projet. Si vous avez r\u00e9ussi \u00e0 terminer l'installation, vous n'avez donc pas de configuration suppl\u00e9mentaire \u00e0 effectuer.</p> <p>Pour utiliser Biome, placez-vous \u00e0 la racine du projet et lancer la commande suivante:</p> <pre><code> npx @biomejs/biome check # Pour checker le code avec le linter et le formater\n npx @biomejs/biome check --write # Pour appliquer les changemnts\n</code></pre> <p>Biome va alors faire son travail sur l'ensemble du projet puis vous dire si des documents ont \u00e9t\u00e9 reformat\u00e9s (si vous avez fait <code>npx @biomejs/biome format --write</code>) ou bien s'il y a des erreurs \u00e0 r\u00e9parer (si vous avez faire <code>npx @biomejs/biome lint</code>) ou les deux (si vous avez fait <code>npx @biomejs/biome check --write</code>).</p> <p>Appeler Biome en ligne de commandes avant de pousser votre code sur Github est une technique qui marche tr\u00e8s bien. Cependant, vous risquez de souvent l'oublier. Or, lorsque le code ne respecte pas les Biomes de qualit\u00e9, la pipeline bloque les PR sur les branches prot\u00e9g\u00e9es.</p> <p>Pour \u00e9viter de vous faire r\u00e9guli\u00e8rement avoir, vous pouvez configurer votre \u00e9diteur pour que Biome fasse son travail automatiquement \u00e0 chaque \u00e9dition d'un fichier. Nous tenterons de vous faire ici un r\u00e9sum\u00e9 pour deux \u00e9diteurs de textes populaires que sont VsCode et Sublime Text.</p> VsCodeSublime Text <p>Biome est fourni par le plugin Biome.</p> <p>Ensuite, ajoutez ceci dans votre configuration :</p> <pre><code>{\n \"[javascript]\": {\n \"editor.formatOnSave\": true,\n \"editor.defaultFormatter\": \"biomejs.biome\"\n },\n \"[typescript]\": {\n \"editor.formatOnSave\": true,\n \"editor.defaultFormatter\": \"biomejs.biome\"\n }\n}\n</code></pre> <p>Tout comme pour ruff, il suffit d'installer un plugin lsp LSP-biome.</p> <p>Et enfin, dans la configuration de votre projet, ajouter les lignes suivantes :</p> <pre><code>{\n \"settings\": {\n \"lsp_format_on_save\": true, // Commun \u00e0 ruff et biome\n \"LSP\": { \n \"LSP-biome\": {\n \"enabled\": true,\n }\n }\n }\n}\n</code></pre>"},{"location":"tutorial/etransaction/","title":"Etransactions","text":"<p>La boutique en ligne n\u00e9cessite une interaction avec la banque pour son fonctionnement.</p> <p>Malheureusement, la mani\u00e8re dont cette interaction marche est trop complexe pour \u00eatre r\u00e9sum\u00e9e ici.</p> <p>Nous ne pouvons donc que vous redirigez vers la doc du cr\u00e9dit agricole : https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/</p>"},{"location":"tutorial/groups/","title":"Gestion des groupes","text":"<p>Il existe deux types de groupes sur le site AE :</p> <ul> <li>l'un se base sur des groupes enregistr\u00e9s en base de donn\u00e9es pendant le d\u00e9veloppement, c'est le syst\u00e8me de groupes r\u00e9els.</li> <li>L'autre est plus dynamique et comprend tous les groupes g\u00e9n\u00e9r\u00e9s pendant l'ex\u00e9cution et l'utilisation du programme. Cela correspond g\u00e9n\u00e9ralement aux groupes li\u00e9s aux clubs. Ce sont les m\u00e9ta-groupes.</li> </ul>"},{"location":"tutorial/groups/#la-definition-dun-groupe","title":"La d\u00e9finition d'un groupe","text":"<p>Les deux types de groupes sont stock\u00e9s dans la m\u00eame table en base de donn\u00e9es, et ne sont diff\u00e9renci\u00e9s que par un attribut <code>is_meta</code>.</p>"},{"location":"tutorial/groups/#les-groupes-reels","title":"Les groupes r\u00e9els","text":"<p>Pour plus diff\u00e9rencier l'utilisation de ces deux types de groupe, il a \u00e9t\u00e9 cr\u00e9\u00e9 une classe proxy (c'est-\u00e0-dire qu'elle ne correspond pas \u00e0 une vraie table en base de donn\u00e9e) qui encapsule leur utilisation. <code>RealGroup</code> peut \u00eatre utilis\u00e9 pour cr\u00e9er des groupes r\u00e9els dans le code et pour faire une recherche sur ceux-ci (dans le cadre d'une v\u00e9rification de permissions par exemple).</p>"},{"location":"tutorial/groups/#les-meta-groupes","title":"Les m\u00e9ta-groupes","text":"<p>Les m\u00e9ta-groupes, comme expliqu\u00e9 pr\u00e9c\u00e9demment, sont utilis\u00e9s dans les contextes o\u00f9 il est n\u00e9cessaire de cr\u00e9er un groupe dynamiquement. Les objets <code>MetaGroup</code>, bien que dynamiques, doivent tout de m\u00eame s'enregistrer en base de donn\u00e9es comme des vrais groupes afin de pouvoir \u00eatre affect\u00e9s dans les permissions d'autres objets, comme un forum ou une page de wiki par exemple. C'est principalement utilis\u00e9 au travers des clubs, qui g\u00e9n\u00e8rent automatiquement deux groupes \u00e0 leur cr\u00e9ation :</p> <ul> <li>club-bureau : contient tous les membres d'un club au dessus du grade d\u00e9fini dans <code>settings.SITH_MAXIMUM_FREE_ROLE</code>.</li> <li>club-membres : contient tous les membres d'un club en dessous du grade d\u00e9fini dans <code>settings.SITH_MAXIMUM_FREE_ROLE</code>.</li> </ul>"},{"location":"tutorial/groups/#les-groupes-reels-utilises","title":"Les groupes r\u00e9els utilis\u00e9s","text":"<p>Les groupes r\u00e9els que l'on utilise dans le site sont les suivants :</p> <p>Groupes g\u00e9r\u00e9s automatiquement par le site :</p> <ul> <li><code>Public</code> : tous les utilisateurs du site</li> <li><code>Subscribers</code> : tous les cotisants du site</li> <li><code>Old subscribers</code> : tous les anciens cotisants</li> </ul> <p>Groupes g\u00e9r\u00e9s par les administrateurs (\u00e0 appliquer \u00e0 la main sur un utilisateur) :</p> <ul> <li><code>Root</code> : administrateur global du site</li> <li><code>Accounting admin</code> : les administrateurs de la comptabilit\u00e9</li> <li><code>Communication admin</code> : les administrateurs de la communication</li> <li><code>Counter admin</code> : les administrateurs des comptoirs (foyer et autre)</li> <li><code>SAS admin</code> : les administrateurs du SAS</li> <li><code>Forum admin</code> : les administrateurs du forum</li> <li><code>Pedagogy admin</code> : les administrateurs de la p\u00e9dagogie (guide des UVs)</li> <li><code>Banned from buying alcohol</code> : les utilisateurs interdits de vente d'alcool (non mineurs)</li> <li><code>Banned from counters</code> : les utilisateurs interdits d'utilisation des comptoirs</li> <li><code>Banned to subscribe</code> : les utilisateurs interdits de cotisation</li> </ul>"},{"location":"tutorial/install-advanced/","title":"Installer le projet (avanc\u00e9)","text":"<p>Si le projet marche chez vous apr\u00e8s avoir suivi les \u00e9tapes donn\u00e9es dans la page pr\u00e9c\u00e9dente, alors vous pouvez d\u00e9velopper. Ce que nous nous vous avons pr\u00e9sent\u00e9 n'est absolument pas la m\u00eame configuration que celle du site, mais elle n'en est pas moins fonctionnelle.</p> <p>Cependant, vous pourriez avoir envie de faire en sorte que votre environnement de d\u00e9veloppement soit encore plus proche de celui en production. Voici les \u00e9tapes \u00e0 suivre pour \u00e7a.</p> <p>Tip</p> <p>Configurer les d\u00e9pendances du projet peut demander beaucoup d'allers et retours entre votre r\u00e9pertoire projet et divers autres emplacements.</p> <p>Vous pouvez gagner du temps en d\u00e9clarant un alias :</p> bash/zshnu <pre><code>alias cdp=\"cd /repertoire/du/projet\"\n</code></pre> <pre><code>alias cdp = cd /repertoire/du/projet\n</code></pre> <p>Chaque fois qu'on vous demandera de retourner au r\u00e9pertoire projet, vous aurez juste \u00e0 faire :</p> <pre><code>cdp\n</code></pre>"},{"location":"tutorial/install-advanced/#installer-les-dependances-manquantes","title":"Installer les d\u00e9pendances manquantes","text":"<p>Pour installer compl\u00e8tement le projet, il va falloir quelques d\u00e9pendances en plus. Commencez par installer les d\u00e9pendances syst\u00e8me :</p> LinuxmacOS Debian/UbuntuArch Linux <pre><code>sudo apt install postgresql redis libq-dev nginx\n</code></pre> <pre><code>sudo pacman -S postgresql redis nginx\n</code></pre> <pre><code>brew install postgresql redis lipbq nginx\nexport PATH=\"/usr/local/opt/libpq/bin:$PATH\"\nsource ~/.zshrc\n</code></pre> <p>Puis, installez les d\u00e9pendances poetry n\u00e9cessaires en prod :</p> <pre><code>poetry install --with prod\n</code></pre> <p>Info</p> <p>Certaines d\u00e9pendances peuvent \u00eatre un peu longues \u00e0 installer (notamment psycopg-c). C'est parce que ces d\u00e9pendances compilent certains modules \u00e0 l'installation.</p>"},{"location":"tutorial/install-advanced/#configurer-redis","title":"Configurer Redis","text":"<p>Redis est utilis\u00e9 comme cache. Assurez-vous qu'il tourne :</p> <pre><code>sudo systemctl redis status\n</code></pre> <p>Et s'il ne tourne pas, d\u00e9marrez-le :</p> <pre><code>sudo systemctl start redis\nsudo systemctl enable redis # si vous voulez que redis d\u00e9marre automatiquement au boot\n</code></pre> <p>Puis ajoutez le code suivant \u00e0 la fin de votre fichier <code>settings_custom.py</code> :</p> <pre><code>CACHES = {\n \"default\": {\n \"BACKEND\": \"django.core.cache.backends.redis.RedisCache\",\n \"LOCATION\": \"redis://127.0.0.1:6379\",\n }\n}\n</code></pre>"},{"location":"tutorial/install-advanced/#configurer-postgresql","title":"Configurer PostgreSQL","text":"<p>PostgreSQL est utilis\u00e9 comme base de donn\u00e9es.</p> <p>Passez sur le compte de l'utilisateur postgres et lancez l'invite de commande sql :</p> <pre><code>sudo su - postgres\npsql\n</code></pre> <p>Puis configurez la base de donn\u00e9es :</p> <pre><code>CREATE DATABASE sith;\nCREATE USER sith WITH PASSWORD 'password';\n\nALTER ROLE sith SET client_encoding TO 'utf8';\nALTER ROLE sith SET default_transaction_isolation TO 'read committed';\nALTER ROLE sith SET timezone TO 'UTC';\n\nGRANT ALL PRIVILEGES ON DATABASE sith TO SITH;\n\\q\n</code></pre> <p>Si vous utilisez une version de PostgreSQL sup\u00e9rieure ou \u00e9gale \u00e0 15, vous devez ex\u00e9cuter une commande en plus, en \u00e9tant connect\u00e9 en tant que postgres :</p> <pre><code>psql -d sith -c \"GRANT ALL PRIVILEGES ON SCHEMA public to sith\";\n</code></pre> <p>Puis ajoutez le code suivant \u00e0 la fin de votre <code>settings_custom.py</code> :</p> <pre><code>DATABASES = {\n \"default\": {\n \"ENGINE\": \"django.db.backends.postgresql\",\n \"NAME\": \"sith\",\n \"USER\": \"sith\",\n \"PASSWORD\": \"password\",\n \"HOST\": \"localhost\",\n \"PORT\": \"\", # laissez ce champ vide pour que le choix du port soit automatique\n }\n}\n</code></pre> <p>Enfin, cr\u00e9ez vos donn\u00e9es :</p> <pre><code>poetry run ./manage.py populate\n</code></pre> <p>Note</p> <p>N'oubliez de quitter la session de l'utilisateur postgres apr\u00e8s avoir configur\u00e9 la db.</p>"},{"location":"tutorial/install-advanced/#configurer-nginx","title":"Configurer nginx","text":"<p>Nginx est utilis\u00e9 comme reverse-proxy.</p> <p>Warning</p> <p>Nginx ne sert pas les fichiers de la m\u00eame mani\u00e8re que Django. Les fichiers statiques servis seront ceux du dossier <code>/static</code>, tels que g\u00e9n\u00e9r\u00e9s par les commandes <code>collectstatic</code> et <code>compilestatic</code>. Si vous changez du css ou du js sans faire tourner ces commandes, ces changements ne seront pas refl\u00e9t\u00e9s.</p> <p>De mani\u00e8re g\u00e9n\u00e9rale, utiliser nginx en dev n'est pas tr\u00e8s utile, voire est g\u00eanant si vous travaillez sur le front. Ne vous emb\u00eatez pas avec \u00e7a, sauf par curiosit\u00e9 intellectuelle, ou bien si vous voulez tester sp\u00e9cifiquement des interactions avec le reverse proxy.</p> <p>Placez-vous dans le r\u00e9pertoire <code>/etc/nginx</code>, et cr\u00e9ez les dossiers et fichiers n\u00e9cessaires :</p> <pre><code>cd /etc/nginx/\nsudo mkdir sites-enabled sites-available\nsudo touch sites-available/sith.conf\nsudo ln -s /etc/nginx/sites-available/sith.conf sites-enabled/sith.conf\n</code></pre> <p>Puis ouvrez le fichier <code>sites-available/sith.conf</code> et mettez-y le contenu suivant :</p> <pre><code>server {\n listen 8000;\n\n server_name _;\n\n location /static/;\n root /repertoire/du/projet;\n }\n location ~ ^/data/(products|com|club_logos)/ {\n root /repertoire/du/projet;\n }\n location ~ ^/data/(SAS|profiles|users|.compressed|.thumbnails)/ {\n # https://nginx.org/en/docs/http/ngx_http_core_module.html#internal\n internal;\n root /repertoire/du/projet;\n }\n\n location / {\n proxy_pass http://127.0.0.1:8001;\n include uwsgi_params;\n }\n}\n</code></pre> <p>Ouvrez le fichier <code>nginx.conf</code>, et ajoutez la configuration suivante :</p> <pre><code>http {\n # Toute la configuration\n # \u00e9ventuellement d\u00e9j\u00e0 l\u00e0\n\n include /etc/nginx/sites-enabled/sith.conf;\n}\n</code></pre> <p>V\u00e9rifiez que votre configuration est bonne :</p> <pre><code>sudo nginx -t\n</code></pre> <p>Si votre configuration n'est pas bonne, corrigez-la. Puis lancez ou relancez nginx :</p> <pre><code>sudo systemctl restart nginx\n</code></pre> <p>Dans votre <code>settings_custom.py</code>, remplacez <code>DEBUG=True</code> par <code>DEBUG=False</code>.</p> <p>Enfin, d\u00e9marrez le serveur Django :</p> <pre><code>cd /repertoire/du/projet\npoetry run ./manage.py runserver 8001\n</code></pre> <p>Et c'est bon, votre reverse-proxy est pr\u00eat \u00e0 tourner devant votre serveur. Nginx \u00e9coutera sur le port 8000. Toutes les requ\u00eates vers des fichiers statiques et les medias publiques seront seront servies directement par nginx. Toutes les autres requ\u00eates seront transmises au serveur django.</p>"},{"location":"tutorial/install-advanced/#mettre-a-jour-la-base-de-donnees-antispam","title":"Mettre \u00e0 jour la base de donn\u00e9es antispam","text":"<p>L'anti spam n\u00e9cessite d'\u00eatre \u00e0 jour par rapport \u00e0 des bases de donn\u00e9es externes. Il existe une commande pour \u00e7a qu'il faut lancer r\u00e9guli\u00e8rement. Lors de la mise en production, il est judicieux de configurer un cron pour la mettre \u00e0 jour au moins une fois par jour.</p> <pre><code>python manage.py update_spam_database\n</code></pre>"},{"location":"tutorial/install/","title":"Installer le projet","text":""},{"location":"tutorial/install/#dependances-du-systeme","title":"D\u00e9pendances du syst\u00e8me","text":"<p>Certaines d\u00e9pendances sont n\u00e9cessaires niveau syst\u00e8me :</p> <ul> <li>poetry</li> <li>libssl</li> <li>libjpeg</li> <li>zlib1g-dev</li> <li>python</li> <li>gettext</li> </ul>"},{"location":"tutorial/install/#installer-wsl","title":"Installer WSL","text":"<p>Si vous utilisez Windows, je suis navr\u00e9 de vous annoncer que, certaines d\u00e9pendances \u00e9tant uniquement disponibles sur des syt\u00e8mes UNIX, il n'est pas possible de d\u00e9velopper le site sur ce syst\u00e8me d'exploitation.</p> <p>Heureusement, il existe une alternative qui ne requiert pas de d\u00e9sinstaller votre OS ni de mettre un dual boot sur votre ordinateur : <code>WSL</code>.</p> <ul> <li>Pr\u00e9requis: vous devez \u00eatre sur Windows 10 version 2004 ou ult\u00e9rieure (build 19041 &amp; versions ult\u00e9rieures) ou Windows 11.</li> <li>Plus d'info: docs.microsoft.com</li> </ul> <pre><code># dans un shell Windows\nwsl --install\n\n# afficher la liste des distribution disponible avec WSL\nwsl -l -o\n\n# installer WSL avec une distro (ubuntu conseill\u00e9)\nwsl --install -d &lt;nom_distro&gt;\n</code></pre> <p>Une fois <code>WSL</code> install\u00e9, mettez \u00e0 jour votre distribution et installez les d\u00e9pendances (voir la partie installation sous Ubuntu).</p> <p>Pour acc\u00e9der au contenu d'un r\u00e9pertoire externe \u00e0 <code>WSL</code>, il suffit d'utiliser la commande suivante :</p> <pre><code># oui c'est beau, simple et efficace\ncd /mnt/&lt;la_lettre_du_disque&gt;/vos/fichiers/comme/dhab\n</code></pre> <p>Note</p> <p>\u00c0 ce stade, si vous avez r\u00e9ussi votre installation de <code>WSL</code> ou bien qu'il \u00e9tait d\u00e9j\u00e0 install\u00e9, vous pouvez effectuer la mise en place du projet en suivant les instructions pour votre distribution.</p>"},{"location":"tutorial/install/#installer-les-dependances","title":"Installer les d\u00e9pendances","text":"LinuxmacOS Debian/UbuntuArch Linux <p>Avant toute chose, assurez-vous que votre syst\u00e8me est \u00e0 jour :</p> <pre><code>sudo apt update\nsudo apt upgrade\n</code></pre> <p>Puis, si ce n'est pas d\u00e9j\u00e0 fait, installez Python : <pre><code>sudo apt install python3\n# on sait jamais\nsudo apt install python-is-python3\n</code></pre> Si vous utilisez Ubuntu 22.04 ou Ubuntu 24.04, votre version de Python devrait \u00eatre compatible par d\u00e9faut avec celle du projet. Si ce n'est pas le cas, nous vous conseillons d'utiliser pyenv</p> <p>Puis installez les autres d\u00e9pendances :</p> <pre><code>sudo apt install build-essential libssl-dev libjpeg-dev zlib1g-dev python-dev npm \\\n libffi-dev python-dev-is-python3 pkg-config \\\n gettext git pipx\n\npipx install poetry\n</code></pre> <pre><code>sudo pacman -Syu # on s'assure que les d\u00e9p\u00f4ts et le syst\u00e8me sont \u00e0 jour\n\nsudo pacman -S python\n\nsudo pacman -S gcc git gettext pkgconf python-poetry npm\n</code></pre> <p>Pour installer les d\u00e9pendances, il est fortement recommand\u00e9 d'installer le gestionnaire de paquets <code>homebrew &lt;https://brew.sh/index_fr&gt;</code>_. Il est \u00e9galement n\u00e9cessaire d'avoir install\u00e9 xcode</p> <pre><code>brew install git python pipx npm\npipx install poetry\n\n# Pour bien configurer gettext\nbrew link gettext # (suivez bien les instructions suppl\u00e9mentaires affich\u00e9es)\n</code></pre> <p>Note</p> <p>Si vous rencontrez des erreurs lors de votre configuration, n'h\u00e9sitez pas \u00e0 v\u00e9rifier l'\u00e9tat de votre installation homebrew avec :code:<code>brew doctor</code></p>"},{"location":"tutorial/install/#finaliser-linstallation","title":"Finaliser l'installation","text":"<p>Clonez le projet (depuis votre console WSL, si vous utilisez WSL) et installez les d\u00e9pendances :</p> <pre><code>git clone https://github.com/ae-utbm/sith.git\ncd sith\n\n# Cr\u00e9ation de l'environnement et installation des d\u00e9pendances\npoetry install # D\u00e9pendances backend\nnpm install # D\u00e9pendances frontend\npoetry run ./manage.py install_xapian\n</code></pre> <p>Note</p> <p>La commande <code>install_xapian</code> est longue et affiche beaucoup de texte \u00e0 l'\u00e9cran. C'est normal, il ne faut pas avoir peur.</p> <p>Maintenant que les d\u00e9pendances sont install\u00e9es, nous allons cr\u00e9er la base de donn\u00e9es, la remplir avec des donn\u00e9es de test, et compiler les traductions. Cependant, avant de faire cela, il est n\u00e9cessaire de modifier la configuration pour signifier que nous sommes en mode d\u00e9veloppement. Pour cela, nous allons cr\u00e9er un fichier <code>sith/settings_custom.py</code> et l'utiliser pour surcharger les settings de base.</p> <pre><code>echo \"DEBUG=True\" &gt; sith/settings_custom.py\necho 'SITH_URL = \"localhost:8000\"' &gt;&gt; sith/settings_custom.py\n</code></pre> <p>Enfin, nous pouvons lancer les commandes suivantes :</p> <pre><code># Activation de l'environnement virtuel\npoetry shell\n\n# Pr\u00e9pare la base de donn\u00e9es\npython ./manage.py setup\n\n# Installe les traductions\npython ./manage.py compilemessages\n</code></pre> <p>Note</p> <p>Pour \u00e9viter d'avoir \u00e0 utiliser la commande <code>poetry shell</code> syst\u00e9matiquement, il est possible de consulter direnv.</p>"},{"location":"tutorial/install/#demarrer-le-serveur-de-developpement","title":"D\u00e9marrer le serveur de d\u00e9veloppement","text":"<p>Il faut toujours avoir pr\u00e9alablement activ\u00e9 l'environnement virtuel comme fait plus haut et se placer \u00e0 la racine du projet. Il suffit ensuite d'utiliser cette commande :</p> <pre><code>python manage.py runserver\n</code></pre> <p>Note</p> <p>Le serveur est alors accessible \u00e0 l'adresse http://localhost:8000 ou bien http://127.0.0.1:8000/.</p> <p>Tip</p> <p>Vous trouverez \u00e9galement, \u00e0 l'adresse http://localhost:8000/api/docs, une interface swagger, avec toutes les routes de l'API.</p>"},{"location":"tutorial/install/#generer-la-documentation","title":"G\u00e9n\u00e9rer la documentation","text":"<p>La documentation est automatiquement mise en ligne \u00e0 chaque envoi de code sur GitHub. Pour l'utiliser en local ou globalement pour la modifier, il existe une commande du site qui g\u00e9n\u00e8re la documentation et lance un serveur la rendant accessible \u00e0 l'adresse http://localhost:8080. Cette commande g\u00e9n\u00e8re la documentation \u00e0 chacune de ses modifications, inutile de relancer le serveur \u00e0 chaque fois.</p> <p>Note</p> <p>Les d\u00e9pendances pour la documentation sont optionnelles. Avant de commencer \u00e0 travailler sur la doc, il faut donc les installer avec la commande <code>poetry install --with docs</code></p> <pre><code>mkdocs serve\n</code></pre>"},{"location":"tutorial/install/#lancer-les-tests","title":"Lancer les tests","text":"<p>Pour lancer les tests, il suffit d'utiliser la commande suivante :</p> <pre><code># Lancer tous les tests\npytest\n\n# Lancer les tests de l'application core\npytest core\n\n# Lancer les tests de la classe UserRegistrationTest de core\npytest core/tests/tests_core.py::TestUserRegistration\n</code></pre> <p>Note</p> <p>Certains tests sont un peu longs \u00e0 tourner. Pour ne faire tourner que les tests les plus rapides, vous pouvez ex\u00e9cutez pytest ainsi :</p> <pre><code>pytest -m \"not slow\"\n\n# vous pouvez toujours faire comme au-dessus\npytest core -m \"not slow\"\n</code></pre> <p>A l'inverse, vous pouvez ne faire tourner que les tests lents en rempla\u00e7ant <code>-m \"not slow\"</code> par <code>-m slow</code>.</p> <p>De cette mani\u00e8re, votre processus de d\u00e9veloppement devrait \u00eatre un peu plus fluide. Cependant, n'oubliez pas de bien faire tourner tous les tests avant de push un commit.</p>"},{"location":"tutorial/perms/","title":"Gestion des permissions","text":""},{"location":"tutorial/perms/#les-permissions","title":"Les permissions","text":"<p>Le fonctionnement de l'AE ne permet pas d'utiliser le syst\u00e8me de permissions int\u00e9gr\u00e9 \u00e0 Django tel quel. Lors de la conception du Sith, ce qui paraissait le plus simple \u00e0 l'\u00e9poque \u00e9tait de concevoir un syst\u00e8me maison afin de se calquer sur ce que faisais l'ancien site.</p>"},{"location":"tutorial/perms/#proteger-un-modele","title":"Prot\u00e9ger un mod\u00e8le","text":"<p>La gestion des permissions se fait directement par mod\u00e8le. Il existe trois niveaux de permission :</p> <ul> <li>\u00c9diter des propri\u00e9t\u00e9s de l'objet</li> <li>\u00c9diter certaines valeurs l'objet</li> <li>Voir l'objet</li> </ul> <p>Chacune de ces permissions est v\u00e9rifi\u00e9e par une m\u00e9thode d\u00e9di\u00e9e de la classe User :</p> <ul> <li>Editer les propri\u00e9ts : User.is_owner(obj)</li> <li>Editer les valeurs : User.can_edit(obj)</li> <li>Voir : User.can_view(obj)</li> </ul> <p>Ces m\u00e9thodes vont alors r\u00e9soudre les permissions dans cet ordre :</p> <ol> <li>Si l'objet poss\u00e8de une m\u00e9thode <code>can_be_viewed_by(user)</code> (ou <code>can_be_edited_by(user)</code>, ou <code>is_owned_by(user)</code>) et que son appel renvoie <code>True</code>, l'utilisateur a la permission requise.</li> <li>Sinon, si le mod\u00e8le de l'objet poss\u00e8de un attribut <code>view_groups</code> (ou <code>edit_groups</code>, ou <code>owner_group</code>) et que l'utilisateur est dans l'un des groupes indiqu\u00e9s, il a la permission requise.</li> <li>Sinon, on regarde si l'utilisateur a la permission de niveau sup\u00e9rieur (les droits <code>owner</code> impliquent les droits d'\u00e9dition, et les droits d'\u00e9dition impliquent les droits de vue).</li> <li>Si aucune des \u00e9tapes si dessus ne permet d'\u00e9tablir que l'utilisateur n'a la permission requise, c'est qu'il ne l'a pas.</li> </ol> <p>Voici un exemple d'impl\u00e9mentation de ce syst\u00e8me :</p> Avec les m\u00e9thodesAvec les groupes de permission <pre><code>from django.db import models\nfrom django.utils.translation import gettext_lazy as _\n\nfrom core.models import User, Group\n\n# Utilisation de la protection par fonctions\nclass Article(models.Model):\n\n title = models.CharField(_(\"title\"), max_length=100)\n content = models.TextField(_(\"content\"))\n\n # Donne ou non les droits d'\u00e9dition des propri\u00e9t\u00e9s de l'objet\n # Un utilisateur dans le bureau AE aura tous les droits sur cet objet\n def is_owned_by(self, user):\n return user.is_board_member\n\n # Donne ou non les droits d'\u00e9dition de l'objet\n # L'objet ne sera modifiable que par un utilisateur cotisant\n def can_be_edited_by(self, user):\n return user.is_subscribed\n\n # Donne ou non les droits de vue de l'objet\n # Ici, l'objet n'est visible que par un utilisateur connect\u00e9\n def can_be_viewed_by(self, user):\n return not user.is_anonymous\n</code></pre> <pre><code>from django.db import models\nfrom django.conf import settings\nfrom django.utils.translation import gettext_lazy as _\n\nfrom core.models import User, Group\n\nclass Article(models.Model):\n title = models.CharField(_(\"title\"), max_length=100)\n content = models.TextField(_(\"content\"))\n\n # relation one-to-many\n # Groupe poss\u00e9dant l'objet\n # Donne les droits d'\u00e9dition des propri\u00e9t\u00e9s de l'objet\n owner_group = models.ForeignKey(\n Group, related_name=\"owned_articles\", default=settings.SITH_GROUP_ROOT_ID\n )\n\n # relation many-to-many\n # Tous les groupes qui seront ajout\u00e9 dans ce champ auront les droits d'\u00e9dition de l'objet\n edit_groups = models.ManyToManyField(\n Group,\n related_name=\"editable_articles\",\n verbose_name=_(\"edit groups\"),\n blank=True,\n )\n\n # relation many-to-many\n # Tous les groupes qui seront ajout\u00e9 dans ce champ auront les droits de vue de l'objet\n view_groups = models.ManyToManyField(\n Group,\n related_name=\"viewable_articles\",\n verbose_name=_(\"view groups\"),\n blank=True,\n )\n</code></pre>"},{"location":"tutorial/perms/#appliquer-les-permissions","title":"Appliquer les permissions","text":""},{"location":"tutorial/perms/#dans-un-template","title":"Dans un template","text":"<p>Il existe trois fonctions de base sur lesquelles reposent les v\u00e9rifications de permission. Elles sont disponibles dans le contexte par d\u00e9faut du moteur de template et peuvent \u00eatre utilis\u00e9es \u00e0 tout moment.</p> <ul> <li>can_edit_prop(obj, user) : \u00e9quivalent de <code>obj.is_owned_by(user)</code></li> <li>can_edit(obj, user) : \u00e9quivalent de <code>obj.can_be_edited_by(user)</code></li> <li>can_view(obj, user) : \u00e9quivalent de <code>obj.can_be_viewed_by(user)</code></li> </ul> <p>Voici un exemple d'utilisation dans un template :</p> <pre><code>{# ... #}\n{% if can_edit(club, user) %}\n &lt;a href=\"{{ url('club:tools', club_id=club.id) }}\"&gt;{{ club }}&lt;/a&gt;\n{% endif %}\n</code></pre>"},{"location":"tutorial/perms/#dans-une-vue","title":"Dans une vue","text":"<p>G\u00e9n\u00e9ralement, les v\u00e9rifications de droits dans les templates se limitent aux urls \u00e0 afficher puisqu'il ne faut normalement pas mettre de logique autre que d'affichage \u00e0 l'int\u00e9rieur (en r\u00e9alit\u00e9, c'est un principe qu'on a beaucoup viol\u00e9, mais promis on le fera plus). C'est donc habituellement au niveau des vues que cela a lieu.</p> <p>Notre syst\u00e8me s'appuie sur un syst\u00e8me de mixin \u00e0 h\u00e9riter lors de la cr\u00e9ation d'une vue bas\u00e9e sur une classe. Ces mixins ne sont compatibles qu'avec les classes r\u00e9cup\u00e9rant un objet ou une liste d'objet. Dans le cas d'un seul objet, une permission refus\u00e9e est lev\u00e9e lorsque l'utilisateur n'a pas le droit de visionner la page. Dans le cas d'une liste d'objet, le mixin filtre les objets non autoris\u00e9s et si aucun ne l'est, l'utilisateur recevra une liste vide d'objet.</p> <p>Voici un exemple d'utilisation en reprenant l'objet Article cr\u00e9e pr\u00e9c\u00e9demment :</p> <pre><code>from django.views.generic import CreateView, ListView\n\nfrom core.views import CanViewMixin, CanCreateMixin\n\nfrom com.models import WeekmailArticle\n\n# Il est important de mettre le mixin avant la classe h\u00e9rit\u00e9e de Django\n# L'h\u00e9ritage multiple se fait de droite \u00e0 gauche et les mixins ont besoin\n# d'une classe de base pour fonctionner correctement.\nclass ArticlesListView(CanViewMixin, ListView):\n model = WeekmailArticle\n\n\n# M\u00eame chose pour une vue de cr\u00e9ation de l'objet Article\nclass ArticlesCreateView(CanCreateMixin, CreateView):\n model = WeekmailArticle\n</code></pre> <p>Les mixins suivants sont impl\u00e9ment\u00e9s :</p> <ul> <li>CanCreateMixin : l'utilisateur peut-il cr\u00e9er l'objet ?</li> <li>CanEditPropMixin : l'utilisateur peut-il \u00e9diter les propri\u00e9t\u00e9s de l'objet ?</li> <li>CanEditMixin : L'utilisateur peut-il \u00e9diter l'objet ?</li> <li>CanViewMixin : L'utilisateur peut-il voir l'objet ?</li> <li>UserIsRootMixin : L'utilisateur a-t-il les droit root ?</li> <li>FormerSubscriberMixin : L'utilisateur a-t-il d\u00e9j\u00e0 \u00e9t\u00e9 cotisant ?</li> <li>UserIsLoggedMixin : L'utilisateur est-il connect\u00e9 ? (\u00e0 \u00e9viter ; pr\u00e9f\u00e9rez <code>LoginRequiredMixin</code>, fourni par Django)</li> </ul> <p>Performance</p> <p>Ce syst\u00e8me maison de permissions fonctionne et r\u00e9pond aux attentes de l'\u00e9poque de sa conception. Mais d'un point de vue performance, il est souvent plus que probl\u00e9matique. En effet, toutes les permissions sont dynamiquement calcul\u00e9es et n\u00e9cessitent plusieurs appels en base de donn\u00e9es qui ne se r\u00e9sument pas \u00e0 une \u00ab\u00a0simple\u00a0\u00bb jointure mais \u00e0 plusieurs requ\u00eates diff\u00e9rentes et difficiles \u00e0 optimiser. De plus, \u00e0 chaque calcul de permission, il est n\u00e9cessaire de recommencer tous les calculs depuis le d\u00e9but. La solution \u00e0 \u00e7a est de mettre du cache de session sur les tests effectu\u00e9s r\u00e9cemment, mais cela engendre son autre lot de probl\u00e8mes.</p> <p>Sur une vue o\u00f9 on manipule un seul objet, passe encore. Mais sur les <code>ListView</code>, on peut arriver \u00e0 des temps de r\u00e9ponse extr\u00eamement \u00e9lev\u00e9s.</p>"},{"location":"tutorial/perms/#api","title":"API","text":"<p>L'API utilise son propre syst\u00e8me de permissions. Ce n'est pas encore un autre syst\u00e8me en parall\u00e8le, mais un wrapper autour de notre syst\u00e8me de permissions, afin de l'adapter aux besoins de l'API.</p> <p>En effet, l'interface attendue pour manipuler le plus ais\u00e9ment possible les permissions des routes d'API avec la librairie que nous utilisons est diff\u00e9rente de notre syst\u00e8me, tout en restant adaptable. (Pour plus de d\u00e9tail, voir la doc de la lib).</p> <p>Si vous avez bien suivi ce qui a \u00e9t\u00e9 dit plus haut, vous ne devriez pas \u00eatre perdu, \u00e9tant donn\u00e9 que le syst\u00e8me de permissions de l'API utilise des noms assez similaires : <code>IsInGroup</code>, <code>IsRoot</code>, <code>IsSubscriber</code>... Vous pouvez trouver des exemples d'utilisation de ce syst\u00e8me dans cette partie.</p>"},{"location":"tutorial/structure/","title":"Structure du projet","text":""},{"location":"tutorial/structure/#la-structure-dun-projet-django","title":"La structure d'un projet Django","text":"<p>Un projet Django est structur\u00e9 en applications. Une application est un package Python contenant un ensemble de vues, de mod\u00e8les, de templates, etc. S\u00e9mantiquement, une application repr\u00e9sente un ensemble de fonctionnalit\u00e9s coh\u00e9rentes. Par exemple, dans notre cas, nous avons une application charg\u00e9e de la gestion des comptoirs, une autre de la gestion des clubs, une autre de la gestion du SAS, etc.</p> <p>On trouve g\u00e9n\u00e9ralement dans un projet Django une application principale qui contient les fichiers de configuration du projet, les urls et \u00e9ventuellement des commandes d'administration.</p>"},{"location":"tutorial/structure/#arborescence-du-projet","title":"Arborescence du projet","text":"<p>Le code source du projet est organis\u00e9 comme suit :</p> <pre><code>sith/\n\u251c\u2500\u2500 .github/\n\u2502 \u251c\u2500\u2500 actions/ (1)\n\u2502 \u2514\u2500\u2500 workflows/ (2)\n\u251c\u2500\u2500 accounting/ (3)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 club/ (4)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 com/ (5)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 core/ (6)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 counter/ (7)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 docs/ (8)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 eboutic/ (9)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 election/ (10)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 forum/ (11)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 galaxy/ (12)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 launderette/ (13)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 locale/ (14)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 matmat/ (15)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 pedagogy/ (16)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 rootplace/ (17)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 sas/ (18)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 sith/ (19)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 subscription/ (20)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 trombi/ (21)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 antispam/ (22)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 staticfiles/ (23)\n\u2502 \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 .coveragerc (24)\n\u251c\u2500\u2500 .envrc (25)\n\u251c\u2500\u2500 .gitattributes\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 .mailmap\n\u251c\u2500\u2500 .env.exemple\n\u251c\u2500\u2500 manage.py (26)\n\u251c\u2500\u2500 mkdocs.yml (27)\n\u251c\u2500\u2500 poetry.lock\n\u251c\u2500\u2500 pyproject.toml (28)\n\u2514\u2500\u2500 README.md\n</code></pre> <ol> <li>Dossier contenant certaines actions r\u00e9utilisables dans des workflows Github. Par exemple, l'action <code>setup-project</code> installe poetry puis appelle la commande <code>poetry install</code>.</li> <li>Dossier contenant les fichiers de configuration des workflows Github. Par exemple, le workflow <code>docs.yml</code> compile et publie la documentation \u00e0 chaque push sur la branche <code>master</code>.</li> <li>Application de gestion de la comptabilit\u00e9.</li> <li>Application de gestion des clubs et de leurs membres.</li> <li>Application contenant les fonctionnalit\u00e9s destin\u00e9es aux responsables communication de l'AE.</li> <li>Application contenant la mod\u00e9lisation centrale du site. On en reparle plus loin sur cette page.</li> <li>Application de gestion des comptoirs, des permanences sur ces comptoirs et des transactions qui y sont effectu\u00e9es.</li> <li>Dossier contenant la documentation.</li> <li>Application de gestion de la boutique en ligne.</li> <li>Application de gestion des \u00e9lections.</li> <li>Application de gestion du forum</li> <li>Application de gestion de la galaxie ; la galaxie est un graphe des niveaux de proximit\u00e9 entre les diff\u00e9rents \u00e9tudiants.</li> <li>Gestion des machines \u00e0 laver de l'AE</li> <li>Dossier contenant les fichiers de traduction.</li> <li>Fonctionnalit\u00e9s de recherche d'utilisateurs.</li> <li>Le guide des UEs du site, sur lequel les utilisateurs peuvent \u00e9galement laisser leurs avis.</li> <li>Fonctionnalit\u00e9s utiles aux utilisateurs root.</li> <li>Le SAS, o\u00f9 l'on trouve toutes les photos de l'AE.</li> <li>Application principale du projet, contenant sa configuration. </li> <li>Gestion des cotisations des utilisateurs du site. </li> <li>Outil pour faciliter la fabrication des trombinoscopes de promo. </li> <li>Fonctionnalit\u00e9s pour g\u00e9rer le spam. </li> <li>Gestion des statics du site. Override le syst\u00e8me de statics de Django. Ajoute l'int\u00e9gration du scss et de webpack de mani\u00e8re transparente pour l'utilisateur. </li> <li>Fichier de configuration de coverage. </li> <li>Fichier de configuration de direnv. </li> <li>Fichier g\u00e9n\u00e9r\u00e9 automatiquement par Django. C'est lui qui permet d'appeler des commandes de gestion du projet avec la syntaxe <code>python ./manage.py &lt;nom de la commande&gt;</code></li> <li>Le fichier de configuration de la documentation, avec ses plugins et sa table des mati\u00e8res. </li> <li>Le fichier o\u00f9 sont d\u00e9clar\u00e9s les d\u00e9pendances et la configuration de certaines d'entre elles.</li> </ol>"},{"location":"tutorial/structure/#lapplication-principale","title":"L'application principale","text":"<p>L'application principale du projet est le package <code>sith</code>. Ce package contient les fichiers de configuration du projet, la racine des urls.</p> <p>Il est organis\u00e9 comme suit :</p> <pre><code>sith/\n\u251c\u2500\u2500 settings.py (1)\n\u251c\u2500\u2500 settings_custom.py (2)\n\u251c\u2500\u2500 toolbar_debug.py (3)\n\u251c\u2500\u2500 urls.py (4)\n\u2514\u2500\u2500 wsgi.py (5)\n</code></pre> <ol> <li>Fichier de configuration du projet. Ce fichier contient les param\u00e8tres de configuration du projet. Par exemple, il contient la liste des applications install\u00e9es dans le projet.</li> <li>Configuration maison pour votre environnement. Toute variable que vous d\u00e9finissez dans ce fichier sera prioritaire sur la configuration donn\u00e9e dans <code>settings.py</code>.</li> <li>Configuration de la barre de debug. C'est inutilis\u00e9 en prod, mais c'est tr\u00e8s pratique en d\u00e9veloppement.</li> <li>Fichier de configuration des urls du projet.</li> <li>Fichier de configuration pour le serveur WSGI. WSGI est un protocole de communication entre le serveur et les applications. Ce fichier ne vous servira sans doute pas sur un environnement de d\u00e9veloppement, mais il est n\u00e9cessaire en production.</li> </ol>"},{"location":"tutorial/structure/#les-applications","title":"Les applications","text":"<p>Les applications sont des packages Python. Dans ce projet, les applications sont g\u00e9n\u00e9ralement organis\u00e9es comme suit :</p> <pre><code>.\n\u251c\u2500\u2500 migrations/ (1)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 templates/ (2)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 static/ (3)\n\u2502 \u2514\u2500\u2500 webpack/ (4)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 api.py (5)\n\u251c\u2500\u2500 admin.py (6)\n\u251c\u2500\u2500 models.py (7)\n\u251c\u2500\u2500 tests.py (8)\n\u251c\u2500\u2500 schemas.py (9)\n\u251c\u2500\u2500 urls.py (10)\n\u2514\u2500\u2500 views.py (11)\n</code></pre> <ol> <li>Dossier contenant les migrations de la base de donn\u00e9es. Les migrations sont des fichiers Python qui permettent de mettre \u00e0 jour la base de donn\u00e9es. cf. Gestion des migrations</li> <li>Dossier contenant les templates jinja utilis\u00e9s par cette application.</li> <li>Dossier contenant les fichiers statics (js, css, scss) qui sont r\u00e9cp\u00e9r\u00e9e par Django.</li> <li>Dossier contenant du js qui sera process avec webpack. Le contenu sera automatiquement process et accessible comme si \u00e7a avait \u00e9t\u00e9 plac\u00e9 dans le dossier <code>static/webpack</code>.</li> <li>Fichier contenant les routes d'API li\u00e9es \u00e0 cette application</li> <li>Fichier de configuration de l'interface d'administration. Ce fichier permet de d\u00e9clarer les mod\u00e8les de l'application dans l'interface d'administration.</li> <li>Fichier contenant les mod\u00e8les de l'application. Les mod\u00e8les sont des classes Python qui repr\u00e9sentent les tables de la base de donn\u00e9es.</li> <li>Fichier contenant les tests de l'application.</li> <li>Sch\u00e9mas de validation de donn\u00e9es utilis\u00e9s principalement dans l'API.</li> <li>Configuration des urls de l'application.</li> <li>Fichier contenant les vues de l'application. Dans les plus grosses applications, ce fichier peut \u00eatre remplac\u00e9 par un package <code>views</code> dans lequel les vues sont r\u00e9parties entre plusieurs fichiers.</li> </ol> <p>L'organisation peut \u00e9ventuellement \u00eatre un peu diff\u00e9rente pour certaines applications, mais le principe g\u00e9n\u00e9ral est le m\u00eame.</p>"}]}