Utiliser l’application Django Sites est assez sympatique afin de permettre de placer plusieurs sites différents sur le même projet.
Ainsi dans le cas d’un site multilingue avec un domaine par langue, j’ai un seul projet, un site par langue et tout roule comme en 40 :)

Mais c’est à ce moment que vient la problématique de la gestion des droits (oui. Après l’article sur les ACL dans Rails d’hier, je suis beaucoup dans les problèmes d’autorisation en ce moment).
En effet on veut pouvoir autoriser des utilisateurs à modifier des éléments sur une langue. Mais pas forcément sur une autre.

Alors déjà, mise en contexte :
J’ai un modèle « Language », qui contient toutes mes langues, leur nom et un slug qui les définit pour mettre les chaines appropriées.

class Language(models.Model):
  slug = models.SlugField(max_length=4)
  name = models.CharField(max_length=200)
  site = models.ForeignKey(Site)

  group = models.ManyToManyField(Group)

  def __unicode__(self):
    return self.name

  def get_absolute_url(self):
    return self.site.get_absolute_url()

Vous constaterez que cette langue est reliée à un site. Ainsi chaque langue a un site.
De même chaque langue peut avoir un ou plusieurs groupes.
J’ai créé un middleware afin de définir le site et la langue appropriés en fonction du domaine.
Je ne rentre pas dans les détails de ce middleware ici. Si ça vous intéresse, merci de le signaler dans les commentaires. Je ferai un autre article.

Maintenant que nous avons une langue qui est en relation avec un ou plusieurs groupes, nous pouvons n’autoriser l’accès que aux utilisateurs étant dans le groupe relatif à cette langue.
Pour la gestion de nos accès, nous allons avoir besoin de deux méthodes :

from django.http import HttpResponse
from app.pages.models import Language

def group_required(func):
  def _decorator(request, *args, **kwargs):
    if not in_group(request):
      return HttpResponse("Access denied")
    return func(*args, **kwargs)
  return _decorator

def in_group(request):
  language = Language.objects.get(site__domain__exact=request.get_host())
  for group in language.group.all():
    if request.user in group.user_set.all():
      return True
  return False

La méthode in_group retourne vrai si l’utilisateur est dans l’un des groupes autorisés pour la langue.
Elle retournera faux si ce n’est pas le cas.

la méthode group_required est un décorateur, qui retournera la méthode passée en paramètres si l’utilisateur y a accès. Et un access denied dans le cas contraire.

Deux possibilités d’implémentation maintenant.

Dans une vue

Supposons que vous ayez une méthode de vue bateau :

def single(request, slug):
  return object_detail(
    request,
    queryset = Page.objects.filter(published=True).all(),
    slug = slug
  )

Nous allons utiliser le décorateur afin de vérifier que notre utilisateur a bien accès à cette langue.

def single(request, slug):
  return object_detail(
    request,
    queryset = Page.objects.filter(published=True).all(),
    slug = slug
  )
single = group_required(single)

Si l’utilisateur a accès au groupe, la méthode single restera inchangée. Dans le cas contraire, elle retournera un « Access Denied ».

Dans l’admin

Dans l’application django admin, on ne peut redéfinir les méthodes de cette façon. On peut faire la chose de manière bien plus class ! :)

Voici mon admin :

class PageAdmin(admin.ModelAdmin):
  list_display = ('title',)
  search_fields = ['title']

  fieldsets = [
    (None, {'fields': ['title', 'slug', 'published']}),
  ]

  def has_add_permission(self, request):
    if not in_group(request):
      return False
    return super(PageAdmin, self).has_add_permission(request)

Toute la partie jusqu’aux fieldsets est basique. La ou ça devient intéressant c’est la méthode has_add_permission.
Méthode normalement définie par ModelAdmin, que l’on redéfinit ici afin de vérifier que notre utilisateur a bien accès à la langue :)

Si l’utilisateur n’a pas accès à la langue, on retourne directement faux. On ne cherche pas à aller plus loin.
Dans le cas contraire … On fait appel à la classe parente. Si l’utilisateur a le droit de modifier cet élément lui même, il peut le faire !

La méthode has_add_permission n’est bien évidemment pas la seule à réecrire. Il nous faut également redéfinir :
has_change_permission et has_delete_permission

A vous ensuite de définir vos règles d’autorisation en fonction des besoins précis de votre application. Plutôt cool non ? :)

Regardons un peu les formulaires Django. Et plus particulièrement les ChoiceField.
Avec ceux-ci, nous plaçons une liste déroulante dans notre formulaire.

champ = forms.ChoiceField(choices=((1, 'valeur'), (2, 'seconde_valeur')))

Nous créons ici un ChoiceField dans notre formulaire en y placant également deux valeurs avec les identifiants 1 et 2.

C’est sympa. Mais ça serait mieux de pouvoir récupérer les valeurs de notre base. Gogo alors !

champ = forms.ChoiceField(choices=[[r.id,r.name] for r in Model.objects.all()])

Pour définir les valeurs, nous récupérons toutes les données disponibles avec le modèle « Model » et plaçons les champs id et name. Vous avez donc un ChoiceField dynamique.

Malheureusement (ou heureusement, cela dépends du point de vue), Django place le contenu des formulaires en cache. La requête SQL de récupération de vos modèles ne sera donc executée que si ceux-ci sont modifiés.
Dans un usage basique, cela suffit amplement. Si c’est le cas, je vous invite fortement à conserver cette méthode car ce que j’explique plus bas n’est absolument pas DRY.

Dans mon cas par exemple, j’ai une condition sur mon modèle. Le site est multilingue et je cherche à ne récupérer que les uplets relatifs à la langue en cours.
Avec la mise en cache donc, je n’ai pas les bons éléments car Django ne fait pas particulièrement attention au fait que j’ai changé la requête entre deux (notemment parce que la condition n’est pas dans la requête mais au niveau supérieur, dans le manager).

Je ne peux donc ajouter les éléments directement depuis le formulaire.
Je définis mon champ dans mon formulaire :

champ = model.ChoiceField()

Et dans mon contrôleur (oui c’est pas DRY je l’ai déjà dit) :

if request.method == 'POST':
    form = MyForm(request.POST)
    form.fields['champ'].choices = [[r.id,r.name] for r in Model.objects.all()]
else:
    form = MyForm()
    form.fields['champ'].choices = [[r.id,r.name] for r in Model.objects.all()]

Mon formulaire est correctement défini et avec les bonnes valeurs :)

Et vous, avez-vous déjà eu un problème similaire de cache sur des ChoiceFields de Django ? Avez-vous résolu le problème de manière similaire ou avez-vous réussi à faire quelque chose de plus propre ?

Lorsque vous développez une application en utilisant un framework quelconque, vous vous retrouvez inévitablement à avoir un layout qui contient tout ce qui est censé être le design global de votre application.
Et parce qu’il faut des exceptions qui confirment la règle de ce design global, vous avez des pages qui doivent se comporter différemment :
Ne pas afficher le menu de gauche ou avoir un body avec une class différente.

Pour cela, Django et Symfony proposent deux concepts fortement intéressants qui sont similaires, bien qu’ayant des noms différents et que le concept soit beaucoup développé avec Django.
Je vais faire l’explication avec Django. Et je vous montrerai ensuite l’équivalent en PHP avec Symfony.

Avec Django

Dans votre layout, nous avons le code suivant :
<div>
  {% block 'menu' %}
    Contenu par défaut
  {% endblock %}
</div>

Et dans la vue de l’une de vos actions :
{% extend 'layout' %}
{% block 'menu' %}
  Contenu spécifique à la vue
{% endblock %}

Notre vue va charger le layout (celui à charger est précisé par le extend).
Le layout, en détectant le block va alors aller voir si la vue n’en définit pas un elle-même. Vu qu’elle en définit un, à l’endroit du block du layout sera affiché le contenu du block de la vue.

En gros ici, nous aurons le message « Contenu spécifique à la vue » d’affiché.
Si la vue ne définissait pas de block menu, nous aurions le message « Contenu par défaut d’affiché ».
Vous pouvez ainsi définir divers éléments de votre design spécifiques à votre vue tout en ayant également une valeur « par défaut ».

C’est d’ailleurs ainsi qu’il est conseillé de placer, en plus des éléments du design, le contenu de la vue en lui même, en créant un block « content ».
Plus plus d’informations, je vous invite à voir la documentation de Django.

Avec Symfony

Les slots de Symfony sont beaucoup moins utilisés que les blocks de Django. Il s’agit d’une fonctionnalité disponible dans les templates. Pas de la fonctionnalité autour de laquelle vous construisez vos templates.

Dans notre nous allons donc définir un slot dans notre layout :
<div>
  <?php if (has_slot('menu')): ?>
    <?php include_slot('menu') ?>
  
    Contenu par défaut
  
</div>

Et en redéfinir son contenu dans notre vue :
<?php slot('menu') ?>
  Contenu spécifique à la vue
<?php end_slot() ?>

Ou encore, si vous avez peu de contenu à définir dans votre slot :
<?php slot('menu', 'Contenu spécifique à la vue') ?>

Le première forme en fera tiquer certains puisqu’il s’agit d’un yield-like (pour les autres, c’est le genre de choses qui rendrait php encore mieux si c’était implémenté).
Le code donné ici fonctionne exactement de la même manière que celui donné plus haut pour Django.
Pour plus d’informations, je vous invite à voir la documentation Symfony.

Ainsi avec les slots et blocks à utiliser en fonction de la technologie que vous utilisez, vous pouvez rendre votre design entièrement DRY sans pour autant vous fixer de limites :)

Quelque soit le langage, un élément est toujours délicat à traiter : créer des url cool, avec des vrais mots.
Mais en ne plaçant pas de caractères qui vont rendre l’url toute pas jolie avec des %20 et autres.

Pour cela, il faut supprimer tous les accents (et les remplacer par leur lettre sans accent); supprimer les espaces (et les remplacer par des -) et supprimer les signes de ponctuation (et ne pas les remplacer).

Avec Django, pas besoin de s’embêter avec ça :)
Il y a les slugs.
Par principe, un slug contiendra toujours une valeur qu’il est possible de mettre en url et sera toujours unique.
A vous par la suite de l’utiliser pour créer l’url de vos objets.

Mais il faut encore le définir correctement :)
Pour cela, dans votre administration, nous allons placer un prepopulated_field.
Placez dans vos fieldsets le titre de l’uplet et son slug.

Puis placez le code suivant en dessous de la définition des fieldsets :

prepopulated_fields = {'slug': ('title',)}

Ou slug est le nom du champ « slug » (je vous conseille de l’appeller slug).
Et ou title est le nom de votre champ de titre.

Chargez votre administration, remplissez votre titre et admirez votre champ de slug se remplir tout seul :)
Vous constaterez également qu’il ne se remplit que lors de la première édition, pour éviter un changement d’url inopportun lorsque vous changez le titre de la page.
Rien ne vous empêche par ailleurs de placer le slug de votre choix.

Vous pouvez par ailleurs personnaliser la chose dans votre application.
Par exemple dans un template, pour afficher la valeur d’un string slugifié :

{{ "mon Test"|slugify }}

Affichera : mon-test

Et dans vos fichiers .py directement également

from django.template import defaultfilters
slug = defaultfilters.slugify("Mon Test")

la variable slug contiendra alors mon-test.

Et vous pouvez faire de belles URLs. N’oubliez pas d’utiliser les vues génériques pour trouver le contenu qui correspond à un slug et le charger :)

Vous avez normalement déjà manipulé les commandes console de Django, accessibles via manage.py.
./manage.py runserver

Cependant il peut être utile, dans plus d’une application, d’avoir des méthodes console pour lesquelles vous désirez obtenir l’environnement web, mais en console.
Pour charger des données depuis un flux XML par exemple. Ou encore vider une base de données.
Ainsi, vous pourrez faire :

./manage.py votre_commande

Dans l’application de votre choix, et qui doit être présente dans le INSTALLED_APPS de settings.py, créez l’arborescence suivante :
- management
|- __init__.py
|- commands
|– __init__.py

Ainsi, vous aurez ceci :
- votre_application
|- management
|– __init__.py
|– commands
|— __init__.py

Puis créez un fichier, que vous nommerez comme votre nouvelle commande dans le dossier commands.
Par exemple export.py

Dans ce fichier, vous placerez le code suivant :
from django.core.management.base import NoArgsCommand
class Command(NoArgsCommand):
  help = "Message d'aide concernant votre commande"
  def handle_noargs(self, **options):
    # Le contenu de votre commande
    print("Hello World")

Si, maintenant, vous tapez dans une console :

./manage.py export

Et le code présent dans votre méthode handle_noargs() sera exécuté :)

Supposons le cas suivant :
J’ai des pages et des catégories. Une page peut avoir une catégorie.
Nous avons donc les modèles suivants :
class Category(models.Model):
  name = models.CharField(max_length=200)
  slug = models.SlugField(max_length=200,unique=True)
class Page(models.Model):
  title = models.CharField(_('title'), max_length=200)
  content = models.TextField(_('content'))
  category = models.ForeignKey(Category, related_name = 'cat')

Maintenant supposons que à cela, nous désirions ajouter, pour une catégorie, une page principale.
Dans mon cas, cela me permet, si j’ai une page principale, d’afficher le contenu de celle-ci lorsque l’on charge la page de la catégorie.
Mais après, à vous de voir ce que vous désirez faire :)

Dans notre modèle de catégorie, nous allons donc ajouter une relation vers le modèle Page. Logique !
Cependant, page = models.ForeignKey(Page,blank=True,null=True)
Ne fonctionnera pas. En effet la classe Page n’est pas encore définie lorsque vous définissez la classe Category.

La solution est cependant très simple. Il suffit de ne pas faire un appel direct à la classe Page. Mais de mentionner son nom dans une chaine de caractères :
page = models.ForeignKey('Page', blank=True,null=True)

Ainsi vous ne faites pas d’appel à la classe Page lors de l’instanciation de Category. Et lorsque Django initialize vos modèles et qu’il cherche la classe Page, celle-ci a déjà été déclarée.
Vous avez défini votre relation entre modèles à deux sens.

Un petit article rapide. Lorsque vous testez vos applications, l’envoi d’email est toujours embettant. Vous n’avez pas envie de l’email parte vraiment et souhaitez vérifier qu’il pars correctement.

Du coup il faut utiliser un Mock, qui va simuler l’envoi d’email mais n’enverra, en fait, rien.
Et qui vous permettra tout de même de surveiller l’email envoyé et de vérifier qu’il est correct.

Commençons, dans notre vue, par envoyer notre email :
from django.core.mail import send_mail
def envoi(self):
  send_mail(
    'A propos des tests',
    'C\'est fun quand même',
    'from@example.com',
    'to@example.com'
)

Et testons notre méthode :
from django.core import mail
def test_send(self):
  response = self.client.post('/envoi')
  self.failUnlessEqual(response.status_code, 302)
  self.assertEquals(len(mail.outbox), 1)
  self.assertEquals(mail.outbox[0].subject, 'A propos des tests')

Ainsi lorsque nous lançons notre test, en supposant que l’url /envoi soit configurée pour diriger vers notre action envoi, celle-ci sera exécutée, l’email sera envoyé de manière fictive.
Et vous aurez accès à un tableau dans mail.outbox qui contiendra tous les emails envoyés avec leur contenu, leur sujet et les personnes à qui ils ont été envoyés.

A vous, par la suite, d’écrire les tests appropriés au bon fonctionnement de votre application.

Le générateur automatique d’administration de Django est super sympa. En trois lignes de code, vous avez une interface pour ajouter, modifier et supprimer des données de votre modèle.
Cependant dans le cas de textareas, il peut arriver d’avoir besoin d’avoir un éditeur de texte avancé.

Nous allons donc ici voir comment implémenter TinyMCE aux textareas d’une page de l’administration particulière.
Mais cette technique peut également très bien fonctionne avec FCKEditor ou tout autre éditeur WYSIWYG. Le tout est, après, de changer le code javascript.

Prenons le fichier admin.py de notre application :
from django.contrib import admin
from project.application.models import Page
class PageAdmin(admin.ModelAdmin):
  list_display = ('name', 'url')
  fieldsets = [
    (None, {'fields': ['name', 'url']}),
    ('Content', {'fields': ['content']}),
  ]
admin.site.register(Page, PageAdmin)

Nous définissons donc une interface d’administration pour notre modèle Page qui contient les champs name, url et contenu.
Ce dernier champ est celui qui va prendre un textarea.

Commencons par ajouter des documents javascript externes spécifiquement pour ce modèle.
Après fieldsets[], ajouter
class Media:
  js = (
    '/js/tiny_mce/tiny_mce.js',
    '/js/admin_pages.js'
  )

Nous incluons donc les deux documents javascript supplémentaires dans la page des pages. Et uniquement celle-ci.

Installez tinymce dans /js/. Et créer le fichier /js/admin_pages.js
Dans lequel vous placerez
tinyMCE.init({
  mode : 'textareas',
  theme : "simple"
});

Ainsi, vous transformez tous les textareas de la page en champs tinymce (mais dans notre cas, il s’agit d’un seul).
Redémarrez votre application et vous verrez votre bel éditeur de texte enrichi :)

Libre à vous par la suite d’enrichir votre administration avec du javascript de cette manière.

Après avoir vu la mise en place de l’environnement de test et des tests unitaires avec Django, voyons maintenant comment mettre en place nos tests fonctionnels.
En écrivant cet article, je considère que vous avez lu l’article précédent et que vous avez donc compris ma méthode de hiérarchisation des tests.

Voici un test d’exemple (j’allais dire « de test ». Mais c’est un chouilla trop répétitif).
from django.test import TestCase
from django.test.client import Client
from projet.pages.models import Pager
class PagesTest(TestCase):
  def setUp(self):
    self.client = Client()
  def test_view_page(self):
    Page(
      name='Conditions d\'utilisation',
      url='conditions',
      content='nothing for now'
    ).save()
    response = self.client.get('/conditions')
    self.failUnlessEqual(response.status_code, 200)
  def test_view_unknown_page(self):
    response = self.client.get('/nothing/')
    self.failUnlessEqual(response.status_code, 404)

Et analysons le code.
Nous commençons par importer les diverse classes nécessaires à ces tests :

  • TestCase, le framework de test
  • Client, le client HTTP de test
  • Page, le modèle qui nous permet d’ajouter des données permettant ici les tests

Puis nous commençons les tests, en initialisant self.client à une nouvelle instance de Client afin de pouvoir faire des requêtes HTTP.
Dans le premier test, nous créons un élément Page puis nous testons que lorsque nous appellons celui-ci, notre page est bien rendue.
Et dans le second, nous vérifions que nous avons une bien une erreur 404 si nous appellons une page qui n’existe pas.

Vous noterez la méthode self.client.get() permettant de faire un appel HTTP avec la méthode du même nom.

Plusieurs méthodes sont ainsi disponibles pour faire les appels HTTP.

  • get(path, data={}, follow=False) – Fait une requête HTTP get
  • post(path, data={}, follow=False) – Fait une requête HTTP post
  • head(path, data={}, follow=False) – Fait une requête HTTP head
  • options(path, data={}, follow=False) – Fait une requête HTTP options
  • put(path, data={}, follow=False) – Fait une requête HTTP put
  • delete(path, data={}, follow=False) – Fait une requête HTTP delete

A cela s’ajoutent deux méthodes permettant d’identifier et de déconnecter l’utilisateur.

  • login(options) – Identifie l’utilisateur et retourne true si celle-ci a réussi.
    Exemple : self.client.login(username='test', password='test')
  • logout() – Déconnecte l’utilisateur auparavant connecté

Lorsque vous faites une requête quelconque, vous pouvez en tester le résultat. Voici les différentes méthodes disponibles.

  • client – Le client de test qui a été utilisé pour faire la requête
  • content – Le contenu de la page rendue
  • context – L’instance de la classe Context qui a été utilisée pour afficher le contenu de la page. Si il y a eu plusieurs templates de rendus, il s’agira d’une liste de contextes triés par ordre de rendu.
  • request – Les données request qui ont servi à faire la requête
  • status_code – Le code HTTP retourné par la page
  • template – L’instance de template qui a été utilisée afin d’afficher le contenu de la page. Si il y en a eu plusieurs, un tableau de templates triés par ordre d’affichage sera retourné.

Du coup vous avez maintenant toutes les clés en main pour commencer à faire vos tests fonctionnels. Y’a plus qu’à coder !

Par défaut, Django inclue deux frameworks de tests différents. Le premier, Doctest, impose de placer ses tests dans une chaine de caractères.
Le second, Unit Tests, est plus conventionnel et requiert de créer une méthode pour chacun de nos tests.
Le premier nous faisant perdre toute colorisation syntaxique, nous allons donc voir uniquement le second dans cet article.

Chacune de vos applications Django peut prendre des tests. Ceux-ci sont exécutés avec la commande manage.py tests
Qui va exécuter, pour chaque application, toutes les méthodes contenues dans le fichier tests.py de votre application.

Bien évidemment, placer tous nos tests dans ce fichier serait illisible.
Créons donc notre architecture de test :
|- application
|– tests.py
|– test
|— functional
|—- my_functional_test.py
|— unit
|—- my_unit_test.py

Nous séparons donc nos tests unitaires et fonctionnels et permettons de créer plusieurs fichiers pour chaque si nous le désirons.
Puis dans tests.py, il faut les inclure ces fichiers.
from project.application.test.functional.my_test import HomepageTests
from project.application.test.unit.my_test import UserTests

Remplissons maintenant nos tests unitaires.
Dans chaque fichier de test, nous devons faire appel au framework approprié en haut de page.
from django.test import TestCase

Puis nous créons la méthode UserTests, importée plus haut et qui contiendra nos tests.
class SimpleTest(TestCase):
  def test_basic_addition(self):
    self.failUnlessEqual(1 + 1, 2)

Nous testons ici que 1 + 1 est bien égal à 2. A vous par la suite de créer vos tests selon vos besoins.

Les méthodes permettant de faire les tests sont les suivantes :

  • setUp() – Méthode appellée pour préparer les fixtures des tests et donc avant ceux-ci. Il est inutile de l’appeller. En revanche, vous pouvez la surcharger.
  • tearDown() – Méthode appellée après que le test ait été exécuté (qu’il ait réussi ou non).
  • assertTrue(expression, message) – Vérifie que l’expression est bien égale à vrai.
  • assertEqual(first, second, message) – Vérifie que la première expression est égale à la seconde
  • failUnlessEqual(first, second, message) – Alias de assertEqual
  • assertNotEqual(first, second, message) – Vérifie que la première expression est différente de la seconde
  • failIfEqual(first, second, message) – Alias de assertNotEqual
  • assertAlmostEqual(first, second, décimales, msg) – Vérifie que les deux valeurs sont approximativement égales en calculant leur différence, puis l’arrondit à son nombre de décimales (défault : 7) et en les comparant à zéro.
  • failUnlessAlmostEqual(first, second, décimales, msg) – Alias de assertAlmostEqual
  • assertNotAlmostEquat(first, second, décimales, msg) – Vérifie que les deux valeurs sont approximativement égales en calculant leur différence, puis l’arrondit à son nombre de décimales (défault : 7) et en les comparant à zéro.
  • failIfAlmostEqual(first, second, décimales, msg) – Alias de assertNotAlmostEqual
  • assertRaises(exception, callable, …) – Vérifie qu’une exception a été levée. Le test passera si l’exception définie est levée et échouera si il s’agit d’une autre ou qu’aucune exception n’est levée
  • failUnlessRaises(exception, callable) – Alias de assertRaises()
  • failIf(expr, msg) – Opposé de failUnless
  • assertFalse(expr, msg) – Alias de failIf
  • fail(msg) – Retourne une erreur

Vous pouvez donc maintenant tester toutes les méthodes de vos modèles de manière efficace.
Vous pouvez donc continuer avec les tests fonctionnels.

 
Fork me on GitHub