Mots-clé : ajax

Django : internationalisation des fichiers javascript : tutoriel complet

Django vient tout faire pour l’internationalisation de pages HTML, mais saviez-vous qu’il est aussi possible et de manière très simple d’internationaliser vos échanges AJAX ?

Vous avez un tutoriel ici.

Seul point noir : ils ont oublié deux éléments essentiels, je vais donc faire une explication courte ici, puis ajouter un résumé dans ma cheatsheet Django.

Dans l’ordre :

  • Configuration de urls.py :
    Ajouter le dictionnaire dans lequel vous précisez vos « packages ». Vos « packages », ce sont simplement les dossiers qui correspondent à l’application que vous voulez afficher. Normalement c’est un dossier racine de votre projet, moi je l’appelle très souvent « app » pour qu’il soit tout en haut des dossiers racine. Donc ici :

    js_info_dict = {
        'packages': ('app',)
    }
  • Configuration de urls.py :
    Ajouter l’URL jsi18n. Attention ! L’aide ne précise pas du tout où la mettre et c’est là où j’ai perdu énormément de temps, faites attention : il faut le mettre dans les patterns traduits, soit ici :

    urlpatterns += i18n_patterns(
        url(r'^jsi18n/$', javascript_catalog, js_info_dict,
            name='javascript_catalog'),
            # blabla...
    }
  • Includes des fichiers js dans les pages :
    <script src="{% url 'javascript_catalog' %}"></script>
  • Includes des fichiers js dans les pages :
    <script src="{% static 'js/globals.js' %}"></script>
  • Dans le fichier précédent, j’ai le code suggéré dans la documentation :
    function _(a) {
         return gettext(a);
    }
  • Générer les chaînes à traduire. Là aussi dans la documentation ils en parlent mais très mal. Pour résumer, il faut préciser le domaine djangojs :
    Exemple d’ordre qui lance la recherche de toutes les chaînes à traduire, en ignorant le dossier third_party (où je mets les outils externes que je ne veux pas toucher) :

    makemessages -d djangojs -i third_party --locale fr --locale en
  • Traduire. Ce sont les fichiers djangojs.domaine djangojs.po qu’il faut modifier, pas le classique django.po (sans strong>js).

Et à partir de là, tout fonctionne : dans tous les fichiers JavaScript qui suivent, je peux traduire à la fois ce qui arrive en AJAX, ou bien tout simplement afficher un message via _(). Exemple :

$('#menu').empty().append(
    $('<h5 />').html(_('Waiting...'))
);

Django cheatsheet

Django aide-mémoire

Templating

Héritage du parent {% block menu %}
{{ block.super }}
{% endblock %}

Astuces

Afficher des messages uniques à l’utilisateur
Ex: « Merci de vous être inscrit » etc.
Infrastructure des messages

SQL

Requête directe

Lancer la console python (Tools » Python console),
puis taper :
from django.db import connection
cursor = connection.cursor()
cursor.execute("PRAGMA table_info(langue)")
for c in cursor.fetchall():
    print(c)


Autre exemple :
from django.db import connection
cursor = connection.cursor()
cursor.execute("delete from app_persongame where person_id=1")
cursor.fetchall()
cursor.execute("update app_persongame set state=1 where person_id=2")
cursor.fetchall()

Sauvegarde / restauration

Lancer via manage.py ([CTRL] + [ALT] + r sous PyCharm) :
Sauvegarde : dumpdata -o data.json
Restauration : loaddata data.json

Multilangue

Créer tous les fichiers en ignorant mes librairies tierces :
makemessages --locale fr --locale en --ignore third_party
Multilangue JavaScript : préciser le domaine
makemessages -d djangojs -i third_party --locale fr --locale en

Multilangue / JavaScript

Declaration dans urls.py

Attention, ici « app » = dossier d’une application qu’on veut traduire, moi je les appelle souvent « app ». :
js_info_dict = {
    'packages': ('app',)
}

urlpatterns = [
    url(r'^i18n/', include('django.conf.urls.i18n')),
    # blabla
]
urlpatterns += i18n_patterns(
    url(r'^jsi18n/$', javascript_catalog, js_info_dict,
        name='javascript_catalog'),
    # blabla
)

Inclure les fichiers js dans la page HTML

<script src="{% url 'javascript_catalog' %}"></script>

Ajouter aussi le fichier personnel où je mets ma fonction « raccourci » pour la traduction :
<script src="{% static 'js/globals.js' %}"></script>

Ma fonction « raccourci » pour la traduction :
function _(a) {
    return gettext(a);
}

Extraction des chaines et traduction

Très important : récupérer les chaines en précisant le domaine djangojs :
makemessages -d djangojs -i third_party --locale fr --locale en

À partir de là, deux fichiers de traduction : le classique django.po et le nouveau djangojs.po.

Exemple de code JavaScript qui fera partie des chaînes à traduire

$('#menu').empty().append(
    $('<h5 />').html(_('Waiting...'))
);

Installation

Python 2.7

virtualenv venvpython2.7/
source venvpython2.7/bin/activate
pip install --upgrade pip
pip install 'django==1.8'
mkdir htdocs
cd htdocs

Python 3.6

virtualenv -p /usr/local/bin/python3.6 venvpython3.6
source venvpython3.6/bin/activate
pip install --upgrade pip
pip install 'django==1.11'

Installez django_markdown_app au lieu de django_markdown (car c’est le successeur, django_markdown n’est plus maintenu !)

pip install django_markdown_app
pip install django-compressor
pip install python3-openid
pip install pytz
mkdir htdocs
cd htdocs

PostgreSQL

Création d’un utilisateur + mot de passe

Être root. De là, taper :
sudo -i -u postgres
psql [nom base de donnees]
CREATE USER interro WITH PASSWORD 'i';
Très important : lui donner tous les droits sur la base :
GRANT ALL PRIVILEGES ON DATABASE "interro" to interro;
Et si l’utilisateur existe déjà :
ALTER USER interro WITH PASSWORD 'i';

Changement d’owner des tables

Se connecter à la base. Allez je mets la commande Windows :
"\Program Files\PostgreSQL\9.5\bin\psql.exe" -U postgres interro
. De là, taper (j’ai bien mis en gras le nom de l’utilisateur à qui on donne les tables) :
SELECT 'ALTER TABLE '|| schemaname || '.' || tablename ||'
OWNER TO interro;' FROM pg_tables
WHERE NOT schemaname IN ('pg_catalog', 'information_schema')
ORDER BY schemaname, tablename;

Et PostgreSQL va sortir une floppée de « ALTER TABLE...« , il suffit juste de les copier/coller dans le prompt pour les appliquer.

Vues Login / logout

– Créer l’URL de login :
url(r'^login/$', LoginView.as_view(), name='login'),

– Créer la vue LoginView
from django.contrib.auth import views

class LoginView(views.LoginView):
    template_name = 'login.html'

    def __init__(self):
        pass

– Créer l’URL de logout :
url(r'^logout/$', LogoutView.as_view(), name='logout'),

– Créer la vue LogoutView
from django.contrib.auth import views

class LogoutView(views.LogoutView):

    def __init__(self):
        self.next_page = '/'

<form action="{% url 'login' %}" method="post" accept-charset="utf-8">
    {% csrf_token %}
    {% for field in form %}
        <label>{{ field.label }}</label>
        {% if field.errors %}
            {{ field.errors }}
        {% endif %}
        {{ field }}
    {% endfor %}
    <input type="hidden" name="next" value="{{ next }}" />
    <input class="button small" type="submit" value="Submit"/>
</form>

Ingesup – Y-nov à terminer après les vacances

Terminer l’appel à la déconnexion en AJAX

$(document).ready(function () {
    $('div[role="navigation"]')
        .find('form')
        .submit(function () {
            $(this).fadeOut(function() {

                $.ajax({
                    url:    $(this).attr('action'),
                    method: $(this).attr('method'),
                    params: $(this).serialize()
                })
                .done(function (data) {
                    if (data.success) {
                        /* montrer le bouton de déconnexion */      
                    } else {
                        /* montrer un message d'erreur (PAS alert) et
                         * remontrer la form de connexion      
                         */
                        
                    }
                })
                .error(function () {
                    alert('Erreur');
                });
            });

            return false;
        });
});

Google Maps API

Tutoriel :

https://developers.google.com/maps/documentation/javascript/tutorial?hl=FR

Fichier JSON qu’il faut créer :

{
  pos: {
    "lat": 0.123123,
    "lng": 0.5934257
  },
  "titre": "Titre Ma maison",
  "contenu": "Maison de chez mes parents etc etc"
}

Exemple de début de code Javascript qui charge le document :


$.getJSON("maison.json")
.done(function (data) {
    /* Code qui crée une google map dans un div */
})
.error(function(obj) {
    alert('Erreur, regardez la console');
    console.log(obj);
});

Dans l’idéal, voici ce qu’il faudrait faire pour le ready() :

$(document).ready(function() {
    /* fonction qui lance le chargement du fichier JSON,
     * et le met dans le div concerné
     */
    ajaxChargeGoogleMaps(
        "maison_parents.json",
        "#id-map1"
    );
    ajaxChargeGoogleMaps(
        "maison_personnelle.json",
        "#id-map2"
    );
});

ExtJs autoLoad et store : howto / comment faire

Si vous ne connaissez pas ExtJs, c’est « à peine » du JavaScript, « à peine » utilisé par IBM, et plein d’autres « petites » entreprises 😉 .

Très puissant mais il manque parfois d’exemple.

En voici un pratique que j’ai eu du mal à mettre en place : pourtant c’est du classique de chez classique.

Je veux ouvrir un onglet qui charge automatiquement des informations de base de données : un onglet dans lequel il y a les informations d’un dossier : nom, prénom, date de naissance, etc.

Assez classique.

Mais disons… que je veux en faire le moins possible. Avec ExtJs, c’est très simple. Enfin simple… après avoir bien cherché !

En fait, l’astuce est qu’il faut appeler la fonction autoLoad, pour que le chargement soit automatique, et avant même d’afficher l’onglet.

Dans mon cas, je voulais afficher Homme ou Femme dans un combo. Donc il fallait avant même d’afficher l’onglet, qu’il précharge le truc dans un magasin de données = store.

Php, AJAX et problèmes de cache sur iOS : comment les résoudre

J’ai expliqué qu’il fallait mettre, avant de faire le tout dernier ordre de sortie echo json_encode($resultat_final); les headers JSON.

header('Cache-Control: no-cache, must-revalidate');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Content-type: application/json');

On m’a demandé à quoi servaient les entête de cache. J’ai expliqué que ça servait à s’assurer que les interrogations AJAX se feraient toujours (notez le futur), c’est à dire à demander au navigateur de ne rien garder en cache.

Comme toujours, ma mémoire me fait défaut mais mon expérience reste, et je savais qu’il faut éviter tout risque de problème cache.
J’ai retrouvé une explication concrète du problème : certaines versions du navigateur Safari sur iOS6 ne réinterrogent pas les appels AJAX.

Il suffit donc de faire deux choses :

  • Soit modifier la configuration Apache (c’est pour ça que j’ai consacré quelques heures sur les hôtes virtuels et leur configuration) pour qu’il y ait ces entêtes par défaut :
    Header set Cache-Control "no-cache"
  • Soit faire ma solution directement en Php (mais ça implique de le faire dans tous les retours AJAX – ce qui ne gêne en rien pour les petits projets)
  • Soit préciser en JavaScript de modifier le header :
    $.ajaxSetup({
        type: 'POST',
        headers: { "cache-control": "no-cache" }
    });

Toutes les solutions sont prises via le site de questions/réponses de reférence : en Anglais ici.

jQuery Mobile : comment forcer le rechargement d’une page ?

Si vous faites des boutons de navigation, votre code jQuery Mobile va ressembler à ceci :

<a href="/">Home</a>

Le seul hic, c’est qu’en cliquant dessus, le navigateur ne va pas recharger réellement la page, il va se baser sur son cache, en supposant que la page n’a pas changé. Ce qui peut poser problème. Par exemple, sur mon site mobile, lorsqu’un commande est validée, je vide le panier côté serveur et je fais un lien vers la page racine, le « home ». Avec le code précédent, le navigateur ne recharge pas réellement la page, et garde le panier toujours « rempli » car il garde les informations et variables JavaScript en mémoire. Dans ce cas, il faut forcer au rechargement de la page. Comment faire ?

Grâce à l’astuce de ce site, voici la solution : il suffit d’ajouter cette balise : rel="external".

Et la page sera rechargée.

<a href="/" rel="external">Home</a>

En espérant que cela aide du monde !

Sencha / ExtJS : comment garder une colonne triée avec une grid ?

Quand on fait une grille de données (datagrid) et qu’on la lie avec un magasin (store) c’est facile.

On peut autoriser à trier par colonnes dans la datagrid.

Seul problème : si le store est un stocké sur le serveur, il fait une seule fois l’appel et ensuite c’est la datagrid qui gère les tris.

Quand on modifie un enregistrement, il est envoyé au serveur, le serveur l’enregistre, et renvoie le résultat de ce qu’il a enregistré. Généralement, il renvoie exactement ce qu’il a reçu. Le seul problème, c’est qu’au retour, la grille ne rafraichit pas l’ordre de tri selon les colonnes qu’on a choisies.

Exemple concret : vous avez une grille avec plein de noms. Vous cliquez sur la colonne « nom », pour la trier par ordre alphabétique. Vous changez le nom « Albert » par « Zoé ». Voici ce qu’il se passe :

  • Le store envoie id=54, nom="Zoé" au serveur ;
  • Le serveur fait la modification en base, et renvoie id=54, nom="Zoé" en retour ;
  • Le store reçoit id=54, nom="Zoé", fait son changement en interne et le transmet à la datagrid ;
  • La datagrid se rafraichit mais ne change pas le tri et laisse "Zoé" à la même place.

La solution : dans le store, lors de l’événement qui signale que le résultat de l’écriture a été intégré (« write« ) il faut forcer l’appel à sort(); qui sera répercuté sur la datagrid automatiquement.

Voici mon code (raccourci à l’extrême sur ma classe de store qui gère les exceptions et plein d’autres choses) :

Ext.define('Ext.data.StoreHandleErrors', {
    extend: 'Ext.data.Store',
    alias: 'data.storehandleerrors',

    constructor: function(config) {
        this.callParent([config]);

        this.on(
            'write',
            function(me, opts) {
                this.sort();
            },
            this
        );
    }
});

ExtJs : dériver un Store « générique » et s’en servir

Ça fait plus d’une journée que je cherche comment faire un Store générique, c’est à dire que j’ai plusieurs Store qui sont tous basés sur le même modèle avec le même type de proxy, etc.

Donc au lieu de faire un copier coller pour chaque Store, j’ai cherché comment en faire un « générique » auquel je pourrai appliquer une configuration « par défaut ».

Voilà le code complet résultat, avec les fonctions qui gère les erreurs possibles renvoyées par Php (session expirée, problème d’écriture en base de données, etc).

Ce qui m’a pris le plus de temps à trouver c’est que pour « surcharger » le constructeur, ça n’est pas la fonction classique « initComponent: function(){ } » mais la fonction de base "constructor: function(config) { }".

Il ne faut, de plus, surtout pas oublier d’appeler le parent, non pas via this.callParent(); mais via this.callParent([config]);.

Ci suit du code, le code de plus d’une journée de travail, en deux parties (j’espère qu’il sauvera du temps à des personnes, ou qu’il les mettra sur la bonne voie !) :

  • première partie = la surcharge
  • seconde partie = exemple d’utilisation de cette surcharge

Première partie : code de la classe

Ext.define('Ext.data.StoreHandleErrors', {
    extend: 'Ext.data.Store',
    alias: 'data.storehandleerrors',

    constructor: function(config) {
        /* (!!) Réécriture par dessus certaines propriétés
         *      du proxy : si jamais elles existent déjà,
         *      elles vont être réécrites.
         */
        config.autoLoad= true;
        config.autoSync= true;
        config.proxy.type= 'ajax';
        config.proxy.reader= {
            type: 'json',
            successProperty: 'success',
            root: 'data',
            messageProperty: 'message'
        };  
        config.proxy.writer= {
            type: 'json',
            writeAllFields: true,
            root: 'data'
        };  
        config.proxy.listeners= {

            exception: function(proxy, response, operation) {

                var error=operation.getError(),
                    title='Erreur du serveur';

                if (error instanceof Array) {
                  error=error.join("
"); } switch(response.status) { case 200: if (response.responseText!='') { var b = Ext.JSON.decode(response.responseText); if (b.title) { title=b.title; } if (b.success==false) { if (b.timeout==true) { windowLoginPanel.show(); } } } break; case -1: var error= 'Le serveur met trop de temps à répondre'+ '

'+ 'On ne peut rien faire, essayez '+ 'd\'actualiser la page.'; break; case 500: var error= 'Le serveur a une erreur interne.'+ '

'+ 'On ne peut rien faire, essayez '+ 'd\'actualiser la page.'; break; default: var error= 'Erreur renvoyée par le serveur non gérée.
'+ 'Détails :
'+ response.statusText+ '

'+ 'On ne peut rien faire, essayez '+ 'd\'actualiser la page.'; break; } Ext.MessageBox.show({ title: title, msg: error, icon: Ext.MessageBox.ERROR, buttons: Ext.Msg.OK }); } }; this.callParent([config]); this.on( 'write', function(proxy, operation) { if ( (operation.action == 'create') || (operation.action == 'update') ) { var m = this.getById( parseInt( operation.resultSet.records[0].internalId ) ); } else if (operation.action == 'destroy') { var m = this.getAt(0); } if (m) { this.panelGridEtEdit.gsGrid.getSelectionModel().select(m); } else { this.panelGridEtEdit.gsGrid.getSelectionModel().deselectAll(); } Ext.example.msg( Ext.String.capitalize(operation.action), operation.resultSet.message ); }, this ); } });

Seconde partie : utilisation de la classe

    var storeAdresses = Ext.create('Ext.data.StoreHandleErrors', {
        model: 'Intranet.Adresse',
        proxy: {
            api: {
                read: '/json/intranet/liste/adresses/',
                create:  '/json/intranet/item/adresse/?mode=create',
                update:  '/json/intranet/item/adresse/?mode=update',
                destroy: '/json/intranet/item/adresse/?mode=destroy'
            }   
        }   
    });