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

Un exemplu de lucru cu formulare dinamice, în Django

Django | Python | jQuery
2013 jan

[1] Modele de date pentru reflectarea situaţiei şcolare, cu Python şi Django

Să considerăm nişte modele de date (precum cele din [1]) pentru reflectarea dinamică a situaţiei şcolare: folosind interfaţa de administrare oferită, dirigintele introduce elevii (după modelul Elev) şi înregistrează periodic notele şi absenţele pe semestrul curent (după modelul Notesem), putând institui în orice moment o "situaţie finală" - cuprinzând toate mediile (pe elevi şi pe clasă, pe obiecte şi arii curriculare), ca şi când anul şcolar s-ar încheia în acel moment.

Iar situaţia finală "momentană" creată astfel devine disponibilă tuturor elevilor clasei respective - ideea fiind că elevul şi-ar putea semnala (la timp, înainte de încheierea reală a semestrului) că locul său în cadrul ierarhiei "finale" poate fi încă îmbunătăţit, cu anumite eforturi.

Desigur, situaţia finală "momentană" va sintetiza mediile pe semestrul curent pe baza înregistrărilor de note existente la acel moment în baza de date; dar în cazul semestrului II, ea va trebui totuşi să redea şi mediile rezultate pe semestrul anterior (mai sintetizând şi mediile anuale "momentane").

În acest scop, să presupunem conceput un model de date corespunzător formularului alăturat - să-i zicem Medan.

Medan conţine o referinţă la elevul respectiv ("foreign key", cu valori în mulţimea indecşilor tabelului Elev) şi un câmp de tip "text" redat alături prin elementul <textarea> (etichetat "Medii"). Când vor fi înregistrate note pentru semestrul II, situaţia finală "momentană" va trebui să completeze datele - de exemplu înlocuind linia prezentă [OS] Religie: 9 una ca [OS] Religie: 9 10 9.50 - copiind pur şi simplu ceea ce era deja înscris pentru semestrul I şi completând cu valorile sintetizate pentru semestrul al doilea şi pentru tot anul.

Această modelare a datelor - bazată pe câmpuri "text" în care sunt pre-înscrise denumirile obiectelor, urmând ca dirigintele să completeze cu notele/mediile corespunzătoare - nu este totuşi aşa de convenabilă (cum apreciasem pentru modelul Notesem în [1]); ne scuteşte într-adevăr, de conceperea unei interfeţe proprii de administrare (putând implica fără modificări pe cea standard, oferită de Django - de pe care am şi redat imaginea de mai sus) dar sunt de scris şi de manevrat câteva metode "delicate" de filtrare şi prelucrare a textului conţinut de câmpul respectiv. Până la urmă, am ajuns la concluzia că tot modelele "clasice" (Medan leagă Obiect şi Elev şi mai conţine un câmp simplu pentru medie; analog, Notesem) sunt de preferat…

Să presupunem deci, un model Medan cum am descris mai sus şi să vedem cum am putea crea o interfaţă de administrare cât mai simplă - nu cea standard, de pe care am redat mai sus - pentru cazul când dirigintele ar folosi aplicaţia abia din semestrul al doilea; fiindcă în mod firesc, "situaţia finală" necesită şi mediile din primul semestru (care nu au putut fi sintetizate, fiindcă dirigintele nu a înscris note pe semestrul I) - acestea trebuie introduse direct.

Deci avem de creat un formular care să redea obiectele respective împreună cu câte o casetă pentru introducerea mediei (elevul fiind deja selectat din lista elevilor).

Acest formular va fi simplu şi comod de completat: se tastează media şi se poate folosi tasta TAB, pentru a trece la alt obiect (iar în final - click pe butonul "Înscrie mediile").

Datele transmise prin acest formular (mediile şi indecşii obiectelor) vor trebui "concatenate" pentru a obţine valoarea "text" specificată de modelul Medan pentru câmpul de "Medii".

Obiectele la care trebuie înscrise mediile sunt valori extrase din tabelul de date corespunzător modelului Obiect; diriginţii pot adăuga obiecte, după necesităţi şi pot eventual modifica denumirile acestora - prin urmare avem de construit un formular dinamic, definindu-i câmpurile (obiect: medie) în momentul execuţiei.

Obţinerea listei elevilor fără medii

Mai întâi, să creem o listă a elevilor (din clasa respectivă) pentru care trebuie înscrise mediile pe semestrul I, astfel încât la click pe un item al acestei liste să se instanţieze alături un formular pentru înscrierea mediilor elevului selectat (cum se vede pe figura de mai sus).

În views.py definim funcţia următoare:

def final_semi(request):
    clasa = request.user.username[:2] # '1A' pentru IX-A, '2A' = X-A, etc.
    elevs = Elev.objects.filter(clasa__cod=clasa).exclude(medan__isnull=False)
    return render_to_response('stare/final-semi.html', 
        {'elevs': elevs},
        context_instance = RequestContext(request)
    )

De pe cererea HTTP (presupunând un 'user' autentificat corespunzător) se determină codul clasei (primele două caractere din "username") şi se constituie lista 'elevs' a elevilor acestei clase care nu sunt referiţi din vreun Medan (deci nu li s-au înscris mediile). Putem verifica într-un terminal, obţinerea acestei liste (folosind utilitarul manage.py oferit de Django):

# python manage.py shell
from stare.models import Clasa, Elev, Medan
clasa = '2A'
elevs = Elev.objects.filter(clasa__cod=clasa).exclude(medan__isnull=False)
print elevs
[<Elev: Bar Car>, <Elev: Bor Cor>, <Elev: Cas Dar>, ]

Un obiect <Elev: ...> are clasa de bază <class 'stare.models.Elev'> definită în fişierul /stare/models.py, având structura Elev(id, nume, pren, cnp, foto, clasa_id).

Fişierul-şablon /final-semi.html căruia i se transmite lista de obiecte 'elevs' pentru definitivarea răspunsului final (către browserul de la care se primise cererea), poate arăta astfel:

 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
28
29
30
31
32
33
34
35
36
37
{% extends 'base.html' %}
{% block content %}
<ul id="lista-elevi">
    {% for elev in elevs %}
        <li><a href="" id="elev{{elev.id}}">{{elev}}</a></li>
    {% endfor %}
</ul>

<div id="set-medii1"></div>

<script type="text/javascript">
    $.ajaxSetup({
        data: {csrfmiddlewaretoken: '{{ csrf_token }}' },
    });
    $(function() {
        $('#lista-elevi').click(function(event){
            var clicked = $(event.target), 
                elev_id = clicked.attr('id');
            if(elev_id) {
                $(this).find('a').removeClass('bold');
                clicked.addClass('bold');
                elev_id = elev_id.replace('elev', '');
                $.post(
                        "{% url set_medii_elev %}",
                        {'elev': elev_id},
                        function(response) {
                            $('#set-medii1').html(response)
                                            .find('input:eq(1)').focus();
                        }
                );
            }
            return false;
        });
        $('#lista-elevi a:first').click();
    });
</script> 
{% endblock %}

Prin liniile 3-7 se constituie o listă HTML cu identificatorul "lista-elevi" şi apoi prin linia 9 - o diviziune HTML identificată prin "set-medii1".

În liniile 11-36 se defineşte un element <script>, al cărui conţinut va fi executat după încărcarea în browser a paginii respective (este implicată în mod tacit, biblioteca javaScript jQuery). În liniile 16-32 se ataşează listei "lista-elevi" un eveniment click, iar în linia 34 se declanşează funcţia respectivă pentru primul element al acestei liste.

Fiecare item al listei "lista-elevi" conţine un element <a> (vezi linia 5) al cărui atribut "id" este citit la click în linia 18, fiind transmis prin metoda jQuery $.post() (vezi liniile 23-30) view-ului Django corespunzător cererii HTTP pentru "set_medii_elev" (modulul stare/urls.py prevede legăturile între cererile HTTP (URL-uri) şi funcţiile din stare/views.py); în final (liniile 26-29), răspunsul returnat de funcţia activată de "set_medii_elev" este înscris în diviziunea "set-medii1".

Ceea ce vrem să apară în diviziunea "set-medii1" este formularul prin care utilizatorul să poată înscrie şi transmite mediile elevului selectat în "lista-elevi".

Adăugarea formularului pentru preluarea mediilor elevului

Putem formula funcţia din stare/views.py contactată prin $.post() de pe pagina /final-semi.html descrisă mai sus, astfel:

def set_medii_elev(request):
    if request.is_ajax():
        el = request.POST.get('elev')
        elev = get_object_or_404(Elev, id=el)
        return render_to_response('stare/set_medii.html', 
            {'elev': elev,
             'form': MedieForm(None, obiecte = get_lista_obiecte()),},
             context_instance = RequestContext(request)
        )

Se determină obiectul Elev al cărui "id" a fost transmis (linia 25 din listingul precedent), pasându-l şablonului /set-medii.html - împreună cu un obiect MedieForm() care reprezintă formularul necesar pentru înscrierea mediilor elevului respectiv. Vom construi mai târziu acest formular, dar este clar că definirea lui necesită lista curentă a obiectelor la care trebuie înscrise mediile.

Lista obiectelor (transmisă lui MedieForm()) poate fi obţinută apelând următoarea funcţie:

# stare/views.py
def get_lista_obiecte():
    return [ob.id_arob() for ob in Obiect.objects.all()]

unde id_arob() este o metodă inclusă în Obiect:

# stare/models.py 
class Obiect(models.Model):
    nume = models.CharField(max_length=64, unique=True)
    arie = models.ForeignKey(Arie) # aria curriculară
    
    def id_arob(self):
        return (self.id, '['+self.arie.acro+'] '+self.nume)
    
    # ...

care returnează un tuplu precum (1, '[MŞ] Matematică') - unde prima componentă este valoarea câmpului "id" al obiectului, iar a doua concatenează aria curriculară şi denumirea obiectului:

# python manage.py shell
from stare.models import Arie, Obiect
from django.shortcuts import get_object_or_404
obiect = get_object_or_404(Obiect, id=1)
obiect.id_arob()
Out[5]: (1L, u'[MŞ] Matematică')

Şablonul de pagină /set-medii.html are de prezentat formularul HTML corespunzător clasei MedieForm() (aceasta rămânând de definit, mai încolo):

<form action="{% url set_medan_elev elev.id %}" method="post">
    {% csrf_token %}
    <table class="set_medii">
        {{form}}
    </table>
    <input type="submit" value="Înscrie mediile"> la {{elev}}
</form>

Django acompaniază datele transferate (în cazul de faţă, aceste vor fi tupluri (id_obiect, medie)) cu un câmp "ascuns" csrf_token (implicat şi în liniile 12-14 din /final-semi.html), setat cu o valoare care depinde de cererea HTTP respectivă (şi de sesiune curentă de lucru în browserul utilizatorului respectiv) - vezi mai bine, Cross Site Request Forgery protection.

Preluarea şi prelucrarea datelor transmise

Formularul HTML tocmai redat prevede prin atributul "action" ca datele înscrise să fie trimise (la acţionarea butonului "Înscrie datele") acelei funcţii din views.py care corespunde şablonului URL numit 'set_medan_elev' (în stare/urls.py):

# stare/urls.py
from django.conf.urls import patterns, url
from stare.views import *
urlpatterns = patterns('stare.views',
    #url( ... ),
    url(r'^final_semi$', final_semi, name='final_semi'),
    url(r'^set_medii_elev$', set_medii_elev, name='set_medii_elev'),
    url(r'^set_medan_elev(?P<el_id>\d+)/$', set_medan_elev, name='set_medan_elev'),
)

Această funcţie trebuie să determine Elev-ul indicat prin valoarea parametrului "el_id", iar dacă datele primite sunt validate de către metodele prevăzute de MedanForm() - atunci ea trebuie să creeze un obiect Medan pentru elevul respectiv (şi să-l salveze în baza de date):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def set_medan_elev(request, el_id):
    elev = get_object_or_404(Elev, id=el_id)
    form = MedieForm(request.POST or None, obiecte=get_lista_obiecte())
    if form.is_valid():
        medan1 = []
        for (obi, med) in form.obiect_medie():
            medan1.append(obi + ': ' + str(med))
        mdn = Medan.objects.create( 
                    anscol = Anscol.objects.all()[0],
                    elev = elev,
                    clasa = request.user.username[:2],
                    medii = '\n'.join(medan1),
        )
        mdn.save()
        return HttpResponseRedirect(reverse('final_semi'))        
    return render_to_response('stare/set_medii.html', 
        {'elev': elev, 'form': form},
        context_instance = RequestContext(request)
    )

Un "form", precum MedieForm() are în fond, rolul de a converti valorile "brute" (înscrise de către utilizator pe formularul prezentat) în anumite obiecte Python care să fie apoi integrate în contextul aplicaţiei. Aceasta implică şi anumite validări de date (de exemplu, MedieForm() poate impune ca "media" să fie număr întreg între 0 şi 10); dacă (la linia 4) datele nu reuşesc să treacă de validatorii prevăzuţi, atunci se trece la linia 16, iar aici se returnează şablonul /set-medii.html prezentat anterior - dar acum formularul "reafişat" va conţine valorile tocmai înregistrate şi dovedite ca invalide, împreună cu mesaje care semnalează utilizatorului erorile depistate.

Dacă MedieForm() acceptă datele respective, atunci acestea trebuie "concatenate" - reunind denumirile obiectelor şi mediile corespunzătoare - pentru a forma textul corespunzător câmpului "medii" din Medan(); în acest scop va trebui să definim în MedieForm() o metodă obiect_medie() (anticipată aici în linia 6), care să furnizeze tuplul (etichetă_obiect, medie_introdusă).

După salvarea obiectului Medan() în tabelul corespunzător din baza de date (vezi liniile 14, 15), se reia funcţia final_semi() - dar de această dată lista produsă pentru "elevii fără medii" îl va exclude pe elevul căruia tocmai i s-a asociat un Medan().

Construirea formularului pentru înscrierea mediilor

În cadrul funcţiilor set_medan_elev() şi set_medii_elev() s-a procurat câte un obiect MedieForm() pe baza parametrului obiecte (lista obiectelor, creată de funcţia get_lista_obiecte() din stare/views.py - constând din tupluri ca (20, '[OS] Religie')). Pentru a obţine formularul de înscriere a mediilor, putem defini MedieForm() (într-un fişier stare/forms.py) astfel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django import forms
class MedieForm(forms.Form):
    def __init__(self, *args, **kwargs):
        obiecte = kwargs.pop('obiecte')
        super(MedieForm, self).__init__(*args, **kwargs)

        for ob in obiecte:
            self.fields['obiect_%s' % ob[0]] = forms.IntegerField(
                        label=ob[1], 
                        min_value=0, max_value=10, 
                        required=False,
            )

    def obiect_medie(self):
        for eticheta, medie in self.cleaned_data.items():
            if eticheta.startswith('obiect_'):
                if medie:
                    yield (self.fields[eticheta].label, medie)    

MedieForm() extinde (liniile 1 şi 2) clasa de bază Form() din modulul django.forms.forms; lansând Python din linia de comandă şi instanţiind myForm = forms.Form() putem vedea atributele iniţiale ale unui obiect Form():

>>> import pprint
>>> from django import forms
>>> myForm = forms.Form()
>>> pprint.pprint(myForm.__dict__)
{'_changed_data': None,
 '_errors': None,
 'auto_id': 'id_%s',
 'data': {},
 'empty_permitted': False,
 'error_class': <class 'django.forms.util.ErrorList'>,
 'fields': {},
 'files': {},
 'initial': {},
 'is_bound': False,
 'label_suffix': ':',
 'prefix': None}

Unele dintre aceste atribute sunt setate (pe valori implicite) prin metoda __init__() ("constructorul" de obiecte, în Python), altele sunt întreţinute de către unele dintre metodele prevăzute; iar unele metode adaugă noi atribute, cum ar fi dicţionarul cleaned_data (unde se vor înregistra datele care au fost acceptate de către metodele de validare), implicat în linia 15 mai sus.

Metoda __init__() definită mai sus pentru Medan() apelează metoda __init__() a clasei de bază, în linia 5 - permiţând iniţializarea atributelor specificate mai sus (după ce, în prealabil, extrage din lista argumentelor parametrul "obiecte", cu care clasa de bază nu are de-a face); apoi, în liniile 7-12 înregistrează în dicţionarul fields câmpurile dorite în cadrul formularului (pentru înscrierea mediilor).

Putem face un mic experiment (folosind utilitarul Django manage.py):

# python manage.py shell
from stare.forms import MedieForm
testForm = MedieForm(None, obiecte = [(20, '[OS] Religie'),])
testForm??

Am creat un obiect "testForm" de tip MedieForm(), transmiţând ca parametru "obiecte" o listă (formată aici dintr-un singur tuplu) de genul celeia constituite de funcţia get_lista_obiecte(); comanda din ultima linie testForm?? ne redă informaţii despre "testForm", dintre care "cităm" forma HTML instituită pentru obiectul creat astfel:

<tr>
    <th><label for="id_obiect_20">[OS] Religie:</label></th>
    <td><input name="obiect_20" id="id_obiect_20" type="text"></td>
</tr>

Metoda obiect_medie() (liniile 14-18) produce tupluri de forma (label, valoare), unde label este conţinutul elementului <label>, iar valoare este media înscrisă în elementul <input> - ceea ce a permis simplificarea prelucrării în view-ul care a primit datele (set_medan_elev(), liniile 5-7).

Dacă am elimina metoda obiect_medie(), atunci prelucrarea menţionată era posibilă, dar devenea mai complicată: de exemplu, form.cleaned_data['obiect_20'] ne dă valoarea care a fost înscrisă în <input> (şi care a fost în prealabil, validată) - dar "numele" obiectului respectiv ("[OS] Religie") trebuie determinat izolând id-ul 20 din cheia 'obiect_20' şi accesând Obiect-ul cu acest id, urmând să mai concatenăm numele găsit astfel, cu acronimul ariei curriculare de care ţine Obiect-ul.

vezi Cărţile mele (de programare)

docerpro | Prev | Next