Mots-clé : django

Django >= 1.11 : faire des widgets de formulaires sur mesure

Les « widgets » sont les composants affichés dans les formulaires qui permettent la saisie des informations (combo, checkboxes, etc.).
A partir de la version 1.11, les widgets ne sont plus du tout construits comme sur les versions précédentes.
Ce qu’il faut retenir : avant, c’était le développeur qui pouvait surcharger les widgets, j’ai fait un petit article ici.
Ce qu’il faut retenir sur la nouvelle gestion des widgets : le graphiste peut avoir accès à l’habillage, et configurer sur mesure l’affichage des widgets. C’est un changement très important du point de vue de travail en équipe, surtout si vous avez un graphiste / front end dans votre équipe.
La documentation est très fournie, jetez un coup d’oeil par là.
Mais son inconvénient, c’est qu’elle est, justement, très fournie… voici donc un résumé.

« surcharger » les widgets

  • Dans settings.py, rajouter dans les applications installées (INSTALLED_APPS) l’application 'django.forms'
  • Toujours dans settings.py, dans la directive TEMPLATES, ajoutez la directive 'DIRS' s’il elle n’y est pas déjà, et précisez dans un tableau les répertoires supplémentaires, par exemple pour moi j’ai :
    'DIRS': [os.path.join(BASE_DIR, 'templates')],
  • Surchargez le composant qui vous intéresse en déclarant le template, exemple :
    class MonCheckboxSelectMultiple(CheckboxSelectMultiple):
        template_name = 'include/widget/checkbox_select.html'
  • Organisez vous dans votre dossier templates à l’identique de la déclaration. Pour remprendre mon exemple j’ai un fichier templates/include/widget/checkbox_select.html dans lequel j’ai copié collé le code source du template Django, puis je l’ai modifié pour qu’il convienne à mes besoins.

Allez chercher les templates, et profitez-en pour lire le code source des formulaires, vous verrez il est simple, et vous verrez comment les templates sont déclarés : Python36/Lib/site-packages/django/forms.

Django : mémo des formulaires « sur mesure »

Voici un mémo des « étapes » à remplir dans l’ordre pour faire un formulaire sur mesure en Django « dans l’ordre »:

  • Créer un formulaire. Classique. Faire un ModelForm et utiliser la classe Meta pour déclarer le modèle via model, et déclarer les champs via fields.
  • Construire ces champs du modèle dans le formulaire. Attention, ne pas confondre le types du champ (qui est en base = le champ du modèle), c’est ce que vous créez, et le widget dans lequel ce champ sera rendu (cf mon exemple dans l’article précédent).
  • Construire la vue, dans le cas update du principe CRUD, j’hérite de generic.UpdateView
  • Dans le formulaire d’origine, créer la fonction dit si le formulaire est valide ou pas : def is_valid(self)
  • Dans la vue, après que le formulaire ait été validé, écrire la fonction def form_valid(self, form) qui est appelée lorsque le formulaire est valide, et utiliser les données nettoyées du formulaire form.cleaned_data pour enregistrer ce que l’on veut
  • Pré-remplir les champs du formulaire, y compris les champs « sur mesure » : c’est dans la vue, via def get_initial(self)

Ouf ! Une fois que tout ça est implémenté, Django construit la vue en lecture (GET) selon un chemin qui ressemble en gros à ça :

  • vue -> form_class
  • forme -> class Meta -> model + fields
  • forme -> constructeur __init__ (ajout champs sur mesure)
  • vue -> get_initial (remplir les valeurs de tous les champs du formulaire)

Django construit la vue en écriture (POST) selon un chemin qui ressemble en gros à ça :

  • vue -> form_class
  • forme -> class Meta -> model + fields
  • forme -> constructeur __init__ (ajout champs sur mesure)
  • forme -> def is_valid(self)
  • forme -> def clean_xx(self) (code qui valide/ou pas le champ xx)
  • forme -> def clean(self) (code qui valide/ou pas tous les champs)
  • vue -> def form_valid(self, form) (sauver ici avec form.cleaned_data)

Alors oui je sais c’est compliqué. Mais quand on y réfléchit bien, sur le modèle MVC, on ne peut pas faire autrement, et c’est le mieux possible. Et encore je n’ai pas parlé de la routine « save() » qui est dans le formulaire : elle est automatiquement appelée lors is_valid() renvoie true, et dans le cas d’un ModelForm elle sauvegarde le formulaire.

Django : les formulaires « sur mesure »

Voici les étapes lorsqu’on sort des sentiers battus et qu’on veut créer un formulaire sur mesure, ainsi qu’une vue sur mesure.

J’ai toujours eu besoin d’un ModelForm : je garde les champs que je veux, voici un code basique qui crée le champ label, avec toutes les traductions nécessaires :

class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question
        fields = ('label', )
    e = {'required': _(u'This field is required'),
        'invalid': _(u'This field contains invalid data')}

    a = u'{}<span class="important-field">*</span>'.format(_(u'Question:'))
    label = forms.CharField(
        label=a, localize=True, required=True,
        widget=widgets.TextInput(attrs={
            'title': a,
            'class': 'form-control form-control'}),
        error_messages=e)

Ensuite pour cette classe, je crée un champ dynamiquement via le constructeur :

    def __init__(self, *args, **kwargs):
        super(QuestionForm, self).__init__(*args, **kwargs)
        # -----------------------------------------------------
        # Création dynamique de champs custom
        # self.fields est de type OrderedDict(), qui se base
        # sur l'ordre d'ajout des éléments. Alors si on veut
        # un autre ordre, pas d'autre choix que de reconstruire
        # le dictionnaire en y appliquant l'ordre qu'on veut :
        #
        # création dynamique de l'image :
        a = _(u'Picture')
        photo = ImageField(
            label=a, allow_empty_file=True, required=False,
            widget=forms.FileInput(attrs={
                'title': a,
                'placeholder': _(u'picture'),
                'class': 'form-control',
                'accept': "image/*", }),
            error_messages=e)
        new_fields = OrderedDict([
            ('label', self.fields['label']),
            ('photo', photo),
        ])
        self.fields = new_fields

Linux : compilation de Python en local

A l’IUT d’Aix en Provence, tous les ordinateurs sont gérés à distance, et sous Linux, aucune possibilité d’installer un logiciel.
Par contre, il est possible de compiler en local !

Compilation Python

Récupérez les dernières sources sur le site officiel.
Puis allez dans le dossier source et tapez :

./configure --prefix=$HOME/python

Puis, si tout s’est bien passé = pas de message d’erreur :

make && make install

A partir de là, sous PyCharm, précisez le chemin Python qui correspondra à votre $HOME/python et précisez que vous voulez installer les packages dans le répertoire de l’utilisateur courant (une case à cocher).

Et voilà : vous aurez un serveur Web Python et pourrez développer et installer tous les packages et librairies que vous voulez !

Django annotate : à quoi ça sert ?

Je n’avais pas bien compris l’utilité de « annotate » dans les queries Django jusqu’à maintenant…

Voici quel était mon problème :

J’avais un modèle de base, Activite. Une activité contenait des choses concernant… une activité. Si si je vous jure !

Par exemple, une activité consistait à ajouter un voyage. Ou avoir une nouvelle relation. Ou faire un témoignage comme quoi on aimait bien le site. Etc.
Je voulais que ces activités soient « partageables » entre les amis.
J’ai crée un modèles ActiviteShare. Facile jusque là.

Mais lorsque je demandais à récupérer toutes les activités, il fallait que je les trie en me basant sur le fait de savoir si elles étaient partagées. Pour exagérer, imaginons une activité qui était faite en 2010. Vous étiez parti à Malibu. Génial ! Et maintenant, en 2016, un ami s’inscrit, devient votre ami, et vous voulez partager cette activité avec lui. Il faut qu’elle s’affiche dans votre mur, comme activité « partagée récemment », mais qu’elle garde sa date de 2010, ok ?

Donc je devais trier d’abord en fonction de la date de ActiviteShare puis seulement après par la date de création de l’Activite.

Seul hic : si vous aviez partagé cette activité avec 10 contacts, au moment du tri, Django renvoyait dix fois le même enregistrement.

Mon tri était super basique, du genre :

Activite.objects.filter(
    # blabla
).order_by(
    '-activiteshared__date_last_modif',
    '-date_last_modif',
    '-date_publication',)

Mais ça ne marchait pas. 🙁

La solution ? Annotate.

Pour vous expliquer avant de mettre le code : je lui demande, lors de la requête, de garder la date maximale parmi toutes les dates partagées, et de l’appeler « mx ». Ensuite, je demande de trier simplement d’abord par « mx », puis par la date de dernière modification de l’activité.

Activite.objects.filter(
    # blabla
).annotate(mx=Max('activiteshared__date_last_modif')).order_by(
    '-mx',
    '-date_last_modif',
    '-date_publication',)

Merci à ce lien qui m’a bien aidé.

Django : internationalisation des fichiers javascript : tutoriel complet

Django vient tout faire pour l’internationalisation de pages HTML, mais saviez-vous qu’il est aussi possible et de manière très simple d’internationaliser vos échanges AJAX ?

Vous avez un tutoriel ici.

Seul point noir : ils ont oublié deux éléments essentiels, je vais donc faire une explication courte ici, puis ajouter un résumé dans ma cheatsheet Django.

Dans l’ordre :

  • Configuration de urls.py :
    Ajouter le dictionnaire dans lequel vous précisez vos « packages ». Vos « packages », ce sont simplement les dossiers qui correspondent à l’application que vous voulez afficher. Normalement c’est un dossier racine de votre projet, moi je l’appelle très souvent « app » pour qu’il soit tout en haut des dossiers racine. Donc ici :

    js_info_dict = {
        'packages': ('app',)
    }
  • Configuration de urls.py :
    Ajouter l’URL jsi18n. Attention ! L’aide ne précise pas du tout où la mettre et c’est là où j’ai perdu énormément de temps, faites attention : il faut le mettre dans les patterns traduits, soit ici :

    urlpatterns += i18n_patterns(
        url(r'^jsi18n/$', javascript_catalog, js_info_dict,
            name='javascript_catalog'),
            # blabla...
    }
  • Includes des fichiers js dans les pages :
    <script src="{% url 'javascript_catalog' %}"></script>
  • Includes des fichiers js dans les pages :
    <script src="{% static 'js/globals.js' %}"></script>
  • Dans le fichier précédent, j’ai le code suggéré dans la documentation :
    function _(a) {
         return gettext(a);
    }
  • Générer les chaînes à traduire. Là aussi dans la documentation ils en parlent mais très mal. Pour résumer, il faut préciser le domaine djangojs :
    Exemple d’ordre qui lance la recherche de toutes les chaînes à traduire, en ignorant le dossier third_party (où je mets les outils externes que je ne veux pas toucher) :

    makemessages -d djangojs -i third_party --locale fr --locale en
  • Traduire. Ce sont les fichiers djangojs.domaine djangojs.po qu’il faut modifier, pas le classique django.po (sans strong>js).

Et à partir de là, tout fonctionne : dans tous les fichiers JavaScript qui suivent, je peux traduire à la fois ce qui arrive en AJAX, ou bien tout simplement afficher un message via _(). Exemple :

$('#menu').empty().append(
    $('<h5 />').html(_('Waiting...'))
);

From Django/Python to NodeJS. Une année après : from NodeJS to Django/Python

Tiens, un blog d’un geek qui explique pourquoi il est passé de python à NodeJS.

Why I’m switching from Python to NodeJS

Ah j’oubliais le plus important : un an après avoir utilisé nodejs en production :

After a year of nodejs in production: back to Python

Au moins ça vient d’un développeur talentueux qui a bossé jour et nuit sur du NodeJS pendant toute une année et il sait bien de quoi il parle.

Spoiler :

  • Easy to learn, impossible to master
  • Good luck handling errors: You’ll need to double your client invoices to makeup for debug time
  • Would I recommend it for large-scale products? Absolutely not.

Traduction de ce qui importe le plus : votre client devra payer le double pour le même projet : développement + autant de temps pour déboguer.

Je vous laisse faire votre propre expérience, moi j’ai la mienne qui se rapproche assez de ces billets de blog.

Django : créer un checkbox multiple choice avec bootstrap

Je me sers de cet habillage ici.

C’est, en gros, une « surcharge » de bootstrap avec quelques composants en plus.

Seulement j’aurais aimé avoir une sélection de plusieurs choix = cases à cocher qui soit « compatible » avec cet habillage.
Je n’aurais jamais cru que c’était aussi simple.
En fait Django a généralisé plein d’affichages HTML à tel point qu’il suffit de faire du pas à pas et de regarder ce qu’il fait pour comprendre.
Même le rendu par « item » de chaque « Renderer » est surchargeable. Donc voici le code qui prend… 10 lignes, et qui réhabille totalement le rendu des cases à cocher :

from django.forms import CheckboxSelectMultiple
from django.forms.widgets import ChoiceFieldRenderer, \
    CheckboxChoiceInput

class BootstrapChoiceFieldRenderer(ChoiceFieldRenderer):
    choice_input_class = CheckboxChoiceInput
    outer_html = '<div{id_attr}>{content}</div>'
    inner_html = '<div class="checkbox">'
                 '{choice_value}{sub_widgets}'
                 '</div>'

class CheckboxSelectMultipleBootstrap(CheckboxSelectMultiple):
    renderer = BootstrapChoiceFieldRenderer

Django : comment changer un label de fieldset dynamiquement

Voici ma classe :

class GameAdmin(admin.ModelAdmin):
    fieldsets = (
        (None, {
            'fields': (('name', 'number_of_players'), )
        }),
        (_(u'Validity'), {
            'classes': ('collapse',),
            'fields': ('date_v_debut', 'date_v_fin')
        }),
    )
    inlines = (GamePersonsInline,)

Mon problème est que je voulais changer dynamiquement 'Validity' afin de montrer soit, si le jeu n’était plus valide, soit s’il était toujours en cours.
L’idée est que s’il a une date de fin de validité, c’est que le jeu n’est plus en cours.

Voici la solution complète, extrêmement simple, et qui me permet sans avoir à déplier le groupe « dates de validité », de voir si la partie n’est plus en cours :

class GameAdmin(admin.ModelAdmin):

    def get_fieldsets(self, request, obj=None):
        fieldsets = super(GameAdmin, self).get_fieldsets(
            request, obj)
        retour = [list(x) for x in fieldsets]
        if obj.date_v_fin:
            retour[1][0] = _(u'Expired: {}').format(
                obj.date_v_fin.strftime('%Y-%m-%d %H:%M:%S'))
        else:
            retour[1][0] = _(u'Running!')
        return retour

    fieldsets = (
        (None, {
            'fields': (('name', 'number_of_players'), )
        }),
        (_(u'Validity'), {
            'classes': ('collapse',),
            'fields': ('date_v_debut', 'date_v_fin')
        }),
    )

Après c’est vrai, il y a une chose de codée en dur, le [1][0], mais je ne voyais pas d’autre solution.

Django : Not naive datetime (blah) (tzinfo is already set)

Django a une manière de gérer la date et l’heure de façon très pratique… encore faut-il la comprendre !

Il y a la date et l’heure qui prend en compte le fuseau horaire.
Imaginez qu’un type en Chine envoie une date et une heure avec le fuseau horaire de Chine sur votre serveur, via un formulaire Web.
Le plus pratique est donc d’enregistrer au format UTC = 0 décalage horaire en se souvenant que l’heure est basée sur le fuseau de Chine.
Comme ça, si un Français demande cette heure, hop, il voit que c’est pour un fuseau +1 (ou +2 selon la saison) et fait le calcul tout seul.
Génial non ?

Seule chose à laquelle il faut faire attention : Python a son package datetime, et il y a une fonction now() qui n’est pas la même que la fonction now() de Django !

Donc si vous voulez enregistrer une date « style Django », il faut faire cet import :

from django.utils.timezone import now

Ensuite, vous pourrez, par exemple, faire des différences de secondes qui fonctionneront :
def is_expired(self, nb_seconds):
    return (now()-self.date_last_update).total_seconds() > nb_seconds

(Attention au code précédent : deux choses : le now() qui doit être importé comme je l’ai dit, et total_seconds() auquel il faut faire attention, car il y a une propriété seconds qui ne fait pas du tout la même chose….)

et surtout ne pas faire from datetime import datetime

Si vous faites comme moi et faites le mauvais import, l’heure ne sera pas « recentrée » à fuseau + 0 afin que tout le monde puisse la lire !