momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

O aplicaţie Web cu Django şi jQuery: colecţii de partide

Django | HTML | MySQL | Python | jQuery
2014 aug

În [2] am demarat aplicaţia /slightchess şi am constituit fişierul models.py; în [1] am sintetizat apoi procedurile prin care populăm baza de date aferentă acestor modele Django, plecând de la un fişier PGN conţinând o colecţie de partide de şah. Arătăm cum am dezvolta slightchess - alegând cele mai simple şi fireşti soluţii, problemelor din contextul intenţionat pentru aplicaţie.

Intenţia de bază este următoarea: alegând un "coach" şi un "partner" - utilizatorul va obţine lista partidelor dintre aceştia doi, putând să urmărească (printr-o anumită interfaţă grafică) desfăşurarea uneia sau alteia (iar dacă este autorizat, va putea să efectueze anumite operaţii asupra partidei).

Interludiu: cum operează Django

Putem investiga direct diverse aspecte şi concepte specifice Django (vezi şi [3]), printr-un script slightchess/helper.py în care definim întâi DJANGO_SETTINGS_MODULE (dispunând apoi de ceea ce am configurat iniţial (pentru baza de date, aplicaţii, etc.) în fişierul "settings.py" al proiectului):

vb@vb:~/slightchess$ python helper.py
<ol>
      <li value="1">vlad.bazon</li>
      <li value="9">Joe666</li>
</ol>
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'slightchess.settings')

from django import template
from games.models import *

coachs = Selector.objects.all()

response_template = template.Template('''
<ol>
    {% for coach in coachs %}
        <li value="{{coach.id}}">{{coach.instant_username}}</li>
    {% endfor %}
</ol>''')

context = template.Context({'coachs': coachs})

print response_template.render(context)

În coachs se obţine lista obiectelor Selector (corespunzătoare înregistrărilor din tabelul games_selector - vezi [2]). Apoi, prin clasa Template() se construieşte un şablon de răspuns, iar prin clasa Context() se specifică într-un dicţionar Python, valorile cu care să fie înlocuite variabilele existente în acest şablon. Metoda render() va compila şablonul respectiv, înlocuind 'coachs' (din pseudo-instrucţiunea {% for %}) cu lista coachs şi producând câte un element HTML <li> pentru fiecare obiect al acestei liste, folosind valorile câmpurilor id şi instant_username din obiectul curent.

Aici am prevăzut un şablon de răspuns ca şir Python; de obicei, şabloanele care vor servi pentru formularea răspunsurilor la diversele cereri receptate în cadrul aplicaţiei sunt constituite ca fişiere .html într-un director /templates, specificat printre celelalte configurări din settings.py.

Dezvoltarea aplicaţiei 'slightchess'

Este suficientă o pagină conţinând două zone distincte: o diviziune "slgaction" conţinând elemente <select> pentru "coach" şi "partner" şi o diviziune "slggame" pentru lista partidelor. Vom neglija aici, link-urile pentru autentificare ("Login", etc.), precum şi implicarea unui widget ca PGN-browser prin care să se vizualizeze desfăşurarea unei partide.

Cel mai simplu este să bazăm pagina pe următoarele specificaţii CSS:

#slgcontainer-----------------------------
                            |            
    #slggame                | #slgaction 
                            |            
------------------------------------------
    /*   ~/slightchess/static/CSS/slight.css    */
#slgcontainer {
    display: table;
}
#slggame, #slgaction {
    display: table-cell;
}
#slggame {
    width: 650px;
}

display: table şi display: table-cell vor permite redarea paginii ca şi când ar fi vorba de un tabel cu două coloane (dintre care prima are lăţimea fixată convenabil).

Presupunem deja constituit un "virtual host" www.slightchess; următorul fişier index.html va putea produce (cum urmează să arătăm) întreaga funcţionalitate vizată mai sus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>  <!-- ~/slightchess/templates/index.html -->
<html>
<head>
    <meta charset="utf-8" />
    <title>slight-chess Collections</title>
    <link href="{{STATIC_URL}}CSS/slight.css" rel="stylesheet" />
    <script src="{{STATIC_URL}}JS/jquery-1.11.1.min.js"></script>
</head>
<body>
    <div id="slgcontainer">
        <div id="slggame">
            {% include "slggame.html" %}
        </div>
        <div id="slgaction">
            <p>Coach ({{coachs.count}}) <select name="coach">
            {% for coach in coachs %}
                <option value="coach{{coach.id}}" 
                    {%if forloop.last%}selected="selected"{%endif%}>
                    {{coach.instant_username}}</option>
            {% endfor %}</select></p>
            <div id="change_coach" data-chchid="{{coachs.last.id}}">  
                {% include "change_coach.html" %}
            </div>
        </div>
    </div>
</body>
</html>

Django conectează cererile receptate de la clienţii aplicaţiei cu funcţiile existente pentru deservirea acestora, prin intermediul unor fişiere urls.py:

#   ~/slightchess/slightchess/urls.py 
from django.conf.urls import patterns, include, url
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
    url(r'^$', 'games.views.home', name='home'),
    url(r'^admin/', include(admin.site.urls)),
)

Conform primeia dintre cele două specificaţii url(), cererea http://www.slightchess/ va fi deservită de funcţia home() din fişierul views.py al aplicaţiei ~/slightchess/games (înfiinţate în [2]; al doilea url() leagă cererea www.slightchess/admin/ de aplicaţia "admin").

Fişierul index.html redat mai sus este un şablon al răspunsului pe care va trebui să-l formuleze funcţia home() şi putem evidenţia ce înlocuiri vor trebui făcute, încărcându-l direct în browser (prin pseudo-protocolul file:///home/.../index.html) - caz în care am obţine "pagina":

home(request) va trebui să contextualizeze variabila-şablon 'coachs' din linia 16 (înlocuind cu lista obiectelor Selector), derivând apoi şi valorile variabilelor din liniile 17 (variabila {{coach.id}}) şi 19. Se constituie astfel, elementul HTML <select> pentru obiectele coachs (iar în linia 18 este marcat ca selectat, ultimul din listă); constituirea elementului HTML <select> pentru obiectele Partner este lăsată şablonului "change_coach.html" - dat fiind că vrem să listăm partenerii corespunzători alegerii efectuate pe prima listă ('Coach').

Pe diviziunea care va include rezultatul contextualizării şablonului "change_coach.html" - din liniile 21-23 - am montat atributul data-chchid, alegând ca valoare iniţială valoarea câmpului id al ultimei înregistrări efectuate în tabelul games_selector (valoare modelată prin {{coachs.last.id}}, în linia 21). home(request) va putea disocia (consultând request) cazul iniţial (când data-chchid n-a fost modificat) de cazul când utilizatorul alege un alt element din lista 'Coach' - furnizând şablonului "change_coach.html" lista obiectelor Partner corespunzătoare elementului selectat din 'Coach' şi încă, furnizând şablonului "slggame.html" partidele aferente valorilor selectate din aceste două liste.

Realizăm transmiterea către funcţia home() a valorilor selectate de utilizator, montând pe cele două elemente <select> câte un handler de eveniment "onChange" - linia 63 - care angajează metoda jQuery post() (linia 70 şi respectiv, linia 77):

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<!-- ~/slightchess/templates/change_coach.html -->
{% load dct_sort %} <!-- asigură "tagul" |sort - ordonează un dicţionar după chei -->
<p>Land ({{lan_par|length}})<br>Partner <select name="lan_par">
{% for land, parts in lan_par.items|sort %}
    <optgroup label="{{land}}">
        {% for p_nume, p_id in parts.items|sort %}
            <option value="par{{p_id}}">{{p_nume}}</option>
        {% endfor %}
    </optgroup>
{% endfor %}</select></p>

<script>
$(function() {
    $('#slgaction').find('select').on('change', function(event) {
        event.preventDefault();
        var option = $(this).val();
        var model = option.match(/^\D+/)[0];
        var id = option.match(/\d+$/)[0]; 
        switch(model) {
        case 'coach': 
            $.post("{%url 'home'%}", 
                   {'coach_id': id, 'csrfmiddlewaretoken': '{{ csrf_token }}'}, 
                   function(response) {
                      $('#change_coach').html(response).data('chchid', id);
                   }
            ); break;
        case 'par': 
            $.post("{%url 'home' %}",
                   {'par_id': id, 'chchid': $('#change_coach').data('chchid'), 
                    'csrfmiddlewaretoken': '{{ csrf_token }}'},
                   function(response) {
                       $('#slggame').html(response);
                   }
            ); break;
        }
    });
});
</script>

Când utilizatorul va selecta un alt element din lista 'Coach', va fi executată secvenţa 70-75: se va transmite funcţiei home() 'id'-ul obiectului selectat şi răspunsul returnat de aceasta va fi înscris în diviziunea '#change_coach', actualizând în acelaşi timp valoarea atributului data-chchid al acesteia.

Când se va selecta un alt element din lista 'Partner' (marcată la linia 52) - va fi executată secvenţa 77-83: se va transmite funcţiei home() 'id'-ul partenerului selectat, împreună cu valoarea curentă a atributului data-chchid; astfel (având şi partner_id şi coach_id), home() va putea determina care partide trebuie returnate, pentru a fi înscrise (linia 81) în diviziunea '#slggame' - iar răspunsul respectiv va fi obţinut contextualizând şablonul 'slggame.html':

<!--    ~/slightchess/templates/slggame.html    -->
{% for game in games %}
    <div>{{game}} 
        <textarea>{{game.pgn}}</textarea>
    </div>
{% endfor %}

Fiecare partidă din lista 'games' va fi reprezentată printr-o diviziune conţinând "titlul" partidei (aşa cum este el formulat de metoda __unicode__() din clasa Game, în [2]) şi un element <textarea> în care va fi înscris textul PGN al partidei. Ulterior, vom ataşa acestor elemente <textarea> câte un widget-ul pgnbrowser(), asigurând posibilitatea vizualizării grafice, în mod interactiv a desfăşurării partidelor; deasemenea, va fi de completat 'slggame.html' cu link-urile şi handlerele necesare pentru a asigura utilizatorului autorizat anumite operaţii asupra uneia sau alteia dintre partidele redate (adăugând şi în views.py funcţiile specifice realizării acestor operaţii).

home() determină (folosind definiţiile şi metodele din models.py) obiectele necesare şabloanelor HTML redate mai sus, luând în consideraţie atât cererile obişnuite (http://slightchess/, din bara de adresă a unui browser), cât şi cererile de la metodele jQuery.post() (din liniile 70 şi 77) - distincţia fiind făcută (în linia 106) prin metoda is_ajax() (a obiectului Django HttpRequest):

100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#   ~/slightchess/games/views.py
from django.shortcuts import render
from models import Selector, Land, Partner, Game
import random

def home(request):
    if request.is_ajax():
        if 'coach_id' in request.POST:
            coach_id = request.POST.get('coach_id')
            lan_par = Selector.objects.get(id=coach_id).land_partners()
            return render(request, 'change_coach.html', {'lan_par': lan_par})  
        elif 'par_id' in request.POST:
            par_id = request.POST.get('par_id')
            ch_id = request.POST.get('chchid')
            games = Game.objects.filter(partner_id=par_id, coach_id=ch_id)
            return render(request, 'slggame.html', {'games': games})
    else:
        coachs = Selector.objects.all()
        lan_par = coachs.last().land_partners()
        game = random.choice(Game.objects.all())
        context = {'coachs': coachs, 'lan_par': lan_par, 'games': [game]}
        return render(request, 'index.html', context)

Cererile transmise de metoda jQuery.post() din linia 70 (respectiv, din linia 77) sunt rezolvate de secvenţa 108-110 (şi respectiv, 112-115); secvenţa 117-121 rezolvă cererile obişnuite.

În linia 109 şi apoi, în linia 118 este invocată metoda Selector.land_partners() - care însă nu apare în [2] (unde am constituit models.py, importat aici în linia 102); adăugăm deci această metodă:

# adăugare în 'models.py': metoda Selector.land_partners()
class Selector(AbstractUser):
    instant_username = models.CharField(max_length=64, unique=True)
    
    def land_partners(self):
        from django.db import connection
        cursor = connection.cursor()
        cursor.execute("""
            SELECT DISTINCT games_partner.nume, games_partner.id, games_land.cod
            FROM games_game 
            INNER JOIN games_selector ON (games_game.coach_id = games_selector.id)  
            INNER JOIN games_partner ON (games_game.partner_id = games_partner.id)  
            INNER JOIN games_land ON (games_partner.land_id = games_land.id)  
            WHERE games_game.coach_id = %s""", [self.id])
        result = {}
        for row in cursor.fetchall():
            result.setdefault(row[2], {})[row[0]] = row[1]
        return result

Pentru obiectul Selector din care este invocată, metoda land_partners() va produce un dicţionar având drept chei coduri de ţară din înregistrările existente în tabelul games_land şi drept valori, câte un dicţionar {nume_partner: partner_id} cuprinzând toţi partenerii din acea ţară, care au partide cu "selectorul" respectiv.

Lista lan_par rezultată astfel (linia 108, 118) va intra în contextul şabloanelor indicate în liniile 110 şi 121, metoda render() asigurând înscrierea codurilor de ţară în elementele <optgroup> indicate în linia 54 şi a numelor şi id-urilor partenerilor în elementele <option> indicate în linia 56. Următoarea imagine sugerează rezultatul final:

Prin selectarea în a doua coloană, a celor două elemente ('Coach' şi 'Partner'), am obţinut în prima coloană (în diviziunea "slggame") partidele corespunzătoare acestora. Alegerea unui alt 'Coach' va actualiza imediat, lista 'Partner'; reîncărcarea paginii (tastând F5, sau CTRL+F5) înseamnă în fond o "cerere obişnuită" (nu prin "AJAX"), încât se va executa secvenţa 117-121: se marchează ca selectat ultimul element din lista 'Coach', actualizând corespunzător lista 'Partner'; însă în prima coloană se va înscrie (în scop demonstrativ…) o partidă oarecare, extrasă aleatoriu (la linia 119).

vezi Cărţile mele (de programare)

docerpro | Prev | Next