L’idea che sta dietro alle form personalizzate, chiamate anche plugin, è che tu puoi aggiungere un intero form associato a famiglie di progetti (o risorse, o anche genericamente presente nel menu di Twproject, a seconda del contesto e dei diritti dell’utente) semplicemente creando un singolo file jsp: non è necessario compilare classi, creare uno schema del database, o gestire transizioni, anche se definisci nuovi campi da salvare. Ovviamente, se vuoi anche creare nuovi classi di supporto, o aggiungere jars al classpath, sei libero di farlo.
Le form personalizzate sono visibili come tab nella pagina principale del progetto e delle risorse: le forms sono usate per estendere le proprietà degli oggetti di Twproject. I plugins sono di solito intesi per azioni automattizate (e.g. wizards) o per estendere la capacità di report.
Uso delle form personalizzate
In realtà nell’installazione standard di Twproject ci sono già alcuni esempi di form personalizzate su progetto e risorse. Per esempio il form per il calcolo della rilevanza, della complessità e del rischio sono un esempio di custom form all’opera!
Altri non sono visibili di default ed appaiono solo quando un progetto o una risorsa azienda si chiamano “Twproject test form”.
Sui progetti dove sono abilitati, clicca sul tab project forms:
Crea la tua form
Questa sezione richiede alcune capacità di programmazione Java.
Questa sezione non è per i deboli di cuore 🙂 solo chi conosce Java può trarre beneficio da questa lettura.
I forms/reports/plugins hanno senso solo se “personalizzati”.
Quindi in questa sezione spiegheremo come lavorano e come modificare/creare il tuo.
Ci sono vari esempi di form forniti; per iniziare, usa simpleCustomForm.jsp: è ben commentato, e contiene esempi di campi diversi (stringhe, date, numeri…) che possono essere usati in una form. Copialo in nuovo file nella tua cartella delle customizzazioni, e inizia a modificarlo.
Prima di tutto, ciò che rende le form personalizzate pratiche è che sono “senza problemi”. Puoi estendere un progetto con decine di nuove proprietà senza occuparti del salvataggio/modifica/eliminazione dei dati, che è gestito dal framework. Il livello di persistenza è completamente nascosto da Twproject.
Dove sono le form personalizzate
Le form personalizzate di default sono pagine .jsp nella cartella:
[root]/applications/teamwork/plugins
Devi mettere le tue form personalizzate in:
[root]/applications/teamwork/customers/ACME/plugins
Dove ACME è il nome/short code della tua azienda.
Per elencare i plugin ativi vai in strumenti -> admin page, poi clicca su “forms and plugin” nel box “Customization”.
Quando Twproject si avvia fa una verifica su questa cartella e inizializza ogni plugin. Puoi forzare una nuova verifica cliccando sul bottone “reload plugins”.
Come funziona
Caricamento: All’avvio, Twproject chiamerà il metodo inizializza sulle jsp trovate, e quelle che non lanciano un’eccezione vengono caricate in memoria tra i plguin disponibili.
Visibilità: un plugin può apparire nei seguenti luoghi: nel menu “tools” di Twproject, sull’editor del progetto e sull’editor delle risorse. Se verranno visualizzati è determinato dal risultato della chiamata “isVisibleInThisContext” sulla pagina jsp.
Persistenza: Dove vengono salvati i dati e in che modo? Dato che una form può cambiare in ogni momento i campi presenti su di essa, i suoi dati non sono soggetti a integrità referenziale. Tutti i dati sono salvati nella tabella olpl_des_data and olpl_des_data_value. Lo sviluppatore non deve fare niente per la persistenza dei dati: tutti i campi presenti sulla form saranno salvati, e automaticamente associati all’entità attraverso la quale sei giunto alla form. Quindi per esempio, se uno è su un progetto, i dati scritti sulle form per quel progetto saranno salvati in olpl_des_data_value, e collegati al progetto attraverso un record in olpl_des_data: referenceId sarà l’id del progetto, referenceClassName la classe del progetto, e designerName sarà una form normalizzata del nome del file jsp.
Analisi del plugin
Ok, adesso inizia la parte più tosta….
Quando un plugin è inizializzato, esso registra se stesso in un gruppo, e inietta una inner class che estende PagePlugin, usata per capire se il plugin dovrebbe essere visibile nel contesto corrente. Diamo uno sguardo al codice (l’esempio viene da simpleCustomForm.jsp):
<%@ page import="com.twproject.resource.Person, … lots of import removed … %><%@ page contentType="text/html; charset=utf-8" pageEncoding="UTF-8" %><%! /** * This inner class is used by Twproject to know if this form applies to current context. * PagePlugin classes are loaded at startup (or by hand) in memory to be performant. * */ public class PagePluginExt extends PagePlugin { public boolean isVisibleInThisContext(PageState pagestate) { boolean ret = false; if (pagestate.mainObject != null && pagestate.mainObject.getClass().equals(Task.class)) { Task task = (Task) pagestate.mainObject; // ----- begin test condition on task ----------------- // this form will be visible only on root tasks ret = task.getParent() == null; // ----- end test condition on task ----------------- } return ret; } } %>
L’inner class jsp deve implementare il metodo isVisibleInThisContext().
Questo metodo basato sui dati ottenuti dall’istanza PageState e principalmente dal campo “mainObject” verifica di essere nel contesto appropriato.
In questo caso stiamo controllando se il mainObject è un Progetto e se il progetto è uno di root. Se entrambe le condizioni sono vere il form sarà visibile in questo contesto.
Ogni form personalizzata è composta da due parti chiamate in diversi cicli dell’applicazione. La prima parte è l’inizializzazione. Questa parte è chiamata all’avvio e inietta l’istanza PagePlugin nel sistema.
Il meotodo PagePluginExt.isVisibleInThisContext è chiamato ogni volta che Twproject sta creando link per plugin per il gruppo “TASK_FORMS”.
<% /* */ // ############################# BEGIN INITIALIZE ############################################### if (JspIncluder.INITIALIZE.equals(request.getParameter(Commands.COMMAND))) { PluginBricks.getPagePluginInstance("TASK_FORMS", new PagePluginExt(), request); // ############################ END INITIALIZE ################################################
In realtà Twproject usa quattro gruppi: “REPORTS”, “RESOURCE_FORMS”, “TASK_FORMS”, “TASKLOG” che sono mostrati rispettivamente in progetto/risorse lista/editor, documenti della risorsa, documenti del progetto, log del progetto.
La seconda parte è la definizione della form.
La definizione è composta da due parti: definizione dei dati e del layout html.
} else if (Designer.DRAW_FORM.equals(request.getAttribute(JspIncluder.ACTION))) { // ------- recover page model and objects ----- BEGIN DO NOT MOFIFY -------------- PageState pageState = PageState.getCurrentPageState(request); Task task = (Task) PersistenceHome.findByPrimaryKey(Task.class, pageState.mainObjectId); Designer designer = (Designer) JspIncluderSupport.getCurrentInstance(request); task.bricks.buildPassport(pageState); // ------- recover page model and objects ----- END DO NOT MOFIFY -------------- // check security and set read_only modality designer.readOnly = !task.bricks.canWrite; // ################################ BEGIN FORM DATA DEFINITION ############################## if (designer.fieldsConfig) {
Puoi avere un selettore come bottone radio
CodeValueList cvl = new CodeValueList(); cvl.add("0", "list value 0"); cvl.add("1", "list value 1"); cvl.add("2", "list value 2"); cvl.add("3", "list value 3"); cvl.add("4", "list value 4"); DesignerField dfr = new DesignerField(CodeValue.class.getName(), "RADIO", "Checklist Example as radio", false, false, null); dfr.separator = " "; dfr.cvl = cvl; dfr.displayAsCombo = false; designer.add(dfr); DesignerField dfl = new DesignerField(CodeValue.class.getName(), "COMBO", "Checklist Example as list", false, false, null); dfl.separator = "</td><td>"; dfl.cvl = cvl; dfl.displayAsCombo = true; designer.add(dfl);
O come lista
DesignerField dfStr = new DesignerField(String.class.getName(), "STRING", "String example", false, false, "preloaded value"); dfStr.separator = "</td><td>"; dfStr.fieldSize = 20; designer.add(dfStr); standard text fields DesignerField dfNote = new DesignerField(String.class.getName(), "NOTES", "Text example (limited to 2000)", false, false, ""); dfNote.fieldSize = 80; dfNote.rowsLength = 5; dfNote.separator = "<br>"; designer.add(dfNote); text area DesignerField dfInt = new DesignerField(Double.class.getName(), "INTEGER", "Integer example", false, false, ""); dfInt.separator = "</td><td>"; dfInt.fieldSize = 4; designer.add(dfInt); DesignerField dfdouble = new DesignerField(Double.class.getName(), "DOUBLE", "Double example", false, false, ""); dfdouble.separator = "</td><td>"; dfdouble.fieldSize = 4; designer.add(dfdouble); numeric fields DesignerField dfdate = new DesignerField(Date.class.getName(), "DATE", "Date example", false, false, null); dfdate.separator = "</td><td>"; designer.add(dfdate); date DesignerField dffile = new DesignerField(PersistentFile.class.getName(), "FILE", "Upload example", false, false, null); dffile.fieldSize = 40; dffile.separator = "</td><td colspan=3>"; designer.add(dffile); uploaded files DesignerField dfperson = new DesignerField(Person.class.getName(), "PERSON", "Any persistent (Identifiable) object example, here Person", false, false, null); dfperson.separator = "</td><td>"; dfperson.fieldSize = 40; designer.add(dfperson); lookup on other Twproject’s entities DesignerField dfbool = new DesignerField(Boolean.class.getName(), "BOOLEAN", "Check if agree", false, false, ""); designer.add(dfbool); boolean // Master Detail example. You can add a detail to the form and then add field to detail. Detail detail = designer.addDetail("DETAIL"); detail.label = "Master-Detail example"; DesignerField dfitem = new DesignerField(String.class.getName(), "ITEM", "Item", false, false, ""); dfitem.fieldSize=55; detail.add(dfitem); DesignerField dfqty = new DesignerField(Integer.class.getName(), "QTY", "Qty", false, false, ""); dfqty.fieldSize = 4; detail.add(dfqty); even master detail sections // ########################### END FORM DATA DEFINITION ##################################### } else {
Una volta che hai dichiarato il campo che vuoi usare, devi definirlo nel layout html della pagina
// ########################### BEGIN FORM LAYOUT DEFINITION ################################# // create a container around the form Container c = new Container(pageState); c.title = "<big>Custom form DEMO</big> for task: " + task.getDisplayName(); c.start(pageContext);
Creiamo un contenitore intorno alla form
// you can extract data to enrich your form using data from current task. // In this case we will extract missing days from current task String daysMissing = pageState.getI18n("UNSPECIFIED"); if (task.getSchedule() != null && task.getSchedule().getEndDate() != null) { if (task.getSchedule().getValidityEndTime() > new Date().getTime()) { long missing = task.getSchedule().getValidityEndTime() - new Date().getTime(); daysMissing = DateUtilities.getMillisInDaysHoursMinutes(missing); } else daysMissing = "<b>" + pageState.getI18n("OVERDUE") + "</b>"; } %> <%-- ---------------------- BEGIN TASK DATA ---------------------- ---------------------- You can use the task recovered before to display cue data --%> <br> <table border="0"> <tr> <th colspan="2"> Some data from current task:</th> </tr> <tr> <td ><%=pageState.getI18n("RELEVANCE")%></td><td> <%=task.getRelevance()%></td> </tr><tr> <td> <%=pageState.getI18n("TASK_END")%></td> <td> <%=task.getSchedule() != null && task.getSchedule().getEndDate() != null ? JSP.w(task.getSchedule().getEndDate()) : " - "%></td> </tr><tr> <td> <%=pageState.getI18n("TASK_REMAINING")%></td> <td> <%=daysMissing%></td> </tr><tr> <td> <%=pageState.getI18n("TASK_PROGRESS")%></td><td> <% PercentileDisplay pd = TaskBricks.getProgressBarForTask(task, pageState); pd.toHtml(pageContext); %> </td> </tr> </table> <%-- ------------------- END TASK DATA ----------------- --%>
Sappiamo che in questo contesto l’oggetto principale è un progetto quindi possiamo usarlo per estrarre alcuni dati per arricchire la form.
<%-- ------------------- BEGIN HTML GRID ----------------- --%> <table border="0"> <tr> <td colspan="4"><%designer.draw("RADIO", pageContext);%></td> </tr> <tr> <td><%designer.draw("COMBO", pageContext);%></td> <td><%designer.draw("STRING", pageContext);%></td> </tr> <tr> <td colspan="4"><%designer.draw("NOTES", pageContext);%></td> </tr> <tr> <td><%designer.draw("INTEGER", pageContext);%></td> <td><%designer.draw("DOUBLE", pageContext);%></td> </tr> <tr> <td><%designer.draw("DATE", pageContext);%></td> <td><%designer.draw("PERSON", pageContext);%> <%designer.draw("BOOLEAN", pageContext);%> </td> </tr> <tr> <td><%designer.draw("FILE", pageContext);%></td> </tr> </table> We call designer.draw for every declared field <table><tr><td><%designer.draw("DETAIL", pageContext);%></td></tr></table> Then the master-detail <%-- ------------------- END HTML GRID ----------------- --%> And the html grid is closed <% double testUseValues = 0; //sum of weights testUseValues += designer.getEntry("INTEGER", pageState).intValueNoErrorCodeNoExc(); testUseValues += designer.getEntry("DOUBLE", pageState).doubleValueNoErrorNoCatchedExc(); %> <hr> <b><big>Test of sum of stored values: <%=JSP.w(testUseValues)%></big></b>
Possiamo aggiungere alcuni calcoli sui valori inseriti. Possiamo eventualmente mischiare dati dalla form e dal progetto.
<% c.end(pageContext); } // ############################## END FORM LAYOUT DEFINITION ################################ } %>
Questo è tutto. I bottoni “stampa” e “salva” vengono aggiunti automaticamente.