diff --git a/asciidoc-to-tex.tex b/asciidoc-to-tex.tex index d468111..fd34f65 100644 --- a/asciidoc-to-tex.tex +++ b/asciidoc-to-tex.tex @@ -75,636 +75,9 @@ environnement}\label{_structure_finale_de_notre_environnement}} -\hypertarget{_ruxe9initialisation_dune_ou_plusieurs_migrations}{% -\subsection{Réinitialisation d'une ou plusieurs -migrations}\label{_ruxe9initialisation_dune_ou_plusieurs_migrations}} - -\hypertarget{_shell}{% -\section{Shell}\label{_shell}} - -\hypertarget{_administration}{% -\section{Administration}\label{_administration}} - -Woké. On va commencer par la \textbf{partie à ne \emph{surtout} -(\emph{surtout} !!) pas faire en premier dans un projet Django}. Mais on -va la faire quand même: la raison principale est que c - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} -\ImportTok{from}\NormalTok{ django.urls }\ImportTok{import}\NormalTok{ path} - -\ImportTok{from}\NormalTok{ gwift.views }\ImportTok{import}\NormalTok{ wish\_details} - -\NormalTok{urlpatterns }\OperatorTok{=}\NormalTok{ [} -\NormalTok{ path(}\StringTok{\textquotesingle{}admin/\textquotesingle{}}\NormalTok{, admin.site.urls), } -\NormalTok{ [...]} -\NormalTok{]} -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - Cette URL signifie que la partie \texttt{admin} est déjà active et - accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin} -\end{itemize} - -C'est le seul prérequis pour cette partie. - -Chaque application nouvellement créée contient par défaut un fichier -\texttt{admin.py}, dans lequel il est possible de déclarer quel ensemble -de données sera accessible/éditable. Ainsi, si nous partons du modèle -basique que nous avions détaillé plus tôt, avec des souhaits et des -listes de souhaits: - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# gwift/wish/models.py} - -\ImportTok{from}\NormalTok{ django.db }\ImportTok{import}\NormalTok{ models} - - -\KeywordTok{class}\NormalTok{ WishList(models.Model):} -\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} - - -\KeywordTok{class}\NormalTok{ Item(models.Model):} -\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)} -\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ models.ForeignKey(WishList, on\_delete}\OperatorTok{=}\NormalTok{models.CASCADE)} -\end{Highlighting} -\end{Shaded} - -Nous pouvons facilement arriver au résultat suivant, en ajoutant -quelques lignes de configuration dans ce fichier \texttt{admin.py}: - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ django.contrib }\ImportTok{import}\NormalTok{ admin} - -\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ Item, WishList } - - -\NormalTok{admin.site.register(Item) } -\NormalTok{admin.site.register(WishList)} -\end{Highlighting} -\end{Shaded} - -\begin{itemize} -\item - Nous importons les modèles que nous souhaitons gérer dans l'admin -\item - Et nous les déclarons comme gérables. Cette dernière ligne implique - aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui - n'activera simplement aucune opération de lecture ou modification. -\end{itemize} - -Il nous reste une seule étape à réaliser: créer un nouvel utilisateur. -Pour cet exemple, notre gestion va se limiter à une gestion manuelle; -nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons -créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}. - -\begin{Shaded} -\begin{Highlighting}[] -\NormalTok{λ }\ExtensionTok{python}\NormalTok{ manage.py createsuperuser} -\ExtensionTok{Username}\NormalTok{ (leave blank to use }\StringTok{\textquotesingle{}fred\textquotesingle{}}\NormalTok{)}\BuiltInTok{:}\NormalTok{ fred} -\ExtensionTok{Email}\NormalTok{ address: fred@root.org} -\ExtensionTok{Password}\NormalTok{: ******} -\ExtensionTok{Password}\NormalTok{ (again)}\BuiltInTok{:}\NormalTok{ ******} -\ExtensionTok{Superuser}\NormalTok{ created successfully.} -\end{Highlighting} -\end{Shaded} - -\begin{figure} -\centering -\includegraphics{images/django/django-site-admin.png} -\caption{Connexion au site d'administration} -\end{figure} - -\begin{figure} -\centering -\includegraphics{images/django/django-site-admin-after-connection.png} -\caption{Administration} -\end{figure} - -\hypertarget{_quelques_conseils_de_base}{% -\subsection{Quelques conseils de -base}\label{_quelques_conseils_de_base}} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Surchargez la méthode \texttt{str(self)} pour chaque classe que vous - aurez définie dans le modèle. Cela permettra de construire une - représentation textuelle pour chaque instance de votre classe. Cette - information sera utilisée un peu partout dans le code, et donnera une - meilleure idée de ce que l'on manipule. En plus, cette méthode est - également appelée lorsque l'administration historisera une action (et - comme cette étape sera inaltérable, autant qu'elle soit fixée dans le - début). -\item - La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle - on peut accéder pour obtenir les détails d'une instance. Par exemple: -\end{enumerate} - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{def}\NormalTok{ get\_absolute\_url(}\VariableTok{self}\NormalTok{):} - \ControlFlowTok{return}\NormalTok{ reverse(}\StringTok{\textquotesingle{}myapp.views.details\textquotesingle{}}\NormalTok{, args}\OperatorTok{=}\NormalTok{[}\VariableTok{self}\NormalTok{.}\BuiltInTok{id}\NormalTok{])} -\end{Highlighting} -\end{Shaded} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Les attributs \texttt{Meta}: -\end{enumerate} - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ ordering }\OperatorTok{=}\NormalTok{ [}\StringTok{\textquotesingle{}{-}field1\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}field2\textquotesingle{}}\NormalTok{]} -\NormalTok{ verbose\_name }\OperatorTok{=} \StringTok{\textquotesingle{}my class in singular\textquotesingle{}} -\NormalTok{ verbose\_name\_plural }\OperatorTok{=} \StringTok{\textquotesingle{}my class when is in a list!\textquotesingle{}} -\end{Highlighting} -\end{Shaded} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Le titre: - - \begin{itemize} - \item - Soit en modifiant le template de l'administration - \item - Soit en ajoutant l'assignation suivante dans le fichier - \texttt{urls.py}: - \texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}. - \end{itemize} -\item - Prefetch -\end{enumerate} - -\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64} - -\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614} - -En gros, le problème de l'admin est que si on fait des requêtes -imbriquées, on va flinguer l'application et le chargement de la page. La -solution consiste à utiliser la propriété \texttt{list\_select\_related} -de la classe d'Admin, afin d'appliquer une jointure par défaut et et -gagner en performances. - -\hypertarget{_admin_modeladmin}{% -\subsection{admin.ModelAdmin}\label{_admin_modeladmin}} - -La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement -dans le fichier \texttt{admin.py} de chaque application contiendra la -définition de ce que l'on souhaite faire avec nos données dans -l'administration. Cette classe (et sa partie Meta) - -\hypertarget{_laffichage}{% -\subsection{L'affichage}\label{_laffichage}} - -Comme l'interface d'administration fonctionne (en trèèèès) gros comme un -CRUD auto-généré, on trouve par défaut la possibilité de : - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Créer de nouveaux éléments -\item - Lister les éléments existants -\item - Modifier des éléments existants -\item - Supprimer un élément en particulier. -\end{enumerate} - -Les affichages sont donc de deux types: en liste et par élément. - -Pour les affichages en liste, le plus simple consiste à jouer sur la -propriété \texttt{list\_display}. - -Par défaut, la première colonne va accueillir le lien vers le formulaire -d'édition. On peut donc modifier ceci, voire créer de nouveaux liens -vers d'autres éléments en construisant des URLs dynamiquement. - -(Insérer ici l'exemple de Medplan pour les liens vers les postgradués -:-)) - -Voir aussi comment personnaliser le fil d'Ariane ? - -\hypertarget{_les_filtres}{% -\subsection{Les filtres}\label{_les_filtres}} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - list\_filter -\item - filter\_horizontal -\item - filter\_vertical -\item - date\_hierarchy -\end{enumerate} - -\hypertarget{_les_permissions}{% -\subsection{Les permissions}\label{_les_permissions}} - -On l'a dit plus haut, il vaut mieux éviter de proposer un accès à -l'administration à vos utilisateurs. Il est cependant possible de -configurer des permissions spécifiques pour certains groupes, en leur -autorisant certaines actions de visualisation/ajout/édition ou -suppression. - -Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les -méthodes suivantes: - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{def}\NormalTok{ has\_add\_permission(}\VariableTok{self}\NormalTok{, request):} - \ControlFlowTok{return} \VariableTok{True} - -\KeywordTok{def}\NormalTok{ has\_delete\_permission(}\VariableTok{self}\NormalTok{, request):} - \ControlFlowTok{return} \VariableTok{True} - -\KeywordTok{def}\NormalTok{ has\_change\_permission(}\VariableTok{self}\NormalTok{, request):} - \ControlFlowTok{return} \VariableTok{True} -\end{Highlighting} -\end{Shaded} - -On peut accéder aux informations de l'utilisateur actuellement connecté -au travers de l'objet \texttt{request.user}. - -\begin{enumerate} -\def\labelenumi{\alph{enumi}.} -\item - NOTE: ajouter un ou deux screenshots :-) -\end{enumerate} - -\hypertarget{_les_relations}{% -\subsection{Les relations}\label{_les_relations}} - -\hypertarget{_les_relations_1_n}{% -\subsubsection{Les relations 1-n}\label{_les_relations_1_n}} - -Les relations 1-n sont implémentées au travers de formsets (que l'on a -normalement déjà décrits plus haut). L'administration permet de les -définir d'une manière extrêmement simple, grâce à quelques propriétés. - -L'implémentation consiste tout d'abord à définir le comportement du type -d'objet référencé (la relation -N), puis à inclure cette définition au -niveau du type d'objet référençant (la relation 1-). - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{class}\NormalTok{ WishInline(TabularInline):} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Wish} - - -\KeywordTok{class}\NormalTok{ Wishlist(admin.ModelAdmin):} -\NormalTok{ ...} -\NormalTok{ inlines }\OperatorTok{=}\NormalTok{ [WishInline]} -\NormalTok{ ...} -\end{Highlighting} -\end{Shaded} - -Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist}) -pourra directement gérer des relations multiples vers des souhaits. - -\hypertarget{_les_auto_suggestions_et_auto_compluxe9tions}{% -\subsubsection{Les auto-suggestions et -auto-complétions}\label{_les_auto_suggestions_et_auto_compluxe9tions}} - -Parler de l'intégration de select2. - -\hypertarget{_la_pruxe9sentation}{% -\subsection{La présentation}\label{_la_pruxe9sentation}} - -Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper -des champs dans des groupes, ajouter un peu de javascript, -\ldots\hspace{0pt} - -\hypertarget{_les_actions_sur_des_suxe9lections}{% -\subsection{Les actions sur des -sélections}\label{_les_actions_sur_des_suxe9lections}} - -Les actions permettent de partir d'une liste d'éléments, et autorisent -un utilisateur à appliquer une action sur une sélection d'éléments. Par -défaut, il existe déjà une action de \textbf{suppression}. - -Les paramètres d'entrée sont : - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - L'instance de classe -\item - La requête entrante -\item - Le queryset correspondant à la sélection. -\end{enumerate} - -\begin{Shaded} -\begin{Highlighting}[] -\KeywordTok{def}\NormalTok{ double\_quantity(}\VariableTok{self}\NormalTok{, request, queryset):} - \ControlFlowTok{for}\NormalTok{ obj }\KeywordTok{in}\NormalTok{ queryset.}\BuiltInTok{all}\NormalTok{():} -\NormalTok{ obj.field }\OperatorTok{+=} \DecValTok{1} -\NormalTok{ obj.save()} -\NormalTok{double\_quantity.short\_description }\OperatorTok{=} \StringTok{"Doubler la quantité des souhaits."} -\end{Highlighting} -\end{Shaded} - -Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi -lui passer un petit message: - -\begin{Shaded} -\begin{Highlighting}[] -\ControlFlowTok{if}\NormalTok{ rows\_updated }\OperatorTok{=} \DecValTok{0}\NormalTok{:} - \VariableTok{self}\NormalTok{.message\_user(request, }\StringTok{"Aucun élément n\textquotesingle{}a été impacté."}\NormalTok{)} -\ControlFlowTok{else}\NormalTok{:} - \VariableTok{self}\NormalTok{.message\_user(request, }\StringTok{"}\SpecialCharTok{\{\}}\StringTok{ élément(s) mis à jour"}\NormalTok{.}\BuiltInTok{format}\NormalTok{(rows\_updated))} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_la_documentation}{% -\subsection{La documentation}\label{_la_documentation}} - -Nous l'avons dit plus haut, l'administration de Django a également la -possibilité de rendre accessible la documentation associée à un modèle -de données. Pour cela, il suffit de suivre les bonnes pratiques, puis -\href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer -la documentation à partir des URLs}: - -\begin{Shaded} -\begin{Highlighting}[] - -\end{Highlighting} -\end{Shaded} - -\hypertarget{_forms}{% -\section{Forms}\label{_forms}} - -\begin{quote} -Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et -que l'ensemble reste cohérent. Il (le form) n'a pas à savoir que tu as -implémenté des closure tables dans un graph dirigé acyclique. -\end{quote} - -Ou comment valider proprement des données entrantes. - -\includegraphics{images/xkcd-327.png} - -Quand on parle de \texttt{forms}, on ne parle pas uniquement de -formulaires Web. On pourrait considérer qu'il s'agit de leur objectif -principal, mais on peut également voir un peu plus loin: on peut en fait -voir les \texttt{forms} comme le point d'entrée pour chaque donnée -arrivant dans notre application: il s'agit en quelque sorte d'un -ensemble de règles complémentaires à celles déjà présentes au niveau du -modèle. - -L'exemple le plus simple est un fichier \texttt{.csv}: la lecture de ce -fichier pourrait se faire de manière très simple, en récupérant les -valeurs de chaque colonne et en l'introduisant dans une instance du -modèle. - -Mauvaise idée. On peut proposer trois versions d'un même code, de la -version simple (lecture du fichier csv et jonglage avec les indices de -colonnes), puis une version plus sophistiquée (et plus lisible, à base -de -\href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}), -et la version + à base de form. - -Les données fournies par un utilisateur \textbf{doivent} -\textbf{toujours} être validées avant introduction dans la base de -données. Notre base de données étant accessible ici par l'ORM, la -solution consiste à introduire une couche supplémentaire de validation. - -Le flux à suivre est le suivant: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Création d'une instance grâce à un dictionnaire -\item - Validation des données et des informations reçues -\item - Traitement, si la validation a réussi. -\end{enumerate} - -Ils jouent également deux rôles importants: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Valider des données, en plus de celles déjà définies au niveau du - modèle -\item - Contrôler le rendu à appliquer aux champs. -\end{enumerate} - -Ils agissent come une glue entre l'utilisateur et la modélisation de vos -structures de données. - -\hypertarget{_flux_de_validation}{% -\subsection{Flux de validation}\label{_flux_de_validation}} - -\textbar{} .Validation \textbar{} .is\_valid \textbar{} .clean\_fields ↓ -.clean\_fields\_machin - -A compléter ;-) - -\hypertarget{_duxe9pendance_avec_le_moduxe8le}{% -\subsection{Dépendance avec le -modèle}\label{_duxe9pendance_avec_le_moduxe8le}} - -Un \textbf{form} peut dépendre d'une autre classe Django. Pour cela, il -suffit de fixer l'attribut \texttt{model} au niveau de la -\texttt{class\ Meta} dans la définition. - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ django }\ImportTok{import}\NormalTok{ forms} - -\ImportTok{from}\NormalTok{ wish.models }\ImportTok{import}\NormalTok{ Wishlist} - -\KeywordTok{class}\NormalTok{ WishlistCreateForm(forms.ModelForm):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Wishlist} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}name\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}description\textquotesingle{}}\NormalTok{)} -\end{Highlighting} -\end{Shaded} - -De cette manière, notre form dépendra automatiquement des champs déjà -déclarés dans la classe \texttt{Wishlist}. Cela suit le principe de -\texttt{DRY\ \textless{}don’t\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ qu’une\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ l’attribut\ \textasciigrave{}fields}, -nous pourrons nous assurer de faire évoluer le formulaire en fonction du -modèle sur lequel il se base. - -\hypertarget{_rendu_et_affichage}{% -\subsection{Rendu et affichage}\label{_rendu_et_affichage}} - -Le formulaire permet également de contrôler le rendu qui sera appliqué -lors de la génération de la page. Si les champs dépendent du modèle sur -lequel se base le formulaire, ces widgets doivent être initialisés dans -l'attribut \texttt{Meta}. Sinon, ils peuvent l'être directement au -niveau du champ. - -\begin{Shaded} -\begin{Highlighting}[] -\ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ date} - -\ImportTok{from}\NormalTok{ django }\ImportTok{import}\NormalTok{ forms} - -\ImportTok{from}\NormalTok{ .models }\ImportTok{import}\NormalTok{ Accident} - - -\KeywordTok{class}\NormalTok{ AccidentForm(forms.ModelForm):} - \KeywordTok{class}\NormalTok{ Meta:} -\NormalTok{ model }\OperatorTok{=}\NormalTok{ Accident} -\NormalTok{ fields }\OperatorTok{=}\NormalTok{ (}\StringTok{\textquotesingle{}gymnast\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}educative\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}date\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}information\textquotesingle{}}\NormalTok{)} -\NormalTok{ widgets }\OperatorTok{=}\NormalTok{ \{} - \StringTok{\textquotesingle{}date\textquotesingle{}}\NormalTok{ : forms.TextInput(} -\NormalTok{ attrs}\OperatorTok{=}\NormalTok{\{} - \StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}form{-}control\textquotesingle{}}\NormalTok{,} - \StringTok{\textquotesingle{}data{-}provide\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}datepicker\textquotesingle{}}\NormalTok{,} - \StringTok{\textquotesingle{}data{-}date{-}format\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}dd/mm/yyyy\textquotesingle{}}\NormalTok{,} - \StringTok{\textquotesingle{}placeholder\textquotesingle{}}\NormalTok{ : date.today().strftime(}\StringTok{"}\SpecialCharTok{\%d}\StringTok{/\%m/\%Y"}\NormalTok{)} -\NormalTok{ \}),} - \StringTok{\textquotesingle{}information\textquotesingle{}}\NormalTok{ : forms.Textarea(} -\NormalTok{ attrs}\OperatorTok{=}\NormalTok{\{} - \StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}form{-}control\textquotesingle{}}\NormalTok{,} - \StringTok{\textquotesingle{}placeholder\textquotesingle{}}\NormalTok{ : }\StringTok{\textquotesingle{}Context (why, where, ...)\textquotesingle{}} -\NormalTok{ \})} -\NormalTok{ \}} -\end{Highlighting} -\end{Shaded} - -\hypertarget{_squelette_par_duxe9faut}{% -\subsection{Squelette par défaut}\label{_squelette_par_duxe9faut}} - -On a d'un côté le \{\{ form.as\_p \}\} ou \{\{ form.as\_table \}\}, mais -il y a beaucoup mieux que ça ;-) Voir les templates de Vitor et en -passant par \texttt{widget-tweaks}. - -\hypertarget{_crispy_forms}{% -\subsection{Crispy-forms}\label{_crispy_forms}} - -Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela -permet de valider des données reçues en entrée et d'afficher (très) -facilement des formulaires à compléter par l'utilisateur. - -Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu -l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, -modifier les types de contrôleurs, les placer au pixel près, -\ldots\hspace{0pt} Tout ça demande énormément de temps. Et c'est là -qu'intervient -\href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}. -Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation -et uni-form) et permet de contrôler entièrement le \textbf{layout} et la -présentation. - -(c/c depuis le lien ci-dessous) - -Pour chaque champ, crispy-forms va : - -\begin{itemize} -\item - utiliser le \texttt{verbose\_name} comme label. -\item - vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si - le champ est obligatoire. -\item - utiliser le type de champ pour définir le type de la balise - \texttt{\textless{}input\textgreater{}}. -\item - récupérer les valeurs du paramètre \texttt{choices} (si présent) pour - la balise \texttt{\textless{}select\textgreater{}}. -\end{itemize} - -\url{http://dotmobo.github.io/django-crispy-forms.html} - -\hypertarget{_en_conclusion}{% -\subsection{En conclusion}\label{_en_conclusion}} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Toute donnée entrée par l'utilisateur \textbf{doit} passer par une - instance de \texttt{form}. -\item - euh ? -\end{enumerate} - \hypertarget{_authentification}{% \section{Authentification}\label{_authentification}} -Comme on l'a vu dans la partie sur le modèle, nous souhaitons que le -créateur d'une liste puisse retrouver facilement les éléments qu'il aura -créé. Ce dont nous n'avons pas parlé cependant, c'est la manière dont -l'utilisateur va pouvoir créer son compte et s'authentifier. La -\href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation} -est très complète, nous allons essayer de la simplifier au maximum. -Accrochez-vous, le sujet peut être complexe. - -\hypertarget{_muxe9canisme_dauthentification}{% -\subsection{Mécanisme -d'authentification}\label{_muxe9canisme_dauthentification}} - -On peut schématiser le flux d'authentification de la manière suivante : - -En gros: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - La personne accède à une URL qui est protégée (voir les décorateurs - @login\_required et le mixin LoginRequiredMixin) -\item - Le framework détecte qu'il est nécessaire pour la personne de se - connecter (grâce à un paramètre type LOGIN\_URL) -\item - Le framework présente une page de connexion ou un mécanisme d'accès - pour la personne (template à définir) -\item - Le framework récupère les informations du formulaire, et les transmets - aux différents backends d'authentification, dans l'ordre -\item - Chaque backend va appliquer la méthode \texttt{authenticate} en - cascade, jusqu'à ce qu'un backend réponde True ou qu'aucun ne réponde -\item - La réponse de la méthode authenticate doit être une instance d'un - utilisateur, tel que définit parmi les paramètres généraux de - l'application. -\end{enumerate} - -En résumé (bis): - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Une personne souhaite se connecter; -\item - Les backends d'authentification s'enchaîne jusqu'à trouver une bonne - correspondance. Si aucune correspondance n'est trouvée, on envoie la - personne sur les roses. -\item - Si OK, on retourne une instance de type current\_user, qui pourra être - utilisée de manière uniforme dans l'application. -\end{enumerate} - -Ci-dessous, on définit deux backends différents pour mieux comprendre -les différentes possibilités: - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\item - Une authentification par jeton -\item - Une authentification LDAP -\end{enumerate} \begin{Shaded} \begin{Highlighting}[] diff --git a/chapters/administration.tex b/chapters/administration.tex index 7f5a483..1b528c9 100644 --- a/chapters/administration.tex +++ b/chapters/administration.tex @@ -38,3 +38,275 @@ Comme expliqué ci-dessus, le modèle de données est constité d'un ensemble de Si vous vous rappelez de l'application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante: +\begin{minted}{python} +from django.contrib import admin +from django.urls import path +from gwift.views import wish_details + +urlpatterns = [ + path('admin/', admin.site.urls), + [...] +] +\end{minted} + +Cette URL signifie que la partie \texttt{admin} est déjà active et accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}. +C'est le seul prérequis pour cette partie. + +Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer quel ensemble de données sera accessible/éditable. +Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits: + +\begin{minted}{python} +# gwift/wish/models.py + +from django.db import models + + +class WishList(models.Model): + name = models.CharField(max_length=255) + + +class Item(models.Model): + name = models.CharField(max_length=255) + wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE) +\end{minted} + +Nous pouvons facilement arriver au résultat suivant, en ajoutant +quelques lignes de configuration dans ce fichier \texttt{admin.py}: + +\begin{minted}{python} +from django.contrib import admin + +from .models import Item, WishList + + +admin.site.register(Item) +admin.site.register(WishList) +\end{minted} + + +\begin{itemize} + \item + Nous importons les modèles que nous souhaitons gérer dans l'admin + \item + Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui n'activera simplement aucune opération de lecture ou modification. +\end{itemize} + +Il nous reste une seule étape à réaliser: créer un nouvel utilisateur. +Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}. + +\begin{verbatim} + $ python manage.py createsuperuser + Username (leave blank to use 'fred'): fred + Email address: fred@root.org + Password: ****** + Password (again): ****** + Superuser created successfully. +\end{verbatim} + + +\begin{figure}[H] + \centering + \includegraphics{images/django/django-site-admin.png} + \caption{Connexion au site d'administration} +\end{figure} + +\begin{figure}[H] + \centering + \includegraphics{images/django/django-site-admin-after-connection.png} + \caption{Administration} +\end{figure} + +\section{Quelques conseils de base} + +\begin{enumerate} + \item + Surchargez la méthode \texttt{str(self)} pour chaque classe que vous aurez définie dans le modèle. + Cela permettra de construire une représentation textuelle pour chaque instance de votre classe. + Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule. + En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début). + \item + La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle on peut accéder pour obtenir les détails d'une instance. Par exemple: +\end{enumerate} + +\begin{minted}{python} +def get_absolute_url(self): + return reverse('myapp.views.details', args=[self.id]) +\end{minted} + + \begin{enumerate} + \def\labelenumi{\arabic{enumi}.} + \item + Les attributs \texttt{Meta}: + \end{enumerate} + +\begin{minted}{python} +class Meta: + ordering = ['-field1', 'field2'] + verbose_name = 'my class in singular' + verbose_name_plural = 'my class when is in a list!' +\end{minted} + +\begin{enumerate} + + \item + Le titre: + + \begin{itemize} + \item + Soit en modifiant le template de l'administration + \item + Soit en ajoutant l'assignation suivante dans le fichier + \texttt{urls.py}: + \texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}. + \end{itemize} + \item + Prefetch +\end{enumerate} + +\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64} + +\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614} + +En gros, le problème de l'admin est que si on fait des requêtes +imbriquées, on va flinguer l'application et le chargement de la page. La +solution consiste à utiliser la propriété \texttt{list\_select\_related} +de la classe d'Admin, afin d'appliquer une jointure par défaut et et +gagner en performances. + +\subsection{admin.ModelAdmin} + +La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement +dans le fichier \texttt{admin.py} de chaque application contiendra la +définition de ce que l'on souhaite faire avec nos données dans +l'administration. Cette classe (et sa partie Meta) + +\subsection{L'affichage} + +Comme l'interface d'administration fonctionne (en trèèèès) gros comme un +CRUD auto-généré, on trouve par défaut la possibilité de : + +\begin{enumerate} +\def\labelenumi{\arabic{enumi}.} +\item + Créer de nouveaux éléments +\item + Lister les éléments existants +\item + Modifier des éléments existants +\item + Supprimer un élément en particulier. +\end{enumerate} + +Les affichages sont donc de deux types: en liste et par élément. + +Pour les affichages en liste, le plus simple consiste à jouer sur la propriété \texttt{list\_display}. + +Par défaut, la première colonne va accueillir le lien vers le formulaire d'édition. +On peut donc modifier ceci, voire créer de nouveaux liens vers d'autres éléments en construisant des URLs dynamiquement. + +Voir aussi comment personnaliser le fil d'Ariane ? + +\section{Filtres} + +\begin{enumerate} +\def\labelenumi{\arabic{enumi}.} +\item + list\_filter +\item + filter\_horizontal +\item + filter\_vertical +\item + date\_hierarchy +\end{enumerate} + +\section{Permissions} + +On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs. +Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression. + +Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les méthodes suivantes: + +\begin{minted}{python} +def has_add_permission(self, request): + return True + +def has_delete_permission(self, request): + return True + +def has_change_permission(self, request): + return True +\end{minted} + +On peut accéder aux informations de l'utilisateur actuellement connecté au travers de l'objet \texttt{request.user}. + +\section{Relations} + +\subsection{Relations 1-N} + +Les relations 1-n sont implémentées au travers de formsets (que l'on a normalement déjà décrits plus haut). L'administration permet de les définir d'une manière extrêmement simple, grâce à quelques propriétés. + +L'implémentation consiste tout d'abord à définir le comportement du type d'objet référencé (la relation -N), puis à inclure cette définition au niveau du type d'objet référençant (la relation 1-). + +\begin{minted}{python} +class WishInline(TabularInline): + model = Wish + +class Wishlist(admin.ModelAdmin): + ... + inlines = [WishInline] + ... +\end{minted} + +Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist}) pourra directement gérer des relations multiples vers des souhaits. + +\subsection{Autocomplétion} + +Parler de l'intégration de select2. + +\section{Présentation} + +Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper des champs dans des groupes, ajouter un peu de JavaScript, ... + +\section{Actions sur des sélections} + +Les actions permettent de partir d'une liste d'éléments, et autorisent +un utilisateur à appliquer une action sur une sélection d'éléments. Par +défaut, il existe déjà une action de \textbf{suppression}. + +Les paramètres d'entrée sont : + +\begin{enumerate} +\def\labelenumi{\arabic{enumi}.} +\item + L'instance de classe +\item + La requête entrante +\item + Le queryset correspondant à la sélection. +\end{enumerate} + +\begin{minted}{python} +def double_quantity(self, request, queryset): + for obj in queryset.all(): + obj.field += 1 + obj.save() + double_quantity.short_description = "Doubler la quantité des souhaits." +\end{minted} + + +Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi +lui passer un petit message: + +\begin{minted}{python} +if rows_updated = 0: + self.message_user(request, "Aucun élément n'a été impacté.") +else: + self.message_user(request, "{} élément(s) mis à jour".format(rows_updated)) +\end{minted} + +\section{Documentation} + +Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données. +Pour cela, il suffit de suivre les bonnes pratiques, puis \href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer la documentation à partir des URLs}: + diff --git a/chapters/authentication.tex b/chapters/authentication.tex new file mode 100644 index 0000000..76d14b7 --- /dev/null +++ b/chapters/authentication.tex @@ -0,0 +1,82 @@ +\chapter{Authentification} + +Comme on l'a vu dans la partie sur le modèle, nous souhaitons que le créateur d'une liste puisse retrouver facilement les éléments qu'il aura créée. +Ce dont nous n'avons pas parlé cependant, c'est la manière dont l'utilisateur va pouvoir créer son compte et s'authentifier. +La \href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation} est très complète, nous allons essayer de la simplifier au maximum. +Accrochez-vous, le sujet peut être complexe. + +\section{Mécanisme d'authentification} + +On peut schématiser le flux d'authentification de la manière suivante : + +En gros: + +\begin{enumerate} +\item + La personne accède à une URL qui est protégée (voir les décorateurs @login\_required et le mixin LoginRequiredMixin) +\item + Le framework détecte qu'il est nécessaire pour la personne de se connecter (grâce à un paramètre type LOGIN\_URL) +\item + Le framework présente une page de connexion ou un mécanisme d'accès pour la personne (template à définir) +\item + Le framework récupère les informations du formulaire, et les transmets aux différents backends d'authentification, dans l'ordre +\item + Chaque backend va appliquer la méthode \texttt{authenticate} en cascade, jusqu'à ce qu'un backend réponde True ou qu'aucun ne réponde +\item + La réponse de la méthode authenticate doit être une instance d'un utilisateur, tel que définit parmi les paramètres généraux de l'application. +\end{enumerate} + +En résumé (bis): + +\begin{enumerate} +\item + Une personne souhaite se connecter; +\item + Les backends d'authentification s'enchaîne jusqu'à trouver une bonne + correspondance. Si aucune correspondance n'est trouvée, on envoie la + personne sur les roses. +\item + Si OK, on retourne une instance de type current\_user, qui pourra être + utilisée de manière uniforme dans l'application. +\end{enumerate} + +Ci-dessous, on définit deux backends différents pour mieux comprendre +les différentes possibilités: + +\begin{enumerate} +\item + Une authentification par jeton +\item + Une authentification LDAP +\end{enumerate} + +\begin{minted}{python} +from datetime import datetime + +from django.contrib.auth import backends, get_user_model +from django.db.models import Q + +from accounts.models import Token + +UserModel = get_user_model() + +class TokenBackend(backends.ModelBackend): + def authenticate(self, request, username=None, password=None, **kwargs): + """Authentifie l'utilisateur sur base d'un jeton qu'il a reçu. + On regarde la date de validité de chaque jeton avant d'autoriser + l'accès. + """ + token = kwargs.get("token", None) + current_token = Token.objects.filter( + token=token, validity_date__gte=datetime.now() + ).first() + + if current_token: + user = current_token.user + current_token.last_used_date = datetime.now() + current_token.save() + + return user + + return None +\end{minted} \ No newline at end of file diff --git a/chapters/forms.tex b/chapters/forms.tex new file mode 100644 index 0000000..70b8e1e --- /dev/null +++ b/chapters/forms.tex @@ -0,0 +1,125 @@ +\chapter{Forms} + +\includegraphics{images/xkcd-327.png} + + +\begin{quote} + Le form, il s'assure que l'utilisateur n'a pas encodé de conneries et + que l'ensemble reste cohérent. Il (le form) n'a pas à savoir que tu as + implémenté des closure tables dans un graph dirigé acyclique. +\end{quote} + +Ou comment valider proprement des données entrantes. + +Quand on parle de \texttt{forms}, on ne parle pas uniquement de formulaires Web. +On pourrait considérer qu'il s'agit de leur objectif principal, mais on peut également voir un peu plus loin: on peut en fait voir les \texttt{forms} comme le point d'entrée pour chaque donnée arrivant dans notre application: il s'agit en quelque sorte d'un ensemble de règles complémentaires à celles déjà présentes au niveau du modèle. + +L'exemple le plus simple est un fichier \texttt{.csv}: la lecture de ce fichier pourrait se faire de manière très simple, en récupérant les valeurs de chaque colonne et en l'introduisant dans une instance du modèle. + +Mauvaise idée. On peut proposer trois versions d'un même code, de la version simple (lecture du fichier csv et jonglage avec les indices de colonnes), puis une version plus sophistiquée (et plus lisible, à base +de \href{https://docs.python.org/3/library/csv.html\#csv.DictReader}{DictReader}), et la version + à base de form. + +Les données fournies par un utilisateur \textbf{doivent} \textbf{toujours} être validées avant introduction dans la base de données. +Notre base de données étant accessible ici par l'ORM, la solution consiste à introduire une couche supplémentaire de validation. + +Le flux à suivre est le suivant: + +\begin{enumerate} +\item + Création d'une instance grâce à un dictionnaire +\item + Validation des données et des informations reçues +\item + Traitement, si la validation a réussi. +\end{enumerate} + +Ils jouent également deux rôles importants: + +\begin{enumerate} +\item + Valider des données, en plus de celles déjà définies au niveau du modèle +\item + Contrôler le rendu à appliquer aux champs. +\end{enumerate} + +Ils agissent come une glue entre l'utilisateur et la modélisation de vos structures de données. + +\section{Flux de validation} + +\textbar{} .Validation \textbar{} .is\_valid \textbar{} .clean\_fields ↓ +.clean\_fields\_machin + +A compléter ;-) + +\section{Dépendance avec le modèle} + +Un \textbf{form} peut dépendre d'une autre classe Django. +Pour cela, il suffit de fixer l'attribut \texttt{model} au niveau de la \texttt{class\ Meta} dans la définition. + +\begin{minted}{python} +from django import forms + +from wish.models import Wishlist + + +class WishlistCreateForm(forms.ModelForm): + class Meta: + model = Wishlist + fields = ('name', 'description') +\end{minted} + +De cette manière, notre form dépendra automatiquement des champs déjà déclarés dans la classe \texttt{Wishlist}. +Cela suit le principe de \texttt{DRY\ \textless{}don’t\ repeat\ yourself\textgreater{}\textasciigrave{}\_,\ et\ évite\ qu’une\ modification\ ne\ pourrisse\ le\ code:\ en\ testant\ les\ deux\ champs\ présent\ dans\ l’attribut\ \textasciigrave{}fields}, +nous pourrons nous assurer de faire évoluer le formulaire en fonction du +modèle sur lequel il se base. + +\section{Rendu et affichage} + +Le formulaire permet également de contrôler le rendu qui sera appliqué lors de la génération de la page. +Si les champs dépendent du modèle sur lequel se base le formulaire, ces widgets doivent être initialisés dans +l'attribut \texttt{Meta}. +Sinon, ils peuvent l'être directement au niveau du champ. + +\subsection{Squelette par défaut} + +On a d'un côté le \{\{ form.as\_p \}\} ou \{\{ form.as\_table \}\}, mais il y a beaucoup mieux que ça ;-) Voir les templates de Vitor et en passant par \texttt{widget-tweaks}. + +\subsection{Crispy-forms} + +Comme on l'a vu à l'instant, les forms, en Django, c'est le bien. Cela +permet de valider des données reçues en entrée et d'afficher (très) +facilement des formulaires à compléter par l'utilisateur. + +Par contre, c'est lourd. Dès qu'on souhaite peaufiner un peu +l'affichage, contrôler parfaitement ce que l'utilisateur doit remplir, +modifier les types de contrôleurs, les placer au pixel près, +\ldots\hspace{0pt} Tout ça demande énormément de temps. Et c'est là +qu'intervient +\href{http://django-crispy-forms.readthedocs.io/en/latest/}{Django-Crispy-Forms}. +Cette librairie intègre plusieurs frameworks CSS (Bootstrap, Foundation +et uni-form) et permet de contrôler entièrement le \textbf{layout} et la +présentation. + +(c/c depuis le lien ci-dessous) + +Pour chaque champ, crispy-forms va : + +\begin{itemize} +\item + utiliser le \texttt{verbose\_name} comme label. +\item + vérifier les paramètres \texttt{blank} et \texttt{null} pour savoir si + le champ est obligatoire. +\item + utiliser le type de champ pour définir le type de la balise + \texttt{\textless{}input\textgreater{}}. +\item + récupérer les valeurs du paramètre \texttt{choices} (si présent) pour + la balise \texttt{\textless{}select\textgreater{}}. +\end{itemize} + +\url{http://dotmobo.github.io/django-crispy-forms.html} + +\section{Conclusions} + +Toute donnée entrée par l'utilisateur \textbf{doit} passer par une instance de \texttt{form}. diff --git a/main.tex b/main.tex index bffbfb2..40d0b21 100644 --- a/main.tex +++ b/main.tex @@ -58,10 +58,8 @@ \include{chapters/models.tex} \include{chapters/migrations.tex} - -\chapter{Administration} - -\chapter{Forms} +\include{chapters/administration.tex} +\include{chapters/forms.tex} \chapter{Processus d'authentification}