The idea behind custom forms, also called plugins, is that you can add an entire form associated to families of projects (or resources, or even generically present in Twproject’s menus, depending on context and user rights) just by creating a single, self standing jsp file: no new class compilation, database schema creation, or transaction handling is necessary, even if you define new fields to be saved. Of course, if you also want to create supporting classes, or add jars to the classpath, you are free to do so.
Custom forms are usually visible in the document section of projects and resources editors: forms are used to extend properties of Twproject’s objects. Plugins are generally intended for automating actions (e.g. wizards) or for extending reporting capability.
Custom forms usage
Actually in Twproject’s standard installation you will already have some custom forms on projects and resources. For instance Project relevance, complexity and risk are three custom forms at work!
These by default are not visible unless some conditions are satisfied:
Others are not visible by default and appear only when a project or company is called “Twproject test form”.
On projects where these are enabled, just go on the last tab “projects form”
Create your own
This section require some Java programming skills.
This section is not for the faint of heart only those who know Java can benefit from this reading.
Custom forms/reports/plugins make sense only when “customized”. So in this section we will try to explain how they work and how to modify/create your own.
There are various examples forms provided; in order to start, use simpleCustomForm.jsp: it is extensively commented, and contains examples of the different fields (strings, dates, numbers, pointers to objects…) which may be used in a form. Copy it in a new file in your customization folder, and start modifying it.
First of all, what makes custom forms practical is that they are “hassle free”. You can extend a project with tens of new properties without caring about saving/changing/removing data, which is done by the framework. The persistence layer is completely hidden by Twproject.
Where are custom forms
Default Custom forms are .jsp pages in the folder:
[root]/applications/teamwork/plugins
You have to put your custom forms on:
[root]/applications/teamwork/customers/ACME/plugins
folder, Where ACME is the name/short code of your company.
In order to list active plugins go to tools -> admin page, then click on “forms and plugin” listed inside “Customization section”.
When Twproject starts-up it scans that folder and initializes each plugin. You can force a new directory scanning by clicking on “reload plugins” button.
How does it work
Load: At startup, Twproject will try to call the initialize method on the jsp files found, and those that do not throw an exception are loaded in memory among the available plugins.
Visibility: A plugin can appear in the following locations: in Twproject “tools” menu, on the project editor or on the resource editor. Whether they will appear there is entirely determined by the result of the call “isVisibleInThisContext” on the jsp page.
Persistence: Where does data get saved, and how? As a form can change any moment the type of fields present in it, its data cannot be subject to referential integrity. All data is saved in the tables olpl_des_data and olpl_des_data_value. There is nothing the developer needs to do to make data persistent: all fields present in the form will be saved, and automatically associated to the entity through which one has gone through to reach the form. So for example, if one is on a project, data written on the forms for that project will be saved in olpl_des_data_value, and linked to the project through a record in olpl_des_data: referenceId will be the id of the project, referenceClassName the project class, and designerName will be a normalized form of the jsp file name.
Plugin dissection
Ok, now starts the hard core….
When a plugin is initialized, it registers itself in a group, and injects an inner class extending PagePlugin, used to understand if the plugin should be visible in the current web context. Let’s have a look to the code (the example is from 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 project ----------------- // this form will be visible only on root project ret = task.getParent() == null; // ----- end test condition on project ----------------- } return ret; } } %>
The jsp inner class must implement the isVisibleInThisContext() method.
This method based on data got from the PageState instance and mainly the “mainObject” field check if we are in the appropriate context.
In this case we are checking if the mainObject is a project instance and if the project is a root one. If both condition are true the form will be visible in this context.
Each custom form is composed by two parts called in different application life-cycle. The first part is the initialization. This part is called at startup and injects PagePlugin instance in the system.
The PagePluginExt.isVisibleInThisContext method is called every time Twproject is creating links for plugins for the group “TASK_FORMS”.
<% /* */ // ############################# BEGIN INITIALIZE ############################################### if (JspIncluder.INITIALIZE.equals(request.getParameter(Commands.COMMAND))) { PluginBricks.getPagePluginInstance("TASK_FORMS", new PagePluginExt(), request); // ############################ END INITIALIZE ################################################
Actually Twproject uses four of groups: “REPORTS”, “RESOURCE_FORMS”, “TASK_FORMS”, “TASKLOG” that are displayed respectively in project/resource list/editor, resource documents, project documents, project log.
The second part is the definition of the form.
Definition is composed of two parts: form data definition and form html layout.
} 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) {
You can have a selector as 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);
Or as list
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 {
Once you have declared the field you intend to use, you must define it in the html layout of the page.
// ########################### 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);
We create a container around the form
// you can extract data to enrich your form using data from current project. // In this case we will extract missing days from current project 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 PROJECT DATA ---------------------- ---------------------- You can use the project 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 PROJECT DATA ----------------- --%>
We know that in this context the main object is a project so we can use it to extract some data to enrich the 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>
We can add some computation on inserted values. We can eventually mix data from the form and data from the project.
<% c.end(pageContext); } // ############################## END FORM LAYOUT DEFINITION ################################ } %>
That’s all. “print” and “save” buttons are added automatically.