Create your own custom form

The idea behind custom forms, also called plugins, is that you can add an entire form associated to families of tasks (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.

screen981

Custom forms are usually visible in the document section of tasks 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 sample custom forms on tasks and resources. These by default are not visible unless some conditions are satisfied:

Simple Custom Form: visible only when the task name is TESTFORM.

Project Complexity: visible only when the task is root and its relevance is >= 80.

Project Value: visible only when the task is root and its relevance is >= 80.

Project Risk: visible only when the task is root and its relevance is >= 80 and the task type is PRODUCTION.

On tasks where these are enabled, just go to the “document” tab of task/resource’ editor:

simpleCustomFormLink

Click on “simple custom form”:

screen976

This chaotic form is meant just as an example of the spectrum of fields that you can add on forms.

 

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 the same folder, and start modifying it.

First of all, what makes custom forms practical is that they are “hassle free”. You can extend a task 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.

adminCustomForms

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 task 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 task, data written on the forms for that task will be saved in olpl_des_data_value, and linked to the task through a record in olpl_des_data: referenceId will be the id of the task, referenceClassName the task 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 task -----------------

// this form will be visible only on root tasks

ret = task.getParent() == null;

// ----- end test condition on task -----------------

}

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 Task instance and if the task is a root one. If bothe 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 task/resource list/editor, resource documents, task documents, task 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 = "&nbsp;";
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 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()) : "&nbsp;-&nbsp;"%></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 ----------------- --%>

We know that in this context the main object is a task 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);%> &nbsp;
<%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:&nbsp;<%=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 task.

<%
c.end(pageContext);
}
// ############################## END FORM LAYOUT DEFINITION ################################
}
%>

That’s all. “print” and “save” buttons are added automatically.