[1] Sintetizarea etapelor de înregistrare a datelor dintr-o colecţie PGN
[2] Modelarea şi înregistrarea datelor dintr-o colecţie de partide de şah
[3] Experimente de modelare a datelor (Python, Django şi SQL)
Î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).
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
.
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)