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 :)

Derrière ce mot compliqué « circulaire » se cache l’un des effets pervers de Python.
En effet, pour obtenir accès à des méthodes situées dans des fichiers différents, Python requiert que l’on importe celles-ci.

from mon_fichier import ma_methode
Dans l’idée, c’est cool. Ca permet de ne charger que la méthode désirée en mémoire et évite la surcharge.

Dans la pratique, supposons le cas suivant :
- Un fichier comments.py, qui contient la méthode getPost.
Cette méthode permet d’obtenir le billet dans lequel a été fait le commentaire.
- Un fichier posts.py, qui contient la méthode getComments.
Cette méthode permet d’obtenir tous les commentaires d’un billet.

Dans comments.py, vous faites :
from posts import *
Avec un * parce que vous désirez pouvoir obtenir non seulement le getComments, mais également toutes les méthodes de posts, qui peuvent être aussi diverses que votre imagination le permet.

Dans posts.py, vous faites :
from comments import *
Le *, pour la même raison que précédemment.
La, c’est balot. Mais le second import ne fonctionnera pas.
Avec une erreur du genre :

NameError: name ‘getPost’ is not defined

Python n’arrive pas à charger la classe posts car elle a déjà été chargée auparavant.

Heureusement, nous avons une solution ! :)
Ne faites pas de from class import *. Mais plutôt :
import posts
import comments

La différence est au premier abord anodine. Nous n’aurons plus accès directement à notre méthode getPost, mais à comments.getPost.
Du coup nous allons, en spécifiant l’espace de noms, pouvoir inclure nos classes en les croisant, l’une dans l’autre.

Je ne suis pas sur que cela soit très clair et je ne vois pas trop comment expliquer cela différemment. Donc un petit résumé :
Si vous cherchez à importer une méthode de la classe qui a importé la méthode dans laquelle vous êtes (la, déjà, si vous êtes perdus, on est mal barrés), ne faites pas de from class import method
Mais plutôt un import class.
Puis appellez votre méthode avec un class.method.

Merci à Philippe et à Guillaume Ayoub (pour qui je n’ai pas de lien. Si il passe par ici, qu’il n’hésite pas à faire signe).

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é :)

De la même manière que en Ruby, Django propose une solution d’envoi d’email.
Mais toute application Python n’utilise pas forcément Django. Et il peut arriver d’avoir besoin d’envoyer un email sans Django.

C’est ce que j’ai fait en développant l’outil de rapport de positions par email de RefStats.

Pour cela, nous allons utiliser la librairie smtplib.
Commençons par importer cette librairie.
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

Puis envoyons notre email.
msg = MIMEMultipart('alternative')
msg['Subject'] = 'Sujet de l\'email'
msg.attach(MIMEText('contenu texte', 'text'))
msg.attach(MIMEText('contenu html', 'html'))
email = smtplib.SMTP()
email.connect('localhost')
# email d'envoi - email de réception - message
email.sendmail('noreply@dmathieu.com', 'you@example.org', msg.as_string())
email.close()

Nous instancions l’email, y définissions son sujet.
Ainsi que du contenu html et du contenu texte.
Puis nous nous connectons au serveur smtp (ici localhost) et envoyons l’email.

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.

Après avoir démarré le serveur Open Office, voyons maintenant comment utiliser ce que celui-ci nous apporte.

Je ne vais pas détailler toutes les possibilitées offertes par celui-ci puisqu’elles sont simples : vous pouvez faire tout ce que vous faites avec Open Office en mode bureautique. Sauf que la, c’est du python et c’est automatique !

Commençons donc par ouvrir notre fichier.
cSourceDoc = "/path/to/your/file.html"
url = unohelper.systemPathToFileUrl(cSourceDoc)
args = (PropertyValue('FilterName', 0, 'HTML (StarWriter)', 0),)
model = desktop.loadComponentFromURL(url, "_blank", 0, args)

Nous précisons donc le nom du fichier; nous en définissons les paramètres d’ouverture en indiquant qu’il s’agit d’un document HTML.
Puis nous ouvrons.
Une liste de tous les FilterNames disponibles est accessibles ici.
Mais, malheureusement, comme un peu tout avec PyUNO, elle est très mal documentée.

Maintenant que nous avons ouvert notre document, il faut l’enregistrer dans un autre format.
theTargetFile = '/path/to/your/file.doc'
url = unohelper.systemPathToFileUrl(theTargetFile)
args = (PropertyValue('FilterName', 0, 'MS Word 97', 0), PropertyValue('Unpacked', 0, False, 0),)

model.storeAsURL(url, args)
Nous définissons donc le chemin vers le fichier; le format final et enregistrons.

A cela nous ajoutons la fermeture du client.
print('Closing the client ...')
model.close(True)
ctx.ServiceManager

Et, comme d’habitude, vous pouvez voir le code complet.

Open Office propose une solution permettant d’exécuter toutes les fonctions proposées par le logiciel de manière programmatique. Il s’agit de PyUNO.
Cet article est le premier d’une série qui visera à présenter la librairie Python permettant de manipuler ce serveur Open Office.

Dans ce premier article, nous allons voir comment, dans un script Python, se connecter au serveur et le lancer si cela n’est pas déjà fait.

def start_client():
  print('Starting the client ...')
  localContext = uno.getComponentContext()
  resolver = localContext.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext)
  return resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")

Cette méthode start_client se connecte au serveur Open Office lancé sur le port 2002. Si cela est impossible, une exception sera soulevée.

Du coup si l’on a une exception de soulevée, on peut considérer que le serveur n’est pas démarré et le lancer.
try:
  ctx = start_client()
except Exception, exc:
print('... Server not Started. Starting it ...')
  status = os.system('"' + os.path.normpath(oo_path) + '" "-accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager" -nologo -headless -nofirststartwizard" &')
  time.sleep(2)
  ctx = start_client()

Nous démarrons le client. Si une exception est soulevée alors, nous démarrons le serveur, lui laissons deux secondes pour se lancer et redémarrons le client.

Pour que le serveur puisse démarrer, il vous faut renseigner la variable oo_path avec le chemin vers l’exécutable soffice

Sous debian avec Open Office installé via apt-get, je place :
oo_path = '/usr/bin/soffice'
Et sous Windows :
oo_path = 'C:/Program Files/OpenOffice.org 3/program/soffice.exe'

Suite à cela, vous pouvez récupérer l’instance de Open Office ouverte et la manipuler (créer un nouveau document, l’enregistrer, le modifier, lui changer son format, …)
smgr = ctx.ServiceManager
desktop = smgr.createInstanceWithContext( "com.sun.star.frame.Desktop",ctx)

Si vous désirez voir le code indiqué dans cet article dans on ensemble, c’est par ici.
Et pour un exemple d’utilisation du client que nous venons de démarrer, vous pouvez lire l’article ouvrir un document HTML et l’enregistrer en DOC

 
Fork me on GitHub