Aspetti implementativi

All’interno del precedente capitolo sono state elencate le principali caratteristiche tecniche che sono state implementate all’interno della piattaforma Argo Plus per far fronte alle esigenze presentate durante la fase di analisi. Di seguito saranno descritte le scelte implementative adottate durante la fase di programmazione.

Lo scopo principale di questo capitolo è dunque quello di offrire un supporto utile alla comprensione e all’eventuale modifica del codice del progetto.

Librerie Esterne

In Argo Plus i sorgenti delle librerie esterne sono memorizzati all’interno della cartella “vendors” (in sotto-cartelle situate all’indirizzo: “static/admin/vendors/nome-libreria/”), l’utilizzo di una cartella denominata “vendors” o “vendor” è uno standard utilizzato in numerosi framework web (ma non obbligatorio in Django).

All’interno delle cartelle “static/admin/css/” e “static/admin/js/” sono invece presenti i file CSS e JavaScript utilizzati per la realizzazione della struttura grafica del sito. I file sono memorizzati in due versioni distinte: una versione “estesa” e una versione “minificata”, un file minificato (processo di minification) è un file a cui sono stati rimossi tutti i caratteri non necessari (in particolare gli spazi e gli a-capo), ma che mantiene tutte le sue funzionalità originali.

Un file minificato gode di due vantaggi:

  1. Il codice contenuto all’interno dei file è offuscato (anche se tecnicamente le tecniche di offuscamento più efficaci richiederebbero l’uso di un offuscatore), pertanto sarà più difficile per un essere umano esaminare il sorgente e scovare eventuali punti deboli (ovviamente un file minificato può facilmente essere riconvertito nella sua forma originaria rendendo così vana l’operazione);
  2. Il file risultante dal processo è significativamente più piccolo rispetto all’originale (questo confronto è più evidente se eseguito su file molto lunghi). Per fare un esempio il file “custom.css” che contiene al suo interno le direttive grafiche di (quasi) tutto il portale ha un peso pari a 30.0 KB, tuttavia una volta minificato esso peserà solamente 11.9 KB con un risparmio di spazio superiore al 50%.

Esistono numerosi software incaricati di eseguire la minificazione di frammenti di codice, tuttavia esistono anche numerosi siti internet che offrono gratuitamente (e senza bisogno di eseguire installazioni di alcun tipo) il medesimo servizio. Per la realizzazione dei sorgenti di Argo Plus si è utilizzato ad esempio il sito www.minifier.org.

La tecnica di programmazione utilizzata nella realizzazione del portale prevede quindi la modifica del file “normale” e la sua successiva trasformazione nella versione “compressa”. All’interno dell’applicazione gli unici file inclusi (e quindi effettivamente richiamati dal browser) sono dunque solo quelli “minificati”, i file “normali” sono mantenuti unicamente a scopo informativo (per permettere una più chiara lettura del codice agli sviluppatori). L’utilizzo di file compressi è essenziale per risparmiare banda durante le fasi di caricamento del sito e di conseguenza per rendere quest’ultimo facilmente fruibile anche da dispositivi connessi a reti a consumo non molto performanti (ad esempio i telefoni senza connessione 3G/4G).

Realizzazione dei grafici e delle Statistiche

Nel secondo capitolo di questo documento, all’interno del sotto paragrafo “Grafici e Statistiche”, sono stati descritti i principali grafici esistenti all’interno della piattaforma Argo Plus ed è stata presentata la libreria JS con cui essi sono stati implementati. In questo paragrafo saranno invece trattate più nel dettaglio le tecniche utilizzate per la loro realizzazione.

L’implementazione dei diversi grafici ha richiesto inizialmente la realizzazione delle query SQL responsabili della selezione dei dati. Ad esempio: le query responsabili della selezione dei dati utilizzati per la realizzazione dei grafici presenti nell’home page del sito sono contenute all’interno del file “admin dashboard.py” (tutti i grafici della piattaforma sono collegati ad un file contenuto all’interno della cartella “/templatetags/” e con prefisso “admin “).

All’interno del file “admin dashboard.py” sono definiti i diversi metodi responsabili della selezione dei dati per i diversi elementi della pagina. Ad esempio, la tabella rappresentante le campagne attualmente in esecuzione è realizzata a partire dai dati acquisiti dai metodi “getCampaignUnderExecution” e “getNumOfCampaignUnderExecution”. Il primo metodo è responsabile della selezione di un certo numero di campagne (nel caso base di 5 campagne differenti), mentre il secondo è responsabile della sola selezione del numero di campagne che rispettano la condizione (tale valore è utilizzato per la paginazione).

@register.simple_tag
def getCampaignUnderExecution(requester_id=0, num=5):
  if(requester_id==0) or (User.objects.filter(pk=requester_id, groups__name__iexact='administrator').exists()):
    result = Campaign.objects.filter(campaign_status_id__name__iexact='under execution').filter(start_date__lte = datetime.datetime.now().date()).filter(finish_date__gte = datetime.datetime.now().date()).order_by('start_date')[:num]
  else:
    result = Campaign.objects.filter(campaign_status_id__name__iexact='under execution').filter(requester_id=requester_id).filter(start_date__lte = datetime.datetime.now().date()).filter(finish_date__gte = datetime.datetime.now().date()).order_by('start_date')[:num]
  return result

@register.simple_tag
def getNumOfCampaignUnderExecution(requester_id=0):
  if(requester_id==0) or (User.objects.filter(pk=requester_id, groups__name__iexact='administrator').exists()):
    result = Campaign.objects.filter(campaign_status_id__name__iexact='under execution').filter(start_date__lte = datetime.datetime.now().date()).filter(finish_date__gte = datetime.datetime.now().date()).count()
  else:
    result = Campaign.objects.filter(campaign_status_id__name__iexact='under execution').filter(requester_id=requester_id).filter(start_date__lte = datetime.datetime.now().date()).filter(finish_date__gte = datetime.datetime.now().date()).count()
  return result

Il primo metodo accetta come parametri l’ID dell’utente che sta consultando la pagina e il numero di elementi da restituire, il secondo invece accetta solo l’ID dell’utente interessato a visualizzare i dati. All’interno di entrambi i metodi, prima di eseguire la query di selezione dei dati, viene controllato l’ID dell’utente passato come primo parametro, se quest’ultimo è 0, oppure appartiene ad un utente inserito all’interno del gruppo degli amministratori, allora la query non subirà variazioni. In caso contrario verranno scaricate le sole informazioni collegate al requester in questione (nell’esempio riportato si tratta ad esempio delle sole campagne di cui esso è il proprietario).

All’interno del template dell’applicazione (file “/templates/admin/index.html”) la chiamata ai metodi (ed il relativo inserimento dei valori all’interno delle variabili) avviene nel modo seguente.

{% getCampaignUnderExecution user.id as campaignUnderExecution_result %}
{% getNumOfCampaignUnderExecution user.id as getNumOfCampaignUnderExecution_count %}

Visualizzare le informazioni corrette

All’interno del backend di Argo Plus è essenziale che i dati inseriti dai diversi requester (in particolare campagne e task) rimangano segreti, ovvero che non vengano mostrati agli altri requester che accedono alla piattaforma (d’altro canto gli amministratori avranno sempre accesso completo a tutti i dati). Django tuttavia non è progettato per gestire nativamente la suddivisione fra i contenuti dal momento che il backend dell’applicazione dovrebbe essere accessibile solamente agli amministratori.

Per risolvere tale problematica si è resa necessaria la riscrittura del metodo “get queryset” contenuto all’interno della maggior parte delle classi presenti all’interno del file “admin.py”. Lo scopo di questo metodo è quello di definire la query responsabile dell’acquisizione dei dati che saranno visualizzati nelle changelist. Nel frammento di codice sottostante viene mostrato il metodo “get queryset”, così come è stato definito all’interno della classe “TaskAdmin”:

def get_queryset(self, request):
  qs = super(TaskAdmin, self).get_queryset(request)
  if request.user.groups.filter(name__iexact='requester').exists():
    return qs.filter(campaign_id__requester=request.user)
  else:
    return qs

Come si può vedere dal frammento di codice precedente, il metodo esegue prima la chiamata al metodo padre (riga 2 – lo stesso metodo così come è stato previsto da Django), successivamente determina il livello di accesso dell’utente (riga 3) e infine, se l’utente in questione è un requester, filtra le campagne scaricate limitandole alle sole inserite dall’utente connesso (riga 4). In questo modo il requester potrà vedere e modificare le sole campagne (ma anche task e worker aderenti alle campagne) che gli appartengono.

Non è però sufficiente modificare il metodo “get queryset” per assicurare la separazione dei dati, Django offre infatti un sistema di filtri che possono essere utilizzati dagli utenti per scremare ulteriormente i dati proposti. È ad esempio necessario che all’interno del filtro “Campagna” (posto nella sezione laterale della changelist) compaiano solamente le campagne inserite dal requester in questione (qualora l’utente connesso sia proprio un requester). In Django un filtro viene normalmente definito nel modo seguente:

list_filter = ['campaign', 'task_status', 'answer_type']

Supponiamo nuovamente di trovarci all’interno della changelist dei task, l’utilizzo del precedente frammento di codice porterà alla definizione di tre filtri differenti:

  1. I nomi delle campagne collegate ai task;
  2. Gli stati dei task;
  3. I tipi di risposte accettati dai task.

Ovviamente così facendo ogni requester avrà accesso completo ai nomi delle campagne inserite all’interno del sistema (comprese quelle inserite dagli altri requester) e questo deve essere assolutamente evitato. Tuttavia, non è possibile utilizzare uno stratagemma simile a quello usato in precedenza poiché Django non mette a disposizione un metodo per gestire le query di generazione dei filtri. In questo caso la scelta è stata quella di inserire la particella “admin.RelatedOnlyFieldListFilter” come secondo membro di una coppia di valori che viene sostituita al semplice nome del campo da trasformare in filtro.

list_filter = [('campaign', admin.RelatedOnlyFieldListFilter), 'task_status', 'answer_type']

Così facendo Django ridurrà automaticamente le voci dei filtri escludendo quelle che non trovano corrispondenze tra i dati mostrati all’utente nella tabella di riepilogo. Dal momento che questi dati sono già stati filtrati grazie all’utilizzo del metodo “get queryset” abbiamo la garanzia che il requester non vedrà mai i riferimenti alle campagne (o ad altri dati) che non gli appartengono.

Aggiungere il permesso di lettura

Una delle sfide più complesse affrontate durante lo sviluppo della piattaforma ha riguardato l’introduzione del nuovo permesso di sola lettura (“can view”). Come già spiegato in precedenza l’introduzione di tale permesso si è reso necessario per permettere ai requester di esaminare la lista dei pattern e dei profile disponibili all’interno del sistema, evitandogli la fatica di dover memorizzare le stringhe associate alle configurazioni (o alle abilità).

La prima modifica eseguita ha riguardato i model aggiunti alla piattaforma (tutti ad eccezione dei model “User” e “Group” previsti nativamente da Django) a cui è stato aggiunto l’attributo “default permissions” in fase di definizione dei “meta”. Segue l’esempio:

class Meta:
  ...
  default_permissions = ('add', 'change', 'delete', 'view')

La modifica ha portato alla generazione di una migration responsabile dell’inserimento del nuovo permesso all’interno del sistema.

Successivamente è stato necessario inserire la gestione dei permessi all’interno del codice della piattaforma, riscrivendo il metodo “has change permission” contenuto all’interno di tutte le classi interessate. Di seguito viene mostrato il codice del metodo contenuto all’interno della classe “TaskAdmn”.

def has_change_permission(self, request, obj=None):
  # user can change?
  if request.user.has_perm('crowdsourcing.change_task'):
    return True

  # user can view changelist_view?
  if (not obj) and (request.user.has_perm('crowdsourcing.view_task')):
    return True

  return False

Il precedente frammento di codice sovrascrive il metodo “has change permission” responsabile della verifica dei permessi di modifica (permesso “can change”) sull’oggetto corrente. La presenza di tale permesso viene verificata automaticamente dal sistema all’apertura della changelist (la pagina riepilogativa di tutte le istanze di un determinato oggetto) e della changeform (la pagina di modifica di una determinata istanza) in modo da impedire l’accesso non autorizzato dell’utente a contenuti riservati. Normalmente in Django il possesso del permesso di modifica garantisce l’accesso ad entrambe le pagine.

La modifica eseguita ha quindi lo scopo di suddividere il comportamento del metodo, facendogli rispondere contemporaneamente alla domanda: “ha i permessi di modifica?” e “ha i permessi di lettura?”. Come si può vedere dal precedente frammento di codice il metodo verifica l’esistenza dei permessi di modifica e ritorna il valore “True” nel caso questi siano presenti. In caso contrario se non è stato aperto un oggetto specifico (tale affermazione si verifica con la condizione “not obj”) verifica l’esistenza dei permessi di lettura e in caso affermativo ritorna nuovamente il valore booleano “True”. Altrimenti il metodo ritorna il valore “False”.

Appare quindi chiaro come i permessi di modifica includano quelli di lettura: un utente non potrà avere i permessi di modifica senza avere anche quelli di lettura, tuttavia potrà avere i permessi di lettura senza avere quelli di modifica.

Infine, è stato necessario modificare il template del menù laterale al fine di nascondere le diverse voci di menù qualora l’utente non fosse autorizzato a consultare le pagine ad esse collegate. In questo caso è stato sufficiente eseguire le diverse chiamate ai metodi di verifica delle autorizzazioni prima della generazione dei diversi elementi.

Modifiche al template change form

Durante lo sviluppo dell’applicazione Argo Plus la componente di Django interessata dal maggior numero di modifiche è stata probabilmente quella di inserimento e di modifica dei dati contenuti all’interno dell’applicazione.

Di seguito sono elencate le principali mancanze riscontrate a seguito dell’utilizzo del framework Django:

  • Incompatibilità con gli input HTML in sola lettura;
  • Mancata evidenziazione dei campi obbligatori a discapito di quelli non obbligatori;
  • Utilizzo della versione standard dell’input di selezione a discapito dell’utilizzo di librerie JS avanzate in grado di offrire compatibilità nativa con le tecniche AJAX;
  • Mancata limitazione delle voci inseribili all’interno degli inline formset che provocano il superamento dei limiti imposti dal server per le dimensioni delle variabili GET e POST.

Durante lo sviluppo dell’applicazione si è reso necessario il superamento di tutte queste problematiche.

Input di sola lettura

Il linguaggio HTML permette la generazione di diverse tipologie di input: testuali, select, checkbox e così via; ad ognuno dei quali possono essere associati diversi attributi in modo da modificarne il comportamento. Tra i numerosi attributi messi a disposizione dal linguaggio il “readonly” e il “disabled” sono probabilmente tra i più utilizzati. Lo scopo di questi due attributi è per certi aspetti molto simile, anche se sostanzialmente differente, entrambi hanno lo scopo di definire un input (non tutti gli input HTML accettano l’attributo readonly o l’attributo disabled) che non sarà modificabile dall’utente ma che potrà ugualmente contenere un valore rappresentabile a schermo. Input dotati di questi attributi sono utilizzati (all’interno della piattaforma corrente) al fine di impedire al requester di modificare alcuni dati della campagna e del task. All’interno del backend di Argo Plus non sarà ad esempio possibile modificare la tipologia di un task dopo averlo salvato (facendolo passare da “text” a “choice” o viceversa), oppure non sarà possibile per un amministratore modificare il requester associato ad una campagna (per quanto questa operazione appaia comunque poco sensata), tuttavia sarà comunque importante poter visualizzare queste informazioni all’interno della pagina di modifica.

La principale differenza esistente tra gli input “readonly” e gli input “disabled” (oltre all’opzionale differenza grafica che può essere applicata dal browser in uso) risiede nel fatto che il valore contenuto all’interno dell’input “disabled” non viene inviato al “processor method” in seguito al submit del form. Questo aspetto è interessante e permette di aggiungere un ulteriore livello di sicurezza al form, utilizzando un semplice input “readonly” un utilizzatore esperto potrebbe comunque modificare il valore contenuto all’interno dell’input (ad esempio utilizzando la console di sviluppo prevista dalla maggior parte dei browser moderni) ed eseguendo la submit ottenere l’effettiva modifica del valore salvato nel database.

In Django tuttavia il sopracitato aspetto programmatico non è stato preso in considerazione, gli input in possesso dell’attributo readonly si comportano allo stesso modo di quelli con attributo disabled, il loro valore viene completamente ignorato in fase di update dal momento che il loro contenuto non viene inviato al “processor method”. Questo ovviamente potrebbe non costituire un problema, a condizione che Django re-inserisca i valori originali all’interno del database qualora questi fossero stati rappresentati nel form come “readonly” o “disabled”, tuttavia questo non avviene.

Ciò che si ottiene, specialmente nei casi in cui gli input settati come “readonly” siano obbligatori, è lo stallo: l’utente si trova davanti un form con uno o più input bloccati e già compilati, ma una volta eseguito il submit riceve uno o più errori in corrispondenza di tali input che segnala la mancata compilazione dello stesso (compilazione di fatto impossibile data la natura dell’input).

La soluzione sviluppata durante la realizzazione della piattaforma è la seguente: all’interno di ogni classe contenuta nel file admin.py viene riscritto il metodo “changeform view” (tale metodo ha il compito di gestire la pagina di inserimento/modifica) inserendo al suo interno il campo “readonly field”. Segue un esempio rappresentante il metodo “changeform view” nella classe “TaskAdmin”:

def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
  extra_context = extra_context or {}
  extra_context['xPanel_title'] = Task._meta.verbose_name
  extra_context['model_name'] = self.model_name
  extra_context['menu_active'] = 'task'
  if object_id:
    extra_context['page_title'] = Task._meta.verbose_name + ' Edit'
    extra_context['readonly_field'] = 'campaign,answer_type'
  else:
    extra_context['page_title'] = Task._meta.verbose_name + ' Add'
  return super(TaskAdmin, self).changeform_view(request, object_id=object_id, form_url=form_url, extra_context=extra_context)

Come si può vedere dal frammento di codice sovrastante il campo “readonly field” (definito all’interno del frammento di codice solo per la pagina di modifica dell’oggetto) conterrà i nomi dei campi da modificare separati da una virgola. All’interno del template “change form.html” il contenuto del campo “readonly field” viene inserito all’interno di un input nascosto denominato “readonly field list” (esiste anche una versione “readonly inline field list” per gli input appartenenti agli inline formset).

Infine, il contenuto dell’input sopra citato verrà utilizzato da uno script JQuery per modificare l’aspetto del form e ottenere l’effetto desiderato. Durante il caricamento della pagina di inserimento/modifica dei dati viene richiamato il file “custom changeForm.min.js” (contenuto nel percorso “static/admin/js/”) all’interno del quale sono salvate tutte le funzioni JS/JQuery utilizzate nel contesto corrente.

In particolare, a partire dalla riga 22 del file “custom changeForm.js” vediamo come lo script esegua lo split (ovvero la suddivisione) dei dati ottenuti dall’input (utilizzando il carattere `,’ come divisorio) e per ogni stringa risultante dalla suddivisione esegua le operazioni qui descritte:

  1. Identifica la tipologia dell’input associato alla stringa (riga 26);
  2. Acquisisce il valore contenuto all’interno dell’input (riga 27);
  3. Trasforma l’input proposto da Django in un input nascosto (riga 29);
  4. Aggiunge un nuovo div contenente il dato da mostrare all’utente (riga 30);
  5. Elimina l’eventuale testo di supporto aggiunto (riga 31).
if($('#readonly_field_list').length && $('#readonly_field_list').val().length>0) {
  var readonly_field = $('#readonly_field_list').val();
  var readonly_field_list = readonly_field.split(',');
  $.each(readonly_field_list, function() {
    if($('form input#id_'+this).attr('type')=="text" || $('form input#id_'+this).attr('type')=="number") {
      var value = $('form input#id_' + this).val();

      $('form input#id_'+this).attr('type', 'hidden');
      $('form input#id_'+this).parent('div').append('<div class="readonly form-control">'+value +'</div>');
      $('form input#id_'+this).parents('.form-group').find('.help').remove();
    }

Ovviamente la procedura non è la stessa per tutti gli input, inoltre all’interno del progetto quest’ultima non è stata sviluppata per tutti gli input previsti dal linguaggio HTML, ma solo per quelli utilizzati in Argo Plus.

Considerazioni simili possono essere fatte per gli input “readonly” presenti all’interno degli inline formset, in questi casi all’interno del metodo “changeform view” della classe sarà utilizzato un campo denominato “readonly inline field” contenente una stringa in formato JSON. La stringa JSON è essenziale per legare il nome del campo da modificare con il suo inline formset di appartenenza.

Evidenziare i campi obbligatori

Il framework Django non prevede l’evidenziazione grafica dei campi obbligatori all’interno dei form, questo può causare problemi in fase di inserimento dei dati e il verificarsi di errori. Per risolvere questa problematica è stato aggiunto un frammento di codice JQuery al file “custom changeForm.min.js” incaricato di evidenziare gli input obbligatori mediante un asterisco rosso posto accanto al nome. Il frammento di codice interessato (riga 85 del file “custom changeForm.js”) si limita ad assegnare alla label del campo obbligatorio una classe CSS responsabile dell’introduzione dell’elemento grafico.

$('form .form-control.required').parents('.form-group').find('label').addClass('labelRequired');

Rilevamento dei duplicati

Come sarà evidenziato in seguito il sistema di importazione JSON disponibile all’interno della pagina di creazione e di modifica delle campagne offre un sistema di riconoscimenti case-insensitive (che non differenzia le lettere maiuscole dalle lettere minuscole) per diverse tipologie di valori, tra questi:

  • Answer type;
  • Task Status;
  • Pattern;
  • Profile.

Se in fase di riconoscimento dei valori il sistema non farà distinzione tra una stringa minuscola e una maiuscola (ad esempio: “TEST” corrisponderà a “test”, oppure a “TeSt”) appare chiaro come lo stesso sistema dovrebbe impedire il verificarsi di situazioni ambigue scongiurando completamente le possibilità di inserimento dei duplicati. Questo significa che se all’interno del sistema sarà presente una stringa “test” lo stesso sistema dovrà impedire l’inserimento di una seconda stringa “TEST”.

Per la realizzazione del vincolo è stato sufficiente modificare i form interessati aggiungendo un controllo aggiuntivo di fase di inserimento dei valori da parte dell’utente. Di seguito viene mostrato il codice python del form “ProfileAdminForm” utilizzato per l’inserimento dei nomi dei profile associati alle skill.

class ProfileAdminForm(forms.ModelForm):
  description = forms.CharField(label='Name')

  class Meta:
    model = Profile
    fields = ('description',)

  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    ...
    if 'instance' in kwargs and kwargs['instance'] is not None:
      self.id = kwargs['instance'].id
    else:
      self.id = None

    def clean(self):
      self.cleaned_data = super(ProfileAdminForm, self).clean()

      if self.cleaned_data and 'description' in self.cleaned_data:
        ck = Profile.objects.filter(description__iexact = self.cleaned_data['description'])
      if self.id:
        ck = ck.exclude(pk=self.id)
      if ck.count() > 0:
        raise forms.ValidationError({'description': ['Value must be unique (system is case insensitive)']})

      return self.cleaned_data

La verifica dell’univocità del valore avviene mediante la selezione di tutti i profile (nell’esempio corrente) in possesso di una descrizione corrispondente a quella inserita dall’utente (aggiungendo la particella “__iexact” alla query di selezione essa funzionerà in modo case-insensitive). Successivamente alla selezione appena eseguita viene sottratto l’ID dell’oggetto in questione (righe 20-22) ed infine vengono contate le righe ottenute. Se il numero di righe selezionate è diverso da zero, allora l’elemento è già stato inserito e verrà mostrato un errore all’utente.

L’esclusione dell’oggetto corrente (qualora questo esistesse) è essenziale per permettere la modifica degli elementi già inseriti (altrimenti il sistema rileverebbe come già presente proprio l’elemento che si sta cercando di modificare). Tale esclusione è possibile solo a seguito dell’inserimento di un altro frammento di codice all’interno del metodo di inizializzazione della classe, questo frammento permette di caricare all’interno di un campo, denominato “id” ed utilizzato in seguito, l’ID dell’oggetto corrente (righe 11-14).

Select VS Select2

Un’importante problematica emersa durante la fase di testing dell’applicativo è stata causata dalla scelta, forse anacronistica di Django, di utilizzare l’input di tipo “select” nativo del linguaggio HTML a discapito di una più recente libreria JQuery. A causa di tale scelta i numerosi input di tipo select inseriti all’interno dell’applicazione si sono dimostrati difficilmente utilizzabili poiché non ottimizzati per gestire un così elevato numero di dati (superiore alle 800 unità). Per risolvere questo e altri problemi si è scelto di sostituire il normale input select con una più recente libreria JQuery: la libreria Select2 (www.select2.org)

L’input generato dalla libreria dispone di un semplice ma efficace sistema di ricerca tramite input testuale che è nettamente superiore al sistema di posizionamento offerto dalle normali select. Inoltre, la libreria Select2 gode di una maggiore ottimizzazione che gli permette di caricare un elevato numero di opzioni senza bloccare oppure rallentare la pagina in cui è inserito. Va comunque sottolineato come l’input ottenuto a seguito dell’utilizzo della libreria Select2 (per come è stato implementato nella piattaforma) non sia in grado di gestire un numero infinito di elementi, pertanto è probabile che una volta raggiunto un certo numero di campagne alcune pagine diventino lente o incorrano in errori. Le pagine a cui ci si riferisce non sono comunque centrali nell’utilizzo dell’utente medio, in particolare è probabile che la pagina interessate dal problema esposto saranno:

  • Pagina di associazione tra i task e le configurazioni;
  • Pagina di associazione tra i task e le skill.

Tali pagine non sono fondamentali per l’esperienza utente dal momento che quest’ultimo potrà comunque svolgere le medesime operazioni sfruttando l’importer JSON (in fase di inserimento), oppure l’inline formset posto all’interno del singolo task (in fase di modifica).

Il problema non è comunque insormontabile dal momento che la libreria Select2 offre il supporto alla tecnica AJAX. Utilizzando questa tecnica la libreria JS sarà in grado di prelevare gruppi di elementi (da inserire successivamente come opzioni nella select) a partire dal più grande insieme di valori possibili (che rimarrà salvato nel database) a seguito della richiesta dell’utente. In parole più semplici utilizzando questa tecnica la Select2 verrà inizializzata priva di elementi e successivamente verrà popolata con gli elementi corrispondenti alla ricerca dell’utente. In questo modo non si otterrà mai la select completa (caricata con tutti gli elementi disponibili) e di conseguenza non si incorrerà nell’errore. Le principali problematiche di implementazione di tale soluzione dipenderanno dalla scarsa affinità tra Django e la libreria Select2.

Attualmente esiste un frammento di codice JQuery, contenuto all’interno del sorgente “custom changeForm.min.js”, incaricato di convertire gli input di tipo select in un input di tipo Select2 in fase di caricamento del form. Il frammento di codice interessato (riga 90 del file “custom changeForm.js”) si limita a richiamare la funzione “select2()” su tutti gli input di tipo select contenuti nel form.

$("form.changeform > fieldset select").each(function() {
  $("#" + this.id).select2();
});

Un frammento di codice del tutto simile a quello appena mostrato è contenuto all’interno del sorgente “custom changeList.min.js” ed è incaricato di convertire i filtri (contenuti all’interno delle pagine di tipo changelist) in input di tipo Select2. L’adozione di questa scelta è stata essenziale per migliorare le performance del sistema e l’esperienza utente, soprattutto se si considera che nativamente il framework prevede la realizzazione dei filtri per mezzo di un elenco puntato di collegamenti ipertestuali.

Limitare gli elementi negli Inline Formset

Infine, un’altra delle problematiche emerse durante la fase di testing dell’applicazione ha riguardato l’utilizzo degli inline formset presenti all’interno delle pagine di inserimento/modifica delle campagne e dei task. Nei casi di inserimento di campagne costituite da un numero molto elevato di task (superiori alle 200 unità) la successiva modifica di quest’ultime (attraverso l’apposito inline formset presente nella pagina di modifica delle campagne) provocava un errore a seguito del submit dovuto al superamento della memoria assegnata alla variabile POST/GET.

Il superamento dei limiti imposti alla memoria era causato dall’elevato numero di informazioni collegate ai task che venivano inutilmente inviate al “processor method” (difficilmente durante la modifica di una campagna vengono apportate modifiche ad un numero consistente di task).

Per risolvere questa problematica è stato fissato un limite pari a 100 elementi all’interno degli inline formset ed è stato aggiunto un messaggio informativo all’interno della pagina di modifica che informa della parziale visualizzazione dei dati. Nel frammento di codice sottostante viene mostrato il codice utile alla definizione di un inline formset:

class TaskInlineFormSet(BaseInlineFormSet):
  limit_display = 100
  def get_queryset(self) :
  qs = super(TaskInlineFormSet, self).get_queryset()
  return qs.order_by('priority')[:self.limit_display]

class TaskInline(admin.StackedInline):
  model = Task
  extra = 0
  form = TaskInlineAdminForm
  formset = TaskInlineFormSet
  limit_display = formset.limit_display

  # list of field to display
  Classes = ['collapse']
  fieldsets = [
    (None, {'fields': ['campaign', 'description', 'content', 'task_status', 'answer_type', 'priority']}),
  ]

Alla riga 2 viene definita la variabile “limit display” contenente il numero massimo di elementi che potranno essere visualizzati nel form, successivamente tra le righe 4 e 5 viene eseguita la query di selezione degli elementi (query che viene limitata dalla sopracitata variabile).

A partire dalla riga 7 viene definito il form vero e proprio, viene collegato il model corretto (riga 8), viene definito il numero iniziale degli elementi proposti (riga 9) e viene definito il form da utilizzare per la rappresentazione dei dati (riga 10). A partire dalla riga 15 vengono definiti i campi che saranno contenuti nel form.

Successivamente, è stato necessario procedere con la modifica del template grafico, aggiungendo le successive righe di codice al file “stacked.html” (memorizzato all’interno della cartella “/templates/admin/edit inline/”):

{% if inline_admin_formset.forms|length >= inline_admin_formset.opts.limit_display %}
<div class="alert alert-info alert-dismissible fade in" role="alert">
  <button type="button" class="close"span aria-hidden="true">x</span></button>
  <b>Ops!</b> This {{inline_admin_formset.model_admin.opts.verbose_name|lower }} contains too many {{ inline_admin_formset.opts.verbose_name_plural|lower }}. The first {{ inline_admin_formset.opts.limit_display }} are shown below, you can see the others by <a target='_blank' href='/admin/{{inline_admin_formset.model_admin.opts.app_label }}/{{inline_admin_formset.opts.opts.model_name}}/?{{inline_admin_formset.model_admin.opts.model_name }}__id__exact={{original.pk }}'>clicking here</a>.
</div>
{% endif %}

Lo scopo del precedente frammento è quello di mostrare un messaggio all’inizio dell’inline formset informando l’utente che i dati mostrati in seguito non sono completi, ma che essi si riferiscono ai soli primi 100 dati estratti (qualora i dati scaricati eccedessero davvero questo limite).

Infine, viene anche fornito un link che permette di visualizzare (mediante l’applicazione di un filtro sulla changelist) l’elenco completo dei valori inseriti. Ad esempio: se per una certa campagna venissero caricati più di 100 task, verrebbe visualizzato il seguente messaggio d’errore contenente un link alla pagina di riepilogo di tutti i task filtrata per la campagna in questione. In questo modo l’utente potrà comunque visualizzare tutti i dati forniti (attraverso la changelist puntata dal link) e procedere con la modifica di uno specifico elemento utilizzando la pagina di modifica del singolo task. Segue un’immagine del messaggio visualizzato nella pagina di modifica della campagna:

Importer JSON

La funzionalità più discussa della piattaforma Argo Plus è senza dubbio l’importer JSON per le campagne e i loro task. Lo scopo di questa funzionalità è quello di facilitare l’inserimento delle campagne all’interno della piattaforma, risparmiando al requester la compilazione manuale di diversi moduli (specialmente alla luce delle problematiche presentate all’interno del sotto paragrafo “Limitare gli elementi negli Inline Formset”). Inoltre, la presenza dell’importatore JSON permette l’importazione dei dati in Argo Plus a partire da gestionali esterni, favorendo così le procedure di migrazione. Per poter essere importata, una stringa JSON deve essere inserita all’interno del campo denominato “JSON” del modulo di creazione della campagna, in questo caso la campagna generata conterrà tutte le informazioni inserite nel form: nome, status, data apertura, eccetera; e tutti i task (con relative informazioni) contenuti all’interno della stringa JSON. All’interno del JSON possono essere inseriti numerosi task (i test condotti hanno evidenziato come sia possibile importare simultaneamente poco più di 400 task senza incorrere in errori), ognuno con specifiche informazioni:

  • Titolo del task;
  • Descrizione del task;
  • Priorità (opzionale), contiene la priorità del singolo task;
  • Tipo, contiene un valore a scelta tra quelli presenti nel modulo “Answer Type”;
  • Stato, contiene un valore a scelta tra quelli presenti nel modulo “Task Status”;
  • ID Originale (opzionale), contiene l’ID del task così come definito nel gestionale di origine;
  • Risposta Corretta (opzionale), viene utilizzata per evidenziare le risposte corrette all’interno del backend;
  • Pattern (opzionale), contiene un valore a scelta tra quelli presenti nel modulo “Setup” (per i requester), oppure “Pattern Setup” (per gli amministratori);
  • Profile (opzionale), contiene un valore a scelta tra quelli presenti nel modulo “Skill” (per i requester), oppure “Profile Skill” (per gli amministratori).

Come già discusso in precedenza lo scopo del campo “Pattern” è quello di contenere un valore alfanumerico (facilmente memorizzabile) che permetta al requester di collegare diverse configurazioni (una per ogni round) al task corrente. Allo stesso modo il campo “Profile” può essere utilizzato per associare al task numerose skill. Per facilitare la procedura di importazione, ridurre gli errori e semplificare il lavoro del requester sono stati introdotti alcuni algoritmi di controllo sui dati inseriti, tra questi citiamo:

  • Rilevamento delle chiavi inserite in modalità case-insensitive (senza distinguere maiuscole o minuscole);
  • Rilevamento e segnalazione delle chiavi impreviste;
  • Rilevamento del mancato utilizzo delle chiavi obbligatorie;
  • Verifica dei pattern e delle skill inserite.

Rilevamento delle chiavi case-insensitive

Per ridurre al minimo gli errori il sistema accetta l’inserimento di stringhe JSON con chiavi scritte in maiuscolo e in minuscolo (in realtà sono accettate anche forme miste, fatta eccezione per la chiave principale “task”). Di seguito viene mostrato il codice contenuto all’interno delle classi “CampaignAdminForm” e “CampaignRequesterAdmin-Form” del file “form.py” responsabili della definizione dei form di creazione/modifica delle campagne per gli amministratori e i requester.

def clean_taskjson(self):
  returnJson = {'tasks': []}

  if self.cleaned_data['taskjson']:
    try:
      jsonMain = json.loads(self.cleaned_data.get('taskjson'))
      ...
      # check master key "tasks"
      if 'tasks' in jsonMain:
        jsonTasks = jsonMain['tasks']
      elif 'TASKS' in jsonMain:
        jsonTasks = jsonMain['TASKS']
      else:
        raise forms.ValidationError('Key \'tasks\' not found in JSON')

      # covert keys to lower
      for index, jsonTask in enumerate(jsonTasks):
        for key in list(jsonTask.keys()):
          if key.lower() != key:
            jsonTasks[index][key.lower()] = jsonTasks[index][key]
            del jsonTasks[index][key]

Come si può facilmente vedere il frammento di codice inizia con il salvataggio del contenuto del campo “taskjson” all’interno della variabile “jsonMain” (riga 75), successivamente si procede con la verifica dell’esistenza della chiave “tasks” (oppure “TASKS”) e il conseguente salvataggio dell’array nella variabile “jsonTasks”. Qualora il sistema non dovesse rilevare la presenza dell’indice scatenerà un’eccezione (riga 83). Infine, il sistema procede con la scansione di tutte le chiavi poste all’interno della singola voce dell’array (e quindi all’interno del singolo task), se quest’ultime non dovessero essere scritte in minuscolo si procederà con la loro riscrittura.

Rilevamento e segnalazione delle chiavi impreviste

Per limitare il numero di errori il sistema non accetta l’introduzione di chiavi diverse da quelle previste al momento della sua definizione. Questa scelta ha lo scopo di aiutare gli utenti distratti, magari colpevoli di un semplice errore di battitura, evitando loro di dover inserire successivamente a mano (quindi task per task) un’informazione che poteva essere importata mediante il JSON. L’importatore di Argo Plus è quindi programmato per ricercare le chiavi impreviste e segnalarle all’utente. Come nel caso precedente il codice responsabile di tale operazione è contenuto all’interno delle classi “CampaignAdminForm” e “CampaignRequesterAdminForm” del file “form.py” ed è evidenziato in seguito:

key_accepted = ['request', 'content', 'priority', 'type', 'choices', 'status', 'id_origin', 'gold_answer', 'pattern', 'profile']
...
# check json
for jsonTask in jsonTasks:
  # check look for unexpected keys
  for key in jsonTask.keys():
    if (key not in key_accepted):
      raise forms.ValidationError('Unexpected key \'' + key + '\' in JSON')

Alla riga 77 viene definito l’array delle chiavi accettate. A partire dalla riga 80 vengono invece eseguiti i controlli di presenza.

Rilevamento del mancato utilizzo di chiavi obbligatorie

Il sistema di importazione è in grado anche di verificare la presenza all’interno del JSON di quelle chiavi che sono state definite come obbligatorie, in modo tale da non permettere l’importazione dei task in assenza di alcune informazioni. Anche in questo caso il codice responsabile dell’operazione è contenuto all’interno delle classi “CampaignAdminForm” e “CampaignRequesterAdminForm” del file “form.py” ed è evidenziato in seguito:

key_accepted = ['request', 'content', 'priority', 'type', 'choices', 'status', 'id_origin', 'gold_answer', 'pattern', 'profile']
key_mandatory = ['request', 'type']
...
# check json
for jsonTask in jsonTasks:
  ...
  # check if all mandatory keys are used
  for key in key_mandatory:
    if (key not in jsonTask.keys()):
      raise forms.ValidationError('Key \'' + key + '\' not found in JSON (key \'' + key + '\' is mandatory)')

La definizione delle righe obbligatorie avviene alla riga 78, come si può vedere gli unici campi richiesti sono il titolo del task e la tipologia di valori che accetta. A partire dalla riga 84 vengono invece eseguiti i controlli di presenza.

Verifica dei pattern e delle skill inserite

Infine, il sistema è in grado di verificare l’esistenza del pattern e del profile inseriti dall’utente, impedendo l’utilizzo di stringhe errate e senza corrispondenza. Anche in questo il sistema semplifica l’utilizzo dell’importatore da parte dell’utente, verificando l’esistenza del pattern e del profile in modalità case-insensitive. Di seguito viene mostrato il sorgente responsabile dell’esecuzione del controllo appena citato, anche in questo caso il frammento di codice è presente all’interno delle classi “CampaignAdminForm” e “CampaignRequesterAdminForm” del file “form.py”:

# check json
for jsonTask in jsonTasks:
  ...
# check value of setup (pattern), of skill (profile)
for jsonTask in jsonTasks:
  if ('pattern' in jsonTask.keys()) and (not
    Task_Setup_Pattern.objects.filter(pattern__description__iexact = jsonTask['pattern'].lower()).exists()):
    raise forms.ValidationError('Unexpected value \'' + jsonTask['pattern'].lower() + '\' for \'pattern\' key')

  if ('profile' in jsonTask.keys()) and (not Profile_Skill.objects.filter(profile__description__iexact = jsonTask['profile'].lower()).exists()):
    raise forms.ValidationError('Unexpected value \'' + jsonTask['profile'].lower() + '\' for \'profile\' key')

Controlli simili vengono effettuati sui valori delle chiavi “type” e “status”.

Struttura del JSON e Funzionalità aggiuntive

Come è già stato accennato all’inizio di questo del paragrafo il modulo di importazione per i task delle campagne è presente sia nella pagina di creazione della campagna che nella pagina di modifica. Lo scopo di tale duplice posizione è quello di permettere l’importazione dei task in fase di creazione della campagna e contemporaneamente di permettere l’inserimento di nuovi task in campagne già esistenti. In fase di aggiornamento della campagna (inserimento di nuovi task all’interno di campagne già esistenti) non sono tuttavia previsti controlli anti-duplicazione su nessuno dei campi inseriti. Di seguito viene riportata la struttura del file JSON da importare così come suggerita all’interno della pagina di importazione.

{
  "tasks": [
    {
      "request": "Task 1",
      "content": "Description of task",
      "priority": 1,
      "type": "Text",
      "status": "Ready to Execute",
      "id_origin": "Origin_ID_1",
      "gold_answer": "Preselect Answer",
      "pattern": "Pattern Name",
      "profile": "Profile Name"
    },
    {
      ...
    },
    {
      "request": "Task 999",
      "content": "Description of task",
      "priority": 999,
      "type": "Choice",
      "choices": {
        "id_1": "Choice_1",
        "id_2": "Choice_2",
        "id_3": "Choice_3",
        "id_4": "Choice_4",
        "id_5": "Choice_5",
        "id_6": "Choice_6"
      },
      "status": "Ready to Execute",
      "pattern": "Pattern Name",
      "profile": "Profile Name"
    }
  ]
}