lindev.fr

Aller au contenu | Aller au menu | Aller à la recherche

samedi, janvier 23 2016

Python pip et son cache local

Afin d'éviter de re-télécharger les paquets pour chaque projet nous allons voir ce petit truc extra simple qui est d'activer le cache local de pip.
Le principe est simple, la première fois que vous installez un paquet, ce dernier est téléchargé, puis mis en cache.
La seconde fois ( s'il s’agit de la même version ), pip va simplement l'installer depuis le cache.

Ok, ça ne va pas vous changer la vie, mais moi je trouve ça sympa à utiliser, surtout lorsque vous devez installer des gros paquets avec une connexion d’hôtel faiblarde, ça peut vous faire gagner de précieuses minutes .

Configuration de PIP

Commençons par éditer le fichier de configuration de pip

vim ~/.pip/pip.conf

Ajouter dans la section "global":

[global]
download-cache=/usr/local/pip/cache

enfin on donne les droits d'écriture

mkdir -p /usr/local/pip/cache
chmod -R 755 /usr/local/pip/cache

Deuxième méthode un peu plus courte

Simplement ajouter dans votre fichier de profil : ~/.bash_profile

export PIP_DOWNLOAD_CACHE=$HOME/.pip_download_cache

Voilà pour ce micro billet.

Bon weekend .

lundi, septembre 15 2014

Réparer son virtualenv aprés un update Systéme/Libs

Après une belle maj de votre système, vous avez, au moment de vous remettre au travail, un sympathique message du genre

ImportError: No module named datetime

detatime !! Cette lib fait pourtant partie du standard de python ! Le fait est que lorsque l'on met à jour python, les liens utilisés à la création du venv sont hs !

Ici c'est la lib datetime qui a été remonté, mais ça peu être n'importe laquelle ..

Solution

Il ne m'a pas fallu longtemps pour résoudre ce problème tout bête, ( beaucoup de cas similaires sur le net ).

Réinitialiser le virtualenv

Par exemple avec mon venv nommé foo

virtualenv /home/cdsl/.virtualenvs/foo
New python executable in /home/foo/.virtualenvs/creasoft3/bin/python
Installing setuptools............done.
Installing pip...............done.

Et voilà ..

vendredi, janvier 17 2014

Django, gérer les FormSets dynamiquement

Les formsets sous Django sont très pratiques, notamment pour gérer les formulaires à "champs multiples".

qu'est-ce que j'entends par "champs multiples" ... ceci :

champs_multiples.png

Dans ce formulaire, j'ai donc deux champs uniques

  1. Largeur
  2. Hauteur

Puis vient enfin mon formset qui est composé de trois champs

  1. Désignation
  2. Quantité
  3. Liste : R | RV

J'ai donc au final un formulaire dont je ne connais pas à l'avance la quantité de données à traiter puisque le formset permet d'ajouter dynamiquement ses champs. Et c'est précisément ce que nous allons voir dans ce billet .

  • Comment créer un formset
  • Comment le rendre dynamique pour l'utilisateur ( ajout / suppression )
  • Comment valider les données en une seule ligne ( Merci Django :) )

Créer un Formset

Nous allons reproduire le formulaire présenté ci-dessus , nous aurons donc un formulaire classique , puis un formset de trois champs .

Formulaire classique

class ConfForm(forms.Form):
        """
        Formulaire de configuration du module
        """
        format_x = forms.IntegerField( label="Largeur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )
        format_y = forms.IntegerField( label="Hauteur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )

On ne peut pas plus .. classique pour les détails je vous laisse voir la doc de Django

Le formset

class SorteForm(forms.Form):

    #Champs multiples
    designation = forms.CharField( label="Désignation", required=True, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    quantite = forms.IntegerField( label="Quantité", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    rectoverso = forms.ChoiceField(label='',choices = [(False,'Recto Seul'),(True,'Recto Verso')],required=True,widget=forms.Select( attrs={'class':'form-control input-sm'}))

Vous allez me dire ... "c'est une déclaration de formulaire classique" .. et bien oui rien ne différencie au niveau déclaration un formulaire qui sera utilisé de façon classique, d'un formulaire qui sera utilisé au sein d'un formset .

Tout ceci pour en venir au fait qu'un formset, n'est ni plus, ni moins qu'un mécanisme proposé par Django pour gérer l'affichage, la validation des données et quelques autres petites choses comme les limitations min/max en un minimum d'efforts pour le développeur ...

Ça tombe bien je suis fainéant pour ce qui est de la gestion de formulaires ( pas vous ? ).

Maintenant que nous avons nos deux déclarations de formulaires, voyons comment les implémenter dans une vue.

Dans la vue

Note : Je ne vais pas expliquer ici la mise en forme CSS du formulaire, ce n'est pas l'objectif du billet, sachez juste que j'utilise BootstrapV3 dans la présentation du début.

Commençons avec une vue standard ... vide

def show_form(self, request):
    """ Affichage du formulaire + formset """

    # Formulaire standard à deux champs
    form = ConfForm(auto_id=True)

    #Création du formset avec une seule itération : extra=1
    sorteForm = formset_factory(SorteForm,extra=1)
        
    # Récupération du formulaire géré par le mécanisme formset
    formset = sorteForm()

    # Affichage du template 
    return render_to_response('my_form.html', {'form':form, 'sorteForm':formset}, context_instance=RequestContext(request) )

Le template

D'habitude, pour afficher un formulaire sans mise en forme particulière , la balise form se suffit à elle même.
Pour le formset , c'est la même chose, si ce n'est que nous allons devoir mettre cette balise dans une itération, car rappelez-vous, un formset peut contenir plusieurs formulaires.

Ce qui va donc nous donner : ( sans mise en forme )

<!-- formulaire standard -->
{{ form }}

<!-- Champs utilisés par le mécanisme formset de Django -->
{{ sorteForm.management_form }}

<!-- itération des formulaires du formset -->
{% for form in sorteForm %}
    
    {{ form }}

{% endfor %}

La balise management_form est utilisée par le mécanisme formset pour gérer les éventuels ajouts dynamiques, modifications, etc ... bref .. fonctionner correctement .
Voici son contenu

<!-- nombre de formulaire (s) -->
<input id="id_form-TOTAL_FORMS" type="hidden" value="1" name="form-TOTAL_FORMS">
<!-- nombre de formulaires à l'origine --> 
<input id="id_form-INITIAL_FORMS" type="hidden" value="1" name="form-INITIAL_FORMS">
<!-- nombre max de formulaires gérés si ajout dynamique -->
<input id="id_form-MAX_NUM_FORMS" type="hidden" value="1000" name="form-MAX_NUM_FORMS">

La validation et l'enregistrement

Oui oui .. je sais .. pour le moment il n'y a rien de dynamique, pas d'ajout / suppression ... patience, commençons par boucler la boucle avec la gestion des données POST, nous verrons ensuite pour dynamiser le formulaire .

En effet, l'étape de validation et traitement, ne varie pas selon le nombre de formulaire(s) dans le formset, c'est là toute la force des formset avec Django .

Voici la vue de validation , qui reçoit les données POST

    def save_form(self):
        """  Check & Enregistre les données   """

        # Check si la méthode est bien en POST
        if request.method == 'POST':

            # Instancie et "bound" le formulaire
            form = ConfForm( request.POST, auto_id=True )

            # Création du formset
            sorteForm = formset_factory( SorteForm )
            # "Bound" le formset
            formset = sorteForm( request.POST )

            #Verifie si les données du formulaire simple sont valides
            if form.is_valid() :
                # Vérifie le(s) formulaire(s) du formset 
                if formset.is_valid():

                    #Affiche les données du formulaire simple
                    print form.cleaned_data

                    # Itération du formset pour afficher les données 
                    for fs in formset:
                        print fs.cleaned_data
                        
                else:
                    print = str( formset.errors )
            else:
                print = str( form.errors )

Forcément ici le code ne sert à rien, si ce n'est à expliquer comment valider formulaire + formset pour un traitement des données .. ( enregistrement en base, actions, mails etc ... ou comme ici un simple affichage ;) )

Dynamisme

Nous voilà avec un super formulaire + formset, qui s'affichent correctement, avec les données validées ( peu importe le nombre de formulaire(s) dans le formset ), mais maintenant nous allons voir comment ajouter / supprimer dynamiquement un formulaire dans le formset .

A partir de maintenant tout se passe dans le template , le principe est simple, nous ajoutons un boutton "+" pour ajouter un formulaire au formset et, pour chaque formulaire du formset un boutton "-" qui permettra de retirer le retirer .

Pour ajouter un formulaire au formset, nous allons faire appel à du javascript, qui va cloner un élément du DOM, ré-indexer les formulaires ( du formset ) puis mettre à jour la valeur du champ de management form-TOTAL_FORMS.

Pourquoi ré-indexer

Si l'on regarde le code généré par le formset, les champs sont indexés pour ne pas mélanger les données entres formulaires, vérifier la cohérence des données etc ..

<!-- Form 1 -->
<input id="id_form-0-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-0-designation">
<input id="id_form-0-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-0-quantite">
....
<!-- Form n -->
<input id="id_form-n-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-n-designation">
<input id="id_form-n-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-n-quantite">
...

Formulaire vide

Quel formulaire copier dans le DOM .. et puis .. je ne souhaite pas copier le contenu des champs ...ça tombe bien , encore une fois Django nous livre tout sur un plateau d'argent, grâce à l'attribut empty_form.

Nous allons donc dans un div hors de la balise form inclure le code suivant {{ sorteForm.empty_form }} ce qui va donc nous donner

<div id="my_form">
    <form action="#" name="form_test">
    <!-- Le formulaire standard -->
    {{ form }}

    <button class="btn btn-success btn-sm" id="bt_add_sorte" type="button">+</button>

    {{ sorteForm.management_form }}
        <!-- Les formulaires du formset -->
        <div id="formsetZone">
            {% for form in sorteForm %}
                <div class="nsorte">
                {{ form }}
                </div>
            {% endfor %}
        </div>
    </form>
</div>

<!-- Element à copier pour un ajout au formset -->
<div style="display:none;">
    <div id="eform" class="nsorte" >
        {{ sorteForm.empty_form }}
        <div class='btn btn-warning btn-sm bt_rm_sorte'>
                <i class='glyphicon glyphicon-trash'></i>
        </div>
    </div>
</div>

Voilà à quoi ressemble ce formulaire vide

<label for="id_form-__prefix__-designation">Désignation&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-designation" name="form-__prefix__-designation" placeholder="" type="text">
<label for="id_form-__prefix__-quantite">Quantité&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-quantite" name="form-__prefix__-quantite" placeholder="" type="text">
<select class="form-control input-sm" id="id_form-__prefix__-rectoverso" name="form-__prefix__-rectoverso">
<option value="False">Recto Seul</option>
<option value="True">Recto Verso</option>
</select>

L'objectif pour l'ajout est donc :

  1. Binder l’événement onclick du bouton avec l'id bt_add_sorte
  2. Cloner le contenu du div avec l'id eform
  3. Ajouter le données clonées à la fin du div id formsetZone
  4. Ré-indexer les formulaires du formset pour remplacer prefix par le bon numéro de formulaire
  5. Incrémenter la valeur du champ de management avec l'id id_form-TOTAL_FORMS

Pour la suppression :

  1. Binder les boutons dont la classe est bt_rm_sorte
  2. Enlever du DOM le formulaire à retirer ( conteneur parent avec la classe nsorte )
  3. Re-indexer les formulaires du Formset
  4. Décrémenter a valeur du champ de management avec l'id id_form-TOTAL_FORMS

vive le javascript

Tout ça en javascript, grâce à JQuery

$(document).ready(function(){

    /**************************************************************************
    *
    *                                      Gesion ADD REMOVE Formset 
    *
    ***************************************************************************/

    index_form = function( fset, index ){

        $(fset).find(':input').each(function() {
            var name = $(this).attr('name').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'id_' + name;
            $(this).attr({'name': name, 'id': id});
        });

        $(fset).find('label').each(function() {
            var newFor = $(this).attr('for').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'label_' + newFor;
            $(this).attr({'id':id, 'for':newFor});
        });

    }

    reindex_formset = function( formset_zone ){

        var formset = $(formset_zone).find( '.nsorte' );
        for( var cpt=0;cpt<formset.length;cpt++ ){
            index_form( formset[cpt], cpt );
        };

        $("#id_form-TOTAL_FORMS").val( parseInt( cpt ) );

    };



    /**************************************************************************
    *
    *                               Gesion Des evenements formulaire
    *
    ***************************************************************************/


    set_event = function(){
            //Bind le(s) bt delete sorte
            $(".bt_rm_sorte").on('click',function(){
                $(this).parents(".nsorte").remove();
                reindex_formset( "#formsetZone" );
            });
    };

    $("#bt_add_sorte").on('click',function(){

        //Copy eform
        $( "#eform" ).clone(true).appendTo( $("#formsetZone") );

        reindex_formset( "#formsetZone" );

    });

    set_event();


});

Conclusion

Nous avons fait le tour pour gérer les formset dynamiquement, au diable les galères pour gérer ce genre de cas , merci Django .

Ch.

jeudi, octobre 31 2013

Déployer votre app Python automatiquement avec fabric .

Petite mise en place

Lorsque vous développez en Python, contrairement à PHP, ce dernier compile le code à la première "interprétation", puis génère des fichiers "bytecode", avec l’extension .pyc.
Une fois cette génération faite, le fichier source .py du même nom sera ignoré, et ce jusqu'au redémarrage du process.
Le redémarrage du process, vérifiera les modifications apportées aux sources ( .py ) afin de les recompiler et exécuter le code mis à jour.

Tout ça pour en venir au fait, que lorsque vous développez en python ( par exemple une app. web avec Django ), lorsque vous envoyez vos maj sur le serveur de production, vos modifications n'auront aucun effet tant que vous n'avez pas redémarré les process ( uwsgi, ou autre .. ) .

Ce qui peut vous surprendre au début, si vous venez du monde PHP ou les modifications sont prises en compte instantanément.
Bien que vous pouvez avoir quelque chose de similaire si vous utilisez PHP avec le cache APC ... mais ce n'est pas le sujet ici.

Étapes manuelles

Donc si je prends mon projet Django, voici les étapes manuelles à appliquer :

Sur mon poste de développement :

git add xx ; s'il y a des fichiers sources à ajouter au dépôt
git commit -a -m "Commentaire de mon commit"
git push

Sur mon serveur de prod :

cd /mon/depot
git pull 
# vim settings.py pour des update de version de cache etc .. 
workon monenvironnementvirtuel
python manage.py collectstatic
supervisorctl MonApp reload

Étapes avec fabric

Sur mon poste de développement :
fab backup
Sur le serveur
fab deploy

Bon ... voyons maintenant comment mettre en place fabric .

Installation et fabfile.py

Commençons par installer la librairie ( sur le serveur et le poste de dev. )

workon monenvironnement
pip install fabric

Puis nous allons créer notre fichier qui va se charger d'automatiser les différentes étapes manuelles: fabfile.py

#! -*- coding:utf-8 -*-
from fabric.api import local,prefix
from fabric.colors import green

import re

PROJECT_NAME = "MonApp" # Nom de l'app
SUPERVISOR_APP_NAME = "MonApp" # Nom de l'app supervisor à redémarrer
VIRTUALENV_NAME = "MonApp"  #Nom de l'environnement virtuel à utiliser

def backup():
    #Sauvegarde les librairies utilisées par le projet ( pour les installer si besoin sur le serveur .. sans réfléchir )
    local('pip freeze > requirements.txt')

    #Ajoute les éventuels fichiers non "trackés" 
    local('git add .')

    git_status = local( 'git status', capture=True )
    print git_status

    regx = re.compile(r"(nothing to commit)")
    if not regx.search( git_status ):

        print(green("Il y a des choses à commiter !"))

        print("Entrez votre commentaire de commit : ")
        comment = raw_input()

        #Renseignement du commentaire du commit
        local('git commit -m "%s"' % comment)
        local('git push')
    else:
        print(green("Rien à commiter !"))


def deploy():
    """
    Déploie l'app sur le "serveur" courant 
    """

    #Récupère la dernière version du dépôt
    local('git pull')

    #Permet de changer la version des fichiers statiques à servir 
    print(green("Version du cache à appliquer []:"))
    vcache = raw_input()
    xs = vcache.strip()
    if xs[0:1] in '+-': xs = xs[1:]
    if xs.isdigit():
        init_cache(int(xs))


    # Active ou désactive le mode DEBUG
    print(green("Désactiver le mode DEBUG ( y/n ) ? [y]"))
    disable_debug = raw_input()
    print disable_debug
    if disable_debug == 'y' or disable_debug=='':
        print(green("Passage du mode DEBUG à FALSE"))
        switch_debug(False)
    else:
        print(green("Passage du mode DEBUG à TRUE"))
        switch_debug(True)

    #Récupère les éventuels nouveaux fichiers statiques 
    local(django_manage( 'collectstatic --link', VIRTUALENV_NAME ),capture=False,shell='/bin/bash')

    #Redémarre le processus supervisord
    local('supervisorctl restart '+SUPERVISOR_APP_NAME)



def init_cache(change_to):

    local( 'cp '+ PROJECT_NAME +'/settings.py '+ PROJECT_NAME +'/settings.bak' )
    sed = "sed 's/^VERSION_JS_CSS = [0-9]*$/VERSION_JS_CSS = %s/' "+ PROJECT_NAME+"/settings.bak > "+PROJECT_NAME+"/settings.py"
    local(sed % (change_to))

    local('rm '+PROJECT_NAME+'/settings.bak')

def switch_debug(change_to):

    local( 'cp '+ PROJECT_NAME +'/settings.py '+ PROJECT_NAME +'/settings.bak' )
    sed = "sed 's/^DEBUG = [a-zA-Z]*$/DEBUG = %s/' "+ PROJECT_NAME+"/settings.bak > "+PROJECT_NAME+"/settings.py"
    local(sed % (change_to))

    local('rm '+PROJECT_NAME+'/settings.bak')

Et voilà en deux commandes, vous avez mis à jour votre application sur le serveur de production, chargé les éventuels fichier statiques dans le répertoire prévu à cet effet, modifié le fichier settings.py pour y indiquer s'il faut invalider les fichiers statiques ( css, js .. ) afin de forcer les navigateurs à charger les nouvelles versions, activé ou non le mode DEBUG de Django, et pour finir relancé les process uwsgi grâce à supervisor ( commande supervisorctl ).

Il est possible de tout faire depuis son poste de développement, Fabric peu se connecter sur les serveurs distants pour déployer l'application, ce qui peu s'avérer très utile lorsque vous avez plusieurs frontaux à mettre à jour .

Backup

Fabric_backup.png

Deploy

Fabric_deploy.png

Source:

Fabric : http://docs.fabfile.org/en/1.8/

mercredi, octobre 2 2013

Mezzanine épisode 2

mezzanine.png Nous avons vu dans un précédent billet, comment installer le CMS mezzanine et créer un nouveau projet.
Après s'être familiarisé avec son utilisation, voyons comment customiser notre projet.

Organisation des Thèmes

Un thème mezzanine = une app ( app: répertoire avec un fichier nommé init.py dedans )

Créons notre thème montheme

mkdir montheme && touch montheme/__init__.py

Puis nous allons le déclarer dans le fichier settings.py , au début de le liste INSTALLED_APPS

INSTALLED_APPS = (
    "montheme", # thème à utiliser
    "django.contrib.admin",
    ...

Bien, maintenant nous allons créer la structure de notre thème, à savoir, créer les répertoires de fichiers qtatiques ("static/") et celui des templates ("templates/")

mkdir -p montheme/static/css && mkdir montheme/static/img && mkdir montheme/static/js
mkdir -p montheme/templates/pages/menus && mkdir montheme/templates/includes

Nous voilà avec un superbe template ...... qui ne fait rien !

Il ne nous reste donc plus qu'à créer nos feuilles de styles et fichiers Html pour surcharger le thème par défaut .

Récupérer le thème par défaut

Pour avoir une base fonctionnel, nous allons récupérer les templates et fichiers statiques du thème mezzanine par défaut.

Toujours à la racine de notre projet,

python manage.py collectstatic
python manage.py collecttemplates

Vous vous retrouvez avec deux nouveaux répertoires templates et static qui contiennent donc respectivement les templates et fichiers statiques nécessaires au fonctionnement de base de Mezzanine.

/!\ Attention : si vous laissez tel quel les répertoires, votre template ne sera jamais pris en compte car les répertoires templates et static à la racine du projet sont prioritaires . ( Scannés en premier par Django )
Renommons donc ces répertoires afin de les rendre inactifs

mv templates templates_orig
mv static static_orig

Création de notre thème

Avant d'entrer dans le vif du sujet, il faut bien comprendre comment Mezzanine cherche ses templates.

Par défaut, Mezzanine cherche le template, qui a le même nom que la page demandée. Par exemple, dans l'interface admin, je crée une page dont le titre est "Authors" et le "slug" ( non .. pas la limace, mais une url propre ) : "About/authors", mezzanine va chercher une des correspondances suivantes :

  1. pages/about/authors.html
  2. pages/about/authors/about.html
  3. pages/about/about.html
  4. pages/about.html
  5. pages/page.html


page.html étant la page générique qui sera appelée en dernier recours, c'est cette page qui est disponible "entres-autres" dans les templates du thème par défaut.
Voilà notre point de départ, nous allons donc commencer par copier cette page dans notre thème, qui sera aussi notre page par défaut.

cp templates_orig/pages/page.html montheme/templates/pages/

Si on édite le fichier page.html, nous pouvons voir qu'il s'agit juste d'une structure d'affichage qui s’intègre dans le template base.html .
Copions le également.

cp templates/base.html montheme/templates/

Feuilles de style et javascript

Afin de ne pas perdre les styles et fonctions javascript de base, nous allons créer les nôtres ( au besoin évidemment )

touch montheme/static/css/montheme.css
touch montheme/static/js/montheme.js

Puis modifiez les blocs suivants pour ajouter montheme.css et montheme.js

block compress css
<link rel="stylesheet" href="{% static "css/montheme.css" %}">
block compress js
<script src="{% static "js/montheme.js" %}"></script>

Conclusion

Nous voilà avec un thème qui pour le moment ne fait que reprendre celui par défaut, mais il est en place ! Il ne reste plus qu'à personnaliser les fichiers page.html et base.html dans un premier temps à votre gout .
Si vous souhaitez allez plus loin sur l'intégration des différents éléments dans votre thème, il suffit de suivre le même principe .

  1. Copier les pages concernées du thème original dans votre thème
  2. Les adapter à votre gout ! c'est tout .

Par exemple, si je souhaite modifier le menu en entête, je vais chercher le fichier dropdown.html qui se trouve dans le répertoire templates/pages/menus/ , je le copie dans mon thème montheme/templates/pages/menus/ et je peux travailler dessus en toute liberté .

N'hésitez pas à activer django_debug_toolbar ( dans votre fichier settings.py ) pour vous aider.

Ch.

- page 1 de 3