Django, les commentaires, les URLs et la modularité
En cours d’écriture Kopi, de mon moteur de blog utilisant Django, je suis arrivé au stade des commentaires. Django propose déjà un module de commentaire assez pratique (avec honeypot, protection csrf…). En voulant rester dans la philosophie « less code is better » (particulièrement quand ça vient de moi), j’ai cherché à l’utiliser et l’étendre pour mes besoins. J’ai passé pas mal de temps à bidouiller et fouiller le code (pas toujours très clair la doc Django) et voulais donc partager mes trouvailles (et entendre vos critiques au cas où je suis complètement à coté de la plaque).
Un des points très important pour mon module de commentaire était de rester indépendant. Si un jour je créais une galerie de photo et que je voulais utiliser mon module sans rien devoir y changer. Il ne pouvait avoir AUCUNE référence à mon module de blog dans le module de commentaire. C’est le module de blog qui utilise le module de commentaire, pas l’inverse. Ce point n’est souvent pas respecté dans les exemples glanés sur le net et pourtant est tout à fait possible, ce grâce à la modularité de Django.
Un des problèmes que j’ai rencontré était la redirection des urls. Nativement, Django me redirige chaque post vers une url /posted/ avec un moche « Thank you for posting » digne d’un « It works » post-installation apache. Heureusement pour éviter cela, Django supporte dans son formulaire une balise <input name="next"/> dont la valeur est, comme on pourrait s’en douter, l’url de la page vers laquelle rediriger. Voici comment je m’y suis pris.
D’abord l’architecture de mon projet :
blog/ templates/ blog/ post_detail.html ... comments/ templates/ comments/ form.html ... views.py urls.py ... kopi/ settings.py urls.py ...
Dans mon fichier kopi.urls.py, j’inclus de manière habituelle les urls concernant mon blog et mes commentaires, aucun lien entre les deux.
1 2 3 4 |
urlpatterns += patterns('', url(r'^comments/', include('comments.urls')), url(r'^', include('blog.urls')), ) |
Du coté du blog, je gère donc mes articles. Une simple Generic View fait très bien ça en 5 lignes. Dans ma template blog/templates/blog/post_detail.html, j’inclus ma template de cette façon :
1 2 |
{% load comments %} {% render_comment_form for current_post %} |
où current_post est le nom de ma variable contenant mon article de blog.
Et voila pour le coté, blog, le reste est du coté des commentaires. Pour le module de base, je suis parti de ce qui a été fait dans django-basic-apps, l’import des méthodes de django.contrib.comments pour que mon module comments fasse exactement la même chose que celui dans les contrib sans avoir à l’importer dans les settings (c’est même conseillé, django est assez chiant quand deux apps portent le même nom). Le fichier comments/urls.py sera par exemple :
1 2 3 4 5 6 7 8 9 10 11 |
from django.conf.urls.defaults import * from django.contrib.comments.urls import urlpatterns urlpatterns = patterns('comments.views', # let's overwrite django.contrib.comments url(r'^post/$', view='custom_comment_post', name='comments-post-comment'), url( r'^', include( 'django.contrib.comments.urls' ) ), ) |
Je reprend donc toutes les urls de django.contrib.comments.urls mais en forçant à utiliser ma view custom_comment_post plutôt que celle de base (Django cherche un match dans l’ordre et s’arrête au premier rencontré). Si vous voulez étendre le modèle pour y ajouter des champs, la logique est la même (Customizing the comments framework).
D’abord, je voulais réécrire ma template de formulaire pour qu’il connaisse la valeur next. J’ai donc créé le fichier comments/templates/comments/form.html avec (version simplifiée)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{% load comments i18n %} <form action="{% comment_form_target %}" method="post">{% csrf_token %} <div>{% if next %} <input type="hidden" name="next" value="{{ next }}" /> {% else %} <input type="hidden" name="next" value="{{ form.target_object.get_absolute_url }}" /></div> {% endif %} </div> {% for field in form %} <p>{{ field.label_tag }} {{ field }}</p> {% endif %} {% endfor %} <p class="submit"> <input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" /> </p> </form> |
Ainsi soit l’on passe une valeur choisie avec la variable next, soit on est redirigé vers l’url de l’objet pour lequel on affiche le formulaire. En mettant le for current_post dans le render_comment_form, j’indique ainsi que current_post sera le target_object. Et ça c’est très cool parce que ça veut dire que pour ma galerie d’image, je n’aurai qu’à faire {% render_comment_form for current_image %} et ne pas changer ma template form.html.
Après un test qu’est ce qu’on voit ? Le formulaire présent sur /monarticle/ fait une requête POST vers /post/, réponse depuis /posted/ (la page moche) mais ne reste pas dessus car a un header avec le code 302, redirection vers /monarticle/?c=42 où 42 est l’ID du commentaire.
Parfait ? Non pas encore complètement, j’aimerais que le visiteur aie une ancre qui l’amène au niveau de son commentaire. Je n’ai pas besoin de ce vieux ?c=42, par contre un #c42, ça m’intéresserait déjà un peu plus… Justement, c’est là qu’intervient mon overwrite au dessus avec la view custom_comment_post. On va forcer à récupérer l’url du commentaire plutôt que l’url du post un peu modifiée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def custom_comment_post(request, next=None, using=None): # on utilise encore le code de la classe contrib response = contrib_comments.post_comment(request, next, using) if type(response) == HttpResponseRedirect: # url in the form /redirect_path/?c=comment_id redirect_path, comment_id = response.get('Location').split( '?c=' ) if comment_id: # get the related comment comment = Comment.objects.get( id=comment_id ) if comment: # url in the form /comments/cr/object_id/comment_id#ccomment_id return HttpResponseRedirect( comment.get_absolute_url() ) return response |
Et là on laisse la magie de Django travailler, la fonction get_absolute_url du module de commentaire de base est bien foutu et va résoudre l’url /monarticle/#c42. Il vous suffit alors dans votre template d’ajouter une ancre pour ce format.
1 2 3 4 5 6 |
{% for comm in comment_list %} <article id="c{{comm.id}}"> <a href="{{ comm.user_url }}">{{ comm.user_name }}</a> says:<br/> <p>{{ comm.comment }}</p> </article> {% endfor %} |
Vous n’aimez pas le format #c{{id}} ? Vous savez que vous êtes compliqué ? Mais ça tombe bien, Django a prévu ça. La fonction comment.get_absolute_url() prend un paramètre optionnel anchor_pattern="#c%(id)s". Il suffira d’appeler la fonction avec par exemple "#commentaire-id-%(id)s" pour obtenir quelque chose de plus agréable.
J’anticipe la question : que faire si je veux que sur mon application de blog j’ai #commentaire-post-%(id)s et sur ma galerie #commentaire-photo-%(id)s. Et bien là j’avoue que je n’ai pas encore trouvé de moyen simple de le faire. On peut y arriver en écrivant un nouveau templatetag (un overwrite de render_comment_form par exemple) mais franchement, ça fait beaucoup de boulot pour pas grand chose. Si vous avez une suggestion simple et élégante, je suis preneur.