[Django] Lister les objets dépendants lors d’une suppression

Tags

,

Dans Django lorsque vous supprimez un objet (obj.delete()), les objets qui en dépendent sont également automatiquement supprimés, ceci afin de garder la base de données cohérentes.

L’espace d’administration de Django affiche les objets qui seront supprimés lors d’une demande de la demande de confirmation, il est possible de reproduire le même principe dans les vues utilisateurs.

django

Demande de confirmation de suppression dans l’admin Django

 

Pour reproduire cette écran dans l’espace utilisateur on va prendre pour exemple la gestion des catégories de tâches dans APM.

La vue gérant la suppression

def delete_view(request, category_id):
    category = get_object_or_404(TaskCategory, id=category_id)
    if not TaskCategory.has_change_permission(request, category):
        return HttpResponseForbidden()

    if request.method == 'POST' and request.POST['post'] == 'yes':
        messages.info(request, '"%s" category successfuly deleted.' % category)
        LogEntry.objects.log_action(
            user_id         = request.user.pk, 
            content_type_id = ContentType.objects.get_for_model(category).pk,
            object_id       = category.id,
            object_repr     = unicode(category), 
            action_flag     = DELETION,
            change_message = unicode(_('Delete the category'))
        )

        category.delete()
        return HttpResponseRedirect(reverse("tasksmanager-category-list"))
    else:

        from django.contrib.admin.util import NestedObjects
        from django.utils.html import escape
        from django.utils.text import capfirst
        def format_callback(obj):
            opts = obj._meta
            return u'%s: %s' % (escape(capfirst(opts.verbose_name)), escape(obj))
        collector = NestedObjects(using='default') # or specific database
        collector.collect([category, ])
        to_delete = collector.nested(format_callback)

        def flat_list(to_delete, level = 0):
            ret = []
            for item in to_delete:
                if type(item) is list:
                    ret += flat_list(item, level + 1)
                else:
                    ret.append( (level, item) )
            return ret
        to_delete = flat_list(to_delete)

        return render_to_response('tasksmanager/category/delete.html',
            {
              "to_delete": to_delete,
              "category": category,
            },
            context_instance=RequestContext(request))

La vue de suppression gère 2 cas:

  • Si on n’a pas une requête de type POST, alors on affiche la demande de confirmation à l’utilisateur.
  • Sinon c’est que l’utilisateur a validé alors on supprime les objets.

Dans le premier cas on a besoin de récupérer la liste des objets qui seront supprimées, pour réaliser ceci on utilise la classe NestedObjects() de Django Admin.

Ensuite il ne reste plus qu’à afficher la liste dans le template:

<ul>
   {% for item in to_delete %}
      <li style="margin-left:{{item.0}}0px;">{{item.1}}</li>
   {% endfor %}
</ul>

Générer un fichier ODT avec Python

Tags

, ,

Il y a quelques semaines je me suis retrouvé devant un problème particulier: générer un fichier ODT depuis un programme python pour le site WEB de la suite CilAPPS. A noter que j’utilise le framework Django et que certains des aspects de mon code y sont liés.

Un fichier ODT est un fichier ZIP contenant des médias et des fichiers XML, il est donc parfaitement possible d’en créer un. Le problème qui peut se poser c’est comment le réaliser simplement.

 

La solution qui a finalement été choisis est d’utiliser la bibliothèque python py3o dans un exemple est disponible sur la page du projet. Elle est simple d’utilisation mais il y manque certaines fonctionnalités. Je vais décrire dans cette article les différents problème que j’ai rencontré et la solution mise en place.

Pour faciliter la mise en place des points qui seront listé ci-dessous j’ai créé une classe nommée ODTParser(). J’explique à la fin de cette article comment installer ODTParser() et patcher py3o.

 

Le fonctionnement de py3o

Je ne vais pas détailler ici le fonctionnement de la bibliothèque, uniquement revenir sur la base. Pour plus de détails vous pouvez vous rendre sur le site officiel.

Le principe de base consiste à:

  • créer un document ODT qui servira de template. Dans ce document vous pouvez utiliser des variables personnalisées et des lien hypertexte pour insérer des blocs if et for.
  • dans votre programme Python, vous faites la fusion entre le template et un dictionnaire Python pour obtenir le document final. Les variables du template font référence aux du dictionnaire Python.

 

Premier problème: le caractère n ne fonctionne pas

Lorsque vous insérer du texte multi-lignes (composé de n), les n sont affichés tels quelle et non interprétés.

La solution pour qu’ils soient interprétées consiste à utiliser la méthode Markup() de genshi.core. tonthon tonthon a mis en ligne un exemple dans ce ticket. Le principal défaut de cette solution c’est que vous ne pourrez plus insérer de caractères composées de ‘<>’ car ils seront tous interprété par LibreOffice ce qui provoquera des erreurs.

 

La méthode xstr(): prend en paramètre un texte et le transforme selon:

  • Si il s’agit d’un booléen elle retourne ‘Yes’ ou ‘False’
  • Si il s’agit d’une date elle retourne strftime(“%d/%m/%y %H:%M”)
  • Si il faut échapper le texte elle retourne sax_escape()
  • Si le texte est “safe” elle retourne Markup()

 

Second problème: Insérer un texte HTML

Ce problème est complexe, il est nécessaire de convertir les balises HTML dans le format ODT. J’ai écrit un morceau de code qui permet de convertir certains caractère HTML, en particulier les style gras, italique.

 

La méthode html(): prend en paramètre un texte et retourne un texte dans le format ODT. Toute les balises HTML sont supprimée, certains sont remplacé par l’équivalent ODT.

Les balises et styles css gérés sont:

  • les balises: p, li, ul, ol, h1, h2, h3, br, strong, em, span et a
  • les styles: text-decoration: underline, text-align: justify, color:, background-color

Il est indispensable que le nom de la variable personnalisée finisse par __ISHTML et que dans le template la variable n’ait pas de style (dans le menu style sélectionner “Effacer le formattage”), ce qui permettra à py3o d’insérer la valeur correctement dans l’ODT.

 

Cette méthode crée une liste de style ODT qu’il est ensuite nécessaire d’insérer dans py3o.

t = Template("template.odt", output)

infos.conclusion__ISHTML = odt_parser.html(conclusion)

t.styles = odt_parser.get_styles()

data = dict(infos = infos)

t.render(data)

 

Le code est sensible, si vous ne mettez pas __HTML ou si dans le template la variable personnalisée a un style, vous aurez une erreur lors de l’ouverture du document final.

Pour éviter une maximum les erreurs toute les balises HTML non converties sont supprimées, le formatage est donc perdu mais le contenu est gardé.

 

Troisième et dernier problème: Comment lire du contenu saisis par un utilisateur dans un document ODT

CilAPPS est une application permettant à des questions dans le cadre d’audit, de diagnostic ou de questionnaire. L’utilisateur lit une question, y répond puis est redirigé vers la question suivante. Il était nécessaire que l’utilisateur puisse générer un document ODT contenant les questions, répondre à chaque question puis charger les réponses dans CilAPPS.

Maintenant qu’on utilise la classe ODTParser avec py3o il est possible de générer le document ODT. mais comment récupérer les réponses ensuite ? il est nécessaire pour cela d’utiliser les contrôleurs de formulaires de LibreOffice. Le principe est le même qu’avec les contrôleurs de formulaires HTML, on insérer des contrôleurs textarea, checkbox ou radiobutton en leur associant à chacun un nom unique.

 

Pour réaliser ceci 4 méthodes ont été ajoutés dans ODTParser:

  • pour créer le fichier ODT
    • form_textarea()
    • form_radio()
    • form_checkbox()
  • pour lire le fichier ODT
    • get_controls_values()

Il est indispensable que le nom de la variable personnalisée finisse par __ISFORM et que dans le template la variable n’ait pas de style (dans le menu style sélectionner “Effacer le formattage”), ce qui permettra à py3o d’insérer la valeur correctement dans l’ODT.

 

Exemple pour générer le fichier ODT

t = Template("referentiel_export.odt", output)

comment__ISFORM = odt_parser.form_textarea('comment','default value', '14cm', '4cm')

t.forms = odt_parser.get_forms()

data = dict(comment__ISFORM=comment__ISFORM)

t.render(data)

 

Exemple pour lire le fichier une fois complété par l’utilisateur

inline = zipfile.ZipFile(fichier, 'r')

content_xml = lxml.etree.parse(StringIO(inline.read('content.xml')))

odt_parser = ODTParser()

controls = odt_parser.get_controls_values(content_xml)

for control incontrols['textarea']:

print '%s: %s' % (control['name'], control['value'])

 

Installer ODTParser()

Vous pouvez télécharger ODTParser() ici.

 

Il est également nécessaire de patcher la bibliothèque py3o pour qu’elle gère les styles et les formulaires.

Dans le fichier template/main.py

class Template(object):
   templated_files = ['content.xml', 'styles.xml', 'META-INF/manifest.xml']
+  styles = []
+  forms = []

 

"""Replace user-type text fields that start with "py3o." with genshi
instructions.
"""

field_expr = "//text:user-field-get[starts-with(@text:name, 'py3o.')]"

for content_tree in self.content_trees:

+    ##ajouts des styles pour les zones HTML
+    for userfield in content_tree.xpath(
+       "//office:automatic-styles",
+       namespaces=self.namespaces
+    ):
+       for style in self.styles:
+          userfield.append(lxml.etree.fromstring(style))
+    ##ajouts des forms
+    for userfield in content_tree.xpath(
+       "//office:forms",
+       namespaces=self.namespaces
+    ):
+       if self.forms and self.forms != '':
+          userfield.append(lxml.etree.fromstring(self.forms))

for userfield in content_tree.xpath(

 

if value_type == 'percentage':
   del npar.attrib[value_attr]
   value = "format_percentage(%s)" % value
   npar.attrib[value_type_attr] = "string"

+ #HTML FORM
+ if value.endswith('__ISHTML') or value.endswith('__ISFORM'):
+    parent_parent = parent.getparent()
+    parent_parent.insert(parent_parent.index(parent), userfield)
+    parent_parent.remove(parent)
+    parent = parent_parent

attribs = dict()
attribs['{%s}strip' % GENSHI_URI] = 'True'
attribs['{%s}content' % GENSHI_URI] = value

Internet Explorer et la sécurité

Le problème que je vais décrire ci-dessous a eu lieu avec Internet Explorer 8 mais il est possible qu’il existe également avec d’autres versions de IE.

 

Le contexte

Un menu déroulant dont chaque élément est une action: “ouvrir”, “supprimer”, “exporter en PDF”. Lorsque l’utilisateur sélectionne un élément, une fonction javascript s’exécute et lance l’action: une redirection:

  • “ouvrir” redirige vers la fiche d’un traitement APM
  • “supprimer” redirige vers une page demandant de confirmer la demande de suppression
  • “exporter en PDF” ouvrir une nouvelle page qui retourne un fichier PDF

Suite à une mise à jour de IE et de la sécurité dans un grand groupe utilisant APM, l’exportation du fichier PDF posait un problème. Lors du premier clique le fichier n’était pas généré et IE affichait le message:

Pour protéger votre ordinateur, Internet Explorer a bloqué le téléchargement de fichiers de ce site vers votre ordinateur, cliquez ici pour afficher plus d’options …

En cliquant sur le message l’utilisateur pouvait alors autoriser le téléchargement mais il fallait alors qu’il relance l’action pour télécharger le fichier. Il pouvait alors télécharger d’autres fichiers mais si il quittait puis revenait sur le site le message revenait et il devait alors à nouveau autoriser le téléchargement. Ceci est très, trop, contraignant pour les utilisateurs.

image002

 

La solution

Après de longues heures de recherches et de tests j’ai fini par émettre l’hypothèse que IE bloque le téléchargement parce qu’il ne fait pas le lien entre le clique de l’utilisateur et le téléchargement (parce que le téléchargement est lancé par javascript) et pense donc que le site lance le téléchargement de lui même. La solution a donc été d’utiliser un lien classique (<a>) pour télécharger les fichiers PDF.

Utiliser IE8 sur Linux pour tester un site Internet

Tags

, ,

Aujourd’hui j’ai eu besoin de tester un bug qu’un client a avec IE8 et une configuration spécifique. La première étape consiste à reproduire le bug sur mon poste, ayant uniquement Ubuntu/Linux j’ai eu besoin d’installer IE8 sur Linux.

Il existe 2 solutions pour installer IE8 sur Linux:

  • Avec Wine et Winetricks, solution légère mais avec des problème de stabilité et de licence.
  • Avec Virtualbox, cette solution install un Windows complet, l’environnement IE est donc plus stable et plus complet (sécurité, …) mais est plus lourd.

Virtualbox

La solution Virtualbox est la plus complète mais aussi la plus lourde. Elle installe un Windows complet dans une machine virtuelle.

Il faut commencer par installer Virtualbox, unar et curl:

sudo apt-get install virtualbox curl curl

puis utiliser ce script pour lancer le téléchargement et la configuration de la VM.

bash ievms.sh

Le script va installer plusieurs versions de Windows et de IE, ceci prendra donc du temps et de l’espace sur votre disque dur.

Finalement vous pouvez choisir la version de IE que vous souhaitez exécuter depuis virtualbox.

ie8

 

Wine & Winetricks

Winetricks permet d’installer facilement différente version de IE, la seule contrainte est d’utiliser wine en 32bits. Cette solution est plus légère parce que Windows n’est pas installé mais est moins stable et ne permet de tester IE au maximum (configuration sécurité, …)

La commande pour lancer l’installation est:

WINEPREFIX=~/.wine32ie8 WINEARCH=win32 winetricks ie8  crypt32


Si vous avez l’erreur suivante:

wine cmd.exe /c echo ‘%ProgramFiles%’ returned empty string

c’est parce que vous avez Wine configuré en 64bits. Il faut donc faire le ménage et reconfigurer Wine:

rm -r ~/.wine32ie8 ~/.wine

WINEARCH=win32 winecfg

Finalement relancer l’installation.


Durant l’installation décocher la case “Install updates”.

ie8

A la fin de l’installation sélectionner “Restart now”.

Ensuite vous pouvez exécuter IE:

WINEPREFIX=~/.wine32ie8 WINEARCH=win32 wine ‘C:Program FilesInternet Exploreriexplore’