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

Form-Django dinamic, cu MultiWidget pentru note şi absenţe

Django | Python
2013 feb

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

[2] Un exemplu de lucru cu formulare dinamice, în Django

[3] Experimente de modelare a datelor (Python, Django şi SQL)

Pentru întreţinerea datelor într-o aplicaţie de reflectare dinamică a situaţiei şcolare [1] ar fi necesar un formular prin care să se poată introduce şi edita notele şi absenţele elevilor la fiecare obiect. Acest formular are un caracter dinamic, ţinând seama de faptul că obiectele de învăţământ depind de nivelul clasei (a IX-a, a XI-a, etc.) şi de profilul ei (matematică-informatică, filologic, etc.).

Deosebirea între formularul dinamic realizat în [2] şi cel necesar aici constă în faptul că acum am avea nu un singur <input> pe obiect (media la acel obiect, în [2]), ci trei <input>-uri: unul pentru introducerea notelor, unul pentru absenţele motivate şi unul pentru absenţele nemotivate. În plus, de data aceasta formularul trebuie să servească nu numai pentru introducerea iniţială a valorilor (cum este cazul în [2]), dar şi pentru modificarea valorilor introduse anterior în baza de date.

Cu alte cuvinte, numărul de câmpuri depinde de clasa de elevi pentru care este instanţiat formularul (adică, avem de-a face cu un formular dinamic) - câmp al formularului însemnând un cuplu constituit dintr-un element <label> - numele obiectului de învăţământ, dependent şi acesta de clasa de elevi - şi un <input>, sau (cum este cazul acum) o secvenţă de elemente <input> care au eventual anumite valori iniţiale (transferate din baza de date).

Considerăm modelele de date specificate deja în [1]: Arie( nume, acro ), Obiect( nume, arie ), Clasa( cod, nume, obiects ), Elev( nume, pren, cnp, foto, clasa ) şi adăugăm acum următoarea definiţie (de bază) pentru notele şi absenţele unui Elev la un Obiect:

class Noteabs(models.Model):
    note = NoteField(max_length=16, blank=True, 
        help_text=u'<b>710817 6</b> pentru notele 7,10,8,1,7 şi teza 6')
    abmo = models.PositiveSmallIntegerField(default=0, verbose_name=u'Motivate')
    abne = models.PositiveSmallIntegerField(default=0, verbose_name=u'Nemotivate')
    obiect = models.ForeignKey(Obiect) # la care obiect
    elev = models.ForeignKey(Elev) # la care elev

Aici, am redus definiţia respectivă la strictul necesar; NoteField(models.CharField()) subclasează tipul de câmp CharField(), pentru a restricţiona valorile posibile ale câmpului 'note' - impunând această regulă: notele trebuie introduse una după alta (fără vreun separator), iar teza poate fi adăugată la sfârşit, separând-o de note prin spaţiu (ca în exemplul dat în atributul 'help_text').

Formularul standard care poate fi asociat unui obiect Noteabs implică elemente <select> pentru câmpurile de tip ForeignKey() - cerând utilizatorului să aleagă obiectul şi elevul pentru care trebuie înscrise notele şi absenţele.

Un asemenea formular este convenabil pentru introducerea/modificarea zilnică de note şi absenţe (la câţiva elevi, pe câteva obiecte) - dar nu şi în cazul când ai avea de făcut aceasta săptămânal sau lunar, pentru toţi elevii şi toate obiectele. În acest caz ar fi de dorit un context precum cel realizat în [2]: se prezintă lista elevilor; la selectare unui elev, se produce un formular conţinând pentru fiecare obiect câte trei <input>-uri având ca valori iniţiale notele şi absenţele înregistrate până la acel moment (dacă există).

De regulă, un formular asociază fiecărui câmp din modelul de date căruia îi este asociat, două elemente HTML: un <label> conţinând numele câmpului şi un (singur) <input>, pentru modificarea sau introducerea valorii acelui câmp. Pentru diversele tipuri de <input> existente (şi numai în scopul de a produce HTML-ul corespunzător şi a prelua valoarea "brută" introdusă de utilizator - nu şi pentru a o valida în vreun fel) Django prevede clasa de bază Widget(), cu diverse subclase specifice (Input(Widget), TextInput(Input), DateInput(Input), Textarea(Widget), Select(Widget), etc.).

În cazul nostru însă, am avea pentru un acelaşi "label" (numele obiectului) nu un singur <input>, ci o secvenţă de mai multe "input"-uri (pentru note, absenţe motivate şi respectiv, absenţe nemotivate). Pentru asemenea situaţii, Django prevede subclasa MultiWidget(Widget); exemplul tipic de folosire este SplitDateTimeWidget(MultiWidget) - în /django/forms/widgets.py - care combină două <input>: un DateInput() (pentru data calendaristică) şi un TimeInput() (oră, minute, secunde).

Creem un fişier widgets.py în aplicaţia "stare", definind analog cu SplitDateTimeWidget():

from django import forms

class NoteAbsWidget(forms.MultiWidget):
    def __init__(self):
        _widgets = (forms.TextInput(attrs={'class': 'width1',}), 
                    forms.TextInput(attrs={'class': 'width2',}), 
                    forms.TextInput(attrs={'class': 'width2',}))
        super(NoteAbsWidget, self).__init__(_widgets)

    def decompress(self, value): # returnează listă cu valorile din input-uri
        if value: # presupunem şir, conţinând cele 3 valori separate prin virgulă
            return value.split(',')
        return ['', '', '']

Input-ul pentru note trebuie să aibă o lăţime mai mare, faţă de cele pentru numărul de absenţe - încât am folosit attrs pentru a asocia "clase de stilare"; în fişierul CSS vom înscrie de exemplu input.width1={width: 60px;} şi input.width2={width: 20px;}.

Putem testa direct (din terminal), folosind un script precum helper.py din [3]:

# vezi "helper.py" in [3], pentru partea iniţializatoare
from stare.models import *
from stare.widgets import NoteAbsWidget
from django import forms

wg = NoteAbsWidget()
print wg.widgets
print wg.render(u'mat', ['678 7', 5, 3])
print wg.decompress('678 7,5,3')

Se afişează lista obiectelor Widget() constituente şi secvenţele HTML corespunzătoare (plus rezultatul decompresării) pentru cazul când obiectul ar fi "mat", cu notele "678 7" şi absenţele 5 şi 3:

vb@vb:~/canrug$ python helper1.py 
[<django.forms.widgets.TextInput object at 0xa22a3cc>, 
 <django.forms.widgets.TextInput object at 0xa22a4ac>,
 <django.forms.widgets.TextInput object at 0xa22a4cc>]

<input type="text" class="width1" value="678 7" name="mat_0" />
<input type="text" class="width2" value="5" name="mat_1" />
<input type="text" class="width2" value="3" name="mat_2" />

['678 7', '5', '3']

În fişierul forms.py definim acum un tip de câmp ("cu mai multe valori") care să aibă ca "widget" NoteAbsWidget() şi care să asigure o primă validare (primul "input" trebuie să conţină -în primul rând- un şir de caractere, celelalte două - câte un întreg), "unificând" în acelaşi timp cele trei valori:

from stare.widgets import NoteAbsWidget

class NoteAbsField(forms.MultiValueField):
    widget = NoteAbsWidget

    def __init__(self, *args, **kwargs):
        fields = (forms.CharField(), # note: şir de caractere
                  forms.IntegerField(), # absenţe motivate: întreg
                  forms.IntegerField()) # absenţe nemotivate: întreg
        super(NoteAbsField, self).__init__(fields, *args, **kwargs)

    def compress(self, data_list): 
        return data_list # constituie o singură valoare, din cele trei input-uri

Definim formularul dinamic necesar (angajând câmpuri NoteAbsField()) în mod similar definiţiei prezentate în [2] pentru MedieForm() - adăugând în forms.py:

import re
ID_OBIECT = re.compile(r"(\d+)$")

class NoteForm(forms.Form):
    def __init__(self, *args, **kwargs):
        obiecte = kwargs.pop('obiecte') # lista obiectelor specifice clasei
        noteact = kwargs.pop('noteact') # valori preexistente (note şi absenţe la obiecte)
        super(NoteForm, self).__init__(*args, **kwargs)

        for obi in obiecte:
            self.fields['obiect_%s' % obi[0]] = NoteAbsField(
                        label=obi[1], 
                        required=False,
                        initial = noteact.get(obi[0], '')
            )

    def obiect_notabs(self):
        for name, value in self.cleaned_data.items():
            if name.startswith('obiect_'):
                if value:
                    yield (re.search(ID_OBIECT, name).group(1), value)    

'obiecte' este exact lista returnată de funcţia get_lista_obiecte() folosită în [2] (doar că acum am montat această funcţie ca metodă a clasei Clasa()). Iar 'noteact' este dicţionarul returnat de următoarea metodă a Elev()-ului:

    # metodă pentru Elev()  (în models.py)
    def noteact(self):
        dna = {nt.obiect_id: nt.note + ',' + str(nt.abmo) + ',' + str(nt.abne) 
                for nt in self.noteabs_set.all()}
        return dna

Această metodă determină toate înregistrările de Noteabs() existente pentru elevul respectiv şi constituie un dicţionar care asociază ID-ul obiectului cu şirul constituit de note, absenţe motivate şi absenţe nomotivate - separate prin virgulă, aşa cum am prevăzut iniţial în metoda decompress() pentru NoteAbsWidget().

NoteForm() înscrie pentru fiecare obiect din lista de obiecte primită câte un câmp NoteAbsField() având drept 'label' numele obiectului (vezi mai sus, linia "label=obi[1],") şi drept valoare iniţială fie valoarea existentă pentru acel obiect în dicţionarul "noteact", fie şirul vid (a vedea linia initial = noteact.get(obi[0], '')); câmpul respectiv nu este obligatoriu de completat (required=False).

Din cadrul unei funcţii din views.py, formularul va putea fi instanţiat astfel:

form = NoteForm(request.POST or None, 
                obiecte=elev.clasa.lista_obiecte(), 
                noteact=elev.noteact())

presupunând că elev este un obiect Elev() precizat.

Rămâne să adăugăm în views.py funcţiile necesare: una care să furnizeze (într-un şablon HTML din /templates/stare) lista elevilor clasei, una care să instanţieze formularul NoteForm() pentru elevul selectat de pe această listă, etc.; toate aceste funcţii sunt similare celor descrise în [2] (unde angajam formularul MedieForm()). Cu anumite ajustări stilistice putem obţine în final:

Funcţia din views.py acţionată de acest formular va trebui să verifice dacă există o înregistrare anterioară Noteabs() corespunzătoare elevului şi obiectului respectiv - caz în care trebuie doar "updatate" valorile de note şi absenţe existente; dacă nu există una anterioară, atunci înregistrarea trebuie creată (cu "elev_id", "obiect_id" şi cu valorile preluate de la utilizator). Acest aspect (fie "update", fie "insert" - după caz) este deosebirea principală faţă de funcţia set_medan_elev(request, el_id) scrisă în [2] (acolo aveam numai cazul "insert").

vezi Cărţile mele (de programare)

docerpro | Prev | Next