Extension: Remote WebForms
What is it?¶
Remote Beautiful WebForm is an extension package for Script Console that allows you to deploy a Beautiful WebForms powered webform created on Content Server on the Script Console engine.
The main purpose of this extension is to simplify the process of gathering the contribution of users that do not have access to Content Server and synchronize these information back on Content Server. An other quite common scenario, is the off-line usage of Content Server webforms: the possibility of accessing, through a locally deployed Script Console instance, a copy of a Content Server webform, even when a connection with Content Server is not available.
In both the cases the information submitted through the remote webform are stored locally within the Script Console to be later synchronize back towards Content Server.
Extension setup¶
Installing the remote-webform extension package on a Script Console instance, is a straight forward procedure which consists of just two steps:
- Run the Script Console master installer and install the Remotable WebForms extension package
- Copy all the static resources from the Beautiful WebForms Module Support in:
<Script Console Home>\config\img\ansbwebform
- Copy all the static resources from the Content Script Module Support (
\support\anscontentscript) in:
<Script Console Home>\config\img\anscontentscript
- Copy all the static resources from the Module Suite for SmartUI Module Support (
\support\anscontentsmartui) in:
<Script Console Home>\config\img\anscontentsmartui
Create remote package¶
Beautiful WebForms deployable packages can be created either programmatically, using the Content Script forms
service or manually, through the Beautiful Webforms Studio application.
Using forms.createExPackage
API¶
Content Script forms.createExPackage
API can be used to programmatically create a deployable Beautiful WebForms remote package.
The API can be used from within a Beautiful WebForms View CLEH script, or from any other Content Script object.
In most of the cases, if used within a stand-alone script, this API is used in conjunction with forms.getFormInfo
or forms.listFormData
APIs.
Properly initialize the form object
It's important that you keep in mind that when the form
object is loaded using the form
service it is not initialized.
You can either initialize it as part of your script or rely on it's OnLoad CLEH for its proper initialization.
Here below an example of how properly initialize the form object:
Minimum initialization required
def formNode= docman.getNodeByPath("Path:to:your:form")
form = formNode.getFormInfo()
forms.addResourceDependencies( form, true, true)
Initialization through the OnLoad script (if any)
def formNode= docman.getNodeByPath("Path:to:your:form")
form = formNode.getFormInfo()
def bwfView = docman.getNode(form.amViewId)
def onLoad = bwfView.childrenFast.find{it.name == "OnLoad"}
if(onLoad){
docman.runContentScript(onLoad, binding)
}
forms.createExPackage(
Form form, // The form to export
String name, // An alpha-numeric identifier for the package to be created
String instructions, // The instruction to be displayed to help the user filling in the form
String nextUrl, // Where to redirect the user upon submission
Date validUpTo, // A date after which the form should no longer be available (can be null)
List<String> viewsToExport, // The names of the views you want to export as part of the package (can be null)
// if null all the views will be exported
String pin, // An optional pin that can be used to protect the access to the form on the console
CSDocument[] arrayOfDocuments // An optional list of documents to be exported as 'attachments' with the package
)
Using Beautiful Webforms Studio¶
Beautiful Webforms Studio which can be found at the following location:
Content Script Volume:CSTools:Beautiful WebForm Studio
Among the possibilities offered the studio application can help you leveraging the forms.createExPackage
through a simplified visual wizard.
The first step is to select Export Remote Form among the available actions.
than you'll be asked for a space on Content Server to be used as the wizard workspace (where objects and content will be created):
finally you will be asked about export configuration parameters
-
Form: the form object to be exported
-
Title: the form's title as it will be displayed on the script console default dashboard
-
Name: the export package name (should be an alpha-numeric value)
-
Description: the form's description as it will be displayed on the script console default dashboard
-
PIN: an optional PIN to be used in order to protect un-authorized access to the form on the console
-
Redirect: an URL where to redirect user's navigation upon submission
-
View: an optional list of views names to be exported
-
Attachment(s): an optional list of documents to be exported
upon submission the export package file will be created in the selected workspace.
How to deploy a Beautiful WebForms remote form package¶
The Beautiful WebForms remote form package is actually a .zip archive containing all objects necessary to the form (view files, scripts, templates, etc.).
You can manually extract its contents in a new folder inside:
<Script Console Home>\config\scripts\ext\forms\forms
for example:
<Script Console Home>\config\scripts\ext\forms\forms\myform
at this point, you should be able to access the form via the Script Console Dashboard, or via direct URL.
Synchronize form data back to Content Server¶
Form data submitted on Script Console can be synchronized back to Content Server in different ways which all are based on the same paradigm: the asynchronous exchange of information is based on data files.
Data files can be moved from the Script Console to Content Server no matter which transportation mechanism is used.
In the following paragraphs we will cover the most common scenarios.
Remote data pack files are produced on Script Console and sent over to Content Server¶
Script Console and Content Server can be isolated
In order to implement this scenario there is no need for the two systems to communicate each other.
In this scenario a local script is executed (or scheduled) on the Script Console in order to collect submitted data and prepare the exchange data files to be sent over Content Server.
The Remotable Beautiful WebForms extension for Script Console comes with several exemplar scripts of this kind that can be found at the following location:
<Script Console Home>\config\scripts\ext\forms
E.g synchLocal.cs
import groovy.json.JsonSlurper
import groovy.io.FileType
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry
formsAvailable = []
system = context.getAttribute("system")
formRepository = system.extensionRepositories.find{
it.repoHome.name == 'forms'
}
formRepositoryDir = new File(formRepository.getAbsolutePath(), "forms")
formRepositoryDirLocal = new File(formRepository.getAbsolutePath(), "inout")
if(formRepositoryDir && formRepositoryDir.isDirectory()){
def deleteFile = []
formRepositoryDirLocal.eachFileRecurse(FileType.FILES){
if(it.name.endsWith(".amf")){
File newForm = new File(formRepositoryDir, it.name-'.amf')
if(!newForm.mkdir()){
return
}
def zipFile = new java.util.zip.ZipFile(it)
zipFile.entries().each {
ins = zipFile.getInputStream(it)
new File(newForm, it.name) << ins
ins.close()
}
zipFile.close();
deleteFile << it
}
}
deleteFile.each {
it.delete()
}
}
if (params.upload == 'true' && params.selform){
list =[]
list.addAll( params.selform)
toBeDeleted = []
list.each{ form->
formRepositoryDir = new File(formRepository.getAbsolutePath(), "data/$form")
if(formRepositoryDir && formRepositoryDir.isDirectory()){
formRepositoryDir.eachFileRecurse(FileType.FILES){
if(it.name == "data.amf"){
File dataPack = it.getParentFile()
String zipFileName = "${dataPack.name}.rpf"
File zipFile = new File(new File(formRepository.getAbsolutePath(), "temp"), zipFileName)
ZipOutputStream zipOS = new ZipOutputStream(new FileOutputStream(zipFile))
zapDir(dataPack.path, zipOS, dataPack.path)
zipOS.close()
zipFile.renameTo(new File(formRepositoryDirLocal, zipFile.name))
toBeDeleted << dataPack
}
}
}
}
toBeDeleted.each{
it.deleteDir()
}
}
def static zapDir(String dir2zip, ZipOutputStream zos, String stripDir) {
File zipDir = new File(dir2zip)
def dirList = zipDir.list()
byte[] readBuffer = new byte[2156]
int bytesIn = 0
dirList.each {
File f = new File(zipDir, it)
if(f.isDirectory())
zapDir(f.path, zos, stripDir)
else {
FileInputStream fis = new FileInputStream(f)
ZipEntry anEntry = new ZipEntry(f.path.substring(stripDir.length()+1))
zos.putNextEntry(anEntry)
while((bytesIn = fis.read(readBuffer)) != -1) {
zos.write(readBuffer, 0, bytesIn);
}
fis.close();
}
}
}
redirect params.nextUrl
If you want to schedule this kind of scripts to be automatically executed by the Script Console you have to configure the job in the cs-console-schedulerConfiguration.xml
file, which is a standard Quartz scheduler configuration file. You should find a sample job in there.
Here below a configuration example:
<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data
xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd"
version="1.8">
<pre-processing-commands>
<delete-jobs-in-group>*</delete-jobs-in-group> <!-- clear all jobs in scheduler -->
<delete-triggers-in-group>*</delete-triggers-in-group> <!-- clear all triggers in scheduler -->
</pre-processing-commands>
<processing-directives>
<!-- if there are any jobs/trigger in scheduler of same name (as in this
file), overwrite them -->
<overwrite-existing-data>true</overwrite-existing-data>
<!-- if there are any jobs/trigger in scheduler of same name (as in this
file), and over-write is false, ignore them rather then generating an error -->
<ignore-duplicates>false</ignore-duplicates>
</processing-directives>
<schedule>
<job>
<name>PollJobSynchronization</name>
<group>Synchronization</group>
<job-class>com.answer.modules.cscript.console.scheduler.CommandLauncherJob</job-class>
<job-data-map>
<entry>
<key>script</key>
<value>ext/forms/synchLocal.cs</value>
</entry>
<entry>
<key>system</key>
<value>LOCAL</value>
</entry>
</job-data-map>
</job>
<trigger>
<cron>
<name>LaunchEvery1Minutes</name>
<group>SynchronizationTriggerGroup</group>
<job-name>PollJobSynchronization</job-name>
<job-group>Synchronization</job-group>
<start-time>2010-02-09T12:26:00.0</start-time>
<end-time>2020-02-09T12:26:00.0</end-time>
<misfire-instruction>MISFIRE_INSTRUCTION_SMART_POLICY</misfire-instruction>
<cron-expression>0 * * ? * *</cron-expression>
<time-zone>America/Los_Angeles</time-zone>
</cron>
</trigger>
</schedule>
</job-scheduling-data>
Later on Content Server the data files are unpacked using the forms
service from within a Content Script that can be either manually executed or scheduled.
E.g.
// remPack is a data pack file, how this file was obtained is not relevant.
// It may have been fetched from an email folder, a ftp server, a shared folder a cloud service,
// or even uploaded on Content Server using web-services, etc...
def packList = forms.getExPackageContent( remPack) // returns a Map<String, CSResource>
if(packList."data.amf"){
def res = packList.find{it.key == "data.amf"}.value
def form = forms.deserializeForm(res.content.getText("UTF-8"))
// The form object can be used for various purposes
// Submitting the data back to Content Server
forms.submitForm(form)
// Starting a workflow
def damageInvestigation = docman.getNodeByPath("Fleet Management:Workflows:Damage Ingestigation Map")
def inst = forms.startWorkFlow(damageInvestigation, form, "Form", "Damage Ingestigation - Veichle: ${form.number.value} - Employee: ${form.employee.value} " )
// Seding on a running workflow
def task = workflow.getWorkFlowTask(form.getAmWorkID(), form.getAmSubWorkID(), form.getAmTaskID())
forms.updateWorkFlowForm(
task, //The task
"Form Name", //The form name
form, //The form object
true // True if the task should be sent on
)
}
Form data are submitted directly from Script Console¶
Script Console and Content Server can't be isolated
In order to implement this scenario the two systems shall be able to communicate each other.
This scenario can be implemented executing or scheduling a script similar to the one reported here below on the Script Console:
import groovy.io.FileType
log.debug("Running Your Form Synch Job")
formsAvailable = []
system = context.get("system")
formRepository = system.extensionRepositories.find{
it.repoHome.name == 'forms'
}
//Synch up
formRepositoryDirParent = new File(formRepository.getAbsolutePath(), "data")
def toBeDeleted = []
formRepositoryDirParent.eachFileRecurse(FileType.DIRECTORIES){ formRepositoryDir->
if(("yourform").equalsIgnoreCase(formRepositoryDir.name)){
if(formRepositoryDir && formRepositoryDir.isDirectory()){
formRepositoryDir.eachFileRecurse(FileType.FILES){
if(it.name == "data.amf"){
formObj = forms.deserializeForm(it.text)
File dataPack = it.getParentFile()
try{
forms.submitForm(formObj)
toBeDeleted << dataPack
}catch(e){
log.error("Unable to synch data back to OTCS",e)
}
}
}
}
}
}
toBeDeleted.each{
it.deleteDir()
}
If you want to schedule this kind of scripts to be automatically executed by the Script Console you have to configure the job in the cs-console-schedulerConfiguration.xml
file, which is a standard Quartz scheduler configuration file. You should find a sample job in there.
Here below a configuration example:
<?xml version="1.0" encoding="UTF-8"?>
<job-scheduling-data
xmlns="http://www.quartz-scheduler.org/xml/JobSchedulingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.quartz-scheduler.org/xml/JobSchedulingData http://www.quartz-scheduler.org/xml/job_scheduling_data_1_8.xsd"
version="1.8">
<pre-processing-commands>
<delete-jobs-in-group>*</delete-jobs-in-group> <!-- clear all jobs in scheduler -->
<delete-triggers-in-group>*</delete-triggers-in-group> <!-- clear all triggers in scheduler -->
</pre-processing-commands>
<processing-directives>
<!-- if there are any jobs/trigger in scheduler of same name (as in this
file), overwrite them -->
<overwrite-existing-data>true</overwrite-existing-data>
<!-- if there are any jobs/trigger in scheduler of same name (as in this
file), and over-write is false, ignore them rather then generating an error -->
<ignore-duplicates>false</ignore-duplicates>
</processing-directives>
<schedule>
<job>
<name>PollJobSynchronization</name>
<group>Synchronization</group>
<job-class>com.answer.modules.cscript.console.scheduler.CommandLauncherJob</job-class>
<job-data-map>
<entry>
<key>script</key>
<value>ext/forms/submitMyFormLocal.cs</value>
</entry>
<entry>
<key>system</key>
<value>LOCAL</value>
</entry>
</job-data-map>
</job>
<trigger>
<cron>
<name>LaunchEvery1Minutes</name>
<group>SynchronizationTriggerGroup</group>
<job-name>PollJobSynchronization</job-name>
<job-group>Synchronization</job-group>
<start-time>2010-02-09T12:26:00.0</start-time>
<end-time>2020-02-09T12:26:00.0</end-time>
<misfire-instruction>MISFIRE_INSTRUCTION_SMART_POLICY</misfire-instruction>
<cron-expression>0 * * ? * *</cron-expression>
<time-zone>America/Los_Angeles</time-zone>
</cron>
</trigger>
</schedule>
</job-scheduling-data>