Informatique

[TIPS] Django : How to select only base classes in a query (remove subclasses)

Monday, 04 July 2016
|
Écrit par
Grégory Soutadé

This was my problem for Dynastie (a static blog generator). I have a main super class Post and a derived class Draft that directly inherit from the first one.

class Post(models.Model):
    title = models.CharField(max_length=255)
    category = models.ForeignKey(Category, blank=True, null=True, on_delete=models.SET_NULL)
    creation_date = models.DateTimeField()
    modification_date = models.DateTimeField()
    author = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    description = models.TextField(max_length=255, blank=True)
    ...

class Draft(Post):
    pass

A draft is a note that will not be published soon. When it's published, the creation date is reset. Using POO and inheritance, it's quick and easy to model this behavior. Nevertheless, there is one problem. When I do Post.objects.all() I get all Post objects + all Draft objects, which is not what I want !!

The trick to obtain only Post is a mix with Python and Django mechanisms called Managers. Managers are at the top level of QuerySet construction. To solve our problem, we'll override the models.Model attribute objects (which is a models.Manager).

Inside inheritance

To find a solution, we need to know what exactly happens when we do inheritance. The best thing to do, is to inspect the database.

CREATE TABLE "dynastie_post" (
    "id" integer NOT NULL PRIMARY KEY,
    "title" varchar(255) NOT NULL,
    "category_id" integer REFERENCES "dynastie_category" ("id"),
    "creation_date" datetime NOT NULL,
    "modification_date" datetime NOT NULL,
    "author_id" integer REFERENCES "auth_user" ("id"),
    "description" text NOT NULL);

CREATE TABLE "dynastie_draft" (
    "post_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "dynastie_post" ("id")
);

We can see that dynastie_draft has a reference to the dynastie_post table. So, doing Post.objects.all() is like writing "SELECT * from dynastie_post" that includes Post part of drafts.

Solution 1 : Without altering base class

The first solution is to create a Manager that will exclude draft id from the request. It has the advantage to keep base class as is, but it's not efficient (especially if there is a lot of child objects).

class PostOnlyManager(models.Manager):
    def get_queryset(self):
        query_set = super(PostOnlyManager, self).get_queryset()
        drafts = Draft.objects.all().only("id")
        return query_set.exclude(id__in=[draft.id for draft in drafts])

class Post(models.Model):
    objects = PostOnlyManager()

class Draft(Post):
    objects = models.Manager()

With this solution, we do two requests at each access. Plus, it's necessary to know every sub class we want to exclude. We have to keep the BaseManager for all subclasses. You can note the use of only method to limit the query and de serialization to minimum required.

Solution 2 : With altering base class

The solution here is to add a field called type that will be filtered in the query set. It's the recommended one in the Django documentation.

class PostOnlyManager(models.Manager):
    def get_query_set(self):
        return super(PostOnlyManager, self).get_queryset().filter(post_type='P')

class Post(models.Model):
    objects = PostOnlyManager()
    post_type = models.CharField(max_length=1, default='P')

class Draft(Post):
    objects = models.Manager()

@receiver(pre_save, sender=Draft)
def pre_save_draft_signal(sender, **kwargs):
    kwargs['instance'].post_type = 'D'

The problem here is the add of one field which increase database size, but filtering is easier and is done in the initial query. Plus, it's more flexible with many classes. I used a signal to setup post_type value, didn't found a better solution for now.

Conclusion

Depending on your constraints you can use the solution 1 or 2. These solutions can also be extended to a more complex filtering mechanism by dividing your class tree in different families.

Dynastie 0.4

Saturday, 04 June 2016
|
Écrit par
Grégory Soutadé

Logo Dynastie

Cela faisait longtemps que Dynastie n'avait plus eu droit à son petit article. C'est donc la version 0.4 qui sort aujourd'hui avec quelques fonctionnalités intéressantes :

  • Possibilité d'inclure un article dans un autre article (les modifications sont automatiquement reportées lors de la re génération). Pratique quand on écrit une fiche sur un groupe par exemple (un article général qui référence chaque sous article)
  • Filtre fail2ban
  • Support de Django 1.8
  • Coloration de code avec la syntaxe Markdown
  • Redirection sur le commentaire écrit (lors de son ajout)
  • Meilleure gestion du cache lors de la génération des articles

Plus deux/trois détails et quelques bugs corrigés. Il y a encore un peu de boulot, mais cette version est un bon cru !

IWLA 0.3

Thursday, 14 April 2016
|
Écrit par
Grégory Soutadé

Capture d'écran IWLA

Bientôt un an depuis la version précédente d'IWLA (mon analyseurs de statistiques web écrit en Python) et, sans m'en rendre compte, il y a des tas de nouveautés ! Au menu:

  • Mise en lumière des différences (par rapport à la dernière analyse) pour les mots clés ainsi que les fichiers téléchargés
  • Rappel du résumé annuel dans les statistiques mensuelles
  • Ajout de la durée de l'analyse
  • Détection des navigateurs
  • Détection des systèmes d'exploitations
  • Détection des pays (grâce à iptogeo)
  • Détection des clientS RSS
  • Statistiques détaillées pour un/des visiteur(s) particuliers
  • Possibilité de mettre à jour des configurationS par rapport à celles par défauts (sans écrasement) avec le suffixe "_append"
  • Statistiques par heures
  • Possibilité de spécifier plusieurs fichiers à analyser
  • Support des fichiers compressés (.gz)
  • Possibilité de ne pas compresser la base de données (option -z)
  • Possibilité de spécifier ses propres moteurs de recherche
  • Requêtes DNS inversés pour les clientS RSS

Quelques bugs ont été corrigés :

  • Le dernier jour du mois n'était pas analysé
  • Les pages accédées à la même seconde n'étaient pas analysées

Comme quoi, un logiciel "stable" n'existe pas. Je suis plutôt content de cette version qui est vraiment complète et avec des fonctionnalités non présentes dans AWStats ! Cela tient surtout en la modularité de l'architecture qui, certes, réduit un peu les performances, mais offre une grande souplesse dans l'écriture de modules (filtres).

IPToGeo 0.1

Thursday, 03 March 2016
|
Écrit par
Grégory Soutadé

IPToGeo

Voici la première version d'IPToGeo. Il s'agit d'un double service (serveur et ligne de commande), qui permet de récupérer l'assignation géographique (code pays ISO) d'une adresse IP. Je parle ici d'assignation, car, même si dans 90% des cas, la localisation du fournisseur est la même que celle de l'utilisateur, il reste 10% où ce n'est pas vrai. Par exemple, un fournisseur de service VPN peut avoir des utilisateurs dans le monde entier, donc les utilisateurs finaux ne seront pas ceux de l'adresse remontée dans le journal. Pour avoir une géolocalisation précise, il faudra se tourner vers d'autres services qui ont des bases de données beaucoup plus fines.

Pour se faire, IPToGeo va récupérer les données des 5 RIR (Regional Internet Registries) : AfriNIC, APNIC, ARIN, LACNIC et RIPE NCC et les compiler en données statiques pour fournir une base de données sous forme d'un arbre non équilibré (pour le langage C). Le but est d'utiliser le moins de ressources possible et d'avoir une très forte réactivité.

La partie serveur est écrite en C, les données sont générées à partir d'un script Python. Les tests sont eux aussi en Python (une classe haut niveau est fournie). Afin d'améliorer encore la sécurité, il y a un support (optionnel) de seccomp. Les adresses IPv4 et IPv6 sont supportées.

Pourquoi iptogeo alors qu'il existe déjà des bases de données gratuites (notamment celle de Maxmind) ? Tout d'abord pour des raisons de confidentialités. Faire une requête sur une IP pour obtenir une position géographique est une source d'information pour celui qui écoute le trafic réseau. Ensuite, parce que c'est simple : la compilation des données et la recherche a demandé peu de travail (de plus, les données sont fournies gratuitement et mises à jour quotidiennement). Ce projet m'a permis de poser les bases d'un serveur optimisé, sécurisé (via seccomp) et qui écoute aussi bien en IPv4 qu'en IPv6. Finalement, parce que la recherche telle qu'implémenté nativement dans AWStats, et qui se base sur l'extension du DNS inversé est mauvaise (impossible de déterminer la position d'un .com grâce à son extension). En effet, le but de la classe Python est du test, mais aussi et surtout pour être intégrée à iwla.

Comme d'habitude, les sources sont disponibles sous licence GPLv3 dans ma forge inDefero.

IPv6

Petit aparté sur IPv6. Naïvement, je pensais qu'IPv6, c'était comme IPv4 avec des adresses plus longues, du coup il était idiot de ne pas migrer, surtout que l'on prédit sans cesse l'épuisement des adresses IPv4 au niveau mondial. Au lieu de réaliser cette migration, les opérateurs réseaux utilisent des techniques plus ou moins louches pour pallier à ce problème (NAT, double NAT, adresses privées...).

Pour rappel, IPv4 est un système d'adressage qui date de janvier 1980, on est donc au début de l'informatique moderne. À ses 18/19 ans, la norme IPv6 a été finalisée par l'IETF. Norme qui a aujourd'hui elle-même 18 ans. 1998, c'est le tout tout début du haut débit en France, le bas débit ayant déjà bien vécu et une solide expérience dans un réseau mondial appelé Internet ayant été acquise par l'IETF.

La norme IPv6 a donc été définie, non pas seulement pour augmenter la taille des adresses IP, mais aussi pour corriger tout un tas de défauts d'IPv4. Les deux normes doivent cohabiter : d'un côté, une norme archaïquerudimentaire, et de l'autre une super norme qui se révèle pourtant beaucoup plus complexe.

C'est cette question de complexité qui fait toute la différence. Tout le monde est capable de comprendre rapidement les mécanismes IPv4 (en plus de pouvoir retenir plus ou moins facilement une adresse de tête). L'administration y est simple, même si la puissance de calcul des routeurs doit être plus importante. C'est ce qui fait sa force (en plus d'être déjà en place depuis des années) là où IPv6 amène plein de nouveaux concepts nécessitant d'adapter tous les étages du réseau (matériel, administration, logiciel...) : auto-configuration, adresse privée + adresse publique, renouvellement d'adresse...

D'un point de vue technique, IPv6 est pourtant implémenté dans les équipements depuis des années. Linux facilite lui aussi la migration en faisant une correspondance automatique IPv6 -> IPv4 pour les serveurs qui n'écoutent qu'en v4. Néanmoins, basculer le réseau mondial en IPv6, c'est potentiellement priver les utilisateurs de réseau à cause d'une mauvaise configuration (qui va assurer le support ?). Risque trop important dans un monde qui se veut "tout connecté". La transition se fait donc extrêmement lentement (tout le monde freine le plus possible) via des services comme des tunnels (6rd) ou encore l'implémentation de double pile au niveau des boxs. Paradoxalement, ces solutions de migrations sont finalement plus complexes à mettre en œuvre que l'IPv6 natif !

How to modify SQLite3 schemas

Sunday, 31 January 2016
|
Écrit par
Grégory Soutadé

SQLite3 is a wonderful library allowing to have a tiny SQL database in a simple file rather than a big (but more powerful) SQL server. I use it a lot for my projects to be "self contained".

Unfortunately, SQLite3 has some limitations. One of them is the unavailability to modify columns constraints or to delete them.

By upgrading from Django 1.5 to Django 1.8, I found that I cannot add a new user to my database. The problem is that the constraint of the last_login column in the User schema (controlled by Django framework) has changed. Now it can be NULL, while the table was created with a non NULL constraint. I got this exception :

NOT NULL constraint failed: auth_user.last_login

I already faces this kind of situation. The solution is pretty simple and does not requires modifying code.

First of all, make a backup of your database.

To get round this problem, we will export database in SQL commands format, modify it with a simple text editor and import it again.

Assume that your database is named denote.db

$ cp denote.db denote.db.bak
$ sqlite3 denote.db
$ .output denote.sql
$ .dump
$ .exit
$ rm denote.db
$ emacs denote.sql
...
$ sqlite3 -init denote.sql denote.db
$ sudo apachectl restart

That's all !

For complex modifications, it can be long/boring, so I recommend to use tools like sed, perl or python scripts, regexp replace mode from emacs...