Mots-clé : django

Django / jQuery / Select2 / autocomplete

Voici un tutoriel sur quelque chose qui m’a pris plusieurs jours à réaliser « proprement » et encore, ça n’est pas si propre, mais c’est le mieux que je puisse faire actuellement, en termes de rapport « propreté / temps passé » raisonnable.

Voici l’idée : je veux qu’on puisse commencer à taper quelques lettres dans un champ, et que ce dernier aille demander en AJAX/JSON si jamais il y a des tags « connus ». Si c’est le cas, le retour renvoie une liste, qui s’ouvre, et l’utilisateur peut choisir dans cette liste en descendant avec les flèches. S’il n’y a aucun retour, l’utilisateur peut aller au bout, et envoyer son formulaire, et c’est là que la magie intervient : plus tard, s’il revient sur le formulaire, il pourra taper quelques lettres, et on lui proposera le champ en question ! Mieux ! Il peut séparer les champs par une virgule, et donc taper plusieurs choix. Exactement comme lorsqu’on commence à entrer le nom d’un destinataire sur gmail. La classe non ?

J’ai voulu faire cela pour plein de tags, mais le client pour lequel je faisais cela n’a pas réellement compris l’intérêt et m’a demandé de faire une liste de choix « fixes » que l’utilisateur peut cocher. Bref, no comment.

Donc voici comment j’ai procédé (je ne dis pas que c’est la meilleure façon, il y en a sûrement d’autres, mais vous pouvez vous en inspirer) :
– création d’un modèle Tag qui a la langue (selon les langues, pas le même Tag) :
– dériver un type de champ de base Django forms.TypedChoiceField afin de permettre une liste de choix, mais qui sera valide de deux manières : il faut surcharger les méthodes qui convertissent les valeurs de retour de ce champ, afin :
    – soit d’essayer de lire une liste d’entiers, séparés par des virgules, qui sont les ids des champs,
    – soit pour chaque champ qui ne peut pas être converti en entier, appeler une méthode « custom_tag« , qui va ajouter le tag en base de données puis renvoyer un entier = pk du tag ajouté
– créer un Widget custom dans lequel on passera une classe spéciale destinée au JavaScript qui recherchera cette classe
– en JavaScript (mon ami de toujours, qui fait que je passe 20% du temps sur Django à m’amuser et 80% du temps sur l’habillage Web à rager et/ou bidouiller), faire une routine qui va chercher les widgets définis au-dessus et y appliquer le select2 tel que défini dans la documentation
– faire une vue destinée de recherche qui prend un paramètre, et renvoie les résultats trouvés, c’est pour remplir les demandes d’AJAX de select2. Elle doit renvoyer un tableau « résultat » et une variable « total » qui est simplement le « length » de « résultat ».

C’est tout… enfin tout… on se comprend !

Mais en pratique, une fois que tout est mis en place, il suffit de déclarer dans n’importe quel formulaire un champ ainsi, et on aura un champ entièrement dynamique, qui s’auto-alimentera avec le temps. Extrêmement pratique :


    a = _("Emails:")
    emails = TagTypedChoiceField(
        label=a, required=False,
        custom_tag=add_tag_to_languages,
        widget=Select2Widget(attrs={
            'title': a,
            'placeholder': _("type an email"),
            'multiple': 'multiple',
            'data-select2-json': reverse_lazy(
                'co_branding_json_tags_emails',
                kwargs={'company': 'ubisoft'}),
            'class': 'form-control form-control select2'}),
        error_messages=e,
        choices=self.get_list_tags(Tag.TYPE_EMAIL))

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.