Mots-clé : administration

Django : un modèle sur mesure : DaysField étape 1/3

Le principe

Je voulais un modèle de données qui stocke les jours d’une semaine que l’utilisateur désire, et qui les stocke sous forme de chaîne simple, avec des numéros qui correspondent aux jours de la semaine.
J’ai appelé ce modèle DaysField.
Les valeurs sont stockées par rapport aux jours choisis. Par exemple, 1,3 signifie que l’utilisateur a choisi lundi et mercredi parmi les jours. 6,7 correspondent à samedi et dimanche. Pour finir, tous les jours de la semaine cochés serait donc : 1,2,3,4,5,6,7

Toujours descendre d’un modèle classique

J’ai retenu ces principes et cela semble suffire pour fonctionner : on descend du modèle qu’on veut écrire en base de données. Ici, je voulais écrire une chaîne de caractères dans la base ⇒ on descend de CharField(), en modifiant la description au passage :
class DaysField(models.CharField):
    description = _("Comma-separated integers between 1 and 7")

Conversion de « sources » différentes en données Python

Pour résumer : les modèles ont deux « arrivées » de données :

  • quand on les données viennent de la base = from_db_value() ;
  • quand on les données viennent d’un formulaire (ou autre, mais en gros quand ça vient pas de la base de données mais du code Python lui-même) = to_python().

Il faut donc gérer ces deux cas, qui, dans les deux cas, doivent renvoyer quelque chose au format Python qui nous intéresse.
Ici, c’est un tableau d’entiers compris entre 1 et 7 qui correspond aux jours de la semaine.

J’ai centralisé la gestion des deux « arrivées » (base et Python) dans une seule fonction : value_to_array() :

    @staticmethod
    def value_to_array(value):
        if value is None:
            return None
        try:
            if isinstance(value, list):
                return [int(a) for a in value]
            elif isinstance(value, str):
                return [int(a) for a in value.split(',')]
        except (TypeError, ValueError):
            raise ValidationError(_("Unexpected value"))
        raise ValidationError(_("Unexpected value"))
    @staticmethod
    def from_db_value(value, expression, connection):
        return DaysField.value_to_array(value)
    def to_python(self, value):
        return DaysField.value_to_array(value)

Peut-être existe-t-il des cas où la provenance du code Python doit être gérée différemment de la provenance de la base de données… ici ce n’est pas le cas – et si vous avez des exemples de gestion différentes pour des entrées de base et de code, laissez-moi un commentaire, car je n’en vois pas…

Conversion de données Python ⇒ données pour la base

C’est la fonction get_prep_value().

Dans le cas DaysField, c’est très simple :
    def get_prep_value(self, value):
        return ','.join([str(a) for a in value]) \
            if value is not None else None

On prend toutes les valeurs du tableau, on les concatène avec la virgule , comme séparateur. C’est plus long de l’expliquer que de le coder !

Django 2.1 : les autorisations de l’administration grandement facilitées

Voici le (petit) souci que tout le monde a forcément dès qu’on commence à vouloir un peu pousser la customization de l’administration Django : on aimerait avoir la possibilité d’accéder à certains modèles, mais uniquement en lecture seule. J’ai des tonnes d’exemples simples, mais très souvent, ce sont des modèles avec des choix « fixes » qui ne risquent pas d’évoluer dans le temps (exemple : sexe « féminin »/ »masculin »). Seulement (1) on ne veut pas le voir apparaître dans l’admin (2) si on ne veut pas le voir apparaître, il ne faut pas le déclarer, et Django ne veut pas accéder à un modèle qui n’est pas déclaré (= erreur), donc on en arrive toujours à (3) on déclare le modèle, on applique une solution « bidouille » que je ne décris pas ici mais même si le modèle n’apparaît pas dans l’interface, en tapant à la main la bonne URL (facile à deviner qui plus est), on peut tout de même modifier ce modèle. Heureusement la version 2.1 de Django pallie à ce manque !

Eh oui, dans la classe interne «Meta», ils ont fait évoluer default_permissions, qui est écrit en base. Donc en passant à Django 2.1 il vous faudra faire le fameux makemigrations migrate.

Là ou cela devient intéressant, c’est que dans les options vous avez default_permissions et elles sont ('add', 'change', 'delete', 'view'). Le plus important et le nouveau est donc 'view' qui pallie parfaitement au problème expliqué en haut !

Il vous suffit de déclarer votre modèle, de faire la classe dérivée de ModelAdmin et de préciser uniquement ('view',).
Attention, je n’ai pas testé, si quelqu’un veut tester et confirmer que ce code fonctionne (ou pas), je suis preneur !

class SexeAdmin(models.ModelAdmin):
    class Meta:
        default_permissions = ('view',)

Plein de code en moins, plus aucune bidouille ici, plus de lisibilité, bref, de mieux en mieux !

PyCharm et Django : comment faire une requête directe

Si, comme moi, vous voulez faire des requêtes directement et voir toutes les tables, pour les modifier manuellement, rien de plus simple, il faut juste chercher sur le Web pendant pas mal de temps. Voici la solution pour économiser de précieuses minutes :

  • Lancer la console python (Tools » Python console)
  • Taper :

    from django.db import connection
    cursor = connection.cursor()
    cursor.execute("PRAGMA table_info(langue)")
    for c in cursor.fetchall():
        print(c)
  • Et vous obtiendrez un résultat qui pourrait ressembler à :
    (0, 'id', 'integer', 1, None, 1)
    (1, 'date_creation', 'datetime', 1, None, 0)
    (2, 'date_last_modif', 'datetime', 1, None, 0)
    (3, 'date_v_debut', 'datetime', 1, None, 0)
    (4, 'date_v_fin', 'datetime', 0, None, 0)
    (5, 'nom', 'varchar(50)', 1, None, 0)
    (6, 'locale', 'varchar(2)', 1, None, 0)
    (7, 'bidirectionnel', 'bool', 1, None, 0)
    (8, 'nom_local', 'varchar(50)', 1, None, 0)
    (9, 'active', 'bool', 1, None, 0)
  • Si vous voulez plus de détail, et que la console a des problèmes avec les accents, voici un code qui remplace les accents par un point d’interrogation :
  • Taper :

    from django.db import connection
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM langue")
    for c in cursor.fetchall():
        for d in c:
            if type(d) is str:
                print(d.encode(
                    sys.stdout.encoding,
                    errors='replace'))
            else:
                print(d)
  • Et vous obtiendrez un résultat qui pourrait ressembler à :
    None
    b'Russian'
    b'ru'
    False
    b'???????'
    False