diff --git a/api/data/DocTree.cfc b/api/data/DocTree.cfc
index 2b3f4d9..56d3c25 100644
--- a/api/data/DocTree.cfc
+++ b/api/data/DocTree.cfc
@@ -165,10 +165,7 @@ component accessors=true {
private string function _getParentPagePathFromPagePath( required string pagePath ) {
var parts = arguments.pagePath.listToArray( "/" );
-
- if ( ArrayLen( parts ) ) {
- parts.deleteAt( parts.len() );
- }
+ parts.deleteAt( parts.len() );
return "/" & parts.toList( "/" );
}
diff --git a/api/rendering/SyntaxHighlighter.cfc b/api/rendering/SyntaxHighlighter.cfc
index 8b30967..a165201 100644
--- a/api/rendering/SyntaxHighlighter.cfc
+++ b/api/rendering/SyntaxHighlighter.cfc
@@ -25,7 +25,11 @@ component {
arguments.language = "cfm";
}
- return highlighter.highlight( arguments.code, arguments.language, false );
+ try {
+ return highlighter.highlight( arguments.code, arguments.language, false );
+ } catch( any e ) {
+ throw( type="docs.syntax.highlight.error", message="Error highlighting code for language [#arguments.language#]: [#arguments.code#]")
+ }
}
// PRIVATE HELPERS
diff --git a/docs/uber.md b/docs/uber.md
deleted file mode 100644
index c68e723..0000000
--- a/docs/uber.md
+++ /dev/null
@@ -1,21925 +0,0 @@
----
-title: Download the docs
-id: download
----
-
-The documentation is available for offline browsing as a [zip file](presidecms-docs.zip) and also as a docset for [Dash](https://kapeli.com/dash)/[Zealdocs](http://zealdocs.org/).
-
-## Dash / Zeal install
-
-You can add the Preside documentation to your Dash or Zealdocs install by adding the following feed:
-
-[https://docs.preside.org/dash/presidecms.xml](https://docs.preside.org/dash/presidecms.xml)
----
-id: quickstart
-title: Quick start guide
----
-
-The quickest way to get started with Preside is to take it for a spin with our [CommandBox commands](https://github.com/pixl8/Preside-CMS-CommandBox-Commands). These commands give you the ability to:
-
-* Create a new skeleton Preside application from the commandline
-* Spin up an ad-hoc Preside server on your local dev machine that runs the Preside application in the current directory
-
-## Install commandbox and Preside Commands
-
-Before starting, you will need CommandBox installed. Head to [https://www.ortussolutions.com/products/commandbox](https://www.ortussolutions.com/products/commandbox) for instructions on how to do so. You will need at least version 5.9.0.
-
-Once you have CommandBox up and running, you'll need to issue the following command to install our Preside specific commands:
-
-```
-CommandBox> install preside-commands
-```
-This adds our custom Preside commands to your box environment :)
-
-## Usage
-
-### Create a new site
-
-From within the CommandBox shell, CD into an empty directory in which you would like to create the new site and type:
-
-```
-CommandBox> preside new site
-```
-
-Follow any prompts that you receive to scaffold a new Preside application with the Preside dependency installed.
-
-### Start a server
-
-From the webroot of your Preside site, enter the following command:
-
-```
-CommandBox> preside start
-```
-
-If it is the first time starting, you will be prompted to enter your database information, **you will need an empty database already setup - we recommend MariaDB or MySQL, though we have some support for PostgreSQL and SQL Server**.
-
-Once started, a browser should open and you should be presented with your homepage. To navigate to the administrator, browse to `/admin/` and you will be prompted to setup the super user account. Complete that and you have a running Preside application and should be able to login to the admin!
-
->>>>>> The admin path setting is editable in your site's `/application/config/Config.cfc` file.
-
----
-id: customerrorpages
-title: Custom error pages & maintenance mode
----
-
-## Overview
-
-Preside provides a simple mechanism for creating custom `401`, `404` and `500` error pages while providing the flexibility to allow you to implement more complex systems should you need it.
-
-
-## 404 Not found pages
-
-### Creating a 404 template
-
-The 404 template is implemented as a Preside Viewlet (see [[[viewlets]]) and a core implementation already exists. The name of the viewlet is configured in your application's Config.cfc with the `notFoundViewlet` setting. The default is "errors.notFound":
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
- settings.notFoundViewlet = "errors.notFound";
- }
-}
-```
-
-For simple cases, you will only need to override the `/errors/notFound` view by creating one in your application's view folder, e.g.
-
-```lucee
-
-
These are not the droids you are looking for
-
Some pithy remark.
-```
-
-#### Implementing handler logic
-
-If you wish to perform some handler logic for your 404 template, you can simply create the Errors.cfc handler file and implement the "notFound" action. For example:
-
-```luceescript
-// /application/handlers/Errors.cfc
-component {
-
- private string function notFound( event, rc, prc, args={} ) {
- event.setHTTPHeader( statusCode="404" );
- event.setHTTPHeader( name="X-Robots-Tag", value="noindex" );
-
- return renderView( view="/errors/notFound", args=args );
- }
-}
-```
-
-#### Defining a layout template
-
-The default layout template for the 404 is your site's default layout, i.e. "Main" (`/application/layouts/Main.cfm`). If you wish to configure a different default layout template for your 404 template, you can do so with the `notFoundLayout` configuration option, i.e.
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
-
- settings.notFoundLayout = "404Layout";
- settings.notFoundViewlet = "errors.my404Viewlet";
- }
-}
-```
-
-You can also programatically set the layout for your 404 template in your handler (you may wish to dynamically pick the layout depending on a number of variables):
-
-```luceescript
-// /application/handlers/Errors.cfc
-component {
-
- private string function notFound( event, rc, prc, args={} ) {
- event.setHTTPHeader( statusCode="404" );
- event.setHTTPHeader( name="X-Robots-Tag", value="noindex" );
- event.setLayout( "404Layout" );
-
- return renderView( view="/errors/notFound", args=args );
- }
-}
-```
-
-### Programatically responding with a 404
-
-If you ever need to programatically respond with a 404 status, you can use the `event.notFound()` method to do so. This method will ensure that the 404 statuscode header is set and will render your configured 404 template for you. For example:
-
-```luceescript
-// someHandler.cfc
-component {
-
- public void function index( event, rc, prc ) {
- prc.record = getModel( "someService" ).getRecord( rc.id ?: "" );
-
- if ( !prc.record.recordCount ) {
- event.notFound();
- }
-
- // .. carry on processing the page
- }
-}
-```
-
-### Direct access to the 404 template
-
-The 404 template can be directly accessed by visiting /404.html. This is achieved through a custom route dedicated to error pages (see [[routing]]).
-
-This is particular useful for rendering the 404 template in cases where Preside is not producing the 404. For example, you may be serving static assets directly through Tomcat and want to see the custom 404 template when one of these assets is missing. To do this, you would edit your `${catalina_home}/config/web.xml` file to define a rewrite URL for 404s:
-
-```xml
-
-
-
- index.cfm
-
-
-
- 404
- /404.html
-
-
-
-```
-
-Another example is producing 404 responses for secured areas of the application. In Preside's default urlrewrite.xml file (that works with Tuckey URL Rewrite), we block access to files such as Application.cfc by responding with a 404:
-
-```xml
-
- Block access to certain URLs
-
- All the following requests should not be allowed and should return with a 404:
-
- * the application folder (where all the logic and views for your site lives)
- * the uploads folder (should be configured to be somewhere else anyways)
- * this url rewrite file!
- * Application.cfc
-
- ^/(application/|uploads/|urlrewrite\.xml\b|Application\.cfc\b)
- 404
- /404.html
-
-```
-
-## 401 Access denied pages
-
-Access denied pages can be created and used in exactly the same way as 404 pages, with a few minor differences. The page can be invoked with `event.accessDenied( reason=deniedReason )` and will be automatically invoked by the core access control system when a user attempts to access pages and assets to which they do not have permission.
-
->>>>>> For a more in depth look at front end user permissioning and login, see [[websiteusersandpermissioning]].
-
-### Creating a 401 template
-
-The 401 template is implemented as a Preside Viewlet (see [[viewlets]]) and a core implementation already exists. The name of the viewlet is configured in your application's Config.cfc with the `accessDeniedViewlet` setting. The default is "errors.accessDenied":
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
- settings.accessDeniedViewlet = "errors.accessDenied";
- }
-}
-```
-
-The viewlet will be passed an `args.reason` argument that will be either `LOGIN_REQUIRED`, `INSUFFICIENT_PRIVILEGES` or any other codes that you might make use of.
-
-The core implementation sets the 401 header and then renders a different view, depending on the access denied reason:
-
-```luceescript
-// /preside/system/handlers/Errors.cfc
-component {
-
- private string function accessDenied( event, rc, prc, args={} ) {
- event.setHTTPHeader( statusCode="401" );
- event.setHTTPHeader( name="X-Robots-Tag" , value="noindex" );
- event.setHTTPHeader( name="WWW-Authenticate", value='Website realm="website"' );
-
- switch( args.reason ?: "" ){
- case "INSUFFICIENT_PRIVILEGES":
- return renderView( view="/errors/insufficientPrivileges", args=args );
- default:
- return renderView( view="/errors/loginRequired", args=args );
- }
- }
-}
-```
-
-For simple cases, you will only need to override the `/errors/insufficientPrivileges` and/or `/errors/loginRequired` view by creating them in your application's view folder, e.g.
-
-```lucee
-
-
Name's not on the door, you ain't coming in
-
Some pithy remark.
-```
-
-```lucee
-
-#renderViewlet( event="login.loginPage", message="LOGIN_REQUIRED" )#
-```
-
-#### Implementing handler logic
-
-If you wish to perform some handler logic for your 401 template, you can simply create the Errors.cfc handler file and implement the "accessDenied" action. For example:
-
-```luceescript
-// /application/handlers/Errors.cfc
-component {
- private string function accessDenied( event, rc, prc, args={} ) {
- event.setHTTPHeader( statusCode="401" );
- event.setHTTPHeader( name="X-Robots-Tag" , value="noindex" );
- event.setHTTPHeader( name="WWW-Authenticate", value='Website realm="website"' );
-
- switch( args.reason ?: "" ){
- case "INSUFFICIENT_PRIVILEGES":
- return renderView( view="/errors/my401View", args=args );
- case "MY_OWN_REASON":
- return renderView( view="/errors/custom401", args=args );
- default:
- return renderView( view="/errors/myLoginFormView", args=args );
- }
- }
-}
-```
-
-#### Defining a layout template
-
-The default layout template for the 401 is your site's default layout, i.e. "Main" (/application/layouts/Main.cfm). If you wish to configure a different default layout template for your 401 template, you can do so with the `accessDeniedLayout` configuration option, i.e.
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
-
- settings.accessDeniedLayout = "401Layout";
- settings.accessDeniedViewlet = "errors.my401Viewlet";
- }
-}
-```
-
-You can also programatically set the layout for your 401 template in your handler (you may wish to dynamically pick the layout depending on a number of variables):
-
-```luceescript
-// /application/handlers/Errors.cfc
-component {
- private string function accessDenied( event, rc, prc, args={} ) {
- event.setHTTPHeader( statusCode="401" );
- event.setHTTPHeader( name="X-Robots-Tag" , value="noindex" );
- event.setHTTPHeader( name="WWW-Authenticate", value='Website realm="website"' );
-
- event.setLayout( "myCustom401Layout" );
-
- // ... etc.
- }
-}
-```
-
-### Programatically responding with a 401
-
-If you ever need to programatically respond with a 401 access denied status, you can use the `event.accessDenied( reason="MY_REASON" )` method to do so. This method will ensure that the 401 statuscode header is set and will render your configured 401 template for you. For example:
-
-```luceescript
-// someHandler.cfc
-component {
-
- public void function reservePlace( event, rc, prc ) {
- if ( !isLoggedIn() ) {
- event.accessDenied( reason="LOGIN_REQUIRED" );
- }
- if ( !hasWebsitePermission( "events.reserveplace" ) ) {
- event.accessDenied( reason="INSUFFICIENT_PRIVILEGES" );
- }
-
- // .. carry on processing the page
- }
-}
-```
-
-## Choosing whether or not to redirect 404 and 401 pages
-
-In `10.10.13`, a feature flag was added to make 404 and 401 pages _redirect_ rather show inline (the default behaviour). To turn on the redirection feature, use the following in your `Config.cfc$configure()` method:
-
-```luceescript
-settings.features.redirectErrorPages.enabled = true;
-```
-
-## 500 Error Pages
-
-The implementation of 500 error pages is more straight forward than the 40x templates and involves only creating a flat `500.htm` file in your webroot. The reason behind this is that a server error may be caused by your site's layout code, or may even occur before Preside code is called at all; in which case the code to render your error template will not be available.
-
-If you do not create a `500.htm` in your webroot, Preside will use its own default template for errors. This can be found at `/preside/system/html/500.htm`.
-
-### Bypassing the error template
-
-In your local development environment, you will want to be able see the details of errors, rather than view a simple error message. This can be achieved with the config setting, `showErrors`:
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
-
- settings.showErrors = true;
- }
-}
-```
-
-In most cases however, you will not need to configure this for your local environment. Preside uses ColdBox's environment configuration to configure a "local" environment that already has `showErrors` set to **true** for you. If you wish to override that setting, you can do so by creating your own "local" environment function:
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
- }
-
- public void function local() {
- super.local();
-
- settings.showErrors = false;
- }
-}
-```
-
->>> Preside's built-in local environment configuration will map URLs like "mysite.local", "local.mysite", "localhost" and "127.0.0.1" to the "local" environment.
-
-## 503 Maintenance mode page
-
-The administrator interface provides a simple GUI for putting the site into maintenance mode (see figure below). This interface allows administrators to enter a custom title and message, turn maintenance mode on/off and also to supply custom settings to allow users to bypass maintenance mode.
-
-![Screenshot of maintenance mode management GUI](images/screenshots/maintenance_mode.png)
-
-### Creating a custom 503 page
-
-The 503 template is implemented as a Preside Viewlet (see [[viewlets]]) and a core implementation already exists. The name of the viewlet is configured in your application's Config.cfc with the `maintenanceModeViewlet` setting. The default is "errors.maintenanceMode":
-
-```luceescript
-// /application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // other settings...
- settings.maintenanceModeViewlet = "errors.maintenanceMode";
- }
-}
-```
-
-To create a custom template, you can choose either to provide your own viewlet by changing the config setting, or by overriding the view and/or handler of the `errors.maintenanceMode` viewlet.
-
-For example, in your site's `/application/views/errors/` folder, you could create a `maintenanceMode.cfm` file with the following:
-
-```html
-
-
-
-
-
-
- #args.title#
-
-
-
-
-
#args.title#
- #args.message#
-
-
-```
-
->>>>>> The maintenance mode viewlet needs to render the entire HTML of the page.
-
-### Manually clearing maintenance mode
-
-You may find yourself in a situation where you application is in maintenance mode and you have no means by which to access the admin because the password has been lost. In this case, you have two options:
-
-#### Method 1: Set bypass password directly in the database
-
-To find the current bypass password, you can query the database with:
-
-```sql
-select value
-from psys_system_config
-where category = 'maintenanceMode'
-and setting = 'bypass_password';
-```
-
-If the value does not exist, create it with:
-
-```sql
-insert into psys_system_config (id, category, setting, `value`, datecreated, datemodified)
-values( '{a unique id}', 'maintenancemode', 'bypass_password', '{new password}', now(), now() );
-```
-
-The bypass password can then be used by supplying it as a URL parameter to your site, e.g. `http://www.mysite.com/?thepassword`. From there, you should be able to login to the administrator and turn off maintenance mode.
-
-#### Method 2: Delete the maintenance mode file
-
-When maintenance mode is activated, a file is created at `/yoursite/application/config/.maintenance`. To clear maintenance mode, delete that file and restart the application.
----
-id: presideforms
-title: Forms system
----
-
-## Introduction
-
-Preside provides a built-in forms system which allows you to define user-input forms that can be used throughout the admin and in your application's front-end.
-
-Forms are defined using xml files that live under a `/forms` directory. A typical form definition file will look like this:
-
-```xml
-
-
-```
-
-An example admin render of a form with multiple tabs and fields might look like this:
-
-![Screenshot showing example of a rendered form in the admin](images/screenshots/formExample.png)
-
-### Referencing forms
-
-Forms are referenced relative to their location under the `/forms` directory of either your application or extension. Slashes in the relative path are replaced with dots (`.`) and the file extension is removed. For example:
-
-```luceescript
-// form definition location:
-/application/forms/eventsmanager/create.event.xml
-
-// form ID
-"eventsmanager.create.event"
-
-// example usage
-var formData = event.getCollectionForForm( "eventsmanager.create.event" );
-```
-
-
-## Further reading
-
-* [[presideforms-anatomy]]
-* [[presideforms-controls]]
-* [[presideforms-i18n]]
-* [[presideforms-rendering]]
-* [[presideforms-processing]]
-* [[presideforms-validation]]
-* [[presideforms-merging]]
-* [[presideforms-dynamic]]
-* [[presideforms-features]]
-* [[presideforms-permissioning]]
-* [[systemforms|Reference: System form definitions]]
-* [[systemformcontrols|Reference: System form controls]]
-
->>>> The Preside forms system is not to be confused with the [[formbuilder|Preside Form builder]]. The form builder is a system in which content editors can produce dynamically configured forms and insert them into content pages. The Preside Forms system is a system of programatically defining forms that can be used either in the admin interface or hard wired into the application's front end interfaces.
-
-
-
-
-
----
-id: presideforms-rendering
-title: Rendering Preside form definitions
----
-
-## Rendering Preside form definitions
-
-Preside form definitions are generally rendered using `renderForm()`, a global helper method that is a proxy to the [[formsservice-renderform]] method of the [[api-formsservice]]. A minimal example might look something like:
-
-```lucee
-
-```
-
-## Dynamic data
-
-A common requirement is for dynamic arguments to be passed to the rendering of forms. For example, you may wish to supply editorially driven form field labels to a statically defined form. **As of 10.8.0**, this can be achieved by passing the `additionalArgs` argument to the `renderForm()` method:
-
-```lucee
-
- additionalArgs = {
- fields = { firstname={ label=dynamicFirstnameLabel } }
- , fieldsets = { personal={ description=dynamicPersonalFieldsetDescription } }
- , tabs = { basic={ title=dynamicBasicTabTitle } }
- };
-
-
-
-```
-
-The `additionalArgs` structure expects `fields`, `fieldsets` and `tabs` keys (all optional). To add args for a specific field, add a key under the `fields` struct that matches the field _name_. For fieldsets and tabs, use the _id_ of the entity to match.
-
-## Rendering process and custom layouts
-
-When a form is rendered using the [[formsservice-renderform]] method, its output string is built from the bottom up. At the bottom level you have field controls, followed by field layout, fieldset layouts, tab layouts and finally a form layout.
-
-### Level 1: form control
-
-The renderer for each individual field's _form control_ is calculated by the field definition and context supplied to the [[formsservice-renderform]] method, see [[presideforms-controls]] for more details on how form controls are rendered.
-
-Each field is rendered using its control and the result of this render is passed to the field layout (level 2, below).
-
-### Level 2: field layout
-
-Each rendered field control is passed to a field layout (defaults to `formcontrols.layouts.field`). This layout is generally responsible for outputting the field label and any error message + surrounding HTML to enable the field control to be displayed correctly in the current page.
-
-The layout's viewlet is passed an `args.control` argument containing the rendered form control from "level 1" as well as any args defined on the field itself.
-
-An alternative field layout can be defined either directly in the form definition or in the [[formsservice-renderform]] method. See examples below
-
-```xml
-...
-
-
-
-...
-```
-
-```lucee
-
-#renderForm(
- formName = "events-management.signup"
- , context = "admin"
- , formId = "signup-form"
- , validationResult = rc.validationResult ?: ""
- , fieldLayout = "events-management.fieldLayout"
-)#
-```
-
-#### Example viewlet
-
-```lucee
-
-
- param name="args.control" type="string";
- param name="args.label" type="string";
- param name="args.help" type="string";
- param name="args.for" type="string";
- param name="args.error" type="string";
- param name="args.required" type="boolean";
-
- hasError = Len( Trim( args.error ) );
-
-
-
-
-
-
-
-
- #args.control#
-
-
-
#args.error#
-
-
-
-
-
-
-
-
-
-```
-
-### Level 3: Fieldset layout
-
-The fieldset layout viewlet is called for each fieldset in your form and is supplied with the following `args`:
-
-* `args.content` containing all the rendered fields for the fieldset
-* any args set directly on the fieldset element in the form definition
-
-The default fieldset layout viewlet is "formcontrols.layouts.fieldset". You can define a custom viewlet either on the fieldset directly or by passing the viewlet to the [[formsservice-renderform]] method.
-
-```xml
-
-
-...
-```
-
-```lucee
-
-#renderForm(
- formName = "events-management.signup"
- , context = "admin"
- , formId = "signup-form"
- , validationResult = rc.validationResult ?: ""
- , fieldsetLayout = "events-management.fieldsetLayout"
-)#
-```
-
-#### Example viewlet
-
-```lucee
-
-
-
-
-
-
-
-
-
-```
-
-### Level 4: Tab layout
-
-The tab layout viewlet is called for each tab in your form and is supplied with the following `args`:
-
-* an `args.content` argument containing all the rendered fieldsets for the tab
-* any args set directly on the tab element in the form definition
-
-The default tab layout viewlet is "formcontrols.layouts.tab". You can define a custom viewlet either on the tab directly or by passing the viewlet to the [[formsservice-renderform]] method.
-
-```xml
-
-
- ...
-
-...
-```
-
-```lucee
-
-#renderForm(
- formName = "events-management.signup"
- , context = "admin"
- , formId = "signup-form"
- , validationResult = rc.validationResult ?: ""
- , tabLayout = "events-management.tabLayout"
-)#
-```
-
-#### Example viewlet
-
-```lucee
-
-
- id = args.id ?: CreateUUId();
- active = args.active ?: false;
- description = args.description ?: "";
- content = args.content ?: "";
-
-
-
-
-
-
#description#
-
-
- #content#
-
-
-```
-
-### Level 4: Form layout
-
-The form layout viewlet is called once per form and is supplied with the following `args`:
-
-* an `args.content` argument containing all the rendered tabs for the form
-* an `args.tabs` array of tabs for the form (can be used to render the tabs header for example)
-* an `args.validationJs` argument containing validation JS string
-* an `args.formId` argument, this will be the same argument passed to the [[formsservice-renderform]] method
-* any args set directly on the form element in the form definition
-
-The default form layout viewlet is "formcontrols.layouts.form". You can define a custom viewlet either on the form directly or by passing the viewlet to the [[formsservice-renderform]] method.
-
-```xml
-
-
-```
-
-```lucee
-
-#renderForm(
- formName = "events-management.signup"
- , context = "admin"
- , formId = "signup-form"
- , validationResult = rc.validationResult ?: ""
- , formLayout = "events-management.formLayout"
-)#
-```
-
-#### Example viewlet
-
-```lucee
-
-
- tabs = args.tabs ?: [];
- content = args.content ?: "";
- validationJs = args.validationJs ?: "";
- formId = args.formId ?: "";
-
-
-
-
-
-
-
-
-
- ( function( $ ){
- $('###formId#').validate( #validationJs# );
- } )( presideJQuery );
-
-
-
-
-```---
-id: presideforms-controls
-title: Preside form controls
----
-
-## Preside form controls
-
-Form controls are named [[viewlets|viewlets]] that are used for rendering form fields with the [[presideforms|Preside forms system]]. All form controls are implemented as viewlets whose path follows the convention `formcontrols.{nameofcontrol}.{renderercontext}`.
-
-For a full reference list of core form controls, see [[systemformcontrols]].
-
-### Renderer context
-
-The _renderer context_ is a string value passed to the `renderForm()` method (see [[presideforms-rendering]]). The purpose of this is to allow form controls to have different viewlets for different contexts; i.e. an "admin" context for rendering controls in the admin vs a "website" context for rendering controls in the front end of your application.
-
-At a bare minimum, form controls should implement a default "index" context for when there is no special renderer for specific contexts passed to `renderForm()`.
-
-### Arguments
-
-The `args` struct passed to your form control's viewlet will be a combination of:
-
-* All attributes defined on the associated form `field` definition
-* A `defaultValue` string that will be either the previously saved value for the field if there is one, _or_ the value of the `default` attribute set on the field definition
-* An `error` string, populated if there are validation errors
-* A `savedData` structure representing any saved data for the entire form
-* A `layout` string that contains the viewlet that will be used to render the layout around the form control (this viewlet will usually take care of error messages and field labels, etc. see [[presideforms-rendering]])
-
-### Examples
-
-#### Simple textinput
-
-A simple 'textinput' form control implemented as just a view (a viewlet without a handler) and with just a default "index" context:
-
-```lucee
-
-
- inputName = args.name ?: "";
- inputId = args.id ?: "";
- inputClass = args.class ?: "";
- defaultValue = args.defaultValue ?: "";
- placeholder = args.placeholder ?: "";
- placeholder = HtmlEditFormat( translateResource( uri=placeholder, defaultValue=placeholder ) );
-
- value = event.getValue( name=inputName, defaultValue=defaultValue );
- if ( not IsSimpleValue( value ) ) {
- value = "";
- }
-
- value = HtmlEditFormat( value );
-
-
-
-
-
-```
-
-#### Select with custom datasource
-
-This example uses a handler based viewlet to retrieve data from a service with which to populate the standard `select` form control. The form control name is `derivativePicker`:
-
-
-```luceescript
-// /handlers/formcontrols/DerivativePicker.cfc
-component {
- property name="assetManagerService" inject="assetManagerService";
-
- public string function index( event, rc, prc, args={} ) {
- var derivatives = assetManagerService.listEditorDerivatives();
-
- if ( !derivatives.len() ) {
- return ""; // do not render the control at all if no derivatives
- }
-
- // translate derivatives into labels and values for select control
- // including default 'none' derivative for picker
- args.labels = [ translateResource( "derivatives:none.title" ) ];
- args.values = [ "none" ];
- args.extraClasses = "derivative-select-option";
-
- for( var derivative in derivatives ) {
- args.values.append( derivative );
- args.labels.append( translateResource( uri="derivatives:#derivative#.title", defaultValue="derivatives:#derivative#.title" ) );
- }
-
- // render default select control using labels and values
- // calculated above
- return renderView( view="formcontrols/select/index", args=args );
- }
-}
-```---
-id: presideforms-merging
-title: Merging Preside form definitions
----
-
-## Merging Preside form definitions
-
-The [[presideforms]] provides logic for merging form definitions. This is used in three ways:
-
-* Extending form definitions
-* Automatic merging of forms that match the same form ID but live in different locations (i.e. core, extensions, your application and site templates)
-* Manual merging of multiple form definitions. For example, site tree page forms are merged from the core page form and form definitions for the page type of the page
-
-## Extending form definitions
-
-Forms can extend one another by using the `extends` attribute. The child form can then make modifications and additions to elements in its parent. For example:
-
-```xml
-
-
-```
-
-## Automatic merging
-
-One of the key features of Preside is the ability to augment and override features defined in the core and in extensions. The forms system is no different and allows any form definition to be modified by extensions, your application and by site templates.
-
-To modify an existing form definition, you must create a corresponding file under your application or extension's `/forms` directory. For example, if you wanted to modify the core [[form-assetaddform]] that lives at `/forms/preside-objects/asset/admin.add.xml`, you would create an xml file at `/application/forms/preside-objects/asset/admin.add.xml` within your application.
-
-All form definitions that match by relative path will be merged to create a single definition.
-
-## Manual merging
-
-The [[api-formsservice]] provides several methods for dealing with combined form definitions. The key methods are:
-
-* [[formsservice-mergeForms]], merges two forms and returns merged definition
-* [[formsservice-getMergedFormName]], returns the registered name of two merged forms and optionally performs the merge if the merge has not already been made
-
-## Merging techniques
-
-### Adding form elements
-
-Form elements can be added simply by defining distinct elements in the secondary form. For example:
-
-```xml
-
-
-
-```
-
-```xml
-
-
-
-```
-
-### Modifying existing elements
-
-Tabs, fieldsets and fields that already exist in the primary form can be modified by defining elements that match `id` (fieldsets and tabs) or `name` (fields) and then defining new or different attributes. For example:
-
-```xml
-
-
-
-```
-
-```xml
-
-
-
-```
-
-### Deleting elements
-
-Elements that exist in the primary form definition can be deleted from the definition by adding a `deleted="true"` flag to element in the secondary form. For example:
-
-
-```xml
-
-
-
-```
-
-```xml
-
-
-
-```---
-id: presideforms-i18n
-title: Preside form definitions and i18n
----
-
-## Preside form definitions and i18n
-
-Labels, help and placeholders for form controls, tabs and fieldsets can all be supplied through i18n properties files using Preside's [[i18n|i18n]] system. Resource URIs can be supplied either directly in your form definitions or by using convention combined with the `i18nBaseUri` attribute on your `form` elements (see [[presideforms-anatomy]]).
-
-```xml
-
-
-```
-
-## Convention based i18n URIs
-
-### Tabs
-
-Tabs can have translatable titles, descriptions and icon classes. Convention is as follows:
-
-* **Title:** `{i18nBaseUri}`tab.`{id}`.title
-* **Description:** `{i18nBaseUri}`tab.`{id}`.description
-* **Icon class:** `{i18nBaseUri}`tab.`{id}`.iconClass
-
-For example, given the form definition below, the following i18n properties file definition will supply title, description and icon class by convention:
-
-```xml
-
-
-```
-
-```properties
-# /i18n/system-config/mailchimp.properties
-tab.credentials.title=Credentials
-tab.credentials.description=Supply your API credentials to connect with your MailChimp account
-tab.credentials.iconClass=fa-key
-```
-
-### Fieldsets
-
-Fieldsets can have translatable titles and descriptions. Convention is as follows:
-
-* **Title:** `{i18nBaseUri}`fieldset.`{id}`.title
-* **Description:** `{i18nBaseUri}`fieldset.`{id}`.description
-
-For example, given the form definition below, the following i18n properties file definition will supply title and description of the fieldset by convention:
-
-```xml
-
-
-```
-
-```properties
-# /i18n/system-config/mailchimp.properties
-fieldset.credentials.title=Credentials
-fieldset.credentials.description=Supply your API credentials to connect with your MailChimp account
-```
-
-### Fields
-
-
-Fields can have translatable labels, help and, for certain controls, placeholders. Convention is as follows:
-
-* **Label:** `{i18nBaseUri}`field.`{name}`.title
-* **Help:** `{i18nBaseUri}`field.`{name}`.help
-* **Placeholder:** `{i18nBaseUri}`field.`{name}`.placeholder
-
-For example, given the form definition below, the following i18n properties file definition will supply label, placeholder and help text:
-
-```xml
-
-
-```
-
-```properties
-# /i18n/event-management/session-form.properties
-field.session_title.title=Session title
-field.session_title.placeholder=e.g. 'Coffee and code'
-field.session_title.help=Title for your session, will be displayed in public event listing pages
-```
-
-## Page types and Preside objects
-
-Forms for page types and preside objects will have a _default_ `i18nBaseUri` set for them:
-
-* **Page types:** page-types.`{pagetype}`:
-* **Preside objects:** preside-objects.`{objectname}`:
----
-id: presideforms-processing
-title: Processing Preside form definitions
----
-
-## Processing Preside form definitions
-
-Once an HTML form has been submitted that contains one or more instances of Preside form definitions, you will likely want to process that submitted data. A typical example follows:
-
-```luceescript
-public void function myHandlerAction( event, rc, prc ) {
- var formName = "my.form.definition";
- var formData = event.getCollectionForForm( formName );
- var validationResult = validateForm( formName, formData );
-
- if ( !validationResult.validated() ) {
- var persist = formData;
- persist.validationResult = validationResult;
-
- setNextEvent(
- url = myEditViewUrl
- , persistStruct = persist
- );
- }
-}
-```
-
-## Getting data from the request
-
-It can be useful to get a structure of data from the request (i.e. the ColdBox `rc` scope) that contains purely the fields for your form. The `event.getCollectionForForm()` helper method is there for that purpose.
-
-The helper can be called in two ways:
-
-```luceescript
-// 1. No arguments - system will detect the preside
-// form(s) that have been submitted and get the data
-// for those
-var formData = event.getCollectionForForm();
-
-// 2. Supplied form name
-var formData = event.getCollectionForForm( "my.form.definition" );
-```
-
-As well as filtering out the request data, the method will also ensure that each field in the form definition exists. If the field was not in the submitted request (for example, a checkbox was left unticked), the field will be defaulted as an empty string.
-
-## Getting the form(s) that were submitted
-
-In usual circumstances, you will know the ID of the form that has been submitted. You may, however, find yourself in a situation where you have multiple dynamic form definitions creating a single HTML form and being submitted. In this scenario, you can use the `event.getSubmittedPresideForms()` method. For example:
-
-```luceescript
-// event.getSubmittedPresideForms(): returns array of
-// submitted form names
-var formNames = event.getSubmittedPresideForms();
-var formData = {};
-
-for( var formName in formNames ) {
- formData[ formName ] = event.getCollectionForForm( formName );
-}
-```
-
-## Validating submissions
-
-There are two helper methods that you can use to quickly validate a submission, `validateForm()` and `validateForms()`. The first method is a proxy to the [[formsservice-validateform]] method of the [[api-formsservice]], the second is a helper to validate multiple forms at once. e.g.
-
-```luceescript
-// example one - explicit
-var formName = "my.form";
-var formData = event.getCollectionForForm( formName );
-var validationResult = validateForm( formName, formData );
-
-// example two - multiple dynamic forms
-// the following validates all forms that were
-// submitted
-var validationResult = validateForms();
-```
-
-See [[presideforms-validation]] for more details of how the [[validation-framework]] is integrated with the form system.
-
-
-## Auto-trimming submitted values
-
-As of 10.11.0, it is possible to configure form submissions so all data returned by `event.getCollectionForForm()` is automatically stripped of leading and trailing whitespace. Application-wide configuration is set in `Config.cfc`:
-
-```luceescript
-// default settings in core Config.cfc
-settings.autoTrimFormSubmissions = { admin=false, frontend=false };
-```
-
-By default, this is turned off for both admin and front-end applications, to maintain the existing behaviour. However, you can enable these in your own application's `Config.cfc`:
-
-```luceescript
-// This will auto-trim all submissions via the front-end of the website
-settings.autoTrimFormSubmissions.frontend = true;
-```
-
-Your application can also override these settings on an individual basis, by specifying an `autoTrim` argument to `event.getCollectionForForm()`. For example:
-
-```luceescript
-var formData = event.getCollectionForForm( formName="my.form", autoTrim=true );
-```
-
-This will auto-trim the submitted data, even if the application default is not to do so. The reverse also applies: you may set `autoTrim=false` even if it is turned on for the application as a whole.
-
-Finally, you can configure this on a per-property basis, either in your object definition or in your form definition. A property with an `autoTrim` setting will *always* obey that setting, regardless of what is defined in the application or in `event.getCollectionForForm()`. For example:
-
-```luceescript
-component {
- property name="a_field_with_preserved_spaces" type="string" dbtype="varchar" autoTrim=false;
-}
-```
-
-or:
-
-```xml
-
-
-```---
-id: presideforms-features
-title: Restricting Preside form elements by feature
----
-
-## Restricting Preside form elements by feature
-
-Preside has a concept of features that are configurable in your application's `Config.cfc`. Features can be enabled and disabled for your entire application, or individual site templates. This can be useful for turning off core features, or features in extensions.
-
-In the Preside forms system, you can tag your forms, tabs, fieldsets and fields with feature names so that those elements are removed from the form definition when the feature is disabled.
-
-### Examples
-
-Tag an entire form with a feature ("cms"). If the feature is turned off, the entire form will be removed from the library of forms in the system:
-
-```xml
-
-
-```
-
-Remove a _tab_ in a form when the "websiteusers" feature is disabled:
-
-```xml
-
-
-```
-
-
-Remove a _fieldset_ in a form when the "websiteusers" feature is disabled:
-
-```xml
-
-
-```
-
-Remove a _field_ in a form when the "websiteusers" feature is disabled:
-
-```xml
-
-
-```---
-id: presideforms-presideobjects
-title: Using Preside data objects with form definitions
----
-
-## Using Preside data objects with form definitions
-
-### Field bindings
-
-The `binding` attribute on field definitions allows you to pull in attributes and i18n defaults from preside object properties:
-
-```xml
-
-```
-
-In the example above, the field's definition will be taken from the `title` property of the `page` object (CFC file). A default [[presideforms-controls|form control]] will be assigned to the field based on the property type and other attributes. The title, help and placeholder will be defaulted to `preside-objects.page:field.title.title`, `preside-objects.page:field.title.help` and `preside-objects.page:field.title.placeholder`.
-
-### Default forms
-
-If you attempt to make use of a form that does not have an XML definition and whose name starts with "preside-objects.name_of_object.", a default form will be returned based on the preside object CFC file (in this case, "name_of_object").
-
-For example, if there is no `/forms/preside-objects/blog_category/admin.add.xml` file defined and we do something like the call below, an automatic form definition will be used based on the `blog_category` preside object:
-
-```luceescript
-renderForm( ... formName="preside-objects.blog_category.admin.add", ... );
-```
-
-A notable use of this convention is in the Data Manager where you can create simple object definitions and just use their default form for adding and editing records.
----
-id: presideforms-dynamic
-title: Dynamically generating Preside form definitions
----
-
-## Dynamically generating Preside form definitions
-
-As of Preside v10.6.0, the [[api-formsservice]] provides a [[formsservice-createform]] method for dynamically creating forms without the need for an XML definition file. This can be useful in scenarios where the form can take on many different fields that will differ depending on the current user context.
-
-Example usage:
-
-```luceescript
-var newFormName = formsService.createForm( function( formDefinition ){
-
- formDefinition.setAttributes(
- i18nBaseUri = "forms.myform:"
- );
-
- formDefinition.addField(
- tab = "default"
- , fieldset = "default"
- , name = "title"
- , control = "textinput"
- , maxLength = 100
- , required = true
- );
-
- formDefinition.addField(
- tab = "default"
- , fieldset = "default"
- , name = "body"
- , control = "richeditor"
- , required = true
- );
-
-} );
-```
-
-As seen in the example above, the method works by supplying a closure that takes a [[api-formdefinition]] object as its argument. You can then use the [[api-formdefinition]] object to build your form definition (see [[api-formdefinition]] for full API documentation).
-
-## Extending existing forms
-
-As well as creating forms from scratch, you can also extend an existing form by supplying the `basedOn` argument:
-
-```luceescript
-var newFormName = formsService.createForm( basedOn="existing.form", generator=function( formDefinition ){
-
- formDefinition.addField(
- tab = "default"
- , fieldset = "default"
- , name = "title"
- , control = "textinput"
- , maxLength = 100
- , required = true
- );
-
- // ...
-} );
-```
-
-## Specifying a form name
-
-By default, a form name will be generated for you and returned. If you wish, however, you can supply your own form name for the dynamically generated form:
-
-```luceescript
-formsService.createForm( basedOn="existing.form", formName="my.new.form", generator=function( formDefinition ){
-
- formDefinition.addField(
- tab = "default"
- , fieldset = "default"
- , name = "title"
- , control = "textinput"
- , maxLength = 100
- , required = true
- );
-
- // ...
-} );
-```
-
->>>> Be careful when specifying a form name. Should two dynamically generated forms share the same name but have different form definitions, you will run into problems. Form names should be unique per distinct definition.---
-id: presideforms-permissioning
-title: Restricting Preside form elements by permission key
----
-
-## Restricting Preside form elements by permission key
-
-As of Preside 10.8.0, the forms system allows you to restrict individual `field`, `fieldset` and `tab` elements by an _admin_ **permission key** (see [[cmspermissioning]] for full details of the admin permissioning system). Simply tag your element with a `permissionKey` attribute to indicate the permission key that controls access to the `field`/`fieldset`/`tab`.
-
-```xml
-
-
-```
-
-### Context permissions
-
-If you are building a custom admin area and you are rendering and validating forms with permissions that are _context aware_ (see [[cmspermissioning]]), you can supply the context and context keys to the various methods for interacting with forms to ensure that the correct permissions are applied. For example:
-
-```lucee
-#renderForm(
- formName = "my.form"
- , permissionContext = "myContext"
- , permissionContextKeys = [ contextId ]
- // , ...
-)#
-```
-
-```luceescript
-var formData = event.getCollectionForForm(
- formName = "my.form"
- , permissionContext = "myContext"
- , permissionContextKeys = [ contextId ]
-);
-var validationResult = validateForm(
- formName = "my.form"
- , formData = formData
- , permissionContext = "myContext"
- , permissionContextKeys = [ contextId ]
-);
-```
-
->>> If you are unsure what context permissions mean, then you probably don't need to worry about them for getting your form permissions to work. The default settings will work well for any situation where you have not created any custom logic for context aware permissioning.---
-id: presideforms-validation
-title: Preside form validation
----
-
-## Preside form validation
-
-The [[presideforms]] integrates with the [[validation-framework]] to provide automatic *validation rulesets* for your preside form definitions and API methods to quickly and easily validate a submitted form (see [[presideforms-processing]]).
-
-The validation rulesets are generated in two ways:
-
-1. Common attributes on fields that lead to validation rules, e.g. `required`, `maxLength`, etc.
-2. Explicit validation rules defined on fields
-
-## Common attributes
-
-The following attributes on field definitions will lead to automatic validation rules being defined for the field. Remember also that any attributes defined on a preside object property will be pulled into a field definition when using ``.
-
-### required
-
-Any field with a `required="true"` flag will automatically have a `required` validator added to the forms ruleset.
-
-### minLength
-
-Any field with a numeric `minLength` attribute will automatically have a `minLength` validator added to the forms ruleset. If the field has both `minLength` and `maxLength`, it will instead have a `rangeLength` validator added.
-
-### maxLength
-
-Any field with a numeric `maxLength` attribute will automatically have a `maxLength` validator added to the forms ruleset. If the field has both `minLength` and `maxLength`, it will instead have a `rangeLength` validator added.
-
-### minValue
-
-Any field with a numeric `minValue` attribute will automatically have a `min` validator added to the forms ruleset. If the field has both `maxValue` and `minValue`, it will instead have a `range` validator added.
-
-### maxValue
-
-Any field with a numeric `maxValue` attribute will automatically have a `max` validator added to the forms ruleset. If the field has both `minValue` and `maxValue`, it will instead have a `range` validator added.
-
-### format
-
-If a string field has a `format` attribute, a pattern matching validation rule will be added.
-
-### type
-
-For preside object properties that are mapped to form fields, the data type will potentially have an associated validation rule that will be added for the field. For example, date fields will get a valid `date` validator.
-
-### uniqueindexes
-
-For preside object properties that are mapped to form fields and that define unique indexes, a `presideObjectUniqueIndex` validator will be automatically added. This validator is server-side only and ensure that the value in the field is unique and will not break the unique index constraint.
-
-### passwordPolicyContext
-
-If a password field has a `passwordPolicyContext` attribute, the field will validate against the given password policy. Current supported contexts are `website` and `admin`.
-
-## Explicit validation rules
-
-Explicit validation rules can be set on a field with the following syntax:
-
-```xml
-
-
-
-
-
-```
-
-Each rule must specify a `validator` attribute that matches a registered [[validation-framework]] validator. An optional `message` attribute can also be supplied and this can be either a plain string message, or [[i18n]] resource URI for translation.
-
-Any configuration parameters for the ruleset are then defined in child `param` tags that always have `name` and `value` attributes.---
-id: presideforms-anatomy
-title: Anatomy of a Preside form definition file
----
-
-## Anatomy of a Preside form definition file
-
-### Form element
-
-All forms must have a root `form` element that contains one or more `tab` elements.
-
-```xml
-
-
-```
-
-#### Attributes
-
-
-
-
-
-
i18nBaseUri (optional)
-
Base i18n resource URI to be used when calculating field labels, tab titles, etc. using convention. For example, "my.form:" would lead to URIs such as "my.form:tab.basic.title", etc.
-
-
-
tabsPlacement (optional)
-
Placement of the tabs UI in the admin. Valid values are: left, right, below and top (default)
-
-
-
extends (optional)
-
ID of another form whose definition this form should inherit and extend. See [[presideforms-merging]] for more details.
-
-
-
-
-
-
-### Tab element
-
-The tab element defines a tab pane. In the admin interface, tabs will appear using a twitter bootstrap tabs UI; how tabs appear in your application's front end is up to you. All forms must have at least one tab element; a form with only a single tab will be displayed without any tabs UI.
-
-A tab element must contain one or more `fieldset` elements.
-
-```xml
-
-
-```
-
-#### Attributes
-
-All attributes below are optional, although `id` is strongly advised. `title` and `description` attributes can be left out and defined using convention in i18n `.properties` file (see the `i18nBaseUri` form attribute above).
-
-
-
-
-
-
id
-
A unique identifier value for the tab, e.g. "standard"
-
-
-
sortorder
-
A value to determine the order in which the tab will be displayed. The lower the number, the earlier the tab will be displayed.
-
-
-
title
-
A value that will be used for the tab title text. If not supplied, this will default to {i18nBaseUrl}tab.{tabID}.title (see [[presideforms-i18n]] for more details).
-
-
-
iconClass
-
Class to use to render an icon for the tab, e.g. "fa-calendar" (we use Font Awesome for icons). If not supplied, this will default to {i18nBaseUrl}tab.{tabID}.iconClass (see [[presideforms-i18n]] for more details).
-
-
-
decription
-
A value that will be used for the tab and generally output within the tab content section. If not supplied, this will default to {i18nBaseUrl}tab.{tabID}.description (see [[presideforms-i18n]] for more details).
-
-
-
-
-
-### Fieldset elements
-
-A fieldset element can be used to group associated form elements together and for providing some visual indication of that grouping.
-
-A fieldset must contain one or more `field` elements.
-
-```xml
-
-
-```
-
-#### Attributes
-
-
-
-
-
-
id
-
A unique identifier value for the fieldset, e.g. "main"
-
-
-
title
-
A value or i18n resource URI that will be used for the fieldset title text. If not supplied, this will default to {i18nBaseUrl}fieldset.{fieldsetID}.title (see [[presideforms-i18n]] for more details).
-
-
-
decription
-
A value or i18n resource URI that will be used for the fieldsets description that will be displayed before any form fields in the fieldset. If not supplied, this will default to {i18nBaseUrl}fieldset.{fieldsetID}.description (see [[presideforms-i18n]] for more details).
-
-
-
sortorder
-
A value to determine the order in which the fieldset will be displayed within the parent tab. The lower the number, the earlier the fieldset will be displayed.
-
-
-
-
-
-### Field elements
-
-`Field` elements define an input field for your form. The attributes required for the field will vary depending on the form control defined (see [[presideforms-controls]]).
-
-A `field` element can have zero or more `rule` child elements for defining customized validation rules.
-
-```xml
-
-
-```
-
-#### Attributes
-
-
-
-
-
-
name
-
Unique name of the form field. Required if binding is not used.
-
-
-
binding
-
Defines a preside object property from which to derive the field definition. Required if name is not used. See [[presideforms-presideobjects]] for further details.
-
-
-
control
-
Form control to use for the field (see [[presideforms-controls]]). If not supplied and a preside object property binding is defined, then the system will automatically select the appropriate control for the field. If not supplied and no binding is defined, then a default of "textinput" will be used.
-
-
-
label
-
A label for the field. If not supplied, this will default to {i18nBaseUrl}field.{fieldName}.title (see [[presideforms-i18n]] for more details).
-
-
-
placeholder
-
Placeholder text for the field. Relevant for form controls that use a placeholder (text inputs and textareas). If not supplied, this will default to {i18nBaseUrl}field.{fieldName}.placeholder (see [[presideforms-i18n]] for more details).
-
-
-
help
-
Help text to be displayed in help tooltip for the field. If not supplied, this will default to {i18nBaseUrl}field.{fieldName}.help (see [[presideforms-i18n]] for more details).
-
-
-
sortorder
-
A value to determine the order in which the field will be displayed within the parent fieldset. The lower the number, the earlier the field will be displayed.
-
-
-
-
-
-### Rule elements
-
-A `rule` element must live beneath a `field` element and can contain zero or more `param` attributes. A rule represents a validation rule and deeply integrates with the [[validation-framework]]. See [[presideforms-validation]] for full details of validation with preside forms.
-
-```xml
-```xml
-
-
-```
-
-Param elements consist of a name and value pair and will differ for each validator.
-
-#### Attributes
-
-
-
-
-
-
validator
-
ID of the validator to use (see [[validation-framework]] for full details on validators)
-
-
-
message
-
Message to display for validation errors. Can be an i18n resource URI for translatable validation messages.
-
-
-
-
---
-id: cmspermissioning
-title: CMS permissioning
----
-
-## Overview
-
-CMS Permissioning is split into three distinct concepts in Preside:
-
-### Permissions and roles
-
-These are defined in configuration and are not editable through the CMS GUI.
-
-* **Permissions** allow you to grant or deny access to a particular action
-* **Roles** provide convenient grant access to one or more permissions
-
-### Users and groups
-
-Users and groups are defined through the administrative GUI and are stored in the database.
-
-* An *active* **user** must belong to one or more groups
-* A **group** must have one or more *roles*
-
-Permissions are granted to a user through the roles that are associated with the groups that she belongs to.
-
-### Contextual permissions
-
-Contextual permissions are fine grained permissions implemented specifically for any given area of the CMS that requires them.
-
-For example, you could deny the "*Freelancers*" user group the "*Add pages*" permission for a particular page and its children in the sitetree; in this case, the context is the ID of the page.
-
-Contextual permissions are granted or denied to user **groups** and always take precedence over permissions granted through groups and roles.
-
->>> If a feature of the CMS requires context permissions, it must supply its own views and handlers for managing them. Preside helps you out here with a viewlet and action handler for some common UI and saving logic, see 'Rolling out Context Permission GUIs', below.
-
-## Configuring permissions and roles
-
-Permissions and roles are configured in your site or extension's `Config.cfc` file. An example configuration might look like this:
-
-```luceescript
-
-public void function configure() {
- super.configure();
-
-// PERMISSIONS
- // here we define a feature, "analytics dashboard" with a number of permissions
- settings.adminPermissions.analyticsdashboard = [ "navigate", "share", "configure" ];
-
- // features can be organised into sub-features to any depth, here
- // we have a depth of two, i.e. "eventmanagement.events"
- settings.adminPermissions.eventmanagement = {
- events = [ "navigate", "view", "add", "edit", "delete" ]
- , prices = [ "navigate", "view", "add", "edit", "delete" ]
- };
-
- // The settings above will translate to the following permission keys being
- // available for use in your Railo code, i.e. if ( hasCmsPermission( userId, permissionKey ) ) {...}:
- //
- // analyticsdashboard.navigate
- // analyticsdashboard.share
- // analyticsdashboard.configure
- //
- // eventmanagement.events.navigate
- // eventmanagement.events.view
- // eventmanagement.events.add
- // eventmanagement.events.edit
- // eventmanagement.events.delete
- //
- // eventmanagement.prices.navigate
- // eventmanagement.prices.view
- // eventmanagement.prices.add
- // eventmanagement.prices.edit
- // eventmanagement.prices.delete
-
-// ROLES
- // roles are simply a named array of permission keys
- // permission keys for roles can be defined with wildcards (*)
- // and can be excluded with the ! character:
-
- // define a new role, with all event management perms except for delete
- settings.adminRoles.eventsOrganiser = [ "eventmanagement.*", "!*.delete" ];
-
- // another new role specifically for analytics viewing
- settings.roles.analyticsViewer = [ "analyticsdashboard.navigate", "analyticsdashboard.share" ];
-
- // add some new permissions to some existing core roles
- settings.adminRoles.administrator = settings.roles.administrator ?: [];
- settings.adminRoles.administrator.append( "eventmanagement.*" );
- settings.adminRoles.administrator.append( "analyticsdashboard.*" );
-
- settings.adminRoles.someRole = settings.roles.someRole ?: [];
-```
-
-### Defining names and descriptions (i18n)
-
-Names and descriptions for your roles and permissions must be defined in i18n resource bundles.
-
-For roles, you should add *name* and *description* keys for each role to the `/i18n/roles.properties` file, e.g.
-
-```properties
-eventsOrganiser.title=Events organiser
-eventsOrganiser.description=The event organiser role grants aspects to all aspects of event management in the CMS except for deleting records (which must be done by the administrator)
-
-analyticsViewer.title=Analytics viewer
-analyticsViewer.description=The analytics viewer role grants permission to view statistics in the analytics dashboard
-```
-
-As of **10.24.0**, you can group your roles. Grouping are defined as `{your role}.group=value` and `roleGroup.{your role group}.title=Label`. For example:
-
-```properties
-roleGroup.event.title=Event
-
-eventsOrganiser.group=event
-```
-
-For permissions, add your keys to the `/i18n/permissions.properties` file, e.g.
-
-
-```properties
-eventmanagement.events.navigate.title=Events management navigation
-eventmanagement.events.navigate.description=View events management navigation links
-
-eventmanagement.events.view=title=View events
-eventmanagement.events.view=description=View details of events that have been entered into the system
-```
-
->>> For permissions, you may only want to create resource bundle entries when the permissions will be used in contextual permission GUIs. Otherwise, the translations will never be used.
-
-## Applying permissions in code with hasCmsPermission()
-
-When you wish to permission control a given system feature, you should use the `hasCmsPermission()` method. For example:
-
-```luceescript
-// a general permission check
-if ( !hasCmsPermission( permissionKey="eventmanagement.events.navigate" ) ) {
- event.adminAccessDenied(); // this is a preside request context helper
-}
-
-// a contextual permission check. In this case:
-// "do we have permission to add folders to the asset folder with id [idOfCurrentFolder]"
-if ( !hasCmsPermission( permissionKey="assetManager.folders.add", context="assetmanagerfolders", contextKeys=[ idOfCurrentFolder ] ) ) {
- event.adminAccessDenied(); // this is a preside request context helper
-}
-```
-
->>> The `hasCmsPermission()` method has been implemented as a ColdBox helper method and is available to all your handlers and views. If you wish to access the method from your services, you can access it via the `permissionService` service object, the core implementation of which can be found at `/preside/system/api/security/PermissionService.cfc`.
-
-## Rolling out Context Permission GUIs
-
-Should a feature you are developing for the admin require contextual permissions management, you can make use of a viewlet helper to give you a visual form and handler code to manage them.
-
-For example, if we want to be able to manage permissions on event management *per* event, we might have a view at `/views/admin/events/managePermissions.cfm`, that contained the following code:
-
-```lucee
-#renderViewlet( event="admin.permissions.contextPermsForm", args={
- permissionKeys = [ "eventmanagement.events.*", "!*.managePerms" ]
- , context = "eventmanager"
- , contextKey = eventId
- , saveAction = event.buildAdminLink( linkTo="events.saveEventPermissionsAction", querystring="id=#eventId#" )
- , cancelAction = event.buildAdminLink( linkTo="events.viewEvent", querystring="id=#eventId#" )
-} )#
-```
-
-Our `admin.events.saveEventPermissionsAction` handler action might then look like this:
-
-```luceescript
-function saveEventPermissionsAction( event, rc, prc ) {
- var eventId = rc.id ?: "";
-
- // check that we are allowed to manage the permissions of this event, or events in general ;)
- if ( !hasCmsPermission( permissionKey="eventmanager.events.manageContextPerms", context="eventmanager", contextKeys=[ eventId ] ) ) {
- event.adminAccessDenied();
- }
-
- // run the core 'admin.Permissions.saveContextPermsAction' event
- // this will save the permissioning configured in the
- // 'admin.permissions.contextPermsForm' form
- var success = runEvent( event="admin.Permissions.saveContextPermsAction", private=true );
-
- // redirect the user and present them with appropriate message
- if ( success ) {
- messageBox.info( translateResource( uri="cms:eventmanager.permsSaved.confirmation" ) );
- setNextEvent( url=event.buildAdminLink( linkTo="eventmanager.viewEvent", queryString="id=#eventId#" ) );
- }
-
- messageBox.error( translateResource( uri="cms:eventmanager.permsSaved.error" ) );
- setNextEvent( url=event.buildAdminLink( linkTo="events.managePermissions", queryString="id=#eventId#" ) );
-}
-```
-
-## System users
-
-Users that are defined as **system users** are exempt from all permission checking. In effect, they are granted access to **everything**. This concept exists to enable web agencies to manage every aspect of a site while setting up more secure access for their clients.
-
-System users are only configurable through your site's `Config.cfc` file as a comma separated list of login ids. The default value of this setting is 'sysadmin'. For example, in your site's Config.cfc, you might have:
-
-```luceescript
- public void function configure() {
- super.configure();
-
- // ...
-
- settings.system_users = "sysadmin,developer"; // both the 'developer' and 'sysadmin' users are now defined as system users
- }
-```
----
-id: sessionmanagement
-title: Session management and stateless requests
----
-
-# Session management
-
-All session management in the core platform is handled by the [SessionStorage ColdBox plugin](http://wiki.coldbox.org/wiki/Plugins:SessionStorage.cfm). Your applications and extensions should also _always_ use this plugin when needing to store data against the session, rather than use the session scope directly.
-
-By default, we use Lucee's session management for our session implementation, but as of Preside 10.12.0, we have created our own implementation which you can turn on.
-
-## Turning on Preside's session management
-
-The advantages of using Preside's Session Management are:
-
-* Very simple database implementation
-* Clean session tidying
-* Simplified cookie management
-* Lean implementation for better performance
-* Simple to use in any environment, including Kubernetes and other containerised environments
-
-To use Preside's session management, modify your app's `Application.cfc` to look something like:
-
-```luceescript
-component extends="preside.system.Bootstrap" {
-
- super.setupApplication(
- id = "my-application"
- , presideSessionManagement = true
- );
-
-}
-```
-
-## Accessing the session storage plugin
-
-### In a handler
-
-```luceescript
-property name="sessionStorage" inject="coldbox:plugin:sessionStorage";
-
-// or...
-
-var sessionStorage = getPlugin( "sessionStorage" );
-```
-
-### In a service
-
-```luceescript
-/**
- * @singleton
- * @presideservice
- *
- */
-component {
-
- /**
- * @sessionStorage.inject coldbox:plugin:sessionStorage
- *
- */
- public any function init( required any sessionStorage ) {
- // set the session storage plugin to some local variable for later use
- }
-
-}
-```
-
-Or
-
-```luceescript
-/**
- * @singleton
- * @presideservice
- *
- */
-component {
-
- property name="sessionStorage" inject="coldbox:plugin:sessionStorage";
-
- // ...
-
-}
-```
-
-## Using the session storage plugin
-
-See the [ColdBox wiki for full documentation](http://wiki.coldbox.org/wiki/Plugins:SessionStorage.cfm).
-
-# Stateless requests
-
-As of v10.5.0, Preside comes with some configuration options for automatically serving "stateless" requests which turn off session management and ensure that no cookies are set. This is useful for things like [[restframework|REST API requests]], scheduled tasks, and known bots and spiders.
-
-## Default implementation
-
-The default implementation will flag the following requests as being stateless and not create sessions or cookies for them:
-
-* Any request path starting with `/api/` (the default pattern for the [[restframework|REST Framework]])
-* Lucee Scheduled Task requests (matching user agent 'CFSCHEDULE')
-* Requests flagged as bot or spider requests, matched on user agent
-
-## Overriding the default implementation
-
-### Method 1: SetupApplication()
-
-In your site's `Application.cfc`, you can pass arrays of user agent and URL regex patterns to the `setupApplication()` method that will be treated as stateless. These will _override_ the core defaults. For example:
-
-```luceescript
-component extends="preside.system.Bootstrap" {
-
- super.setupApplication(
- id = "my-site"
- , statelessUrlPatterns = [ "https?://static\..*" ]
- , statelessUserAgentPatterns = [ "CFSCHEDULE", "bot\b", "spider\b" ]
- );
-
-}
-```
-
-In the example above the `statelessUrlPatterns` argument gives a single URL pattern that states that any URL with a "static." sub-domain will be treated as stateless. The `statelessUserAgentPatterns` argument, specifies that the "CFSCHEDULE" user agent, along with some simple bot patterns will be treated as stateless requests.
-
-### Method 2: isStatelessRequest()
-
-In your site's `Application.cfc`, implement the `isStatelessRequest( fullUrl )` method that must return `true` for stateless requests and `false` otherwise. For example:
-
-```luceescript
-component extends="preside.system.Bootstrap" {
-
- super.setupApplication(
- id = "my-site"
- );
-
- private boolean function isStatelessRequest( required string fullUrl ) {
- var isStateless = false;
-
- // add some custom logic to define stateless requests
- // ...
-
- return isStateless;
- }
-
-}
-```
-
-You could also use a combination of both methods:
-
-```luceescript
-component extends="preside.system.Bootstrap" {
-
- // set custom URL and user agent patterns
- super.setupApplication(
- id = "my-site"
- , statelessUrlPatterns = [ "https?://static\..*" ]
- , statelessUserAgentPatterns = [ "CFSCHEDULE", "bot\b", "spider\b" ]
- );
-
- private boolean function isStatelessRequest( required string fullUrl ) {
- // use the core `isStatelessRequest()` method to act
- // on the URL and User agent patterns
- var isStateless = super.isStatelessRequest( argumentCollection=arguments );
-
- // your own extended logic
- if ( !isStateless ) {
- // add some further custom logic to define stateless requests
- // ...
-
- }
-
- return isStateless;
- }
-
-}
-```---
-id: dataobjectviews
-title: Data object views
----
-
-## Overview
-
-Preside provides a feature that allows you to autowire your data model to your views, completely bypassing hand written handlers and service layer objects. Rendering one of these views looks like this:
-
-```lucee
-#renderView(
- view = "events/preview"
- , presideObject = "event"
- , filter = { event_category = rc.category }
-)#
-```
-
-In the example above, the `/views/events/preview.cfm` view will get rendered for each *event* record that matches the supplied filter, `{ event_category = rc.category }`. Each rendered view will be passed the database fields that it needs as individual arguments.
-
-In order for the `renderView()` function to know what fields to select for your view, the view itself must declare what fields it requires. It does this using the `` custom tag. Using our "event preview" example from above, our view file might look something like this:
-
-```lucee
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
->>> We introduced the `` tag is used by your view to specify what fields it needs to render. Any variable that is declared that starts with "args." will be considered a field on your preside object by default.
-
-If we are rendering a view for a **news** object, the following param will lead to `news.headline` being retrieved from the database:
-
-```lucee
-
-```
-
-
-### Aliases
-
-You may find that you need to have a different variable name to the field that you need to select from the data object. To achieve this, you can use the `field` attribute to specify the name of the field:
-
-```lucee
-
-```
-
-You can use the same technique to do aggregate fields and any other SQL select goodness that you want:
-
-```lucee
-
-
-```
-
-### Getting fields from other objects
-
-For one to many style relationships, where your object is the many side, you can easily select fields from the related object using the `field` attribute shown above. Simply prefix the column name with the name of the foreign key field on your object. For example, if our **news** object has a single **news_category** field that is a foreign key to a category lookup, we could get the title of the category with:
-
-```lucee
-
-
-```
-
-### Front end editing
-
-If you would like a field to be editable in the front end website, you can set the `editable` attribute to **true**:
-
-```lucee
-
-```
-
-### Accepting arguments that do not come from the database
-
-Your view may need some variables that do not come from the database. For example, in the code below, the view is being passed the `showComments` argument that does not exist in the database.
-
-```lucee
-#renderView( view="myview", presideObject="news", args={ showComments=false } )#
-```
-
-To allow this to work, you can specify `field="false"`, so:
-
-```lucee
-
-
-
-```
-
-This looks as though it should not be necessary because we are using the `
-
-
-
-```
-
-### Defining renderers
-
-Each of the fields fetch from the database for your view will be pre-rendered using the default renderer for that field. So fields that use a richeditor will have their Widgets and embedded assets all ready rendered for you. To specify a different renderer, or to specify renderers on calculated fields, do:
-
-```lucee
-
-```
-
-## Caching
-
-You can opt to cache your preside data object views by passing in caching arguments to the [[presideobjectviewservice-renderView]] method. A minimal example:
-
-```luceescript
-rendered = renderView(
- view = "event/detail"
- , presideObject = "event"
- , id = eventId
- , cache = true // cache with sensible default settings
-);
-```
-
-See the [[presideobjectviewservice-renderView]] method documentation for details on all the possible arguments.
-
-
----
-id: multilingualcontent
-title: Multilingual content
----
-
-## Overview
-
-Preside comes packaged with a powerful multilingual content feature that allows you to make your client's pages and other data objects translatable to multiple languages.
-
-Enabling multilingual translations is a case of:
-
-1. Enabling the feature in your `Config.cfc` file
-2. Marking the preside objects that you wish to be multilingual with a `multilingual` flag
-3. Marking the specific properties of preside objects that you wish to be multilingual with a `multilingual` flag
-4. Optionally providing specific form layouts for translations
-5. Providing a mechanism in the front-end application for users to choose from configured languages
-
-Once the multilingual content feature is enabled, Preside will provide a basic UI for allowing CMS administrators to translate content and to configure what languages are available. When selecting data for display in your application, Preside will automatically select translations of your multilingual properties for you when available for the currently selected language. If no translation is available, the system will fall back to the default content.
-
-![Screenshot showing selection of configured languages](images/screenshots/select_translations.png)
-
-## Enabling multilingual content
-
-### Global config
-
-Enabling the feature in your applications's `Config.cfc` file is achieved as follows:
-
-```luceescript
-public void function configure() {
- super.configure();
-
- // ...
-
- settings.features.multilingual.enabled = true;
-```
-
-
-### Configuring specific data objects
-
-Configuring individual [[presidedataobject|Preside Objects]] is done using a `multilingual=true` flag on both the component itself and any properties you wish to be translatable:
-
-```luceescript
-/**
- * @multilingual true
- *
- */
-component {
- property name="title" multilingual=true // ... (multilingual)
- property name="active" // ... (not multilingual)
-}
-```
-
-## Configuring languages
-
-Configuring languages is done entirely through the admin user interface and can be performed by your clients if necessary. To navigate to the settings page, go to *System* -> *Settings* -> *Content translations*:
-
-![Screenshot showing configuration of content translation languages in the admin user interface](images/screenshots/translation_settings.png)
-
-## Customizing translation forms
-
-By default, the forms for translating records will be automatically generated. They will contain no tabs or fieldsets and the order of fields may be unpredictable.
-
-To provide a better experience when dealing with records with many fields, you can define an alternative translation form at:
-
-```
-/forms/preside-objects/_translation_objectname/admin.edit.xml // where 'objectname' is the name of your object
-```
-
-When dealing with page types and pages, this will be:
-
-```
-/forms/preside-objects/_translation_page/admin.edit.xml // for the core page object
-/forms/preside-objects/_translation_pagetypename/admin.edit.xml // where 'pagetypename' is the name of your page type
-```
-
-## Setting the current language
-
-It is up to your application to choose the way in which it will set the language for the current request. One common way in which to do this would be to allow the user to pick from the available languages and to persist their preference.
-
-The list of available languages can be obtained with the `listLanguages()` method of the `multilingualPresideObjectService` object, e.g.:
-
-```luceescript
-component {
- property name="multilingualPresideObjectService" inject="multilingualPresideObjectService";
-
- function someHandlerAction( event, rc, prc ) {
- prc.availableLanguages = multilingualPresideObjectService.listLanguages()
- }
-}
-```
-
-Setting the current language can be done with `event.setLanguage( idOfLanguage )`. An ideal place to do this would be at the beggining of the request. This can be achieved in the `/handlers/General.cfc` handler. For example:
-
-```luceescript
-component extends="preside.system.handlers.General" {
-
- // here, userPreferenceService would be some custom service
- // object that was written to get and set user preferences
- // it is for illustration purposes only and not a core service
- property name="userPreferencesService" inject="userPreferencesService";
-
- function requestStart( event, rc, prc ) {
- super.requestStart( argumentCollection=arguments );
-
- event.setLanguage( userPreferencesService.getLanguage() );
- }
-}
-```
-
->>>>> Notice how the `General.cfc` handler extends `preside.system.handlers.General` and calls `super.requestStart( argumentCollection=arguments )`. Without this logic, the core request start logic would not take place, and the system would likely break completely.
----
-id: customdbmigrations
-title: Database Migrations
----
-
-## Overview
-
-Since the first release, Preside has supported automatic **schema** synchronisation with your Preside Object data model. It has also supported core Preside system data migrations for a long time. Now, as of **10.18.0**, Preside also supplies a straightforward framework for application and extension developers to supply their own one time data migration scripts.
-
-## Implementation
-
-The implementation involves developers supplying a convention-based coldbox handler with either `run()` or `runAsync()` methods that perform any database data migrations necessary with normal Preside/Coldbox code. The convention is `/handlers/dbmigrations/yourmigrationid.cfc`.
-
-Any migrations are run in **name** order. It is recommended therefore that you name your migration handlers in a sensible order friendly way. For example, using the date of handler creation as a prefix.
-
-### Example
-
-```luceescript
-/**
- * Handler at /handlers/dbmigrations/2022-05-25_defaultEventModes.cfc
- *
- */
-component {
-
- private void function run() {
- getPresideObject( "my_object" ).updateData(
- filter = "my_new_flag is null"
- , data = { my_new_flag = true }
- );
- }
-
- // as of 10.20.0 you can now dynamically disable the
- // migration with the following *optional* method
- private boolean function isEnabled() {
- return isFeatureEnabled( "myFeature" );
- }
-
-}
-```
-
-### Synchronous vs Asynchronous running
-
-When you implement a `run()` method, your logic will run during application startup and application startup will not be complete until the migration completes. This is important for **critical** migrations where the application's data **must** be updated in order for correct operation of the application.
-
-If your migration is not essential to the running of the application, you may wish to implement a `runAsync()` method instead. These migrations will be run in a background thread approximately 1 minute after application startup. Great for slow, non-essential migrations.
-
-Both methods operate and are called in exactly the same way. Neither method receives any arguments other than core coldbox `event`, `rc` and `prc`.---
-id: websiteusersandpermissioning
-title: Website users and permissioning
----
-
-## Overview
-
-Preside supplies a basic core system for setting up user logins and permissioning for your front end websites. This system includes:
-
-* Membership management screens in the administrator
-* Ability to create users and user "benefits" (synonymous with user groups)
-* Ability to apply access restrictions to site pages and assets through user benefits and individual users
-* Core system for dealing with access denied responses
-* Core handlers for processing login, logout and forgotten password
-
-The expectation is that, for more involved sites, these core systems will be extended and interacted with to create a fuller membership experience.
-
-## Users and Benefits
-
-We provide a simple model of **users** and **benefits** with two core preside objects, `website_user` and `website_benefit`. A user can have multiple benefits. User benefits are analogous to user groups.
-
->>> We have kept the fields for both objects to a bare minimum so as to not impose unwanted logic to your sites. You are encouraged to extend these objects to add your site specific data needs.
-
-## Login
-
-The `website_user` object provides core fields for handling login and displaying the currently logged in user's name:
-
-* `login_id`
-* `email_address`
-* `password`
-* `display_name`
-
-Passwords are hashed using BCrypt and the default login procedure checks the supplied login id for a match against either the `login_id` or `email_address` field before checking the validity of the password with BCrypt.
-
-### Core handler actions
-
-In addition to the core service logic, Preside also provides a thin handler layer for processing login and logout and for rendering a login page. The handler can be found at `/system/handlers/Login.cfc`. It provides the following direct actions and viewlets:
-
-#### Default (index)
-
-The default action will render the loginPage viewlet. It will also redirect the user if they are already logged in. You can access this action with the URL: mysite.com/login/ (generate the URL with `event.buildLink( linkTo="login" )`).
-
-#### AttemptLogin
-
-The `attemptLogin()` action will process a login attempt, redirecting to the default action on failure or redirecting to the last page accessed (or the default post login page if no last page can be calculated) on success. You can use `event.buildLink( linkTo='login.attemptLogin' )` to build the URL required to access this action.
-
-The action expects the required POST parameters `loginId` and `password` and will also process the optional fields `rememberMe` and `postLoginUrl`.
-
-#### Logout
-
-The `logout()` action logs the user out of their session and redirects them either to the previous page or, if that cannot be calculated, to the default post logout page.
-
-You can build a logout link with `event.buildLink( linkTo='login.logout' )`.
-
-#### Viewlet: loginPage
-
-The `loginPage` viewlet is intended to render the login page.
-
-The core view for this viewlet is just an example and should probably be overwritten within your application. However it should show how things could be implemented.
-
-The core handler ensures that the following arguments are passed to the view:
-
-
-
-
-
-
Name
-
Descriptiojn
-
-
-
-
`args.allowRememberMe`
Whether or not remember me functionality is allowed
-
`args.postLoginUrl`
URL to redirect the user to after successful login
-
`args.loginId`
Login id that the user entered in their last login attempt (if any)
-
`args.rememberMe`
Remember me preference that the user chose in their last login attempt (if any)
-
`args.message`
Message ID that can be used to render a message to the user. Core message IDs are `LOGIN_REQUIRED` and `LOGIN_FAILED`
-
-
-
-
->>> The default implementation of the access denied error handler renders this viewlet when the cause of the access denial is "LOGIN_REQUIRED" so that your login form will automatically be shown when login is required to access some resource.
-
-### Checking login and getting logged in user details
-
-You can check the logged in status of the current user with the helper method, `isLoggedIn()`. Additionally, you can check whether the current user is only auto logged in from a cookie with, `isAutoLoggedIn()`. User details can be retrieved with the helper methods `getLoggedInUserId()` and `getLoggedInUserDetails()`.
-
-For example:
-
-```luceescript
-// an example 'add comment' handler:
-public void function addCommentAction( event, rc, prc ) {
- if ( !isLoggedIn() || isAutoLoggedIn() ) {
- event.accessDenied( "LOGIN_REQUIRED" );
- }
-
- var userId = getLoggedInUserId();
- var emailAddress = getLoggedInUserDetails().email_address ?: "";
-
- // ... etc.
-}
-```
-
-### Login impersonation
-
-CMS administrative users, with sufficient privileges, are able to "impersonate" the login of website users through the admin GUI. Once they have done this, they are treated as a fully logged in user in the front end.
-
-If you wish to restrict these impersonated logins in any way, you can use the `isImpersonated()` method of the `websiteLoginService` object to check to see whether or not the current login is merely an impersonated one.
-
-## Permissions
-
-A permission is something that a user can do within the website. Preside comes with two permissions out of the box, the ability to access a restricted page and the ability to access a restricted asset. These are configured in `Config.cfc` with the `settings.websitePermissions` struct:
-
-```luceescript
-// /preside/system/config/Config.cfc
-component {
-
- public void function configure() {
- // ... other settings ... //
-
- settings.websitePermissions = {
- pages = [ "access" ]
- , assets = [ "access" ]
- };
-
- // ... other settings ... //
-
- }
-
-}
-```
-
-The core settings above produces two permission keys, "pages.access" and "assets.access", these permission keys are used in creating and checking applied permissions (see below). The permissions can also be directly applied to a given user or benefit in the admin UI:
-
-![Screenshot of the default edit benefit form. Benefits can have permissions directly applied to them.](images/screenshots/website_benefit_form.png)
-
-
-The title and description of a permission key are defined in `/i18n/permissions.properties`:
-
-```properties
-# ... other keys ...
-
-pages.access.title=Access restricted pages
-pages.access.description=Users can view all restricted pages in the site tree unless explicitly denied access to them
-
-assets.access.title=Access restricted assets
-assets.access.description=Users can view or download all restricted assets in the asset tree unless explicitly denied access to them
-```
-
-### Applied permissions and contexts
-
-Applied permissions are instances of a permission that are granted or denied to a particular user or benefit. These instances are stored in the `website_applied_permission` preside object.
-
-#### Contexts
-
-In addition to being able to set a grant or deny permission against a user or benefit, applied permissions can also be given a **context** and **context key** to create more refined permission schemes.
-
-For instance, when you grant or deny access to a user for a particular **page** in the site tree, you are creating a grant or deny instance with a context of "page" and a context key that is the id of the page.
-
-
-### Defining your own custom permissions
-
-It is likely that you will want to define your own permissions for your site. Examples might be the ability to add comments, or upload documents. Creating the permission keys requires modifying both your site's Config.cfc and permissions.properties files:
-
-```luceescript
-// /mysite/application/config/Config.cfc
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // ... other settings ... //
-
- settings.websitePermissions.comments = [ "add", "edit" ];
- settings.websitePermissions.documents = [ "upload" ];
-
- // ... other settings ... //
-
- }
-
-}
-```
-
-The settings above would produce three keys, `comments.add`, `comments.edit` and `documents.upload`.
-
-```properties
-# /mysite/application/i18n/permissions.properties
-
-comments.add.title=Add comments
-comments.add.description=Ability to add comments in our comments system
-
-comments.edit.title=Edit comments
-comments.edit.description=Ability to edit their own comments after they have been submitted
-
-documents.upload.title=Upload documents
-documents.upload.description=Ability to upload documents to share with other privileged members
-
-With the permissions configured as above, the benefit or user edit screen would appear with the new permissions added:
-```
-
-![Screenshot of the edit benefit form with custom permissions added.](images/screenshots/website_benefit_form_extended.png)
-
-### Checking permissions
-
->>> The core system already implements permission checking for restricted site tree page access and restricted asset access. You should only require to check permissions for your own custom permission schemes.
-
-You can check to see whether or not the currently logged in user has a particular permission with the `hasWebsitePermission()` helper method. The minimum usage is to pass only the permission key:
-
-```lucee
-
-
-
-```
-
-You can also check a specific context by passing in the `context` and `contextKeys` arguments:
-
-```luceescript
-public void function addCommentAction( event, rc, prc ) {
- var hasPermission = hasWebsitePermission(
- permissionKey = "comments.add"
- , context = "commentthread"
- , contextKeys = [ rc.thread ?: "" ]
- );
-
- if ( !hasPermission ) {
- event.accessDenied( reason="INSUFFIENCT_PRIVILEGES" );
- }
-}
-```
-
->>> When checking a context permission, you pass an array of context keys to the `hasWebsitePermission()` method. The returned grant or deny permission will be the one associated with the first found context key in the array.
-
->>>This allows us to implement cascading permission schemes. For site tree access permissions for example, we pass an array of page ids. The first page id is the current page, the next id is its parent, and so on.
-
-## Partial restrictions in site tree pages
-
-The site tree pages system allows you to define that a page is "Partially restricted". You can check that a user does not have full access to a partially restricted page with `event.isPagePartiallyRestricted()`. This then allows you to implement alternative content to show when the user does not have full access. It is down to you to implement this alternative content. A simple example:
-
-```lucee
-
-
-
- #renderView( "/general/_partiallyRestricted" )
-
- #args.main_content#
-
-```
----
-id: data-tenancy
-title: Configuring data tenancy
----
-
-## Overview
-
-Data tenancy allows you to divide your data up into logical segments, or tenants. A classic example of this might be an application that serves different customers. The application is shared between all the customers, but each customer gets their own users and their own data and cannot see the data of the other customers.
-
-Preside has always come with a concept of "site tenancy", but as of 10.8.0, it also provides a simple framework for defining your own custom tenancies.
-
-## Example
-
-Let's take a real-life scenario where an application maintains articles for on-line and print media. The application serves multiple customers and each article should belong to a single customer (we'll add some complexity to this later).
-
-Article editors should be able to switch customer in the admin interface and automatically have their data filtered for that customer. Article editors require permissions to be able to work on particular customers' articles.
-
-### Configuration
-
-In our example, we have a single object for tenancy, `customer.cfc`. We are going to assume that the permissions model and data for customers is already setup and that we have a preside object for customer that looks something like this:
-
-```luceescript
-/**
- * @labelfield name
- */
-component {
- property name="name";
- // ... other properties
-}
-```
-
-To configure this object for tenancy, you would need to add the following to your application's `/application/config/Config.cfc`:
-
-```luceescript
-settings.tenancy.customer = {
- object = "customer"
- , defaultFk = "customer"
-};
-```
-
-This tells the framework that 'customer' can be used to create tenancy in other data objects. To configure an object to use this tenancy, we add `@tenant customer` to its definition. In our example, we want articles to have customer tenancy, so our `article.cfc` would look like this:
-
-```luceescript
-/**
- * @tenant customer
- * @labelfield title
- */
-component {
- //...
-}
-```
-
-*That's it*. Our data model is now set. The framework will automatically inject the relevant foreign keys into the `article.cfc` object and ensure any indexes and unique indexes also include the `customer` foreign key.
-
-Whenever data is selected from the `article` object, the framework will automatically filter it by the currently set `customer`. Whenever data is inserted into the `article` object store, the `customer` field will be automatically set to the currently active `customer`.
-
-### Setting the active tenant per-request
-
-In order for the framework to be able to auto-filter and maintain tenancy, you need to tell it what the current active tenant is per request. To do so, you can implement a handler action, `tenancy.{configuredtenant}.getId`. This handler should return the ID of the currently active tenant record. This handler action is called very early in the request lifecycle to ensure the active tenants get set before they need to be used.
-
-In our example, our tenancy object is `customer`, so our convention based hander would live at `/handlers/tenancy/customer.cfc` and could look like this:
-
-
-```luceescript
-component {
-
- property name="customerService" inject="customerService";
-
- private string function getId( event, rc, prc ) {
- return customerService.getCurrentlyActiveCustomerId();
- }
-}
-```
-
->>>>> The logic that calculates the current tenant is entirely up to you. You may base it on the first part of the current domain, e.g. `customer.mysite.com`, or it may be based on a custom control in the admin interface that allows the user to switch between different tenants. **The tenancy framework does not provide any of this logic.**
-
-If you do not wish to follow the convention based handler, you can configure a different one in your `settings.tenancy` config in `Config.cfc` using the `getIdHandler` property:
-
-```luceescript
-settings.tenancy.customer = {
- object = "cust"
- , defaultFk = "cust_id"
- , getIdHandler = "customers.getActiveCustomerId"
-};
-```
-
-### Setting default value for tenant
-
-If the tenancy filter value might potentially be empty, you may want to set a default value; this can be implemented via a handler action, `tenancy.{configuredtenant}.getDefaultValue`. This handler should return the desired default value to filter any tenanted query. This feature is available from v10.25.0 and also patched back to following version: v10.17.41, v10.18.51, v10.19.41, v10.20.35, v10.21.31, v10.22.24, v10.23.11 and v10.24.8.
-
-In our example, our tenancy object is `customer`, so our convention based handler would live at `/handlers/tenancy/customer.cfc` and could look like this:
-
-```luceescript
-component {
-
- property name="customerService" inject="customerService";
-
- private string function getDefaultValue( event, rc, prc ) {
- return customerService.getDefaultCustomerId();
- }
-}
-```
-
-## More complex filter scenarios
-
-You may find that the tenancy is less straight forward than a record belonging to a single tenant. You may have a situation where you have one _main_ tenant, and then many optional tenants.
-
-In our customer article's example, an article can belong to a single customer but also be available to other partner customers. Our `article.cfc` may look like this:
-
-```luceescript
-/**
- * @tenant customer
- * @labelfield title
- */
-component {
- // ...
-
- property name="partner_customers" relationship="many-to-many" relatedto="customer" relatedvia="article_partner_customer";
-
- // ...
-}
-```
-
-If our active customer tenant is "Acme LTD", we only want to see articles whose main customer is "Acme LTD" **OR** whose partner customers contain "Acme LTD".
-
-To implement this logic, you need to create a `getFilter()` handler action in your tenancy handler. This method will take four arguments (as well as the standard Coldbox handler arguments):
-
-* `objectName` - the name of the object being filtered (in our example, `article`)
-* `fk` - the name of the foreign key property that is the main tenancy indicator (in our example, `customer`)
-* `tenantId` - the currently active tenant ID
-* `defaultFilter` - the filter that is used by default, return this if you do not require any custom filtering for the given object (you may have multiple objects that use tenancy and some with different filtering requirements)
-
-An example:
-
-```luceescript
-component {
-
- property name="presideObjectService" inject="presideObjectService";
- property name="customerService" inject="customerService";
-
- private string function getId( event, rc, prc ) {
- return customerService.getCurrentlyActiveCustomerId();
- }
-
- private struct function getFilter( objectName, fk, tenantId, defaultFilter ) {
- if ( arguments.objectName == "article" ) {
- var filter = "#objectName#.#fk# = :customer_id or _extra.id is not null";
- var filterParams = { customer_id = { type="cf_sql_varchar", value=tenantId } };
- var subquery = presideObjectService.selectData(
- objectName = "article_partner_customer"
- , getSqlAndParamsOnly = true
- , distinct = true
- , selectFields = [ "article as id" ]
- , filter = "customer = :customer_id"
- , filterParams = filterParams
- );
-
- return { filter=filter, filterParams=filterParams, extraJoins=[ {
- type = "left"
- , subQuery = subQuery.sql
- , subQueryAlias = "_extra"
- , subQueryColumn = "id"
- , joinToTable = arguments.objectName
- , joinToColumn = "id"
- } ] };
- }
-
- return defaultFilter;
- }
-}
-```
-
-If you do not wish to follow the convention based handler, you can configure a different one in your `settings.tenancy` config in `Config.cfc` using the `getFilterHandler` property:
-
-```luceescript
-settings.tenancy.customer = {
- object = "cust"
- , defaultFk = "cust_id"
- , getFilterHandler = "customers.getTenancyFilter"
-};
-```
-
-## Bypassing tenancy
-
-You may wish to bypass tenancy altogether in some scenarios. To do so, you can pass the `bypassTenants` arguments to [[presideobjectservice-selectdata]]:
-
-```luceescript
-presideObjectService.selectData(
- // ...
- , bypassTenants = [ "customer" ]
-);
-```
-
-This will ensure that any tenancy filters are **not** applied for the given tenants. You are also able to specify these bypasses on an object picker in forms:
-
-
-```xml
-
-```
-
-## Overriding the per-request tenant
-
-If you need to select data from a tenant that is not the currently active tenant for the request, you can use the `tenantIds` argument to specify the IDs for specific tenants. For example:
-
-
-```luceescript
-// ...
-var alternativeCustomerAccounts = accounts.selectData(
- selectFields = [ "id", "account_name" ]
- , tenantIds = { customer=alternativeCustomerId }
-);
-// ...
-```
-
-The value of this argument must be a struct whose keys are the names of the tenant and whose values are the ID to use for the tenant. See [[presideobjectservice-selectdata]] for documentation.
----
-id: adminmenuitems
-title: Configuring admin menu items
----
-
-## Introduction
-
-As of Preside **10.17.0**, the main navigation sytem was updated to introduce a core concept of configured admin menu items.
-
-These are implemented in the side bar navigation and in the System drop down menu in the top navigation. See [[adminlefthandmenu]] and [[adminsystemmenu]].
-
-## Config.cfc implementation
-
-Each named menu item, e.g. "sitetree", must be specified in the `settings.adminMenuItems` struct in your `Config.cfc` file. An entry takes the following form:
-
-```luceescript
-settings.adminMenuItems.sitetree = {
- feature = "sitetree" // optional feature flag. Only show menu item when feature is enabled
- , permissionKey = "sitetree.navigate" // optional admin perm key. Only show menu item if current user has access
- , activeChecks = { handlerPatterns="^admin\.sitetree\.*" } // see 'Active checks' below
- , buildLinkArgs = { linkTo="sitetree" } // Structure of args to send to event.buildAdminLink
- , gotoKey = "s" // Optional global shortcut key for the nav item
- , icon = "fa-sitemap" // Optional fontawesome icon
- , title = "cms:sitetree" // Optional i18n uri for the title
- , subMenuItems = [ "item1", "item2" ] // Optional array of child menu items (each referring to another menu item)
-};
-```
-
-### Reference
-
-
-
-
-
-
Key
-
Default
-
Description
-
-
-
-
-
feature
-
empty
-
Optional feature flag. Only show menu item when feature is enabled
-
-
-
permissionKey
-
empty
-
Optional admin permission key. Only show menu item if current user has access
-
-
-
activeChecks
-
empty
-
Optional struct describing common checks to make to decide whether or not the item is active in any given request
-
-
-
buildLinkArgs
-
empty
-
Structure of args to send to `event.buildAdminLink()`
-
-
-
gotoKey
-
empty
-
Optional global shortcut key for the nav item
-
-
-
icon
-
admin.menuitem:{menuItemName}.iconClass
-
Font awesome icon class name, or i18n URI that translates to one
-
-
-
title
-
admin.menuitem:{menuItemName}.title
-
Title of the menu item, or i18n URI that translates to the title
-
-
-
subMenuItems
-
empty
-
Optional array of child menu items (each referring to another menu item)
-
-
-
-
-
-### Active checks structure
-
-Two keys can be used in the `activeChecks` structure to instruct the system to make common checks for the active state of the menu item: `handlerPatterns` and `datamanagerObject`.
-
-#### handlerPatterns
-
-Specify either a plain string regex pattern to match the current handler event, or supply an array of patterns. e.g.
-
-```luceescript
-settings.adminMenuItems.myItem = {
- // ...
- activeChecks = { handlerPatterns="^admin\.myhandler\.myaction" }
-}
-
-// or
-settings.adminMenuItems.myItem = {
- // ...
- activeChecks = { handlerPatterns=[ "^admin\.myhandler\.myaction", "^admin\.anotherhandler\." ] }
-}
-```
-
-#### datamanagerObject
-
-Specify either a single object name (string), or array of object names. When any datamanager page using the specified object(s) is viewed, the item will be considered active. e.g.
-
-```luceescript
-settings.adminMenuItems.myItem = {
- // ...
- activeChecks = { datamanagerObject="my_object" }
-}
-
-// or
-settings.adminMenuItems.myItem = {
- // ...
- activeChecks = { datamanagerObject=[ "my_object", "my_object_two" ] }
-}
-```
-
-## Extending with dynamic functionality
-
-At times, you may wish to have more dynamic control over the behaviour of your items. In addition to any configuration set above, you may also create a convention based handler to extend the item's behaviour. Create the handler at `/handlers/admin/layout/menuitem/{nameOfYourItem}.cfc`. It can then implement any of the methods below:
-
-```luceescript
-component {
-
- /**
- * System will run this once in application life-time
- * to ascertain whether or not to include the menu item.
- * Useful for more complex feature combination checks.
- */
- private boolean function neverInclude( args={} ) {
- return false;
- }
-
- /**
- * Implement this method to run more complex logic
- * to decide whether or not the current user has
- * access to the menu item.
- *
- */
- private boolean function includeForUser( args={} ) {
- return true;
- }
-
- /**
- * Implement this method to run more complex logic
- * to decide whether or not the item is active for
- * the current request
- *
- */
- private boolean function isActive( args={} ) {
- return false;
- }
-
- /**
- * Implement this method to run more complex
- * / dynamic logic for building the link to the item
- *
- */
- private string function buildLink( args={} ) {
- return "";
- }
-
- /**
- * Run this method to dynamically decorate
- * the item configuration structure (passed in as args)
- *
- */
- private void function prepare( args={} ) {
- var dynamicChildren = [ /* ... */ ];
- ArrayAppend( args.subMenuItems, dynamicChildren, true );
- }
-
-
-}
-```---
-id: workingwithmultiplesites
-title: Working with multiple sites
----
-
-## Overview
-
-Preside allows users to create and manage multiple sites. This is perfect for things like microsites, different language sites and any other organisation of workflows and users.
-
-![Screenshot showing the site picker that appears in the administrator for users with access to multiple sites and / or users with access to the site manager.](images/screenshots/site_picker.png)
-
-
-From a development standpoint, the CMS allows developers to create and maintain multiple site templates. A site template is very similar to a Preside Extension, the difference being that the site template is only active when the currently active site is using the template.
-
-Finally, the CMS allows you to easily segment the data in your Preside data objects by site. By doing so, each site will only have access to the data that is unique to it. The developers are in control of which data objects have their data shared across all sites and which objects have their data segmented per site.
-
-## Site templates
-
-Site templates are like a Preside application within another Preside application. They can contain all the same folders and concepts as your main application but are only active when the currently active site is using the template. This means that any widgets, page types, views, etc. that are defined within your site template, will only kick in when the site that uses the template is active. CMS administrators can apply a single template to a site.
-
-![Screenshot of an edit site form where the user can choose which template to apply to the site.](images/screenshots/edit_site.png)
-
-
-### Creating a barebones site template
-
-To create a new site template, you will need to create a folder under your application's `application/site-templates/` folder (create one if it doesn't exist already). The name of your folder will become the name of the template, e.g. the following folder structure will define a site template with an id of `microsite`:
-
-```
-/application
- /site-templates
- /microsite
-```
-
-In order for the site template to appear in a friendly manner in the UI, you should also add an i18n properties file that corresponds to the site id. In the example above, you would create `/application/i18n/site-templates/microsite.properties`:
-
-```properties
-title=Microsite template
-description=The microsite template provides layouts, widgets and page types that are unique to the site's microsites
-```
-
-### Overriding layouts, views, forms, etc.
-
-To override any Preside features that are defined in your main application, you simply need to create the same files in the same directory structure within your site template.
-
-For example, if you wanted to create a different page layout for a site template, you might want to override the main application's `/application/layouts/Main.cfm` file. To do so, simply create `/application/site-templates/mytemplate/layouts/Main.cfm`:
-
-```
-/application
- /layouts
- Main.cfm <-- this will be used when the active site is *not* using the 'microsite' site template
- /site-templates
- /microsite
- /layouts
- Main.cfm <-- this will be used when the active site is using the 'microsite' site template
-```
-
-This technique can be used for Form layouts, Widgets, Page types and i18n. It can also be used for Coldbox views, layouts and handlers.
-
->>>> You cannot make modifications to :doc:`presideobjects` with the intention that they will only take affect for sites using the current site template. Any changes to :doc:`presideobjects` affect the database schema and will always take affect for every single site and site template.
->>>> If you wish to have different fields on the same objects but for different site templates, we recommend defining all the fields in your core application's object and providing different form layouts that show / hide the relevent fields for each site template.
-
-### Creating features unique to the site template
-
-To create features that are unique to the site template, simply ensure that they are namespaced suitably so as not to conflict with other extensions and site templates. For example, to create an "RSS Feed" widget that was unique to your site template, you might create the following file structure:
-
-```
-/application
- /site-templates
- /microsite
- /forms
- /widgets
- microsite-rss-widget.xml
- /i18n
- /widgets
- microsite-rss-widget.properties
- /views
- /widgets
- microsite-rss-widget.cfm
-```
-
----
-id: formbuilder
-title: Working with the form builder
----
-
-As of v10.5.0, Preside provides a system that enables content administrators to build input forms to gather submissions from their site's user base. The form builder system is fully extendable and this guide sets out to provide detailed instructions on how to do so.
-
-See the following pages for detailed documentation:
-
-1. [[formbuilder-overview]]
-2. [[formbuilder-itemtypes]]
-3. [[formbuilder-actions]]
-4. [[formbuilder-styling-and-layout]]
-
-![Screenshot showing a form builder form's workbench](images/screenshots/formbuilder_workbench.jpg)
-
->>>> The form builder system is not to be confused with the [[presideforms|Preside Forms system]]. The form builder is a system in which content editors can produce dynamically configured forms and insert them into content pages. The [[presideforms|Preside Forms system]] is a system of programatically defining forms that can be used either in the admin interface or hard wired into the application's front end interfaces.---
-id: formbuilder-overview
-title: Form Builder overview
----
-
-As of v10.5.0, Preside provides a system that enables content administrators to build input forms to gather submissions from their site's user base.
-
->>> As of **v10.13.0**, Preside offers a v2 data model for form builder and this can be enabled separately. Enabling this feature will effect any forms that are created from that point on, previously created forms will continue to function as they were.
-
->>> This v2 data model makes querying the answers to questions more robust and provides an additional UI to manage a global set of questions that can be asked in forms.
-
-![Screenshot showing a form builder form's workbench](images/screenshots/formbuilder_workbench.jpg)
-
-## Enabling form builder
-
-### Pre 10.13.0
-
-In versions 10.5 to 10.12, the form builder system is disabled by default. To enable it, set the `enabled` flag on the `formbuilder` feature in your application's `Config.cfc$configure()` method:
-
-```luceescript
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // ...
-
- // enable form builder
- settings.features.formbuilder.enabled = true;
-
- // ...
- }
-}
-
-```
-
-### 10.13.0 and above
-
-As of *10.13*, the form builder system is **enabled** by default. However, the v2 of the data model is turned **off** by default. To enable it:
-
-```luceescript
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
- // ...
-
- // enable form builder
- settings.features.formbuilder2.enabled = true;
-
- // ...
- }
-}
-
-```
-
-## Forms
-
-Forms are the base unit of the system. They can be created, configured, activated and locked by your system's content editors. Once created, they can be inserted into content using the Form Builder form widget. A form definition consists of some basic configuration and any number of ordered and individually configured items (e.g. a text input, select box and email address).
-
-![Screenshot showing a list of form builder forms](images/screenshots/formbuilder_forms.jpg)
-
-Useful references for extending the core form object and associated widget:
-
-* [[presideobject-formbuilder_form|Form builder: form (Preside Object)]]
-* [[form-formbuilderformaddform]]
-* [[form-formbuilderformeditform]]
-* [[form-widgetconfigurationformformbuilderform]]
-
-## Form items and item types
-
-Form items are what provide the input and display definition of the form. _i.e. a form without any items will be essentially invisible_. Content editors can drag and drop item types into their form definition; they can then configure and reorder items within the form definition. The configuration options and display of the item will differ for different item _types_.
-
-![Screenshot showing a configuration of a date picker item](images/screenshots/formbuilder_configureitem.jpg)
-
-The core system provides a basic set of item types whose configuration can be modified and extended by your application or extensions. You are also able to introduce new item types in your application or extensions.
-
-See [[formbuilder-itemtypes]] for more detail.
-
-## Form actions
-
-Form actions are configurable triggers that are fired once a form has been submitted. The core system comes with a single 'Email' action that allows the CMS administrator to configure email notification containing the form submission.
-
-![Screenshot showing a form builder actions workbench](images/screenshots/formbuilder_actions.jpg)
-
-Developers can create their own custom actions that are then available to content editors to add to their forms. See [[formbuilder-actions]] for more detail.
-
-## Form builder permissioning
-
-Access to the Form Builder admin system can be controlled through the [[cmspermissioning]] system. The following access keys are defined:
-
-* `formbuilder.navigate`
-* `formbuilder.addform`
-* `formbuilder.editform`
-* `formbuilder.lockForm`
-* `formbuilder.activateForm`
-* `formbuilder.deleteSubmissions`
-* `formbuilder.editformactions`
-
-In addition, a `formbuildermanager` _role_ is defined that has access to all form builder operations:
-
-```luceescript
-settings.adminRoles.formbuildermanager = [ "formbuilder.*" ];
-```
-
-Finally, by default, the `contentadministrator` _role_ has access to all permissions with the exception of `lock` and `activate` form.
-
-### Defining more restricted roles
-
-In your own application, you could provide more fine tuned form builder access rules with configuration along the lines of the examples below:
-
-```luceescript
-// Adding perms to an existing role
-settings.adminRoles.contenteditor.append( "formbuilder.*" );
-settings.adminRoles.contenteditor.append( "!formbuilder.lockForm" );
-settings.adminRoles.contenteditor.append( "!formbuilder.activateForm" );
-settings.adminRoles.contenteditor.append( "!formbuilder.deleteSubmissions" );
-
-// defining a new role
-settings.adminRoles.formbuilderviewer = [ "formbuilder.navigate" ];
-
-```---
-id: formbuilder-itemtypes
-title: Form Builder item types
----
-
-Form items are what provide the input and display definition of the form. _i.e. a form without any items will be essentially invisible_. Content editors can drag and drop item types into their form definition; they can then configure and reorder items within the form definition. The configuration options and display of the item will differ for different item _types_.
-
-![Screenshot showing a configuration of a date picker item](images/screenshots/formbuilder_configureitem.jpg)
-
-The core system provides a basic set of item types whose configuration can be modified and extended by your application or extensions. You are also able to introduce new item types in your application or extensions.
-
-# Anatomy of an item type
-
-## 1. Definition in Config.cfc
-
-An item type must first be registered in the application or extension's `Config.cfc` file. Item types are grouped into item type categories which are used simply for display grouping in the form builder UI. The core definition looks something like this (subject to change):
-
-```luceescript
-settings.formbuilder = { itemtypes={} };
-
-// The "standard" category
-settings.formbuilder.itemTypes.standard = { sortorder=10, types={
- textinput = { isFormField=true }
- , textarea = { isFormField=true }
- // ...
-} };
-
-// The "content" category
-settings.formbuilder.itemTypes.content = { sortorder=20, types={
- spacer = { isFormField=false }
- , content = { isFormField=false }
-} };
-
-```
-
-Introducing a new form field item type in the "standard" category might then look like this:
-
-```luceescript
-settings.formbuilder.itemTypes.standard.types.colourPicker = { isFormField = true };
-```
-
-## 2. i18n labelling
-
-The labels for each item type *category* are all defined in `/i18n/formbuilder/item-categories.properties`. Each category requires a "title" key:
-
-```properties
-standard.title=Basic
-multipleChoice.title=Multiple choice
-content.title=Content and layout
-```
-
-Each item _type_ subsequently has its own `.properties` file that lives at `/i18n/formbuilder/item-types/(itemtype).properties`. A bare minimum `.properties` file for an item type should define a `title` and `iconclass` key, but it could also be used to define labels for the item type's configuration form. For example:
-
-```properties
-# /i18n/formbuilder/item-types/date.properties
-title=Date
-iconclass=fa-calendar
-
-field.minDate.title=Minimum date
-field.minDate.help=If entered, the input date must be greater than this date
-
-field.maxDate.title=Maximum date
-field.maxDate.help=If entered, the input date must be less than this date
-
-field.relativeOperator.title=Relativity
-field.relativeOperator.help=In what way should the value of this field be constrained in relation to the options below
-
-field.relativeToCurrentDate.title=Current date
-field.relativeToCurrentDate.help=Whether or not the date value entered into this field should be constrained relative to today's date
-
-field.relativeToField.title=Another field in the form
-field.relativeToField.placeholder=e.g. start_date
-field.relativeToField.help=The name of the field whose value should be used as a relative constraint when validating the value of this field
-
-tab.validation.title=Date limits
-fieldset.fixed.title=Fixed dates
-fieldset.relative.title=Relative dates
-
-relativeOperator.lt=Less than...
-relativeOperator.lte=Less than or equal to...
-relativeOperator.gt=Greater than...
-relativeOperator.gte=Greater than or equal to...
-```
-
-## 3. Configuration form
-
-An item type can _optionally_ have custom configuration options defined in a Preside form definition. The form must live at `/forms/formbuilder/item-types/(itemtype).xml`. If the item type is a form field, this definition will be merged with the [[form-formbuilderitemtypeallformfields|core formfield configuration form]]. For example:
-
-```xml
-
-
-
-```
-
-## 4. Handler actions and viewlets
-
-The final component of a Form builder item is its handler. The handler must live at `/handlers/formbuilder/item-types/(itemtype).cfc` and can be used for providing one or more of the following:
-
-1. `renderInput()`: a renderer for the form input (required),
-2. `renderResponse()`: a renderer for a response (optional),
-3. `renderResponseForExport()`: a renderer for a response in spreadsheet (optional),
-4. `getExportColumns()`: logic to determine what columns are required in an spreadsheet export (optional),
-5. `getItemDataFromRequest()`: logic to extract a submitted response from the request (optional),
-6. `renderResponseToPersist()`: logic to render the response for saving in the database (optional),
-7. `getValidationRules()`: logic to calculate what _validators_ are required for the item (optional)
-
-### renderInput()
-
-The `renderInput()` action is the only _required_ action for an item type and is used to render the item for the front end view of the form. A simple example:
-
-```luceescript
-// /handlers/formbuilder/item-types/TextArea.cfc
-component {
-
- private string function renderInput( event, rc, prc, args={} ) {
- return renderFormControl(
- argumentCollection = args
- , type = "textarea"
- , context = "formbuilder"
- , id = args.id ?: ( args.name ?: "" )
- , layout = ""
- , required = IsTrue( args.mandatory ?: "" )
- );
- }
-}
-```
-
-The `args` struct passed to the viewlet will contain any saved configuration for the item (see "Configuration form" above), along with the following additional keys:
-
-* **id:** A unique ID for the form item (calculated dynamically per request to ensure uniqueness)
-* **error:** An error message. This may be supplied if the form has validation errors that need to be displayed for the item
-
-#### renderInput.cfm (no handler version)
-
-An alternative example of an input renderer might be for an item type that is _not_ a form control, e.g. the 'content' item type. Its viewlet could be implemented simply as a view, `/views/formbuilder/item-types/content/renderInput.cfm`:
-
-```lucee
-
- #renderContent(
- renderer = "richeditor"
- , data = ( args.body ?: "" )
- )#
-
-```
-
-`args.body` is available to the item type because it is defined in its configuration form.
-
-### renderResponse()
-
-An item type can optionally supply a response renderer as a _viewlet_ matching the convention `formbuilder.item-types.(itemtype).renderResponse`. This renderer will be used to display the item as part of a form submission. If no renderer is defined, the system will fall back on the core viewlet, `formbuilder.defaultRenderers.response`.
-
-An example of this is the `Radio buttons` control that renders the selected answer for an item:
-
-```luceescript
-// /handlers/formbuilder/item-types/Radio.cfc
-component {
- // ...
-
- // args struct contains response (that is saved in
- // the database) and itemConfiguration keys
- private string function renderResponse( event, rc, prc, args={} ) {
- var itemConfig = args.itemConfiguration ?: {};
- var response = args.response;
- var values = ListToArray( itemConfig.values ?: "", Chr( 10 ) & Chr( 13 ) );
- var labels = ListToArray( itemConfig.labels ?: "", Chr( 10 ) & Chr( 13 ) );
-
- // loop through configured radio options
- for( var i=1; i<=values.len(); i++ ) {
-
- // find a match for the response
- if ( values[ i ] == response ) {
-
- // if label + value are different
- // include both the label and the value
- // in the rendered response
- if ( labels.len() >= i && labels[ i ] != values[ i ] ) {
- return labels[ i ] & " (#values[i]#)";
- }
-
- // or just the value if same as label
- return response;
- }
- }
-
- // response did not match, just show
- // the saved response as is
- return response;
- }
-
- // ...
-}
-```
-
-### renderResponseForExport()
-
-This method allows you to render a response specifically for spreadsheet export. When used in conjunction with `getExportColumns()`, the result can be multiple columns of rendered responses.
-
-For example, the `Matrix` item type looks like this:
-
-
-```luceescript
-// /handlers/formbuilder/item-types/Matrix.cfc
-component {
- // ...
-
- // the args struct will contain response and itemConfiguration keys.
- // the response is whatever has been saved in the database for the item
- private array function renderResponseForExport( event, rc, prc, args={} ) {
- var qAndA = _getQuestionsAndAnswers( argumentCollection=arguments );
- var justAnswers = [];
-
- for( qa in qAndA ) {
- justAnswers.append( qa.answer );
- }
-
- // here we return an array of answers corresponding
- // to the question columns that we have defined
- // in the getExportColumns() method (see below)
- return justAnswers;
- }
-
- // ...
-
- // the args struct will contain the item's configuration
- private array function getExportColumns( event, rc, prc, args={} ) {
- var rows = ListToArray( args.rows ?: "", Chr(10) & Chr(13) );
- var columns = [];
- var itemName = args.label ?: "";
-
- for( var row in rows ) {
- if ( !IsEmpty( Trim( row ) ) ) {
- columns.append( itemName & ": " & row );
- }
- }
-
- return columns;
- }
-
- // ...
-
- // this is just a specific utility method used by the matrix item type
- // to extract out questions and their answers from a saved response
- private array function _getQuestionsAndAnswers( event, rc, prc, args={} ) {
- var response = IsJson( args.response ?: "" ) ? DeserializeJson( args.response ) : {};
- var itemConfig = args.itemConfiguration ?: {};
- var rows = ListToArray( Trim( itemConfig.rows ?: "" ), Chr(10) & Chr(13) );
- var answers = [];
-
- for( var question in rows ) {
- if ( Len( Trim( question ) ) ) {
- var inputId = _getQuestionInputId( itemConfig.name ?: "", question );
-
- answers.append( {
- question = question
- , answer = ListChangeDelims( ( response[ inputId ] ?: "" ), ", " )
- } );
- }
- }
-
- return answers;
- }
-}
-```
-
-### getExportColumns()
-
-This method allows us to define a custom set of spreadsheet export columns for a configured item type. This may be necessary if the item type actually results in multiple sub-questions being asked. You do _not_ need to implement this method for simple item types.
-
-A good example of this is the `Matrix` item type that allows editors to configure a set of questions (rows) and a set of optional answers (columns). The `getExportColumns()` method for the `Matrix` item type looks like this:
-
-```luceescript
-// /handlers/formbuilder/item-types/Matrix.cfc
-component {
- // ...
-
- // the args struct will contain the item's configuration
- private array function getExportColumns( event, rc, prc, args={} ) {
- var rows = ListToArray( args.rows ?: "", Chr(10) & Chr(13) );
- var columns = [];
- var itemName = args.label ?: "";
-
- for( var row in rows ) {
- if ( !IsEmpty( Trim( row ) ) ) {
- columns.append( itemName & ": " & row );
- }
- }
-
- return columns;
- }
-}
-```
-
-### getItemDataFromRequest()
-
-This method allows us to extract out data from a form submission in a format that is ready for validation and/or saving to the database for our configured item. For simple item types, such as a text input, this is not necessary as we would simply need to take whatever value is submitted for the item.
-
-An example usage is the `FileUpload` item type. In this case, we want to upload the file in the form field to a temporary location and return a structure of information about the file that can then be validated later in the request:
-
-```luceescript
-// /handlers/formbuilder/item-types/FileUpload.cfc
-component {
- // ...
-
- // The args struct passed to the viewlet will contain inputName, requestData and itemConfiguration keys
- private any function getItemDataFromRequest( event, rc, prc, args={} ) {
- // luckily for us here, there is already a process that
- // preprocesses a file upload and returns a struct of file info :)
- var tmpFileDetails = runEvent(
- event = "preprocessors.fileupload.index"
- , prePostExempt = true
- , private = true
- , eventArguments = { fieldName=args.inputName ?: "", preProcessorArgs={} }
- );
-
- return tmpFileDetails;
- }
-
- // ...
-}
-```
-
-
-### renderResponseToPersist()
-
-This method allows you to perform any manipulation on a submitted response for an item, _after_ form validation and _before_ saving to the database. For simple item types, such as a text input, this is generally not necessary as we can simply take whatever value is submitted for the item.
-
-An example usage of this is the `FileUpload` item type. In this case, we want to take a temporary file and save it to storage, returning the storage path to save in the database:
-
-```luceescript
-// /handlers/formbuilder/item-types/FileUpload.cfc
-component {
- // ...
-
- // The args struct passed to the viewlet will contain the submitted response + any item configuration
- private string function renderResponseToPersist( event, rc, prc, args={} ) {
- // response in this case will be a structure
- // containing information about the file
- var response = args.response ?: "";
-
- if ( IsBinary( response.binary ?: "" ) ) {
- var savedPath = "/#( args.formId ?: '' )#/#CreateUUId()#/#( response.tempFileInfo.clientFile ?: 'uploaded.file' )#";
-
- formBuilderStorageProvider.putObject(
- object = response.binary
- , path = savedPath
- );
-
- return savedPath;
- }
-
- return SerializeJson( response );
- }
-
- // ...
-}
-```
-
-### getValidationRules()
-
-This method should return an array of validation rules for the configured item (see [[validation-framework]] for full documentation on validation rules). These rules will be used both server-side, using the Validation framework, and client-side, using the jQuery Validate library, where appropriate.
-
->>> The core form builder system provides some standard validation rules for mandatory fields, min/max values and min/max lengths. You only need to supply validation rule logic for specific rules that your item type may require.
-
-An example:
-
-```luceescript
-// /handlers/formbuilder/item-types/FileUpload.cfc
-component {
- // ...
-
- // The args struct passed to the viewlet will contain any saved configuration for the item.
- private array function getValidationRules( event, rc, prc, args={} ) {
- var rules = [];
-
- // add a filesize validation rule if the item has
- // been configured with a max file size constraint
-
- if ( Val( args.maximumFileSize ?: "" ) ) {
- rules.append( {
- fieldname = args.name ?: ""
- , validator = "fileSize"
- , params = { maxSize = args.maximumFileSize }
- } );
- }
-
- return rules;
- }
-
- // ...
-}
-```
-
-### getQuestionDataType()
-
->>> v10.13.0 and up only
-
-As of **10.13.0**, your item type can implement the `getQuestionDataType()` private handler action. This is provided with `args.configuration` which you can use to inform the v2 formbuilder data model which field type to save the response against. If not implemented, the system will default to `text` which means querying the responses can not benefit from table indexes.
-
-Possible return responses are:
-
-* `text` - The default, just a clob of data
-* `shorttext` - Maximum 200 chars - can be indexed in the database for faster lookups
-* `date` - A valid date or date time
-* `bool` - A valid boolean value
-* `int` - An integer value
-* `float` - A floating point number
-
-Example from the number item type:
-
-```luceescript
-private string function getQuestionDataType( event, rc, prc, args={} ) {
- var format = args.configuration.format ?: "";
-
- if ( format == "integer" ) {
- return "int";
- }
-
- return "float";
-}
-```
-
-### renderV2ResponsesForDb()
-
->>> v10.13.0 and up only
-
-As of **10.13.0**, your item type can implement a `renderV2ResponsesForDb` handler action to prepare responses for saving in the database.
-
-This action should return either:
-
-1. **A simple value**, for simple item types
-2. **An array of simple values**, for multiple select item types - the order of the values should match the user selected order
-3. **A struct of simple keys with simple values**, for form items that are broken into multiple fields (see matrix for example)
-
-The action receives:
-
-* `args.response` - contains the processed form submission for the question
-* `args.configuration` - struct, the user configuration of the item
-
-Example from the `Matrix` item type:
-
-```luceescript
-private struct function renderV2ResponsesForDb( event, rc, prc, args={} ) {
- var response = {};
- var qAndAs = _getQuestionsAndAnswers( argumentCollection=arguments );
-
- for( var qAndA in qAndAs ) {
- response[ qAndA.question ] = qAndA.answer;
- }
-
- return response;
-}
-```---
-id: formbuilder-actions
-title: Form Builder actions
----
-
-Form actions are configurable triggers that are fired once a form has been submitted. The core system comes with a single 'Email' action that allows the CMS administrator to configure email notification containing the form submission.
-
-![Screenshot showing a form builder actions workbench](images/screenshots/formbuilder_actions.jpg)
-
-Developers can create their own custom actions that are then available to content editors to add to their forms.
-
-# Creating a custom form action
-
-## 1. Register the action in Config.cfc
-
-Actions are registered in your application and extension's `Config.cfc` file as a simple array. To register a new 'webhook' action, simply append 'webhook' to the `settings.formbuilder.actions` array:
-
-```luceescript
-component extends="preside.system.config.Config" {
-
- public void function configure() {
- super.configure();
-
-
- // ...
- settings.formbuilder.actions.append( "webhook" );
-
- // ...
- }
-}
-```
-
-## 2. i18n for titles, icons, etc.
-
-Each registered action should have its own `.properties` file at `/i18n/formbuilder/actions/(action).properties`. It should contain `title`, `iconclass` and `description` keys + any other keys it needs for configuration forms, etc. For example, the `.properties` file for a "webhook" action might look like:
-
-```
-# /i18n/formbuilder/actions/webhook.properties
-
-title=Webhook
-iconclass=fa-send
-description=Sends a POST request to the configured URL containing data about the submitted form
-
-field.endpoint.title=Endpoint
-field.endpoint.placeholder=e.g. https://mysite.com/formbuilder/webhook/
-```
-
-## 3. Create a configuration form
-
-To allow editors to configure your action, supply a configuration form at `/forms/formbuilder/actions/(action).xml`. For example, the "email" configuration form looks like this:
-
-```xml
-
-
-
-
-```
-
-![Screenshot showing a configuration of an email action](images/screenshots/formbuilder_configureaction.jpg)
-
-## 4. Implement an onSubmit handler
-
-The `onSubmit` handler is where your action processes the form submission and does whatever it needs to do. This handler will be a private method in `/handlers/formbuilder/actions/(youraction).cfc`. For example, the email action's submit handler looks like this:
-
-```luceescript
-component {
-
- property name="emailService" inject="emailService";
-
- // the args struct contains:
- //
- // configuration : struct of configuration options for the action
- // submissionData : the processed and saved data of the submission (struct)
- //
- private void function onSubmit( event, rc, prc, args={} ) {
- emailService.send(
- template = "formbuilderSubmissionNotification"
- , args = args
- , to = ListToArray( args.configuration.recipients ?: "", ";," )
- , from = args.configuration.send_from ?: ""
- , subject = args.configuration.subject ?: "Form submission notification"
- );
- }
-
-}
-```
-
-## 5. Implement a placeholder viewlet (optional)
-
-The placeholder viewlet allows you to customize how your configured action appears in the Form builder actions workbench:
-
-![Screenshot showing the placeholder of a configured action](images/screenshots/formbuilder_actionplaceholder.jpg)
-
-The viewlet called will be `formbuilder.actions.(youraction).renderAdminPlaceholder`. For the email action, this has been implemented as a handler method:
-
-```luceescript
-// /handlers/formbuilder/actions/Email.cfc
-
-component {
-
- // ...
-
- private string function renderAdminPlaceholder( event, rc, prc, args={} ) {
- var placeholder = ' ';
- var toAddress = HtmlEditFormat( args.configuration.recipients ?: "" );
- var fromAddress = HtmlEditFormat( args.configuration.send_from ?: "" );
-
- if ( Len( Trim( fromAddress ) ) ) {
- placeholder &= translateResource(
- uri = "formbuilder.actions.email:admin.placeholder.with.from.address"
- , data = [ "#toAddress#", "#fromAddress#" ]
- );
- } else {
- placeholder &= translateResource(
- uri = "formbuilder.actions.email:admin.placeholder.no.from.address"
- , data = [ "#toAddress#" ]
- );
- }
-
- return placeholder;
- }
-}
-```---
-id: formbuilder-styling-and-layout
-title: Form Builder styling and layout
----
-
-The form builder system allows you to provide custom layouts for:
-
-1. Entire forms
-2. Individual form items
-
-These layouts can be used to give your content editors choice about the appearance of their forms.
-
-## Form layouts
-
-Custom form layouts are implemented as viewlets with the pattern `formbuilder.layouts.form.(yourlayout)`. Layouts are registered simply by implementing a viewlet with this pattern (as either a handler or view).
-
-### The viewlet
-
-The `args` struct passed to the viewlet will contain a `renderedForm` key that contains the form itself with all the rendered items and submit button. It will also be passed any custom arguments sent to the [[formbuilderservice-renderform]] method (e.g. custom configuration in the form builder form widget).
-
-The default layout is implemented simply with a view:
-
-```lucee
-
-
-
-
-
- #args.renderedForm#
-
-
-```
-
-### i18n for layout name
-
-For each custom layout that you provide, an entry should be added to the `/i18n/formbuilder/layouts/form.properties` file to provide a title for layout choice menus. For instance, if you created a layout called 'stacked', you would add the following:
-
-```properties
-# /i18n/formbuilder/layouts/form.properties
-
-stacked.title=Stacked layout
-```
-
-## Item layouts
-
-Form item layouts are implemented in a similar way to form layouts. Viewlets matching the pattern `formbuilder.layouts.formfield.(yourlayout)` will be automatically registered as _global_ layouts for _all_ form field items.
-
-In addition, specific layouts for item types can also be implemented by creating viewlets that match the pattern, `formbuilder.layouts.formfield.(youritemtype).(yourlayout)`. If an item type specific layout shares the same name as a global form field layout, the item type specific layout will be used when rendering an item for that type.
-
-### The viewlet
-
-The item layout viewlet will receive an `args` struct with:
-
-* `renderedItem`, the rendered form control
-* `error`, any error message associated with the item
-* all configuration options set on the item
-
-The default item layout looks like:
-
-```lucee
-
-
-
-
-
-
-
-
-
-
-
-
- #args.renderedItem#
-
-
-
-
-
-
-
-```
-
-### i18n for layout names
-
-Human friendly names for layouts should be added to `/i18n/formbuilder/layouts/formfield.properties`. For example, if creating a "twocolumn" layout, you should add the following:
-
-```properties
-# /i18n/formbuilder/layouts/formfield.properties
-
-twocolumn.title=Two column
-```
----
-id: extensions
-title: "Writing Extensions for Preside"
----
-
-## Introduction
-
-Extensions are a fundamental feature of Preside development that enable you to package and share Preside features with other developers and users.
-
-You can find publicly available extensions on [Forgebox](https://forgebox.io/type/preside-extensions).
-
-## Anatomy of an extension
-
-Extensions live under the `/application/extensions` folder within your Preside application, each extension with its own folder, e.g.
-
-```
-/application
- ...
- /extensions
- /my-extension-1
- /my-extension-2
- /my-extension-3
- ...
- ...
-```
-
-Each extension can then contain *all of the valid convention-based folders that your application can contain*, i.e. `/handlers`, `/services`, `/i18n`, `/preside-objects`, etc.:
-
-```
-/my-extension-1
- /config
- Config.cfc
- Wirebox.cfc
- Cachebox.cfc
- /forms
- /preside-objects
- my_extension_object.xml
- /handlers
- MyExtensionHandler.cfc
- /i18n
- /preside-objects
- my_extension_object.properties
- /layouts
- MyExtensionLayout.cfm
- /preside-objects
- my_extension_object.cfc
- /services
- MyExtensionService.cfc
- box.json
- manifest.json
- ModuleConfig.cfc
-
-```
-
-### Extension metadata
-
-#### manifest.json (required)
-
-The `manifest.json` file is a Preside specific file that tells the system about your extension. It is a simple json object with five keys:
-
-```json
-{
- "id" : "preside-ext-my-cool-extension"
- , "title" : "My Cool Extension"
- , "author" : "Pixl8 Group"
- , "version" : "1.0.0+0001"
- , "dependsOn" : [ "preside-ext-another-cool-extension", "preside-ext-calendar-view" ]
-}
-```
-
-* `id`: Extension ID / slug. Used to identify the extension to other extension's `dependsOn` directives
-* `title`: A human readable title of the extension
-* `author`: The author, e.g. you
-* `version`: Current version
-* `dependsOn`: An array of string extension IDs (optional). This informs Preside that your extension should be loaded AFTER any extensions listed here.
-
-#### box.json (optional, recommended)
-
-The `box.json` file is used by [CommandBox](https://www.duckduckgo.com/?q=CommandBox) package management to understand how to publish and install your extension. There are several key attributes that relate to Preside extensions and an additional section that is designed purely to handle Preside specific dependencies of your extension:
-
-```json
-{
-
- // important for Preside extensions
- "type":"preside-extensions",
- "directory":"application/extensions",
-
- // regular CommandBox package management meta
- "name":"PresideCMS Extension: Calendar View",
- "slug":"preside-ext-calendar-view",
- "version":"1.2.0+4958",
- // etc...
-
- // Preside dependency specific meta
- // used during 'box install' process
- // to validate/autoinstall dependencies
- // (optional)
- "preside" : {
- "minVersion" : "10.6.19",// optional minimum version of Preside the extension works with
- "maxVersion" : "10.10",// optional maximum version of Preside the extension works with
-
- // list of preside *extension* dependencies
- // to auto-install if not already installed
- "dependencies":{
- "preside-ext-saml2-sso":{
- "installVersion":"preside-ext-saml2-sso@^4.0.5", // version to auto-install if not already installed (required)
- "minVersion":"3", // (optional) minimum allowed version of dependency
- "maxVersion":"4", // (optional) maximum allowed version of dependency
- }
- },
-
- // list of preside *extension* compatibility issues
- // block install if compatibility issues are found
- "compatibility":{
- "preside-ext-old-ext":{
- "compatible":false, // if completely incompatible
- "message":"Custom message to show if compatibility issue is found"
- },
- "preside-ext-another-old-ext":{
- "minVersion":"1.0.0", // i.e. if another-old-ext is installed, it must be at least 1.0.0 to be compatible with this extension
- "maxVersion":"^1.2.0", // i.e. if another-old-ext is installed, it must be no greater than 1.2.x to be compatible with this extension
- "message":"Custom message to show if compatibility issue is found"
- }
- }
- }
-}
-```
-
->>> The `preside` section of `box.json` will only do anything if you have the latest version of [Preside CommandBox Commands](https://www.github.com/pixl8/Preside-CMS-Commandbox-Commands) (v4.0.0 at time of writing). Install with: `box install preside-commands`.
-
-#### ModuleConfig.cfc (optional)
-
-Preside extensions can act as ColdBox modules! This allows you to:
-
-* Install private module dependencies for your extension. e.g. there may be a specific version of a Module in forgebox that you want to come bundled explicitly with your extension
-* Set an independent mapping for your extension
-* Use any other Coldbox Module features from within your extension
-
-In order to register your extension as a module, simply create a `ModuleConfig.cfc` file in the root directory of the extension. A minimal example might look like:
-
-```luceescript
-component {
- this.title = "My Awesome Extension";
- this.author = "Pixl8 Group";
- this.cfmapping = "myawesomeextension";
-
- function configure(){}
-}
-```
-
-### Config
-
-Coldbox and Wirebox config files that can appear in your application's `/application/config` folder can also appear in your extension's own `/config` folder. Be aware however, that they are defined slightly differently from those of your application. The key difference is that they do not extend any components and receive special references to their methods to use (rather than setting configuration in the scope of the CFCs). See docs below for each file:
-
-#### Config.cfc
-
-This file is for core Preside and Coldbox configuration and configuration overrides. The CFC must define a `configure( required struct config )` method. This method accepts a `config` argument that must be used to augment and modify the application configuration. For example:
-
-```luceescript
-component {
-
- public void function configure( required struct config ) {
- var conf = arguments.config;
- var settings = conf.settings ?: {};
-
- // settings specific to my extension
- settings.features.mynewfeature = { enabled=true };
- settings.myExtensionSettings = settings.myExtensionSettings ?: {
- settingOne = true,
- settingTwo = false
- };
-
- // registering a Coldbox interceptor
- conf.interceptors.append( { class="app.extensions.my-extension.interceptors.MyCoolInterceptor", properties={} } );
-
- // overriding/modifying existing settings:
- settings.adminConfigurationMenuItems.append( "mySystemMenuItem" );
-
- // ... etc
- }
-}
-```
-
-#### Wirebox.cfc
-
-Define this file in order to register custom model files (services) that require manual registration. The CFC must define a `configure( binder )` method that accepts the Wirebox `binder` object that can be used to register instances. For example:
-
-```luceescript
-component {
-
- public void function configure( required any binder ) {
- var settings = arguments.binder.getColdbox().getSettingStructure();
-
- arguments.binder.map( "applePassKeyStorageProvider" ).to( "preside.system.services.fileStorage.FileSystemStorageProvider" )
- .initArg( name="rootDirectory" , value=settings.uploads_directory & "/applePassKeys" )
- .initArg( name="trashDirectory" , value=settings.uploads_directory & "/.trash" )
- .initArg( name="privateDirectory" , value=settings.uploads_directory & "/applePassKeys" )
- .initArg( name="rootUrl" , value="" );
- }
-
-}
-```
-
->>> Any CFC files that are placed beneath the `/services` directory in the root of your extension will *automatically* be registered with Wirebox and do not need to be manually registered.
-
-
-### ColdBox and Preside folders
-
-#### /forms
-
-Define `.xml` form files here in accordance with the [[presideforms|Forms system]]. Any files that match the relative path of forms defined in core Preside, other extensions, or the application, *will be merged* (see [[presideforms]]).
-
-#### /handlers
-
-Define ColdBox handlers here. The system will mix and match handler **actions** from handler files in extensions, core preside and the application. This allows you to augment existing handlers with new actions in your extension.
-
-#### /helpers
-
-Define coldbox UDF helper `.cfm` files in here that will be available to handlers and views throughout the application.
-
-#### /i18n
-
-Define i18n `.properties` file here in accordance with the [[i18n|i18n system]]. Files whose path matches those defined elsewhere will have their property keys merged.
-
-This allows you to supply our own files and also override specific key translations from Preside core/other extensions.
-
-#### /layouts
-
-Define ColdBox layout files here. Any layouts that match the filename of a layout in core Preside, or a layout file in a preceding extension, will override their counterpart. This means you can, for example, create an extension that completely overrides the Preside admin layout (not necessarily advised, but possible!).
-
-#### /preside-objects
-
-Define Preside objects as per the documentation [[dataobjects]] here. If the object name matches that of an already defined object, its properties will be mixed in. This allows you to decorate pre-defined objects in core Preside and other extensions, adding, modifying and removing properties as well as adding annotations to the object itself.
-
-#### /services
-
-Any CFC files in the services directory will be automatically added to Wirebox by name. i.e. if you create `/services/MyService.cfc`, you will be able to retrieve an instance of it with `getModel( 'myService' )`.
-
-Warning: if you create a service with the same name as a service in core Preside or a preceding extension, your extension's service will *replace* it. This can be a useful feature, but should be used with caution.
-
-#### /views
-
-Define ColdBox view files here. Any views that match the relative path and filename of a view in core Preside, or a view file in a preceding extension, will override their counterpart. This means you can, for example, create an extension that completely overrides the Preside admin view for 'add record'.---
-id: spreadsheets
-title: Working with spreadsheets
----
-
-As of v10.5.0, Preside comes with a built in spreadsheet library. Lucee itself does not have any out-of-box `>> In Coldbox 4.0, the file was renamed to `Coldbox.cfc`. However, for backward compatibility, we continue to use `Config.cfc`.
-
-### TODO Lots more documentation of Config.cfc!
-
-### TODO Cachebox.cfc
-
-### TODO Wirebox.cfc
-
-### TODO Routes.cfm
-
-## Injecting Environment variables
-
-Environment variables can be made available to Preside in three ways. **In each instance**, the environment variables will be available to you in the struct: `settings.env`. For example, if a variable 'fu=bar' was injected, you would be able to access and use it with:
-
-```
-settings.fu = settings.env.fu;
-```
-
->>> Prior to 10.11.0, these variables were available to you as `settings.injectedConfig`. This variable will still exist to maintain backward compatibility, but we suggest using `settings.env` from now on.
-
-### Method one: Environment file
-
-As of **10.11.0**, you can create a file named `.env` at the root of your project. Variables are defined as `key=value` pairs on newlines. For example:
-
-```
-syncdb=false
-forcessl=true
-alloweddomains=www.mysite.com,api.mysite.com
-```
-
-_We suggest that this config file is not commited to your repository. Instead, generated it as part of your build or deploy process to dynamically set environment variables per environment._
-
-### Method two: "Injected Configuration" file
-
-Supply a json file at `/application/config/.injectedConfiguration` that contains any settings that you wish to inject. For example:
-
-```json
-{
- "syncDb" : false
- , "forceSsl" : true
- , "allowedDomains" : "www.mysite.com,api.mysite.com"
-}
-```
-
-_We suggest that this config file is not commited to your repository. Instead, generated it as part of your build or deploy process to dynamically set environment variables per environment._
-
-### Method three: OS environment vars
-
-Any operating system environment variables that are prefixed with `PRESIDE_` will automatically be available in your `settings.injectedConfig` struct. For example, you may have the following environment vars available to your server/container:
-
-```
-PRESIDE_syncDb=false
-PRESIDE_forceSsl=true
-PRESIDE_allowedDomains=www.mysite.com,api.mysite.com
-```
-
-These would be available in your application + Config.cfc as (i.e. the `PRESIDE_` prefix is stripped):
-
-```luceescript
-settings.env = {
- syncDb = false
- , forceSsl = true
- , allowedDomains = "www.mysite.com,api.mysite.com"
-};
-```
----
-id: admingritternotifications
-title: "Configuring admin 'gritter' notifications"
----
-
-## Introduction
-
-Gritter notifications appear in the admin after successful inserting, saving and deleting of records, or when an error happens. Up until Preside 10.11.0, these notifications appeared at the top right hand side of the admin UI and this was not configurable.
-
-As of Preside 10.11.0, the default position of these notifications is at the bottom right hand side of the screen and two new configuration options were added that you can set in your application or extension's `Config.cfc$configure()` method:
-
-
-```luceescript
-component {
-
- function configure() {
- // ...
- settings.adminNotificationsSticky = true; // default
- settings.adminNotificationsPosition = "bottom-right"; // default
- // ...
- }
-}
-```
-
-**Sticky** notifications require the user to dismiss the notification before it disappears (default). If set to false, the notification will disappear after some time.
-
-Valid positions for the `adminNotificationsPosition` setting are:
-
-* `top-left`
-* `top-right`
-* `bottom-left`
-* `bottom-right` (default)---
-id: dataobjects
-title: Data objects
----
-
-## Overview
-
-**Preside Data Objects** are the data layer implementation for Preside. Just about everything in the system that persists data to the database uses Preside Data Objects to do so.
-
-The Preside Data Objects system is deeply integrated into the CMS:
-
-* Input forms and other administrative GUIs can be automatically generated for your preside objects
-* [[dataobjectviews]] provide a way to present your data to end users without the need for handler or service layers
-* The Data Manager provides a GUI for managing your client specific data and is based on entirely on Preside Data Objects
-* Your preside objects can have their data tied to individual [[workingwithmultiplesites]], without the need for any extra programming of site filters.
-
-The following guide is intended as a thorough overview of Preside Data Objects. For API reference documentation, see [[api-presideobjectservice]].
-
-## Object CFC Files
-
-Data objects are represented by ColdFusion Components (CFCs). A typical object will look something like this:
-
-```luceescript
-component {
- property name="name" type="string" dbtype="varchar" maxlength="200" required=true;
- property name="email_address" type="string" dbtype="varchar" maxlength="255" required=true uniqueindexes="email";
-
- property name="tags" relationship="many-to-many" relatedto="tag";
-}
-```
-
-A singe CFC file represents a table in your database. Properties defined using the `property` tag represent fields and/or relationships on the table.
-
-### Database table names
-
-By default, the name of the database table will be the name of the CFC file prefixed with **pobj_**. For example, if the file was `person.cfc`, the table name would be **pobj_person**.
-
-You can override these defaults with the `tablename` and `tableprefix` attributes:
-
-```luceescript
-/**
- * @tablename mytable
- * @tableprefix mysite_
- */
-component {
- // .. etc.
-}
-```
-
->>> All of the preside objects that are provided by the core Preside system have their table names prefixed with **psys_**.
-
-### Registering objects
-
-The system will automatically register any CFC files that live under the `/application/preside-objects` folder of your site (and any of its sub-folders). Each .cfc file will be registered with an ID that is the name of the file without the ".cfc" extension.
-
-For example, given the directory structure below, *four* objects will be registered with the IDs *blog*, *blogAuthor*, *event*, *eventCategory*:
-
-```
-/application
- /preside-objects
- /blogs
- blog.cfc
- blogAuthor.cfc
- /events
- event.cfc
- eventCategory.cfc
-```
-
->>> Notice how folder names are ignored. While it is useful to use folders to organise your Preside Objects, they carry no logical meaning in the system.
-
-#### Extensions and core objects
-
-For extensions, the system will search for CFC files in a `/preside-objects` folder at the root of your extension.
-
-Core system Preside Objects can be found at `/preside/system/preside-objects`.
-
-## Properties
-
-Properties represent fields on your database table or mark relationships between objects (or both).
-
-Attributes of the properties describe details such as data type, data length and validation requirements. At a minimum, your properties should define a *name*, *type* and *dbtype* attribute. For *varchar* fields, a *maxLength* attribute is also required. You will also typically need to add a *required* attribute for any properties that are a required field for the object:
-
-```luceescript
-component {
- property name="name" type="string" dbtype="varchar" maxLength="200" required=true;
- property name="max_delegates" type="numeric" dbtype="int"; // not required
-}
-```
-
-### Standard attributes
-
-While you can add any arbitrary attributes to properties (and use them for your own business logic needs), the system will interpret and use the following standard attributes:
-
-
-
-
-
-
Name
-
Required
-
Default
-
Description
-
-
-
-
name
Yes
*N/A*
Name of the field
-
type
No
"string"
CFML type of the field. Valid values: *string*, *numeric*, *boolean*, *date*
-
dbtype
No
"varchar"
Database type of the field to be define on the database table field
-
maxLength
No
0
For dbtypes that require a length specification. If zero, the max size will be used.
-
required
No
**false**
Whether or not the field is required.
-
default
No
""
A default value for the property. Can be dynamically created, see :ref:`presideobjectsdefaults`
-
indexes
No
""
List of indexes for the field, see :ref:`preside-objects-indexes`
-
uniqueindexes
No
""
List of unique indexes for the field, see :ref:`preside-objects-indexes`
-
control
No
"default"
The default form control to use when rendering this field in a Preside Form. If set to 'default', the value for this attribute will be calculated based on the value of other attributes. See :doc:`/devguides/formcontrols` and :doc:`/devguides/formlayouts`.
-
renderer
No
"default"
The default content renderer to use when rendering this field in a view. If set to 'default', the value for this attribute will be calculated based on the value of other attributes. (reference needed here).
-
minLength
No
*none*
Minimum length of the data that can be saved to this field. Used in form validation, etc.
-
minValue
No
*none*
The minumum numeric value of data that can be saved to this field. *For numeric types only*.
-
maxValue
No
*N/A*
The maximum numeric value of data that can be saved to this field. *For numeric types only*.
-
format
No
*N/A*
Either a regular expression or named validation filter (reference needed) to validate the incoming data for this field
-
pk
No
**false**
Whether or not this field is the primary key for the object, *one field per object*. By default, your object will have an *id* field that is defined as the primary key. See :ref:`preside-objects-default-properties` below.
-
generator
No
"none"
Named generator for generating a value for this field when inserting/updating a record with the value of this field ommitted. See "Generated fields", below.
-
generate
No
"never"
If using a generator, indicates when to generate the value. Valid values are "never", "insert" and "always".
-
formula
No
""
Allows you to define a field that does not exist in the database, but can be selected and used in the application. This attribute should consist of arbitrary SQL to produce a value. See "Formula fields", below.
-
relationship
No
"none"
Either *none*, *many-to-one* or *many-to-many*. See :ref:`preside-objects-relationships`, below.
-
relatedTo
No
"none"
Name of the Preside Object that the property is defining a relationship with. See :ref:`preside-objects-relationships`, below.
-
relatedVia
No
""
Name of the object through which a many-to-many relationship will pass. If it does not exist, the system will created it for you. See :ref:`preside-objects-relationships`, below.
-
relationshipIsSource
No
**true**
In a many-to-many relationship, whether or not this object is regarded as the "source" of the relationship. If not, then it is regarded as the "target". See :ref:`preside-objects-relationships`, below.
-
relatedViaSourceFk
No
""
The name of the source object's foreign key field in a many-to-many relationship's pivot table. See :ref:`preside-objects-relationships`, below.
-
relatedViaTargetFk
No
""
The name of the target object's foreign key field in a many-to-many relationship's pivot table. See :ref:`preside-objects-relationships`, below.
-
enum
No
""
The name of the configured enum to use with this field. See "ENUM properties", below.
-
aliasses
No
""
List of alternative names (aliasses) for the property.
-
-
-
-### Default properties
-
-The bare minimum code requirement for a working Preside Data Object is:
-
-```luceescript
-component {}
-```
-
-Yes, you read that right, an "empty" CFC is an effective Preside Data Object. This is because, by default, Preside Data Objects will be automatically given `id`, `label`, `datecreated` and `datemodified` properties. The above example is equivalent to:
-
-```luceescript
-component {
- property name="id" type="string" dbtype="varchar" required=true maxLength="35" generator="UUID" pk=true;
- property name="label" type="string" dbtype="varchar" required=true maxLength="250";
- property name="datecreated" type="date" dbtype="datetime" required=true;
- property name="datemodified" type="date" dbtype="datetime" required=true;
-}
-```
-
-#### The ID Field
-
-The ID field will be the primary key for your object. We have chosen to use a UUID for this field so that data migrations between databases are achievable. If, however, you wish to use an auto incrementing numeric type for this field, you could do so by overriding the `type`, `dbtype` and `generator` attributes:
-
-```luceescript
-component {
- property name="id" type="numeric" dbtype="int" generator="increment";
-}
-```
-
-The same technique can be used to have a primary key that does not use any sort of generator (you would need to pass your own IDs when inserting data):
-
-```luceescript
-component {
- property name="id" generator="none";
-}
-```
-
->>>>>> Notice here that we are just changing the attributes that we want to modify (we do not specify `required` or `pk` attributes). All the default attributes will be applied unless you specify a different value for them.
-
-#### The Label field
-
-The **label** field is used by the system for building automatic GUI selectors that allow users to choose your object records.
-
-![Screenshot showing a record picker for a "Blog author" object](images/screenshots/object_picker_example.png)
-
-
-If you wish to use a different property to represent a record, you can use the `labelfield` attribute on your CFC, e.g.:
-
-```luceescript
-/**
- * @labelfield title
- *
- */
-component {
- property name="title" type="string" dbtype="varchar" maxlength="100" required=true;
- // etc.
-}
-```
-
-If you do not want your object to have a label field at all (i.e. you know it is not something that will ever be selectable, and there is no logical field that might be used as a string representation of a record), you can add a `nolabel=true` attribute to your CFC:
-
-```luceescript
-/**
- * @nolabel true
- *
- */
-component {
- // ... etc.
-}
-```
-
-#### The DateCreated and DateModified fields
-
-These do exactly what they say on the tin. If you use the APIs to insert and update your records, the values of these fields will be set automatically for you.
-
-
-### Default values for properties
-
-You can use the `default` attribute on a property tag to define a default value for a property. This value will be used during an `insertData()` operation when no value is supplied for the property. E.g.
-
-```luceescript
-component {
- // ...
- property name="max_attendees" type="numeric" dbtype="int" required=false default=100;
-}
-```
-
-#### Dynamic defaults
-
-Default values can also be generated dynamically at runtime. Currently, this comes in two flavours:
-
-1. Supplying raw CFML to be evaluated at runtime
-2. Supplying the name of a method defined in your object that will be called at runtime, this method will be passed a 'data' argument that is a structure containing the data to be inserted
-
-For raw CFML, prefix your value with `cfml:`, e.g. `cfml:CreateUUId()`. For methods that are defined on your object, use `method:methodName`. e.g.
-
-```luceescript
-component {
- // ...
- property name="event_start_date" type="date" dbtype="date" required=false default="cfml:Now()";
- property name="slug" type="string" dbtype="varchar" maxlength="200" required=false default="method:calculateSlug";
-
- public string function calculateSlug( required struct data ) {
- return LCase( ReReplace( data.label ?: "", "\W", "_", "all" ) );
- }
-}
-```
-
->>> As of Preside 10.8.0, this approach is deprecated and you should use generated fields instead (see below)
-
-### Generated fields
-
-As of **10.8.0**, generators allow you to dynamically generate the value of a property when a record is first being inserted and, optionally, when a record is updated. The `generate` attribute of a property dictates _when_ to use a generator. Valid values are:
-
-* `never` (default), never generate the value
-* `insert`, only generate a value when a record is first inserted
-* `always`, generate a value on both insert and update of records
-
-The `generator` attribute itself then allows you to use a system pre-defined generator or use your own by prefixing the generator with `method:` (the method name that follows should be defined on your object). For example:
-
-```luceescript
-component {
- // ...
-
- property name="alternative_pk" type="string" dbtype="varchar" maxlength=35 generate="insert" generator="UUID";
- property name="description" type="string" dbtype="text";
- property name="description_hash" type="string" dbtype="varchar" maxlength=32 generate="always" generator="method:hashDescription";
-
- // ...
-
- // The method will receive a single argument that is the struct
- // of data passed to the insertData() or updateData() methods
- public any function hashDescription( required struct changedData ) {
- if ( changedData.keyExists( "description" ) ) {
- if ( changedData.description.len() ) {
- return Hash( changedData.description );
- }
-
- return "";
- }
- return; // return NULL to not alter the value when no description is being updated
- }
-}
-```
-
-The core system provides you with these named generators:
-
-* `UUID` - uses `CreateUUId()` to generate a UUID for your field. This is used by default for the primary key in preside objects.
-* `timestamp` - uses `Now()` to auto generate a timestamp for your field
-* `hash` - used in conjunction with a `generateFrom` attribute that should be a list of other properties which to concatenate and generate an MD5 hash from
-* `nextint` - **introduced in 10.12.0**, gives the next incremental integer value for the field
-* `slug` - takes an optional `generateFrom` attribute that defines which field (if present in the submitted data) should be used to generate the slug; by default it will use the object's label field. A unique slug will be generated, so may be suffixed with `-1`, `-2`, etc.
-
-#### Developer provided generators
-
-As of **10.13.0**, you are able to create convention based handler actions for generators. The convention based handler name for any generator is `generators.{generatorname}`.
-
-For example, the property below would attempt to use a handler action of `generators.my.generator`, i.e. a file `/handlers/generators/My.cfc` with a `generator()` method.
-
-```luceescript
-property name="is_cool" ... generator="my.generator";
-```
-
-Your handler action will receive an `args` struct in the arguments with the following keys:
-
-* `objectName`: the name of the object whose record is being added/updated
-* `id`: the ID of the record (for updates only)
-* `generator`: the full generator string used
-* `data`: a struct with the data being passed to the insert/update operation
-* `prop`: a struct with all the property attributes of the property whos value is being generated
-
-##### Example
-
-```luceescript
-component {
-
- private boolean function generator( event, rc, prc, args={} ) {
- return IsTrue( args.data.under_thirty ?: "" ) && ( ( args.status ?: "" ) == "active" );
- }
-
-}
-```
-
-### Formula fields
-
-Properties that define a formula are not generated as fields in your database tables. Instead, they are made available to your application to be selected in `selectData` queries. The value of the `formula` attribute should be a valid SQL statement that can be used in a SQL `select` statement and include `${prefix}` tokens before any field definitions (see below for an explanation). For example:
-
-```luceescript
-/**
- * @datamanagerGridFields title,comment_count,datemodified
- *
- */
-component {
- // ...
-
- property name="comments" relationship="one-to-many" relatedto="article_comment";
- property name="comment_count" formula="Count( distinct ${prefix}comments.id )" type="numeric";
-
- // ...
-}
-```
-
-```luceescript
-articles = articleDao.selectData(
- selectFields = [ "id", "title", "comment_count" ]
-);
-```
-
-Formula fields can also be used in your DataManager data grids and be assigned labels in your object's i18n `.properties` file.
-
->>> Note that formula fields are only selected when _explicitly defined_ in your `selectFields`. If you leave `selectData` to return "all" fields, only the properties that are stored in the database will be returned.
-
-#### Formula ${prefix} token
-
-The `${prefix}` token in formula fields allows your formula field to be used in more complex select queries that traverse your data model's relationships. Another example, this time a `person` cfc:
-
-```luceescript
-component {
- // ...
- property name="first_name" ...;
- property name="last_name" ...;
-
- property name="full_name" formula="Concat( ${prefix}first_name, ' ', ${prefix}last_name )";
- // ...
-}
-```
-Now, let us imagine we have a company object, with an "employees" `one-to-many` property that relates to our `person` object above. We may want to select employees from a company:
-
-```luceescript
-var employees = companyDao.selectData(
- id = arguments.companyId
- , selectFields = [ "employees.id", "employees.full_name" ]
-);
-```
-
-The `${prefix}` token allows us to take the `employees.` prefix of the `full_name` field and replace it so that the final select SQL becomes: `Concat( employees.first_name, ' ', employees.last_name )`. Without a `${prefix}` token, your formula field will only work when selecting directly from the object in which the property is defined, it will not work when traversing relationships as with the example above.
-
-#### Aggregate functions in formula fields
-
-As of **10.23.0**, a new syntax for aggregate functions within formula fields is available, which gives significant performance gains in the generated SQL queries.
-
-Whereas previously you may have written:
-
-```luceescript
-property name="comment_count" type="numeric" formula="count( distinct ${prefix}comments.id )";
-property name="latest_comment_reply" type="date" formula="max( ${prefix}comments$replies.date )";
-```
-
-...these would now be written like this:
-
-```luceescript
-property name="comment_count" type="numeric" formula="agg:count{ comments.id }";
-property name="latest_comment_reply" type="date" formula="agg:max{ comments$replies.date }";
-```
-
-The syntax takes the form `agg:` followed by the aggregate function name (count, min, max, sum, avg) and then the property to be aggregated contained within curly braces `{}`. Note that `${prefix}` is not required.
-
-The existing syntax will still work, but the new syntax should provide improved performance - especially when multiple formulas are included in the same query, and when the volumes of data involved grow larger. Existing `count()` formulae will automatically be detected and will make use of the optimisation.
-
-
-### ENUM properties
-
-Properties defined with an `enum` attribute implement an application enforced ENUM system. Named ENUM types are defined in your application's `Config.cfc` and can then be attributed to a property which then automatically limits and validates the options that are available to the field. ENUM options are saved to the database as a plain string; we avoid any mapping with integer values to keep the implementation portable and simple. Example ENUM definitions in `Config.cfc`:
-
-```luceescript
-settings.enum = {};
-settings.enum.redirectType = [ "301", "302" ];
-settings.enum.pageAccessRestriction = [ "inherit", "none", "full", "partial" ];
-settings.enum.pageIframeAccessRestriction = [ "inherit", "block", "sameorigin", "allow" ];
-```
-
-In addition to the `Config.cfc` definition, each ENUM type should have a corresponding `.properties` file to define the labels and optional description of each item. The file must live at `/i18n/enum/{enumTypeId}.properties`. For example:
-
-
-```properties
-# /i18n/enum/redirectType.properties
-301.label=301 Moved Permanently
-301.description=A 301 redirect indicates that the resource has been *permanently* moved to the new locations. This is particularly important to use for moved content as it instructs search engines to index the new location, potentially without losing any SEO rankings. Browsers will aggressively cache these redirects to avoid wasted calls to a URL that it has been told is moved.
-
-302.label=302 Found (Temporary redirect)
-302.description=A 302 redirect indicates that the resource has been *temporarily* moved to the new location. Use this only when you know that you will/might reinstate the original source URL at some point in time.
-```
-
-### Defining relationships with properties
-
-Relationships are defined on **property** tags using the `relationship` and `relatedTo` attributes. For example:
-
-```luceescript
-// eventCategory.cfc
-component {}
-
-// event.cfc
-component {
- property name="category" relationship="many-to-one" relatedto="eventCategory" required=true;
-}
-```
-
-If you do not specify a `relatedTo` attribute, the system will assume that the foreign object has the same name as the property field. For example, the two objects below would be related through the `eventCategory` property of the `event` object:
-
-```luceescript
-// eventCategory.cfc
-component {}
-
-// event.cfc
-component {
- property name="eventCategory" relationship="many-to-one" required=true;
-}
-```
-
-#### One to Many relationships
-
-In the examples, above, we define a **one to many** style relationship between `event` and `eventCategory` by adding a foreign key property to the `event` object.
-
-The `category` property will be created as a field in the `event` object's database table. Its datatype will be automatically derived from the primary key field in the `eventCategory` object and a Foreign Key constraint will be created for you.
-
->>> The `event` object lives on the **many** side of this relationship (there are *many events* to *one category*), hence why we use the relationship type, *many-to-one*.
-
-You can also declare the relationship on the other side (i.e. the 'one' side). This will allow you to traverse the relationship from either angle. e.g. we could add a 'one-to-many' property on the `eventCategory.cfc` object; this will not create a field in the database table, but will allow you to query the relationship from the category viewpoint:
-
-```luceescript
-// eventCategory.cfc
-component {
- // note that the 'relationshipKey' property is the FK in the event object
- // this will default to the name of this object
- property name="events" relationship="one-to-many" relatedTo="event" relationshipKey="eventCategory";
-}
-
-// event.cfc
-component {
- property name="eventCategory" relationship="many-to-one" required=true;
-}
-```
-
-#### Many to Many relationships
-
-If we wanted an event to be associated with multiple event categories, we would want to use a **Many to Many** relationship:
-
-```luceescript
-// eventCategory.cfc
-component {}
-
-// event.cfc
-component {
- property name="eventCategory" relationship="many-to-many";
-}
-```
-
-In this scenario, there will be no `eventCategory` field created in the database table for the `event` object. Instead, a "pivot" database table will be automatically created that looks a bit like this (in MySQL):
-
-```sql
--- table name derived from the two related objects, delimited by __join__
-create table `pobj_event__join__eventcategory` (
- -- table simply has a field for each related object
- `event` varchar(35) not null
- , `eventcategory` varchar(35) not null
-
- -- plus we always add a sort_order column, should you care about
- -- the order in which records are related
- , `sort_order` int(11) default null
-
- -- unique index on the event and eventCategory fields
- , unique key `ux_event__join__eventcategory` (`event`,`eventcategory`)
-
- -- foreign key constraints on the event and eventCategory fields
- , constraint `fk_1` foreign key (`event` ) references `pobj_event` (`id`) on delete cascade on update cascade
- , constraint `fk_2` foreign key (`eventcategory`) references `pobj_eventcategory` (`id`) on delete cascade on update cascade
-) ENGINE=InnoDB;
-```
-
->>> Unlike **many to one** relationships, the **many to many** relationship can be defined on either or both objects in the relationship. That said, you will want to define it on the object(s) that make use of the relationship. In the event / eventCategory example, this will most likely be the event object. i.e. `event.insertData( label=eventName, eventCategory=listOfCategoryIds )`.
-
-#### "Advanced" Many to Many relationships
-
-You can excert a little more control over your many-to-many relationships by making use of some extra, non-required, attributes:
-
-```luceescript
-// event.cfc
-component {
- property name = "eventCategory"
- relationship = "many-to-many"
- relatedTo = "eventCategory"
- relationshipIsSource = false // the event object is regarded as the 'target' side of the relationship rather than the 'source' (default is 'source' when relationship defined in the object)
- relatedVia = "event_categories" // create a new auto pivot object called "event_categories" rather than the default "event__join__eventCategory"
- relatedViaSourceFk = "cat" // name the foreign key field to the source object (eventCategory) to be just 'cat'
- relatedViaTargetFk = "ev"; // name the foreign key field to the target object (event) to be just 'ev'
-}
-```
-
-TODO: explain these in more detail. In short though, these attributes control the names of the pivot table and foreign keys that get automatically created for you. If you leave them out, Preside will figure out sensible defaults for you.
-
-As well as controlling the automatically created pivot table name with "relatedVia", you can also use this attribute to define a relationship that exists through a pre-existing pivot object.
-
->>>>>> If you have multiple many-to-many relationships between the same two objects, you will **need** to use the `relatedVia` attribute to ensure that a different pivot table is created for each context.
-
-#### Subquery relationships with "SelectData Views"
-
-In **10.11.0** the concept of [[selectdataviews]] was introduced. These 'views' are loosely synonymous with SQL views in that they allow you to store a complex query and reference it by a simple name.
-
-They can be used in relationship helper properties and result in subqueries being created when querying them. The syntax is the same as that of a `one-to-many` relationship:
-
-```
-component {
- property name="active_posts" relationship="select-data-view" relatedTo="activePosts" relationshipKey="blog_category";
-}
-```
-
-See [[selectdataviews]] for more.
-
-### Defining indexes and unique constraints
-
-The Preside Object system allows you to define database indexes on your fields using the `indexes` and `uniqueindexes` attributes. The attributes expect a comma separated list of index definitions. An index definition can be either an index name or combination of index name and field position, separated by a pipe character. For example:
-
-```luceescript
-// event.cfc
-component {
- property name="category" indexes="category,categoryName|1" required=true relationship="many-to-one" ;
- property name="name" indexes="categoryName|2" required=true type="string" dbtype="varchar" maxlength="100";
- // ...
-}
-```
-
-The example above would result in the following index definitions:
-
-```sql
-create index ix_category on pobj_event( category );
-create index ix_categoryName on pobj_event( category, name );
-```
-
-The exact same syntax applies to unique indexes, the only difference being the generated index names are prefixed with `ux_` rather than `ix_`.
-
-## Keeping in sync with the database
-
-When you reload your application, the system will attempt to synchronize your object definitions with the database. While it does a reasonably good job at doing this, there are some considerations:
-
-* If you add a new, required, field to an object that has existing data in the database, an exception will be raised. This is because you cannot add a `NOT NULL` field to a table that already has data. *You will need to provide upgrade scripts to make this type of change to an existing system.*
-
-* When you delete properties from your objects, the system will rename the field in the database to `_deprecated_yourfield`. This prevents accidental loss of data but can lead to a whole load of extra fields in your DB during development.
-
-* The system never deletes whole tables from your database, even when you delete the object file
-
-## Working with the API
-
-The `PresideObjectService` service object provides methods for performing CRUD operations on the data along with other useful methods for querying the metadata of each of your data objects. There are two ways in which to interact with the API:
-
-1. Obtain an instance the `PresideObjectService` and call its methods directly
-2. Obtain an "auto service object" for the specific object you wish to work with and call its decorated CRUD methods as well as any of its own custom methods
-
-You may find that all you wish to do is to render a view with some data that is stored through the Preside Object service. In this case, you can bypass the service layer APIs and use the [[presidedataobjectviews]] system instead.
-
-
-### Getting an instance of the Service API
-
-We use [Wirebox](http://wiki.coldbox.org/wiki/WireBox.cfm) to auto wire our service layer. To inject an instance of the service API into your service objects and/or handlers, you can use wirebox's "inject" syntax as shown below:
-
-```luceescript
-
-// a handler example
-component {
- property name="presideObjectService" inject="presideObjectService";
-
- function index( event, rc, prc ) {
- prc.eventRecord = presideObjectService.selectData( objectName="event", id=rc.id ?: "" );
-
- // ...
- }
-}
-
-// a service layer example
-// (here at Pixl8, we prefer to inject constructor args over setting properties)
-component {
-
- /**
- * @presideObjectService.inject presideObjectService
- */
- public any function init( required any presideObjectService ) {
- _setPresideObjectService( arguments.presideObjectService );
-
- return this;
- }
-
- public query function getEvent( required string id ) {
- return _getPresideObjectService().selectData(
- objectName = "event"
- , id = arguments.id
- );
- }
-
- // we prefer private getters and setters for accessing private properties, this is our house style
- private any function _getPresideObjectService() {
- return variables._presideObjectService;
- }
- private void function _setPresideObjectService( required any presideObjectService ) {
- variables._presideObjectService = arguments.presideObjectService;
- }
-
-}
-```
-
-### Using Auto Service Objects
-
-An auto service object represents an individual data object. They are an instance of the given object that has been decorated with the service API CRUD methods.
-
-Calling the CRUD methods works in the same way as with the main API with the exception that the objectName argument is no longer required. So:
-
-```luceescript
-record = presideObjectService.selectData( objectName="event", id=id );
-
-// is equivalent to:
-eventObject = presideObjectService.getObject( "event" );
-record = eventObject.selectData( id=id );
-```
-
-#### Getting an auto service object
-
-This can be done using either the `getObject()` method of the Preside Object Service or by using a special Wirebox DSL injection syntax, i.e.
-
-```luceescript
-// a handler example
-component {
- property name="eventObject" inject="presidecms:object:event";
-
- function index( event, rc, prc ) {
- prc.eventRecord = eventObject.selectData( id=rc.id ?: "" );
-
- // ...
- }
-}
-
-// a service layer example
-component {
-
- /**
- * @eventObject.inject presidecms:object:event
- */
- public any function init( required any eventObject ) {
- _setPresideObjectService( arguments.eventObject );
-
- return this;
- }
-
- public query function getEvent( required string id ) {
- return _getEventObject().selectData( id = arguments.id );
- }
-
- // we prefer private getters and setters for accessing private properties, this is our house style
- private any function _getEventObject() {
- return variables._eventObject;
- }
- private void function _setEventObject( required any eventObject ) {
- variables._eventObject = arguments.eventObject;
- }
-
-}
-```
-
-### CRUD Operations
-
-The service layer provides core methods for creating, reading, updating and deleting records (see individual method documentation for reference and examples):
-
-* [[presideobjectservice-selectdata]]
-* [[presideobjectservice-insertdata]]
-* [[presideobjectservice-updatedata]]
-* [[presideobjectservice-deletedata]]
-
-In addition to the four core methods above, there are also further utility methods for specific scanarios:
-
-* [[presideobjectservice-dataexists]]
-* [[presideobjectservice-selectmanytomanydata]]
-* [[presideobjectservice-syncmanytomanydata]]
-* [[presideobjectservice-getdenormalizedmanytomanydata]]
-* [[presideobjectservice-getrecordversions]]
-* [[presideobjectservice-insertdatafromselect]]
-
-
-#### Specifying fields for selection
-
-The [[presideobjectservice-selectdata]] method accepts a `selectFields` argument that can be used to specify which fields you wish to select. This can be done by the field's name or one of it's aliasses. This can be used to select properties on your object as well as properties on related objects and any plain SQL aggregates or other SQL operations. For example:
-
-```luceescript
-records = newsObject.selectData(
- selectFields = [ "news.id", "news.title", "Concat( category.label, category$tag.label ) as catandtag" ]
-);
-```
-
-The example above would result in SQL that looked something like:
-
-```sql
-select news.id
- , news.title
- , Concat( category.label, tag.label ) as catandtag
-
-from pobj_news as news
-inner join pobj_category as category on category.id = news.category
-inner join pobj_tag as tag on tag.id = category.tag
-```
-
->>> The funky looking `category$tag.label` is expressing a field selection across related objects - in this case **news** -> **category** -> **tag**. See relationships, below, for full details.
-
-### Filtering data
-
-All but the **insertData()** methods accept a data filter to either refine the returned recordset or the records to be updated / deleted. The API provides two arguments for filtering, `filter` and `filterParams`. Depending on the type of filtering you need, the `filterParams` argument will be optional.
-
-#### Simple filtering
-
-A simple filter consists of one or more strict equality checks, all of which must be true. This can be expressed as a simple CFML structure; the structure keys represent the object fields; their values represent the expected record values:
-
-```luceescript
-records = newsObject.selectData( filter={
- category = chosenCategory
- , "category$tag.label" = "red"
-} );
-```
-
->>> The funky looking `category$tag.label` is expressing a filter across related objects - in this case **news** -> **category** -> **tag**. We are filtering news items whos category is tagged with a tag whose label field = "red".
-
-#### Complex filters
-
-More complex filters can be achieved with a plain SQL filter combined with filter params to make use of parametized SQL statements:
-
-```luceescript
-records = newsObject.selectData(
- filter = "category != :category and DateDiff( publishdate, :publishdate ) > :daysold and category$tag.label = :category$tag.label"
- , filterParams = {
- category = chosenCategory
- , publishdate = publishDateFilter
- , "category$tag.label" = "red"
- , daysOld = { type="integer", value=3 }
- }
-);
-```
-
->>> Notice that all but the *daysOld* filter param do not specify a datatype. This is because the parameters can be mapped to fields on the object/s and their data types derived from there. The *daysOld* filter has no field mapping and so its data type must also be defined here.
-
-#### Multiple filters
-
-In addition to the `filter` and `filterParams` arguments, you can also make use of an `extraFilters` argument that allows you to pass an array of structs, each with a `filter` and optional `filterParams` key. All filters will be combined using a logical AND:
-
-```luceescript
-records = newsObject.selectData(
- extraFilters = [{
- filter = { active=true }
- },{
- filter = "category != :category and DateDiff( publishdate, :publishdate ) > :daysold and category$tag.label = :category$tag.label"
- , filterParams = {
- category = chosenCategory
- , publishdate = publishDateFilter
- , "category$tag.label" = "red"
- , daysOld = { type="integer", value=3 }
- }
-
- } ]
-);
-```
-
-#### Pre-saved filters
-
-Developers are able to define named filters that can be passed to methods in an array using the `savedFilters` argument, for example:
-
-```luceescript
-records = newsObject.selectData( savedFilters = [ "activeCategories" ] );
-```
-
-These filters can be defined either in your application's `Config.cfc` file or, **as of 10.11.0**, by implementing a convention based handler. In either case, the named filter should resolve to a _struct_ with `filter` and `filterParams` keys that follow the same rules documented above.
-
-##### Defining saved filters in Config.cfc
-
-A saved filter is defined using the `settings.filters` struct. A filter can either be a struct, with `filter` and optional `filterParams` keys, _or_ an inline function that returns a struct:
-
-```luceescript
-settings.filters.activeCategories = {
- filter = "category.active = :category.active and category.pub_date > Now()"
- , filterParams = { "category.active"=true }
-};
-
-// or:
-
-settings.filters.activeCategories = function( struct args={}, cbController ) {
- return cbController.getWirebox.getInstance( "categoriesService" ).getActiveCategoriesFilter();
-}
-```
-
-##### Defining saved filters using handlers
-
-**As of 10.11.0**, these filters can be defined by _convention_ by implementing a private coldbox handler at `DataFilters.filterName`. For example, to implement a `activeCategories` filter:
-
-```luceescript
-// /handlers/DataFilters.cfc
-component {
-
- property name="categoriesService" inject="categoriesService";
-
- private struct function activeCategories( event, rc, prc, args={} ) {
- return categoriesService.getActiveCategoriesFilter();
-
- // or
-
- return {
- filter = "category.active = :category.active and category.pub_date > :category.pub_date"
- , filterParams = { "category.active"=true, "category.pub_date"=Now() }
- }
- }
-
-}
-```
-
-#### Default filters
-
-**As of 10.11.0**, developers can use **saved filters** as default filters. Default filters are filters that will be **automatically** applied to **selectData()**.
-
-##### Using default filters
-
-Default filters can be applied by passing a list of saved filters to the `@defaultFilters` annotations in the object file. For example:
-
-```luceescript
-/**
- * @defaultFilters publishedStuff,approvedStuff
- */
-component {
- // ...
-}
-```
-
-##### Ignoring default filters
-
-In case of needing to ignore the default filters, developers need to pass an array of default filters that wished to be ignored to `ignoreDefaultFilters` argument in their `selectData()`. For example:
-
-```luceescript
-allRecords = recordObject.selectData( ignoreDefaultFilters = [ "publishedStuff", "approvedStuff" ] );
-```
-
-### Making use of relationships
-
-As seen in the examples above, you can use a special field syntax to reference properties in objects that are related to the object that you are selecting data from / updating data on. When you do this, the service layer will automatically create the necessary SQL joins for you.
-
-The syntax takes the form: `(relatedObjectReference).(propertyName)`. The related object reference can either be the name of the related object, or a `$` delimited path of property names that navigate through the relationships (see examples below).
-
-This syntax can be used in:
-
-* Select fields
-* Filters
-* Order by statements
-* Group by statements
-
-To help with the examples, we'll illustrate a simple relationship between three objects:
-
-```luceescript
-
-// tag.cfc
-component {}
-
-// category.cfc
-component {
- property name="category_tag" relationship="many-to-one" relatedto="tag" required=true;
- property name="news_items" relationship="one-to-many" relatedTo="news" relationshipKey="news_category";
- // ..
-}
-
-// news.cfc
-component {
- property name="news_category" relationship="many-to-one" relatedto="category" required=true;
- // ..
-}
-```
-
-#### Auto join example
-
-```luceescript
-// update news items whose category tag = "red"
-presideObjectService.updateData(
- objectName = "news"
- , data = { archived = true }
- , filter = { "tag.label" = "red" } // the system will automatically figure out the relationship path between the news object and the tag object
-);
-```
-
-#### Property name examples
-
-```luceescript
-// delete news items whose category label = "red"
-presideObjectService.deleteData(
- objectName = "news"
- , data = { archived = true }
- , filter = { "news_category.label" = "red" }
-);
-
-// select title and category tag from all news objects, order by the category tag
-presideObjectService.selectData(
- objectName = "news"
- , selectFields = [ "news.title", "news_category$category_tag.label as tag" ]
- , orderby = "news_category$category_tag.label"
-);
-
-// selecting categories with a count of news articles for each category
-presideObjectService.selectData(
- objectName = "category"
- , selectFields = [ "category.label", "Count( news_items.id ) as news_item_count" ]
- , orderBy = "news_item_count desc"
-);
-```
-
->>>> While the auto join syntax can be really useful, it is limited to cases where there is only a single relationship path between the two objects. If there are multiple ways in which you could join the two objects, the system can have no way of knowing which path it should take and will throw an error.
-
-### Caching
-
-By default, all [[presideobjectservice-selectData]] calls have their recordset results cached. These caches are automatically cleared when the data changes.
-
-You can specify *not* to cache results with the `useCache` argument.
-
-### Cache per object
-
-**As of Preside 10.10.55**, an additional feature flag enables the setting of caches _per object_. This greatly simplifies and speeds up the cache clearing and invalidation logic which may benefit certain application profiles. The feature can be enabled in your `Config.cfc` with:
-
-```luceescript
-settings.features.queryCachePerObject.enabled = true;
-```
-
-Configuration of the `defaultQueryCache` then becomes the _default_ configuration for each individual object's own cachebox cache instance.
-
-In addition, you can annotate your Preside object with `@cacheProvider` to use a different cache provider for a specific object. Finally, any other annotation attributes on your object that begin with `@cache` will be treated as properties of the cache box cache.
-
-A common example may be to set a larger cache for a specific object with different reaping frequency and eviction count:
-
-```luceescript
-/**
- * @cacheMaxObjects 10000
- * @cacheReapFrequency 5
- * @cacheEvictCount 2000
- */
-component {
-
-}
-```
-
-## Extending Objects
-
->>>>>> You can easily extend core data objects and objects that have been provided by extensions simply by creating `.cfc` file with the same name.
-
-Objects with the same name, but from different sources, are merged at runtime so that you can have multiple extensions all contributing to the final object definition.
-
-Take the `page` object, for example. You might write an extension that adds an **allow_comments** property to the object. That CFC would look like this:
-
-```luceescript
-// /extensions/myextension/preside-objects/page.cfc
-component {
- property name="allow_comments" type="boolean" dbtype="boolean" required=false default=true;
-}
-```
-
-After adding that code and reloading your application, you would find that the **psys_page** table now had an **allow_comments** field added.
-
-Then, in your site, you may have some client specific requirements that you need to implement for all pages. Simply by creating a `page.cfc` file under your site, you can mix in properties along with the **allow_comments** mixin above:
-
-```luceescript
-// /application/preside-objects/page.cfc
-component {
- // remove a property that has been defined elsewhere
- property name="embargo_date" deleted=true;
-
- // alter attributes of an existing property
- property name="title" maxLength="50"; // strict client requirement?!
-
- // add a new property
- property name="search_engine_boost" type="numeric" dbtype="integer" minValue=0 maxValue=100 default=0;
-}
-```
-
->>> To have your object changes reflected in GUI forms (i.e. the add and edit page forms in the example above), you will likely need to modify the form definitions for the object you have changed.
-
-## Versioning
-
-By default, Preside Data Objects will maintain a version history of each database record. It does this by creating a separate database table that is prefixed with `_version_`. For example, for an object named 'news', a version table named **_version_pobj_news** would be created.
-
-The version history table contains the same fields as its twin as well as a few specific fields for dealing with version numbers, etc. All foreign key constraints and unique indexes are removed.
-
-### Opting out
-
-To opt out of versioning for an object, you can set the `versioned` attribute to **false** on your CFC file:
-
-```luceescript
-/**
- * @versioned false
- *
- */
-component {
- // ...
-}
-```
-
-### Interacting with versions
-
-Various admin GUIs such as the :doc:`datamanager` implement user interfaces to deal with versioning records. However, if you find the need to create your own, or need to deal with version history records in any other way, you can use methods provided by the service api:
-
-* [[presideobjectservice-getrecordversions]]
-* [[presideobjectservice-getversionobjectname]]
-* [[presideobjectservice-objectisversioned]]
-* [[presideobjectservice-getnextversionnumber]]
-
-In addition, you can specify whether or not you wish to use the versioning system, and also what version number to use if you are, when calling the [[presideobjectservice-insertData]], [[presideobjectservice-updateData]] and [[presideobjectservice-deleteData]] methods by using the `useVersioning` and `versionNumber` arguments.
-
-Finally, you can select data from the version history tables with the [[presideobjectservice-selectdata]] method by using the `fromVersionTable`, `maxVersion` and `specificVersion` arguments.
-
-### Many-to-many related data
-
-By default, auto generated `many-to-many` data tables will be versioned along with your record changes. You can opt out of this by adding a `versioned=false` attribute to the `many-to-many` property:
-
-```luceescript
-property name="categories" relationship="many-to-many" relatedTo="category" versioned=false;
-```
-
-Inversely, you may have a `many-to-many` relationship for which you have an explicit join table that you'd like versioned along with the parent record. In this scenario, you can explicitly set `versioned=true`:
-
-```luceescript
-property name="categories" relationship="many-to-many" relatedTo="category" relatedVia="explicit_categories_obj" versioned=true;
-```
-
-### Ignoring changes
-
-By default, when the data actually changes in your object, a new version will be created. If you wish certain fields to be ignored when it comes to determining whether or not a new version should be created, you can add a `ignoreChangesForVersioning` attribute to the property in the preside object.
-
-An example scenario for this might be an object whose data is synced with an external source on a schedule. You may add a helper property to record the last sync check date, if no other fields have changed, you probably don't want a new version record being created just for that sync check date. In this case, you could do:
-
-```luceescript
-property name="_last_sync_check" type="date" dbtype="datetime" ignoreChangesForVersioning=true;
-```
-
-### Only create versions on update
-
-As of **10.9.0**, you are able to specify that a version record is **not** created on **insert**. Instead, the first version record will be created on the first update to the record. This allows you to save on unnecessary version records in your database. To do this, add the `versionOnInsert=false` attribute to you object, e.g.
-
-```luceescript
-/**
- * @versioned true
- * @versionOnInsert false
- */
-component {
- // ...
-}
-```
-
-## Organising data by sites
-
-You can instruct the Preside Data Objects system to organise your objects' data into your system's individual sites (see [[workingwithmultiplesites]]). Doing so will mean that any data reads and writes will be specific to the currently active site.
-
-To enable this feature for an object, simply add the `siteFiltered` attribute to the `component` tag:
-
-```luceescript
-/**
- * @siteFiltered true
- *
- */
-component {
- // ...
-}
-```
-
->>>> As of Preside 10.8.0, this method is deprecated and you should instead use `@tenant site`. See [[data-tenancy]].
-
-
-## Flagging an object record
-
-You are able to flag a record for your objects' data. Doing so will mean you able to filter which records are flagged in the object.
-
-To enable this feature for an object, simple add the `flagEnabled` attribute (disabled by default) to the `component` tag:
-
-```luceescript
-/**
- * @flagEnabled true
- *
- */
-component {
- // ...
-}
-```
-
-If you wish to use a different property to flag a record, you can use the `flagField` attribute on your CFC, e.g.:
-
-```luceescript
-/**
- * @flagField record_flag
- *
- */
-component {
- property name="record_flag" type="boolean" dbtype="boolean" default="0" renderer="none" required=true;
-}
-```
----
-id: presidesuperclass
-title: Using the super class
----
-
-## Overview
-
-Preside comes with its own suite of service objects that you can use in your application just like any of your application's own service objects. In order to make it easy to access the most common core services, we created the [[api-presidesuperclass]] that can be injected into your service objects simply by adding the `@presideService` annotation to your service CFC file:
-
-```luceescript
-/**
- * @presideService
- */
-component {
-
- function init() {
- return this;
- }
-
- // ...
-}
-// or
-component presideService {
-
- function init() {
- return this;
- }
-
- // ...
-}
-```
-
->>> Service CFCs that declare themselves as Preside Services **must** implement an `init()` method, even if it does nothing but `return this;`.
-
-## Usage
-
-Once your service has been flagged as being a "Preside Service", it will instantly have a number of core methods available to it such as `$getPresideObject()` and `$isFeatureEnabled()`. e.g.
-
-```luceescript
-public boolean function updateProfilePicture( required string pictureFilePath ) {
- if ( $isWebsiteUserLoggedIn() && !$isWebsiteUserImpersonated() ) {
- return $getPresideObject( "website_user" ).updateData(
- id = $getWebsiteLoggedInUserId()
- , data = { profile_picture = arguments.pictureFilePath }
- );
- }
-
- return false;
-}
-```
-
-### Helpers
-
-As of **10.11.0**, service components using the Preside Super Class have a `$helpers` object available to them. This object contains all the Coldbox helper UDFs defined in Preside, your application and any extensions you have installed. For example, you can now make use of the `isTrue()` helper with:
-
-```luceescript
-/**
- * @presideService true
- * @singleton true
- */
-component {
- function init() {
- return this;
- }
-
- function someMethod( required any someArg ) {
- if ( $helpers.isTrue( someArg ) ) {
- // do something
- }
- }
-}
-```
-
-### Full reference
-
-For a full reference of all the methods available, see [[api-presidesuperclass]].
-
->>> You will notice that we have prefixed all the function names in the Super Class with `$`. This is to make name conflicts less likely and to indicate that the methods have been injected into your object.
----
-id: emailtemplatingv2
-title: Email centre
----
-
-## Overview
-
-As of 10.8.0, Preside comes with a sophisticated but simple system for email templating that allows developers and content editors to work together to create a highly tailored system for delivering both marketing and transactional email.
-
->>> See [[emailtemplating]] for documentation on the basic email templating system prior to 10.8.0
-
-## Concepts
-
-### Email layouts
-
-Email "layouts" are provided by developers and designers to provide content administrators with a basic set of styles and layout for their emails. Each template can be given configuration options that allow content administrators to tweak the behaviour of the template globally and per email.
-
-An example layout might include a basic header and footer with configurable social media links and company contact details.
-
-See [[creatingAnEmailLayout]].
-
-### Email templates
-
-An email _template_ is the main body of any email and is editorially driven, though developers may provide default content. When creating or configuring an email template, users may choose a layout from the application's provided set of layouts. If only one layout is available, no choice will be given.
-
-Email templates are split into two categories:
-
-1. System email templates (see [[systemEmailTemplates]])
-2. Editorial email templates (e.g. for newsletters, etc.)
-
-Editorial email templates will work out-of-the-box and require no custom development.
-
-### Recipient types
-
-Recipient types are configured to allow the email centre to send intelligently to different types of recipient. Each email template is configured to send to a specific recipient type. The core system provides three types:
-
-1. Website user
-2. Admin user
-3. Anonymous
-
-You may also have further custom recipient types and you may wish to modify the configuration of these three core types. See [[emailRecipientTypes]] for a full guide.
-
-### Service providers
-
-Email service providers are mechanims for performing an email send. You may have a 'Mailgun API' service provider, for example (see our [Mailgun Extension](https://github.com/pixl8/preside-ext-mailgun)).
-
-The core provides a default SMTP provider and you are free to create multiple different providers for different purposes. See [[emailServiceProviders]] for a full guide.
-
-### General settings
-
-Navigating to **Email centre -> Settings** reveals a settings form for general email sending configuration. You may wish to add to this default configuration form, or retrieve settings programmatically. See [[emailSettings]] for a full guide.
-
-## Feature switches and permissions
-
-### Features
-
-The email centre admin UI can be switched off using the `emailCentre` feature switch. In your application's `Config.cfc` file:
-
-```luceescript
-settings.features.emailCenter.enabled = false;
-```
-
-Furthermore, there is a separate feature switch to enable/disable _custom_ email template admin UIs, `customEmailTemplates`:
-
-
-```luceescript
-settings.features.customEmailTemplates.enabled = false;
-```
-
-Both features are enabled by default. The `customEmailTemplates` feature is only available when the the `emailCenter` feature is also enabled; disabling just the `emailCenter` feature has the effect of disabling both features.
-
-As of 10.9.0, the ability to re-send emails sent via the email centre has been added. This is disabled by default, and can be enabled with the `emailCenterResend` feature:
-
-```luceescript
-settings.features.emailCenterResend.enabled = true;
-```
-
-See [[resendingEmail]] for a detailed guide.
-
-
-### Permissions
-
-The email centre comes with a set of permission keys that can be used to fine tune your administrator roles. The permissions are defined as:
-
-```luceescript
-settings.adminPermissions.emailCenter = {
- layouts = [ "navigate", "configure" ]
- , customTemplates = [ "navigate", "view", "add", "edit", "delete", "publish", "savedraft", "configureLayout", "editSendOptions", "send" ]
- , systemTemplates = [ "navigate", "savedraft", "publish", "configurelayout" ]
- , serviceProviders = [ "manage" ]
- , settings = [ "navigate", "manage", "resend" ]
- , blueprints = [ "navigate", "add", "edit", "delete", "read", "configureLayout" ]
- , logs = [ "view" ]
- , queue = [ "view", "clear" ]
- }
-```
-
-The default `sysadmin` and `contentadmin` user roles have access to all of these permissions _except_ for the `emailCenter.queue.view` and `emailCenter.queue.clear` permissions. For a full guide to customizing admin permissions and roles, see [[cmspermissioning]].
-
-## Interception points
-
-As of 10.11.0, there are a number of interception points that can be used to more deeply customize the email sending experience. You may, for example, use the `onSendEmail` interception point to inject campaign tags into all links in an email. Interception points are listed below:
-
-### onPrepareEmailSendArguments
-
-This interception point is announced after the "sendArgs" are prepared ready for sending the email. This include keys such as `htmlBody`, `textBody`, `to`, `from`, etc. You will receive `sendArgs` as a key in the `interceptData` argument and can then modify this struct as you see fit. e.g.
-
-```luceescript
-component extends="coldbox.system.Interceptor" {
-
- property name="smartSubjectService" inject="delayedInjector:smartSubjectService";
-
- public void function onPrepareEmailSendArguments( event, interceptData ) {
- interceptData.sendArgs.subject = smartSubjectService.optimizeSubject( argumentCollection=interceptData.sendArgs );
- }
-}
-```
-
-### preSendEmail
-
-This interception point is announced just before the email is sent. It is near identical to `onPrepareEmailSendArguments` but also contains a `settings` key pertaining to the email service provider sending the email. e.g.
-
-```luceescript
-component extends="coldbox.system.Interceptor" {
-
- // force local testing perhaps??
- public void function preSendEmail( event, interceptData ) {
- interceptData.settings.smtp_host = "127.0.0.1";
- }
-
-}
-```
-
-### postSendEmail
-
-This interception point is announced just after the email is sent and after any logs have been inserted in the database. Receives the same arguments as `preSendEmail`.
-
-```luceescript
-component extends="coldbox.system.Interceptor" {
-
- property name="someService" inject="delayedInjector:someService";
-
- public void function postSendEmail( event, interceptData ) {
- someService.doSomethingAfterEmailSend( argumentCollection=interceptData.sendArgs );
- }
-
-}
-```
----
-id: emailRecipientTypes
-title: Creating and configuring email recipient types
----
-
-## Email recipient types
-
-Defining and configuring recipient types allows your email editors to inject useful variables into their email templates. It also allows the system to keep track of emails that have been sent to specific recipients and to use the correct email address for the recipient.
-
-## Configuring recipient types
-
-There are up to four parts to configuring a recipient type:
-
-1. Declaration in Config.cfc
-2. i18n `.properties` file for labelling
-3. Hander to provide methods for getting the address and variables for a recipient
-4. (optional) Adding foreign key to the core [[presideobject-email_template_send_log]] object for your particular recipient type's core object
-
-### 1. Config.cfc declaration
-
-All email recipient types must be registered in `Config.cfc`. An example configuration might look like this:
-
-```luceescript
-// register an 'eventDelegate' recipient type:
-settings.email.recipientTypes.eventDelegate = {
- parameters = [ "first_name", "last_name", "email_address", "mobile_number" ]
- , filterObject = "event_delegate"
- , gridFields = [ "first_name", "last_name", "email_address", "mobile_number" ]
- , recipientIdLogProperty = "event_delegate_recipient"
-};
-```
-
-#### Configuration options
-
-* `parameters` - an array of parameters that are available for injection by editors into email content and subject lines
-* `filterObject` - preside object that is the source object for the recipient, this can be filtered against for sending a single email to a large audience.
-* `gridFields` - array of properties defined on the `filterObject` that should be displayed in the grid that shows when listing the potential recipients of an email
-* `recipientIdLogProperty` - foreign key property on the [[presideobject-email_template_send_log]] object that should be used for storing the recipient ID in send logs (see below)
-* `feature` - an optional string value indicating the feature that the recipient type belongs to. If the feature is disabled, the recipient type will not be available.
-
-### 2. i18n property file
-
-Each recipient type should have a corresponding `.properties` file to provide labels for the type and any parameters that are declared. The file must live at `/i18n/email/recipientType/{recipientTypeId}.properties`. An example:
-
-```properties
-title=Event delegate
-description=Email sent to delegates of events
-
-param.first_name.title=First name
-param.first_name.description=First name of the delegate
-
-# ...
-```
-
-The recipient type itself has a `title` and `description` key. Any defined parameters can also then have `title` and `description` keys, prefixed with `param.{paramid}.`.
-
-### 3. Handler for generating parameters
-
-Recipient types require a handler for returning parameters for a recipient and for returning the recipient's email address. This should live at `/handlers/email/recipientType/{recipientTypeId}.cfc` and have the following signature:
-
-```luceescript
-component {
- private struct function prepareParameters( required string recipientId ) {}
-
- private struct function getPreviewParameters() {}
-
- private string function getToAddress( required string recipientId ) {}
-
- // as of 10.12.0
- private string function getUnsubscribeLink( required string recipientId, required string templateId ) {}
-}
-```
-
-#### prepareParameters()
-
-The `prepareParameters()` method should return a struct whose keys are the IDs of the parameters that are defined in `Config.cfc` (see above) and whose values are either:
-
-* a string value to be used in both plain text and html emails
-* a struct with `html` and `text` keys whose values are strings to be used in their respective email renders
-
-The purpose here is to allow variables in an email's body and/or subject to be replaced with details of the recipient. The method accepts a `recipientId` argument so that you can make a DB query to get the required details. For example:
-
-```luceescript
-// handlers/email/recipientType/EventDelegate.cfc
-component {
-
- property name="bookingService" inject="bookingService";
-
- private struct function prepareParameters( required string recipientId ) {
- var delegate = bookingService.getDelegate( arguments.recipientId );
-
- return {
- first_name = delegate.first_name
- , last_name = delegate.last_name
- // ... etc
- };
- }
-
- // ...
-}
-```
-
-#### getPreviewParameters()
-
-The `getPreviewParameters()` method has the exact same purpose as the `getParameters()` method _except_ that it should return a static set of parameters that can be used to preview any emails that are set to send to this recipient type. It does not accept any arguments.
-
-For example:
-
-```luceescript
-private struct function getPreviewParameters() {
- return {
- first_name = "Example"
- , last_name = "Delegate"
- // ... etc
- };
-}
-```
-
-#### getToAddress()
-
-The `getToAddress()` method accepts a `recipientId` argument and must return the email address to which to send email. For example:
-
-```luceescript
-private struct function getToAddress( required string recipientId ) {
- var delegate = bookingService.getDelegate( arguments.recipientId );
-
- return delegate.email_address ?: "";
-}
-```
-
-#### getUnsubscribeLink()
-
-As of **10.12.0**. The `getUnsubscribeLink()` method accepts `recipientId` and `templateId` arguments and can return a link to use for unsubscribes (or an empty string for no link).
-
-For example, you may wish to link to an 'edit profile' page, or some page specific to custom fields set on the email template:
-
-```luceescript
-private struct function getUnsubscribeLink( required string recipientId, required string templateId ) {
- var listId = myCustomService.getEmailTemplateUnsubscribeList( arguments.templateId );
-
- return event.buildLink(
- linkto = "mycustomemail.ubsubscribeHandler"
- , queryString = "rid=#arguments.recipientId#&lid=#listId#"
- );
-}
-```
-
-
-```luceescript
-private struct function getToAddress( required string recipientId ) {
- var delegate = bookingService.getDelegate( arguments.recipientId );
-
- return delegate.email_address ?: "";
-}
-```
-
-### 4. Email log foreign key
-
-When email is sent through the [[emailservice-send|emailService.send()]] method, Preside keeps a DB log record for the send in the [[presideobject-email_template_send_log]] object. This record is used to track delivery, opens, clicks, etc. for the email.
-
-In order to be able to later report on which recipients have engaged with email, you should add a foreign key property to the object that relates to the core object of your recipient type. For example, add a `/preside-objects/email_template_send_log.cfc` file to your application/extension:
-
-```luceescript
-/**
- * extend the core email_template_send_log object
- * to add our foreign key for event delegate recipient
- * type
- *
- */
-component {
- // important: this must NOT be a required field
- property name="delegate_recipient" relationship="many-to-one" relatedto="event_delegate" required=false;
-}
-```
-
-This extra property is then referenced in the configuration of your recipient type in your application's/extension's `Config.cfc` file (see above):
-
-```luceescript
-settings.email.templates.recipientTypes.eventDelegate = {
- // ...
- , recipientIdLogProperty = "delegate_recipient"
-};
-```
----
-id: emailServiceProviders
-title: Creating email service providers
----
-
-## Email service providers
-
-Email service providers perform the task of sending email. Preside comes with a standard SMTP service provider that sends mail through `cfmail`. Service providers can be configured through the email centre admin UI.
-
-## Creating an email service provider
-
-There are four parts to creating a service provider:
-
-1. Declaration in Config.cfc
-2. i18n `.properties` file for labelling
-3. xml form definition for configuring the provider
-4. Handler to provide methods for sending and for validating settings
-
-### Declaration in Config.cfc
-
-A service provider must be defined in Config.cfc. Here are a couple of 'mailchimp' examples:
-
-```luceescript
-// use defaults for everything (recommended):
-settings.email.serviceProviders.mailchimp = {};
-
-// or, all options (with defaults):
-settings.email.serviceProviders.mailchimp = {
- configForm = "email.serviceprovider.mailchimp"
- , sendAction = "email.serviceprovider.mailchimp.send"
- , validateSettingsAction = "email.serviceprovider.mailchimp.validateSettings"
-};
-```
-
-#### Configuration options
-
-* `configForm` - path to [[presideforms|xml form definition]] for configuring the provider
-* `sendAction` - coldbox handler action path of the handler action that performs the sending of email
-* `validateSettingsAction` - optional coldbox handler action path of the handler action that will perform validation against user inputted provider settings (using the config form)
-
-### i18n .properties file
-
-Each service provider should have a corresponding `.properties` file to provide labels for the provider and any configuration options in the config form. The default location is `/i18n/email/serviceProvider/{serviceProviderId}.properties`. An example:
-
-```properties
-title=MailGun
-description=A sending provider for that sends email through the MailGun sending API
-iconclass=fa-envelope
-
-# config form labels:
-
-fieldset.default.description=Note that we do not currently send through the mailgun API due to performance issues (it is far slower than sending through native SMTP). Retrieve your SMTP details from the mailgun web interface and enter below.
-
-field.server.title=SMTP Server
-field.server.placeholder=e.g. smtp.mailgun.org
-field.port.title=Port
-field.username.title=Username
-field.password.title=Password
-
-field.mailgun_test_mode.title=Test mode
-field.mailgun_test_mode.help=Whether or not emails are actually sent to recipients or sending is only faked.
-
-```
-
-The only required keys are `title`, `description` and `iconclass`. Keys for your form definition are up to you.
-
-### Configuration form
-
-Service providers are configured in the email centre:
-
-
-![Screenshot showing email service provider configuration](images/screenshots/emailServiceProviderSettings.png)
-
-
-In order for this to work, you must supply a configuration form definition. The default location for your service provider's configuration form is `/forms/email/serviceProvider/{serviceProviderId}.xml`. An example:
-
-```xml
-
-
-```
-
-### Handler
-
-Your service provider must provide a handler with at least a `send` action + an optional `validateSettings()` action. The default location of the file is `/handlers/email/serviceProvider/{serviceProviderId}.cfc`. The method signatures look like this:
-
-```luceescript
-component {
-
- private boolean function send( struct sendArgs={}, struct settings={} ) {}
-
- private any function validateSettings( required struct settings, required any validationResult ) {}
-
-}
-```
-
-#### send()
-
-The send method accepts a structure of `sendArgs` that contain `recipient`, `subject`, `body`, etc. and a structure of `settings` that are the saved configuration settings of your service provider. The method should return `true` if sending was successful.
-
-The code listing below shows the core SMTP send logic at the time of writing this doc:
-
-```luceescript
-private boolean function send( struct sendArgs={}, struct settings={} ) {
- var m = new Mail();
- var mailServer = settings.server ?: "";
- var port = settings.port ?: "";
- var username = settings.username ?: "";
- var password = settings.password ?: "";
- var params = sendArgs.params ?: {};
- var attachments = sendArgs.attachments ?: [];
-
- m.setTo( sendArgs.to.toList( ";" ) );
- m.setFrom( sendArgs.from );
- m.setSubject( sendArgs.subject );
-
- if ( sendArgs.cc.len() ) {
- m.setCc( sendArgs.cc.toList( ";" ) );
- }
- if ( sendArgs.bcc.len() ) {
- m.setBCc( sendArgs.bcc.toList( ";" ) );
- }
- if ( Len( Trim( sendArgs.textBody ) ) ) {
- m.addPart( type='text', body=Trim( sendArgs.textBody ) );
- }
- if ( Len( Trim( sendArgs.htmlBody ) ) ) {
- m.addPart( type='html', body=Trim( sendArgs.htmlBody ) );
- }
- if ( Len( Trim( mailServer ) ) ) {
- m.setServer( mailServer );
- }
- if ( Len( Trim( port ) ) ) {
- m.setPort( port );
- }
- if ( Len( Trim( username ) ) ) {
- m.setUsername( username );
- }
- if ( Len( Trim( password ) ) ) {
- m.setPassword( password );
- }
-
- for( var param in params ){
- m.addParam( argumentCollection=sendArgs.params[ param ] );
- }
- for( var attachment in attachments ) {
- var md5sum = Hash( attachment.binary );
- var tmpDir = getTempDirectory() & "/" & md5sum & "/";
- var filePath = tmpDir & attachment.name
- var remove = IsBoolean( attachment.removeAfterSend ?: "" ) ? attachment.removeAfterSend : true;
-
- if ( !FileExists( filePath ) ) {
- DirectoryCreate( tmpDir, true, true );
- FileWrite( filePath, attachment.binary );
- }
-
- m.addParam( disposition="attachment", file=filePath, remove=remove );
- }
-
- sendArgs.messageId = sendArgs.messageId ?: CreateUUId();
-
- m.addParam( name="X-Mailer", value="Preside" );
- m.addParam( name="X-Message-ID", value=sendArgs.messageId );
- m.send();
-
- return true;
-}
-```
-
-#### validateSettings()
-
-The `validateSettings()` method accepts a `settings` struct that contains the user-defined settings submitted with the form, and a [[api-validationresult|validationResult]] object for reporting errors. It must return the passed in `validationResult`.
-
-The core SMTP provider, for example, validates the SMTP server:
-
-```luceescript
-private any function validateSettings( required struct settings, required any validationResult ) {
- if ( IsTrue( settings.check_connection ?: "" ) ) {
- var errorMessage = emailService.validateConnectionSettings(
- host = arguments.settings.server ?: ""
- , port = Val( arguments.settings.port ?: "" )
- , username = arguments.settings.username ?: ""
- , password = arguments.settings.password ?: ""
- );
-
- if ( Len( Trim( errorMessage ) ) ) {
- if ( errorMessage == "authentication failure" ) {
- validationResult.addError( "username", "email.serviceProvider.smtp:validation.server.authentication.failure" );
- } else {
- validationResult.addError( "server", "email.serviceProvider.smtp:validation.server.details.invalid", [ errorMessage ] );
- }
- }
- }
-
- return validationResult;
-}
-```
-
->>>>>> You are only required to supply custom validation logic here; you do **not** have to provide regular form validation logic that is automatically handled by the regular [[presideforms]] validation system.
-
-
----
-id: systemEmailTemplates
-title: Creating and sending system email templates
----
-
-## System email templates
-
-The development team may provide system transactional email templates such as "Reset password" or "Event booking confirmation". These templates are known as *system* templates and are available through the UI for content editors to _edit_; they cannot be created or deleted by content editors.
-
-## Sending system email templates
-
-System transactional emails are programatically sent using the [[emailservice-send]] method of the [[api-emailservice]] or the [[presidesuperclass-$sendemail]] method of the [[presidesuperclass|Preside super class]] (which proxies to the [[api-emailservice|emailService]].[[emailservice-send]] method).
-
-While the [[emailservice-send]] method takes many arguments, these are chiefly for backwards compatibility. For sending the "new" (as of 10.8.0) style email templates, we only require three arguments:
-
-```luceescript
-$sendEmail(
- template = "bookingConfirmation"
- , recipientId = userId
- , args = { bookingId=bookingId }
-);
-```
-
-* `template` - ID of the configured template (see below)
-* `recipientId` - ID of the recipient. The source object for this ID will differ depending on the [[emailRecipientTypes|recipient type]] of the email.
-* `args` - Any additional data that the email template needs to render the correct information (see below)
-
-## Creating system email templates
-
-There are three parts to creating a system email template:
-
-1. Declaration in Config.cfc
-2. i18n `.properties` file for labelling
-3. Hander to provide methods for generating email variables and default content
-
-### 1. Config.cfc declaration
-
-All system email templates must be registered in `Config.cfc`. An example configuration might look like this:
-
-```luceescript
-// register a 'bookingConfirmation' template:
-settings.email.templates.bookingConfirmation = {
- recipientType = "websiteUser",
- parameters = [
- { id="booking_summary" , required=true }
- , { id="edit_booking_link", required=false }
- ]
-};
-```
-
-#### Configuration options
-
-* `recipientType` - each template _must_ declare a recipient type (see [[emailRecipientTypes]]). This is a string value and indicates the target recipients for the email template.
-* `parameters` - an optional array of parameters that the template makes available for editors to be able insert into dynamic content. Each parameter is a struct with `id` and `required` fields.
-* `feature` - an optional string value indicating the feature that the email template belongs to. If the feature is disabled, the template will not be available.
-
-### 2. i18n .properties file
-
-Each template should have a corresponding `.properties` file to provide labels for the template and any parameters that are declared. The file must live at `/i18n/email/template/{templateid}.properties`. An example:
-
-```properties
-title=Event booking confirmation
-description=Email sent to customers who have just booked on an event
-
-param.booking_summary.title=Booking summary
-param.booking_summary.description=Booking summary text including tickets purchased, etc.
-
-param.edit_booking_link.title=Edit booking link
-param.edit_booking_link.description=A link to the page where delegate's can edit their booking
-```
-
-The template itself has a `title` and `description` key. Any defined parameters can also then have `title` and `description` keys, prefixed with `param.{paramid}.`.
-
-### 3. Handler for generating parameters and defaults
-
-The final part of creating a system transactional email template is the handler. This should live at `/handlers/email/template/{templateId}.cfc` and have the following signature:
-
-```luceescript
-component {
-
- private struct function prepareParameters() {}
-
- private struct function getPreviewParameters() {}
-
- private string function defaultSubject() {}
-
- private string function defaultHtmlBody() {}
-
- private string function defaultTextBody() {}
-
-}
-```
-
-#### prepareParameters()
-
-The `prepareParameters()` is where any real display and processing logic for your email template occurs; _email templates are only responsible for rendering parameters that are available for editors to use in their email content - **not** for rendering an entire email layout_. The method should return a struct whose keys are the IDs of the parameters that are defined in `Config.cfc` (see above) and whose values are either:
-
-* a string value to be used in both plain text and html emails
-* a struct with `html` and `text` keys whose values are strings to be used in their respective email renders
-
-The arguments passed to the `prepareParameters()` method will consist of any extra `args` that were passed to the [[emailservice-send]] method when the email was requested to be sent.
-
-For example:
-
-```luceescript
-// send email call from some other service
-emailService.send(
- template = "bookingConfirmation"
- , recipientId = userId
- , args = { bookingId=bookingId } // used as the arguments set for the prepareParameters() call
-);
-```
-
-```luceescript
-// handlers/email/template/BookingConfirmation.cfc
-component {
-
- property name="bookingService" inject="bookingService";
-
- // bookingId argument expected in `args` struct
- // in all `send()` calls for 'bookingConfirmation'
- // template
- private struct function prepareParameters( required string bookingId ) {
- var params = {};
- var args = {};
-
- args.bookingDetails = bookingService.getBookingDetails( arguments.bookingId );
-
- params.eventName = args.bookingDetails.event_name;
- params.bookingSummary = {
- html = renderView( view="/email/template/bookingConfirmation/_summaryHtml", args=args )
- , text = renderView( view="/email/template/bookingConfirmation/_summaryText", args=args )
- };
-
- return params;
- }
-
- // ...
-}
-```
-
-#### getPreviewParameters()
-
-The `getPreviewParameters()` method has the exact same purpose as the `getParameters()` method _except_ that it should return a static set of parameters that can be used to preview the email template in the editing interface. It does not accept any arguments.
-
-For example:
-
-```luceescript
-private struct function getPreviewParameters() {
- var params = {};
- var args = {};
-
- args.bookingDetails = {
- event_name = "Example event"
- , start_time = "09:00"
- // ... etc
- };
-
- params.eventName = "Example event";
- params.bookingSummary = {
- html = renderView( view="/email/template/bookingConfirmation/_summaryHtml", args=args )
- , text = renderView( view="/email/template/bookingConfirmation/_summaryText", args=args )
- };
-
- return params;
-}
-```
-
-#### defaultSubject()
-
-The `defaultSubject()` method should return a **default** subject line to use for the email should an editor never have supplied one. e.g.
-
-```luceescript
-private struct function defaultSubject() {
- return "Your booking confirmation ${booking_no}";
-}
-```
-
-This is _only_ used to populate the database the very first time that the template is detected by the application.
-
-#### defaultHtmlBody()
-
-The `defaultHtmlBody()` method should return a **default** HTML body to use for the email should an editor never have supplied one. e.g.
-
-```luceescript
-private struct function defaultHtmlBody() {
- return renderView( view="/email/template/bookingConfirmation/_defaultHtmlBody" );
-}
-```
-
-You should create a sensible default that uses the configurable parameters just as an editor would do. This is _only_ used to populate the database the very first time that the template is detected by the application.
-
-
-#### defaultTextBody()
-
-The `defaultTextBody()` method should return a **default** plain text body to use for the email should an editor never have supplied one. e.g.
-
-```luceescript
-private struct function defaultTextBody() {
- return renderView( view="/email/template/bookingConfirmation/_defaultTextBody" );
-}
-```
-
-You should create a sensible default that uses the configurable parameters just as an editor would do. This is _only_ used to populate the database the very first time that the template is detected by the application.
----
-id: resendingEmail
-title: Re-sending emails and content logging
----
-
-## Overview
-
-Preside 10.9.0 introduces the ability to re-send emails via the email centre. It also allows for the logging of the actual generated email content, enabling admin users to view the exact content of emails as they were sent, and also to re-send the original content to a user.
-
-The feature is disabled by default, and can be enabled with the `emailCenterResend` feature:
-
-```luceescript
-settings.features.emailCenterResend.enabled = true;
-```
-
-By default, any logged email content is stored for a period of 30 days, after which it will be automatically removed (although the send and activity logs will still be available). This default can easily be configured:
-
-```luceescript
-settings.email.defaultContentExpiry = 30;
-```
-
->>>> Logging the content of individual emails can potentially use a large amount of database storage, especially if you are logging the content of newsletters sent to large email lists.
-
-Note that if you set `defaultContentExpiry` to 0, email content will not be logged (unless you specifically override this setting for an individual template — see below).
-
-### Email activity log
-
-When viewing the email activity of a message from the send log, you will see one or two re-send action buttons:
-
-**Rebuild and re-send email** will regenerate the email based on the original arguments passed to the `sendMail()` function. This is available for _all_ emails when re-send functionality is enabled. Note that if the template or dynamic data has changed since the email was first sent, the resulting email may be different from the original.
-
-**Re-send original email** is available if content saving is enabled for a template _and_ there is saved email content for the email (i.e. saving was enabled when the email was sent, and the content has not expired). This will re-send an exact copy of the email as it was originally sent.
-
-If there is valid saved content for an email, you will also see the email activity divided into tabs. The main tab is the usual activity log; there are also **HTML** and **Plain text** tabs which allow an admin user to view the content of the email as it was sent:
-
-![Screenshot showing the email activity pane with tabs for viewing sent content.](images/screenshots/email-activity-saved-content.png)
-
-### System email templates
-
-By default, the content of sent system emails is saved for the default period. This can be overridden per template using the `saveContent` setting, as there will be some emails (e.g. those with expiring links or with security considerations) where it is not desirable to store this content. For example, this is the definition of the Admin User Password Reset template, with content saving turned off:
-
-```luceescript
-settings.email.templates.resetCmsPassword = {
- feature = "cms"
- , recipientType = "adminUser"
- , saveContent = false
- , parameters = [ { id="reset_password_link", required=true }, "site_url" ]
-};
-```
-
-You may also define the content expiry (in days) of an individual system template using the `contentExpiry` setting:
-
-```luceescript
-settings.email.templates.templateName.contentExpiry = 15;
-```
-
-The `resetCmsPassword` template above also highlights another potential issue: the reset token used to generate the email expires after a period of time. A simple regeneration of the email will use the original (probably now invalid) reset token, which is stored in the `send_args` property of the email log.
-
-To solve this, add the method `rebuildArgsForResend()` to your template handler. This takes a single argument — the ID of the email log entry in `email_template_send_log`; from this you can do whatever logic is needed to create a `sendArgs` struct to pass to the `sendEmail()` method. As an example, this is the method in the handler `ResetCmsPassword.cfc`:
-
-```luceescript
-private struct function rebuildArgsForResend( required string logId ) {
- var userId = sendLogDao.selectData( id=logId, selectFields=[ "security_user_recipient" ] ).security_user_recipient;
- var tokenInfo = loginService.createLoginResetToken( userId );
-
- return { resetToken="#tokenInfo.resetToken#-#tokenInfo.resetKey#" };
-}
-```
-
-This retrieves the admin user's ID from the email send log, generates a new reset token for that user, and returns the reset token for use in creation of a new email.
-
-
-### Custom email templates
-
-By default, the content of custom email templates _is not saved_. Content saving can be turned on for individual templates via the template's settings page:
-
-![Screenshot showing the content saving options for custom email templates.](images/screenshots/email-resend-custom-templates.png)
-
-If no content expiry is specified — "Save for [x] days" — then the system default value will be used.---
-id: emailtemplating
-title: Email templating (pre-10.8.0)
----
-
-## Overview
-
-Preside comes with a very simple email templating system that allows you to define email templates by creating ColdBox handlers.
-
-Emails are sent through the core email service which in turn invokes template handlers to render the emails and return any other necessary mail parameters.
-
-## Creating an email template handler
-
-To create an email template handler, you must create a regular Coldbox handler under the `/handlers/emailTemplates` directory. The handler needs to implement a single *private* action, `prepareMessage()` that returns a structure containing any message parameters that it needs to set. For example:
-
-```luceescript
-// /mysite/application/handlers/emailTemplates/adminNotification.cfc
-component {
-
- private struct function prepareMessage( event, rc, prc, args={} ) {
- return {
- to = [ getSystemSetting( "email", "admin_notification_address", "" ) ]
- , from = getSystemSetting( "email", "default_from_address", "" )
- , subject = "Admin notification: #( args.notificationTitle ?: '' )#"
- , htmlBody = renderView( view="/emailTemplates/adminNotification/html", layout="email", args=args )
- , textBody = renderView( view="/emailTemplates/adminNotification/text", args=args )
- };
- }
-
-}
-```
-
-An example send() call for this template might look like this:
-
-```luceescript
- emailService.send( template="adminNotification", args={
- notificationTitle = "Something just happened"
- , notificationMessage = "Some message"
-} );
-```
-
-## Supplying message arguments to the send() method
-
-Your email template handlers are not required to supply all the details of the message; these can be left to the calling code to supply. For example, we could refactor the above example so that the `to` and `subject` parameters need to be supplied by the calling code:
-
-```luceescript
-// /mysite/application/handlers/emailTemplates/adminNotification.cfc
-component {
-
- private struct function prepareMessage( event, rc, prc, args={} ) {
- return {
- htmlBody = renderView( view="/emailTemplates/adminNotification/html", layout="email", args=args )
- , textBody = renderView( view="/emailTemplates/adminNotification/text", args=args )
- };
- }
-
-}
-```
-
-```luceescript
-emailService.send(
- template = "adminNotification"
- , args = { notificationMessage = "Some message" }
- , to = user.email_address
- , subject = "Alert: something just happend"
-);
-```
-
->>> Note the missing "from" parameter. The core send() implementation will attempt to use the system configuration setting `email.default_from_address` when encountering messages with a missing **from** address. This default address can be configured by users through the Preside administrator (see [[editablesystemsettings]]).
-
-## Mail server and other configuration settings
-
-The core system comes with a system configuration form for mail server settings. See [[editablesystemsettings]] for more details on how this is implemented.
-
-The system uses these configuration values to set the server and port when sending emails. The "default from address" setting is used when sending mail without a specified from address.
-
-This form may be useful to extend in your site should you want to configure other mail related settings. i.e. you might have default "to" addresses for particular admin notification emails, etc.
-
-
-
-
----
-id: creatingAnEmailLayout
-title: Creating an email layout
----
-
->>> Email layouts were introduced in Preside 10.8.0. See [[emailtemplatingv2]] for more details.
-
-## Creating an email layout
-
-### 1. Create viewlets for HTML and plain text renders
-
-Email layouts are created by convention. Each layout is defined as a pair of [[Viewlets|Preside viewlets]], one for the HTML version of the layout, another for the text only version of the layout. The convention based viewlet ids are `email.layout.{layoutid}.html` and `email.layout.{layoutid}.text`.
-
-The viewlets receive three common variables in their `args` argument:
-
-* `subject` - the email subject
-* `body` - the main body of the email
-* `viewOnlineLink` - a link to view the full email online (may be empty for transactional emails, for example)
-
-In addition, the viewlets will also receive args from the layout's config form, if it has one (see 3, below).
-
-A very simple example:
-
-```lucee
-
-
-
-
- #args.subject#
-
-
- View in a browser
- #args.body#
-
-
-
-```
-
-```lucee
-
-
-#args.subject#
-#repeatString( '=', args.subject.len() )#
-
-View online: #args.viewOnlineLink#
-
-#args.body#
-
-```
-
-### 2. Provide translatable title and description
-
-In addition to the viewlet, each layout should also have translation entries in a `/i18n/email/layout/{layoutid}.properties` file. Each layout should have a `title` and `description` entry. For example:
-
-```properties
-title=Transactional email layout
-description=Use the transactional layout for emails that happen as a result of some user action, e.g. send password reminder, booking confirmation, etc.
-```
-
-### 3. Provide optional configuration form
-
-If you want your application's content editors to be able to tweak layout options, you can also provide a configuration form at `/forms/email/layout/{layoutid}.xml`. This will allow end-users to configure global defaults for the layout and to tweak settings per email. For example:
-
-```xml
-
-
-```
-
-With the form above, editors might be able to configure social media links and the company address that appear in the layout.---
-id: emailSettings
-title: Working with Email centre settings
----
-
-## Email centre settings
-
-The email centre has a general settings form with global email configuration (screenshot below). The form, [[form-emailcentergeneralsettingsform]], is located at `/forms/email/settings/general.xml`. You can provide your own extensions to the form by creating the same file in your application or extension (see [[presideforms]]).
-
-![Screenshot showing email centre general settings](images/screenshots/emailSettingsForm.png)
-
-## Retrieving settings
-
-All settings are saved and retrieved using the `email` category in the [[editablesystemsettings]] system. For example:
-
-```luceescript
-// all settings example:
-var allEmailSettings = $getPresideCategorySettings( "email" );
-
-// specific setting example:
-var defaultFrom = $getPresideSetting( category="email", setting="default_from_address" );
-```
-
----
-id: datamanager
-title: Data Manager
----
-
-## Introduction
-
-Preside's Data Manager is a sophisticated auto CRUD admin for your data objects. With very little configuration, you are able to set up listing screens, add, edit and delete screens, version history screens, auditing, translation, bulk edit functionality, etc. In addition, as of Preside 10.9.0, this system can be highly customized both globally and _per data object_ so that you can rapidly build awesome custom admin interfaces in front of your application's database.
-
-![Screenshot showing example of a Data Manager listing view](images/screenshots/datamanager-example.png)
-
-As there is a lot to cover, we have broken the documentation down, see distinct topics below:
-
-* [[datamanagerbasics]]
-* [[customizingdatamanager]]
-* [[adminrecordviews]]
-* [[enhancedrecordviews]]---
-id: datamanager-customization-gettoprightbuttonsforviewrecord
-title: "Data Manager customization: getTopRightButtonsForViewRecord"
----
-
-## Data Manager customization: getTopRightButtonsForViewRecord
-
-The `getTopRightButtonsForViewRecord` customization allows you to _completely override_ the set of buttons that appears at the top right hand side of the view record listing screen. It must _return an array_ of structs that describe the buttons to display and is provided the `objectName` in the `args` struct.
-
-Note, if you simply want to add, or tweak, the top right buttons, you may wish to use [[datamanager-customization-extratoprightbuttonsforviewrecord]].
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private array function getTopRightButtonsForViewRecord( event, rc, prc, args={} ) {
- var actions = [];
- var objectName = args.objectName ?: "";
- var recordId = prc.recordId ?: ""
-
- actions.append({
- link = event.buildAdminLink( objectName=objectName, operation="reports", recordId=recordId )
- , btnClass = "btn-default"
- , iconClass = "fa-bar-chart"
- , globalKey = "r"
- , title = translateResource( "preside-objects.blog:reports.btn" )
- } );
-
- return actions;
- }
-
-}
-```
-
->>> See [[datamanager-customization-toprightbuttonsformat]] for detailed documentation on the format of the action items.
----
-id: customizingdatamanager
-title: Customizing Data Manager
----
-
-## Introduction
-
-As of Preside 10.9.0, [[datamanager]] comes with a customization system that allows you to customize many aspects of the Data Manager both globally and per object. In addition, you are able to use all the features of Data Manager for your object **without needing to list your object in the Data Manager homepage**. This means that you can create your own custom navigation to your object and not need to write any further code to create your CRUD admin interface - perfect for building custom admin interfaces with dedicated navigation.
-
-## Customization system overview
-
-Customizations are implemented as convention based ColdBox _handlers_. Customizations that should be applied globally belong in `/handlers/admin/datamanager/GlobalCustomizations.cfc`. Customizations that should be applied to a specific object go in `/handlers/admin/datamanager/objectname.cfc`. For example, if you wish to supply customizations for a `blog_author` object, you would create a handler file: `/handlers/admin/datamanager/blog_author.cfc`.
-
-The Data Manager implements a large number of customizations. Each customization will be implemented in your handlers as a **private** handler action. The return type (if any) and arguments supplied to the action will depend on the specific customization.
-
-For example, you may wish to do some extra processing after saving an `employee` record using the `postEditRecordAction` customization:
-
-```luceescript
-// /application/handlers/datamanager/employee.cfc
-
-component {
-
- // as this is a regular coldbox handler
- // we can use wirebox to inject and access our service layer
- property name="notificationService" inject="notificationService";
-
- private void function postEditRecordAction( event, rc, prc, args={} ) {
- // the args struct values will vary depending on the customization point.
- // in this case, we get new and old data (as well as many other fields)
- var newData = args.formData ?: {};
- var oldData = args.existingRecord ?: {};
- var employeeId = args.recordId ?: {}
-
- // here, as an example, we use the notification service to
- // raise a "Date of birth change" notification when the DOB changes
- if ( newData.keyExists( "dob" ) && newData.dob != oldData.dob ) {
- notificationService.createNotification( topic="DOBChange", type="info", data={ employeeId=employeeId } )
- }
-
- // of course, we could do anything we like here. For instance,
- // we could redirect the user to a different screen than the
- // normal "post-edit" behaviour for Data Manager.
- }
-
-}
-```
-
-## Building and customizing links
-
-With the new 10.9.0 customization system comes a new method of building data manager links for objects. Use `event.buildAdminLink( objectName=objectName )` along with optional arguments, `operation` and `recordId` to build various links. For example, to link to the data manager listing page for an object, use the following:
-
-```luceescript
-event.buildAdminLink( objectName=objectName );
-```
-
-To link to the default view for a record, use:
-
-```luceescript
-event.buildAdminLink( objectName=objectName, recordId=recordId );
-```
-
-To link to a specific page or action URL for an object or record, add the `operation` argument, e.g.
-
-```luceescript
-event.buildAdminLink( objectName=objectName, operation="addRecord" );
-event.buildAdminLink( objectName=objectName, operation="editRecord", recordId=recordId );
-// etc.
-```
-
-The core, "out-of-box" operations are:
-
-* `listing`
-* `viewRecord`
-* `addRecord`
-* `addRecordAction`
-* `editRecord`
-* `editRecordAction`
-* `deleteRecordAction`
-* `translateRecord`
-* `sortRecords`
-* `managePerms`
-* `ajaxListing`
-* `multiRecordAction`
-* `exportDataAction`
-* `dataExportConfigModal`
-* `recordHistory`
-* `getNodesForTreeView`
-
-
->>>>>> You can pass extra query string parameters to any of these links with the `queryString` argument. For example:
->>>>>>
-```
-event.buildAdminLink(
- objectName = objectName
- , operation = "addRecord"
- , queryString = "categoryId=#categoryId#"
-);
-```
-
-### Custom link builders
-
-There is a naming convention for providing a custom link builder for an operation: `build{operation}Link`. There are therefore Data Manager customizations named `buildListingLink`, `buildViewRecordLink`, and so on. For example, to provide a completely different link for a view record screen for your object, you could do:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog_author.cfc
-
-component {
-
- private string function buildViewRecordLink( event, rc, prc, args={} ) {
- var recordId = args.recordId ?: "";
- var extraQs = args.queryString ?: "";
- var qs = "id=#recordId#";
-
- if ( extraQs.len() ) {
- qs &= "extraQs#";
- }
-
- // e.g. here we would have a coldbox handler /admin/BlogAuthors.cfc
- // with a public 'view' method for completely controlling the entire
- // view record request outside of Data Manager
- return event.buildAdminLink( linkto="blogauthors.view", querystring=qs );
- }
-}
-```
-
-### Adding your own operations
-
-If you are extending Data Manager to add extra pages for a particular object (for example), you can create new operations by following the same link building convention above. For example, say we wanted to build a "preview" link for an article, we can use the following:
-
-```luceescript
-// /handlers/admin/datamanager/article.cfc
-component extends="preside.system.base.AdminHandler" {
-
-// Public events for extra admin pages and actions
- public void function preview() {
- event.initializeDatamanagerPage(
- objectName = "article"
- , recordId = rc.id ?: ""
- );
-
- event.addAdminBreadCrumb(
- title = translateResource( "preside-objects.article:preview.breadcrumb.title" )
- , linke = ""
- );
-
- prc.pageTitle = translateResource( "preside-objects.article:preview.page.title" );
- prc.pageSubTitle = translateResource( "preside-objects.article:preview.page.subtitle" );
- }
-
-// customizations
- private string function buildPreviewLink( event, rc, prc, args={} ) {
- var qs = "id=#( args.recordId ?: "" )#";
-
- if ( Len( Trim( args.queryString ?: "" ) ) ) {
- qs &= "args.queryString#";
- }
-
- return event.buildAdminLink( linkto="datamanager.article.preview", querystring=qs );
- }
-
-
-
-}
-```
-
-Linking to the "preview" operation can then be done with:
-
-```luceescript
-event.buildAdminLink( objectName="article", operation="preview", id=recordId );
-```
-
->>> Notice that the handler extends `preside.system.base.AdminHandler`. This base handler supplies a preAction that sets the admin layout and checks for logged in users. You should do this when supplying additional public handler actions in your customization.
-
-#### event.initializeDatamanagerPage()
-
-Notice the handy `event.initializeDatamanagerPage()` in the example, above. This method will setup standard breadcrumbs for your page as well as setting up common variables that are available to other data manager pages such as:
-
-* `prc.recordId`: id of the current record being viewed
-* `prc.record`: current record being viewed
-* `prc.recordLabel`: rendered label field for the current record
-* `prc.objectName`: current object name
-* `prc.objectTitle`: translated title of the current object
-* `prc.objectTitlePlural`: translated _plural_ title of the current object
-
-The method expects either one, or two arguments: `objectName`, the name of the object, and `recordId`, the ID of the current record (if applicable).
-
-
-## Customization reference
-
-There are currently more than 60 customization points in the Data Manager and this number is set to grow. We have grouped them into categories below for your reference:
-
-### Record listing table / grid
-
->>> In addition to the specific customizations, below, you can also use the following helper functions in your handlers and views to render a data table / tree view for an object:
->>>
-```luceescript
-renderedListingTable = objectDataTable( objectName="blog_post", args={} );
-renderedTreeView = objectTreeView( objectName="article", args={} );
-```
-
-
-* [[datamanager-customization-listingviewlet|listingViewlet]]
-* [[datamanager-customization-prerenderlisting|preRenderListing]]
-* [[datamanager-customization-postrenderlisting|postRenderListing]]
-* [[datamanager-customization-gettoprightbuttonsforobject|getTopRightButtonsForObject]]
-* [[datamanager-customization-extratoprightbuttonsforobject|extraTopRightButtonsForObject]]
-* [[datamanager-customization-prefetchrecordsforgridlisting|preFetchRecordsForGridListing]]
-* [[datamanager-customization-getadditionalquerystringforbuildajaxlistinglink|getAdditionalQueryStringForBuildAjaxListingLink]]
-* [[datamanager-customization-postfetchrecordsforgridlisting|postFetchRecordsForGridListing]]
-* [[datamanager-customization-decoraterecordsforgridlisting|decorateRecordsForGridListing]]
-* [[datamanager-customization-getactionsforgridlisting|getActionsForGridListing]]
-* [[datamanager-customization-getrecordactionsforgridlisting|getRecordActionsForGridListing]]
-* [[datamanager-customization-extrarecordactionsforgridlisting|extraRecordActionsForGridListing]]
-* [[datamanager-customization-getrecordlinkforgridlisting|getRecordLinkForGridListing]]
-* [[datamanager-customization-listingmultiactions|listingMultiActions]]
-* [[datamanager-customization-getlistingmultiactions|getListingMultiActions]]
-* [[datamanager-customization-getextralistingmultiactions|getExtraListingMultiActions]]
-* [[datamanager-customization-getlistingbatchactions|getListingBatchActions]]
-* [[datamanager-customization-multirecordaction|multiRecordAction]]
-* [[datamanager-customization-renderfooterforgridlisting|renderFooterForGridListing]]
-
-
-### Adding records
-
-* [[datamanager-customization-addrecordform|addRecordForm]]
-* [[datamanager-customization-getaddrecordformname|getAddRecordFormName]]
-* [[datamanager-customization-getquickaddrecordformname|getQuickAddRecordFormName]]
-* [[datamanager-customization-prerenderaddrecordform|preRenderAddRecordForm]]
-* [[datamanager-customization-prequickaddrecordform|preQuickAddRecordForm]]
-* [[datamanager-customization-postrenderaddrecordform|postRenderAddRecordForm]]
-* [[datamanager-customization-addrecordactionbuttons|addRecordActionButtons]]
-* [[datamanager-customization-getaddrecordactionbuttons|getAddRecordActionButtons]]
-* [[datamanager-customization-getextraaddrecordactionbuttons|getExtraAddRecordActionButtons]]
-* [[datamanager-customization-gettoprightbuttonsforaddrecord|getTopRightButtonsForAddRecord]]
-* [[datamanager-customization-extratoprightbuttonsforaddrecord|extraTopRightButtonsForAddRecord]]
-* [[datamanager-customization-addrecordaction|addRecordAction]]
-* [[datamanager-customization-quickAddRecordAction|quickAddRecordAction]]
-* [[datamanager-customization-preaddrecordaction|preAddRecordAction]]
-* [[datamanager-customization-prequickaddrecordaction|preQuickAddRecordAction]]
-* [[datamanager-customization-postaddrecordaction|postAddRecordAction]]
-* [[datamanager-customization-postquickaddrecordaction|postQuickAddRecordAction]]
-
-
-### Viewing records
-
->>> The customizations below allow you to override or decorate the core record rendering system in Data Manager. In addition to these, you should also familiarize yourself with [[adminrecordviews]] as the core view record screen can also be customized using annotations within your Preside Objects.
-
-* [[datamanager-customization-renderrecord|renderRecord]]
-* [[datamanager-customization-prerenderrecord|preRenderRecord]]
-* [[datamanager-customization-postrenderrecord|postRenderRecord]]
-* [[datamanager-customization-prerenderrecordleftcol|preRenderRecordLeftCol]]
-* [[datamanager-customization-postrenderrecordleftcol|postRenderRecordLeftCol]]
-* [[datamanager-customization-prerenderrecordrightcol|preRenderRecordRightCol]]
-* [[datamanager-customization-postrenderrecordrightcol|postRenderRecordRightCol]]
-* [[datamanager-customization-gettoprightbuttonsforviewrecord|getTopRightButtonsForViewRecord]]
-* [[datamanager-customization-extratoprightbuttonsforviewrecord|extraTopRightButtonsForViewRecord]]
-
-### Editing records
-
-* [[datamanager-customization-editrecordform|editRecordForm]]
-* [[datamanager-customization-geteditrecordformname|getEditRecordFormName]]
-* [[datamanager-customization-getquickeditrecordformname|getQuickEditRecordFormName]]
-* [[datamanager-customization-prerendereditrecordform|preRenderEditRecordForm]]
-* [[datamanager-customization-prequickeditrecordform|preQuickEditRecordForm]]
-* [[datamanager-customization-postrendereditrecordform|postRenderEditRecordForm]]
-* [[datamanager-customization-editrecordactionbuttons|editRecordActionButtons]]
-* [[datamanager-customization-geteditrecordactionbuttons|getEditRecordActionButtons]]
-* [[datamanager-customization-getextraeditrecordactionbuttons|getExtraEditRecordActionButtons]]
-* [[datamanager-customization-gettoprightbuttonsforeditrecord|getTopRightButtonsForEditRecord]]
-* [[datamanager-customization-extratoprightbuttonsforeditrecord|extraTopRightButtonsForEditRecord]]
-* [[datamanager-customization-editrecordaction|editRecordAction]]
-* [[datamanager-customization-quickeditrecordaction|quickeditRecordAction]]
-* [[datamanager-customization-preeditrecordaction|preEditRecordAction]]
-* [[datamanager-customization-prequickeditrecordaction|preQuickEditRecordAction]]
-* [[datamanager-customization-posteditrecordaction|postEditRecordAction]]
-* [[datamanager-customization-postquickeditrecordaction|postQuickEditRecordAction]]
-
-### Cloning records
-
-* [[datamanager-customization-clonerecordform|cloneRecordForm]]
-* [[datamanager-customization-getclonerecordformname|getCloneRecordFormName]]
-* [[datamanager-customization-prerenderclonerecordform|preRenderCloneRecordForm]]
-* [[datamanager-customization-postrendereditrecordform|postRenderCloneRecordForm]]
-* [[datamanager-customization-clonerecordactionbuttons|cloneRecordActionButtons]]
-* [[datamanager-customization-getclonerecordactionbuttons|getCloneRecordActionButtons]]
-* [[datamanager-customization-getextraclonerecordactionbuttons|getExtraCloneRecordActionButtons]]
-* [[datamanager-customization-clonerecordaction|cloneRecordAction]]
-* [[datamanager-customization-preclonerecordaction|preCloneRecordAction]]
-* [[datamanager-customization-postclonerecordaction|postCloneRecordAction]]
-
-### Deleting records
-
-* [[datamanager-customization-deleterecordaction|deleteRecordAction]]
-* [[datamanager-customization-predeleterecordaction|preDeleteRecordAction]]
-* [[datamanager-customization-postdeleterecordaction|postDeleteRecordAction]]
-* [[datamanager-customization-prebatchdeleterecordsaction|preBatchDeleteRecordsAction]]
-* [[datamanager-customization-postbatchdeleterecordsaction|postBatchDeleteRecordsAction]]
-* [[datamanager-customization-getdeletionconfirmationmatch|getDeletionConfirmationMatch]]
-
-
-### Building links
-
-* [[datamanager-customization-buildlistinglink|buildListingLink]]
-* [[datamanager-customization-buildviewrecordlink|buildViewRecordLink]]
-* [[datamanager-customization-buildaddrecordlink|buildAddRecordLink]]
-* [[datamanager-customization-buildaddrecordactionlink|buildAddRecordActionLink]]
-* [[datamanager-customization-buildeditrecordlink|buildEditRecordLink]]
-* [[datamanager-customization-buildeditrecordactionlink|buildEditRecordActionLink]]
-* [[datamanager-customization-builddeleterecordactionlink|buildDeleteRecordActionLink]]
-* [[datamanager-customization-buildtranslaterecordlink|buildTranslateRecordLink]]
-* [[datamanager-customization-buildsortrecordslink|buildSortRecordsLink]]
-* [[datamanager-customization-buildmanagepermslink|buildManagePermsLink]]
-* [[datamanager-customization-buildajaxlistinglink|buildAjaxListingLink]]
-* [[datamanager-customization-buildmultirecordactionlink|buildMultiRecordActionLink]]
-* [[datamanager-customization-buildexportdataactionlink|buildExportDataActionLink]]
-* [[datamanager-customization-builddataexportconfigmodallink|buildDataExportConfigModalLink]]
-* [[datamanager-customization-buildrecordhistorylink|buildRecordHistoryLink]]
-* [[datamanager-customization-buildgetnodesfortreeviewlink|buildGetNodesForTreeViewLink]]
-
-### Permissioning
-
-* [[datamanager-customization-checkpermission|checkPermission]]
-* [[datamanager-customization-isoperationallowed|isOperationAllowed]]
-
-### General
-
-* [[datamanager-customization-prelayoutrender|preLayoutRender]]
-* [[datamanager-customization-toprightbuttons|topRightButtons]]
-* [[datamanager-customization-extratoprightbuttons|extraTopRightButtons]]
-* [[datamanager-customization-rootbreadcrumb|rootBreadcrumb]]
-* [[datamanager-customization-objectbreadcrumb|objectBreadcrumb]]
-* [[datamanager-customization-recordbreadcrumb|recordBreadcrumb]]
-* [[datamanager-customization-versionnavigator|versionNavigator]]
-
-
-## Interception points
-
-Your application can listen into several core interception points to enhance the features of the Data manager customization, e.g. to implement custom authentication. See the [ColdBox Interceptor's documentation](http://wiki.coldbox.org/wiki/Interceptors.cfm) for detailed documentation on interceptors.
-
-The Interception points are:
-
-### postExtraTopRightButtonsForObject
-
-Fired after the _extraTopRightButtonsForObject_ customization action had run. Takes `objectName` and `actions` as arguments.
-
-### postGetExtraQsForBuildAjaxListingLink
-
-Fired after the _getAdditionalQueryStringForBuildAjaxListingLink_ customization action (if any) had run. Takes `objectName` and `extraQs` as arguments.
-
-### postExtraRecordActionsForGridListing
-
-Fired after the _extraRecordActionsForGridListing_ customization action had run. Takes `record`, `objectName` and `actions` as arguments.
-
-### onGetListingBatchActions
-
-Fired during the _getListingMultiActions_ customisation action. Takes `args` as arguments.
-
-### postGetExtraListingMultiActions
-
-Fired after the _getExtraListingMultiActions_ customization action had run. Takes `args` as arguments.
-
-### postGetExtraAddRecordActionButtons
-
-Fired after the _getExtraAddRecordActionButtons_ customization action had run. Takes `args` as arguments.
-
-### postExtraTopRightButtonsForAddRecord
-
-Fired after the _extraTopRightButtonsForAddRecord_ customization action had run. Takes `objectName` and `actions` as arguments.
-
-### postExtraTopRightButtonsForViewRecord
-
-Fired after the _extraTopRightButtonsForViewRecord_ customization action had run. Takes `objectName` and `actions` as arguments.
-
-### postGetExtraEditRecordActionButtons
-
-Fired after the _getExtraEditRecordActionButtons_ customization action had run. Takes `args` as arguments.
-
-### postExtraTopRightButtonsForEditRecord
-
-Fired after the _extraTopRightButtonsForEditRecord_ customization action had run. Takes `objectName` and `actions` as arguments.
-
-### postGetExtraCloneRecordActionButtons
-
-Fired after the _getExtraCloneRecordActionButtons_ customization action had run. Takes `args` as arguments.
-
-### postExtraTopRightButtons
-
-Fired after the _extraTopRightButtons_ customization action had run. Takes `objectName`, `action` and `actions` as arguments.
-
-
-## Creating your own customizations
-
-You may wish to utilize the customization system in your extensions to allow implementations to easily override additional data manager features that you may provide. To do so, you can inject the [[api-datamanagercustomizationservice]] into your handler or service and make use of the methods:
-
-* [[datamanagercustomizationservice-runCustomization]]
-* [[datamanagercustomizationservice-objectHasCustomization]]
-
-For example:
-
-
-```luceescript
-if ( datamanagerCustomizationService.objectHasCustomization( objectName, "printPreview" ) ) {
- printPreview = datamanagerCustomizationService.runCustomization(
- objectName = objectName
- , action = "printPreview"
- , args = args
- );
-} else {
- printPreview = renderView( view=defaultView, args=args );
-}
-```
-
-Or:
-
-```luceescript
-printPreview = datamanagerCustomizationService.runCustomization(
- objectName = objectName
- , action = "printPreview"
- , defaultHandler = "myhandler.printPreview"
- , args = args
-);
-```
-
-## Custom navigation to your objects
-
-One of the most powerful changes in 10.9.0 is the ability to have objects use the Data Manager system _without needing to be listed in the Data Manager homepage_. This means that you could have a main navigation link directly to your object(s), for example. In short, you can build highly custom admin interfaces much quicker and with much less code.
-
-### Remove from Data Manager homepage
-
-To allow an object to use Data Manager without appearing in the Data Manager homepage listing, use the `@datamanagerEnabled true` annotation and **not** the `@datamanagerGroup` annotation. For example:
-
-```luceescript
-// /application/preside-objects/blog.cfc
-/**
- * @datamanagerEnabled true
- *
- */
-component {
- // ...
-}
-```
-
-### Example: Add to the admin left-hand menu
-
->>>>>> See [[adminlefthandmenu]] for a full guide to customizing the left-hand menu/navigation.
-
-In your application or extension's `Config.cfc` file, modify the `settings.adminSideBarItems` to add a new entry for your object. For example:
-
-```luceescript
-settings.adminSideBarItems.append( "blog" );
-```
-
-Then, create a corresponding view at `/views/admin/layout/sidebar/blog.cfm`. For _example_:
-
-```luceescript
-// /views/admin/layout/sidebar/blog.cfm
-hasPermission = hasCmsPermission(
- permissionKey = "read"
- , context = "datamanager"
- , contextKeys = [ "blog" ]
-);
-if ( hasPermission ) {
- Echo( renderView(
- view = "/admin/layout/sidebar/_menuItem"
- , args = {
- active = ReFindNoCase( "^admin\.datamanager", event.getCurrentEvent() ) && ( prc.objectName ?: "" ) == "blog"
- , link = event.buildAdminLink( objectName="blog" )
- , gotoKey = "b"
- , icon = "fa-comments"
- , title = translateResource( 'preside-objects.blog:menu.title' )
- }
- ) );
-}
-
-```
-
-### Modify the breadcrumb
-
-By default, your object will get breadcrumbs that start with a link to the Data Manager homepage. Use the breadcrumb customizations to modify this:
-
-* [[datamanager-customization-rootbreadcrumb|rootBreadcrumb]]
-* [[datamanager-customization-objectbreadcrumb|objectBreadcrumb]]
-* [[datamanager-customization-recordbreadcrumb|recordBreadcrumb]]
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function rootBreadcrumb() {
- // Deliberately do nothing so as to remove the root
- // 'Data manager' breadcrumb just for the 'blog' object.
-
- // We could, instead, call event.addAdminBreadCrumb( title=title, link=link )
- // to provide an alternative root breadcrumb
- }
-
-}
-```
-
-## Modify core default page titles and other layout changes
-
-A really useful customization is the [[datamanager-customization-prelayoutrender|preLayoutRender]] customization. This fires before the full admin page layout is rendered and allows you to make adjustments after all the handler logic has run. For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function preLayoutRender( event, rc, prc, args={} ) {
- prc.pageTitle = translateResource(
- uri = "preside-objects.blog:#args.action#.page.title"
- , defaultValue = prc.pageTitle ?: ""
- );
- prc.pageSubTitle = translateResource(
- uri = "preside-objects.blog:#args.action#.page.subtitle"
- , defaultValue = prc.pageSubTitle ?: ""
- );
- prc.pageIcon = "fa-comments";
- }
-
- private void function preLayoutRenderForEditRecord( event, rc, prc, args={} ) {
- prc.pageTitle = translateResource(
- uri = "preside-objects.blog:editRecord.page.title"
- , data = [ prc.recordLabel ?: "" ]
- );
-
- // modify the title of the last breadcrumb
- var breadCrumbs = event.getAdminBreadCrumbs();
- breadCrumbs[ breadCrumbs.len() ].title = prc.pageTitle;
- }
-}
-```---
-id: datamanager-customization-isoperationallowed
-title: "Data Manager customization: isOperationAllowed"
----
-
-## Data Manager customization: isOperationAllowed
-
-Similar to the [[datamanager-customization-checkpermission|checkPermission]] customization, the `isOperationAllowed` customization allows you to completely override the core Data Manager logic for determining whether the given operation is allowed for the object.
-
-It is expected to return a `boolean` value and is given the following in the `args` struct:
-
-* `objectName`: The name of the object
-* `operation`: The operation to check. Core operations are: `add`, `arguments`, `delete`, `edit` and `read`
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private boolean function isOperationAllowed( event, rc, prc, args={} ) {
- var operation = args.operation ?: "";
-
- return operation != "delete";
- }
-
-}
-```
-
->>> For core operations, you are also able to achieve similar results by setting `@dataManagerAllowedOperations` on your preside object. See [[datamanagerbasics]] for documentation.
-
-
-
----
-id: datamanager-customization-postclonerecordaction
-title: "Data Manager customization: postCloneRecordAction"
----
-
-## Data Manager customization: postCloneRecordAction
-
-The `postCloneRecordAction` customization allows you to run logic _after_ the core Data Manager clone record logic is run. It is not expected to return a value and is supplied the following in the `args` struct:
-
-* `objectName`: name of the object
-* `newId`: ID of the newly cloned record
-* `formData`: struct containing the form submission
-* `existingRecord`: struct containing the data from the current record
-* `validationResult`: validation result from general form validation
-
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function postCloneRecordAction( event, rc, prc, args={} ) {
- // redirect to a different than default page
- setNextEvent( event.buildAdminLink(
- objectName = "blog"
- , recordId = ( args.formData.id ?: "" )
- , operation = "preview"
- ) );
- }
-}
-```
-
-See also: [[datamanager-customization-preclonerecordaction|predCloneRecordAction]] and [[datamanager-customization-clonerecordaction|cloneRecordAction]].
-
----
-id: datamanager-customization-renderfooterforgridlisting
-title: "Data Manager customization: renderFooterForGridListing"
----
-
-## Data Manager customization: renderFooterForGridListing
-
->>> This feature was introduced in 10.11.0
-
-The `renderFooterForGridListing` customization allows you render footer text at the bottom of a dynamic data grid in the Data Manager. This may be to show a sum of certain fields based on the search and filters used, or just show a static message. It must return the string of the rendered message.
-
-* `objectName`: The name of the object
-* `records`: The paginated records that have been selected to show
-* `getRecordsArgs`: Arguments that were passed to [[datamanagerservice-getrecordsforgridlisting]], including filters
-
-For example:
-
-
-```luceescript
-// /application/handlers/admin/datamanager/pipeline.cfc
-component {
-
- property name="pipelineService" inject="pipelineService";
-
- private string function renderFooterForGridListing( event, rc, prc, args={} ) {
- var pr = pipelineService.getPipelineTotalReport(
- filter = args.getRecordsArgs.filter ?: {}
- , extraFilters = args.getRecordsArgs.extraFilters ?: []
- , searchQuery = args.getRecordsArgs.searchQuery ?: ""
- , gridFields = args.getRecordsArgs.gridFields ?: []
- , searchFields = args.getRecordsArgs.searchFields ?: []
- );
-
- return translateResource(
- uri = "pipeline_table:listing.table.footer"
- , data = [ NumberFormat( pr.total ), NumberFormat( pr.adjusted ), pr.currencySymbol ]
- );
- }
-
-}
-```---
-id: datamanager-customization-buildviewrecordlink
-title: "Data Manager customization: buildViewRecordLink"
----
-
-## Data Manager customization: buildViewRecordLink
-
-The `buildViewRecordLink` customization allows you to customize the URL for viewing an object's record. It is expected to return the URL as a string and is provided the `objectName` and `recordId` in the `args` struct along with any other arguments passed to `event.buildAdminLink()`. In addition, it may also be given `version` and `language` keys in the `args` struct should versioning and/or multilingual be enabled. e.g.
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function buildViewRecordLink( event, rc, prc, args={} ) {
- var recordId = args.recordId ?: "";
- var version = Val( args.version ?: "" );
- var qs = "id=" & recordId;
-
- if ( version ) {
- qs &= "&version=" & version;
- }
-
- if ( Len( Trim( args.queryString ?: "" ) ) {
- qs &= "&" & args.queryString;
- }
-
- return event.buildAdminLink( linkto="admin.blogmanager.viewrecord", queryString=qs );
- }
-
-}
-```
-
----
-id: datamanager-customization-buildrecordhistorylink
-title: "Data Manager customization: buildRecordHistoryLink"
----
-
-## Data Manager customization: buildRecordHistoryLink
-
-The `buildRecordHistoryLink` customization allows you to customize the URL for viewing an object record's version history. It is expected to return the URL as a string and is provided the `objectName` and `recordId` in the `args` struct along with any other arguments passed to `event.buildAdminLink()`. e.g.
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function buildRecordHistoryLink( event, rc, prc, args={} ) {
- var recordId = args.recordId ?: "";
- var qs = "id=" & recordId;
-
- if ( Len( Trim( args.queryString ?: "" ) ) {
- qs &= "&" & args.queryString;
- }
-
- return event.buildAdminLink( linkto="admin.blogmanager.viewrecordhistory", queryString=qs );
- }
-
-}
-```
-
-
----
-id: datamanager-customization-renderrecord
-title: "Data Manager customization: renderRecord"
----
-
-## Data Manager customization: renderRecord
-
-The `renderRecord` customization allows you to completely override the rendering of a single record for your object. Permissions checking, crumbtrails and page titles will all be taken care of; but the rest is up to you.
-
-The action is expected to return the rendered HTML of the record as a string and is provided the following in the args struct:
-
-* `objectName`: The object name
-* `recordId`: ID of the record
-* `version`: Version number of the record (if the object is versioned)
-
->>>>>> You can also make use of variables in the `prc` scope, such as `prc.record`, that will allow you to potentially not duplicate calls to the database.
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function renderRecord() {
- args.blog = prc.record ?: QueryNew(''); // Data Manager will have already fetched the record for you. Check out the prc scope for other commonly fetched goodies that you can make use of
-
- return renderView( view="/admin/blogs/customRecordView", args=args );
- }
-
-}
-```
-
----
-id: datamanager-customization-getactionsforgridlisting
-title: "Data Manager customization: getActionsForGridListing"
----
-
-## Data Manager customization: getActionsForGridListing
-
-The `getActionsForGridListing` customization allows you to completely rewrite the logic for adding grid actions to an object's listing table (by grid actions, we mean the list of links to the right of each row in the table).
-
-The method must return _an array_. Each item in the array should be a rendered set of actions for the corresponding row in the recordset passed in `args.records`. For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog_post.cfc
-component {
-
- private array function getActionsForGridListing( event, rc, prc, args={} ) {
- var records = args.records ?: QueryNew('');
- var actions = [];
-
- if ( records.recordCount ) {
- // This is a condensed example of a useful general approach.
- // Render *outside* of the loop and use placeholders.
- // Then just replace placeholders when looping the records
- // for much better efficiency
- var template = renderView( view="/admin/my/custom/gridActions", args={ id="{id}" } );
-
- for( var record in records ) {
- actions.append( template.replace( "{id}", record.id, "all" ) );
- }
- }
-
-
- return actions;
- }
-
-}
-```
-
----
-id: datamanager-customization-buildtranslaterecordlink
-title: "Data Manager customization: buildTranslateRecordLink"
----
-
-## Data Manager customization: buildTranslateRecordLink
-
-The `buildTranslateRecordLink` customization allows you to customize the URL for displaying an object's translate record form. It is expected to return the URL as a string and is provided the following in the `args` struct:
-
-* `objectName`: Name of the object
-* `recordId`: ID of the record to be translated
-* `language`: ID of the language
-* `version`: If versioning enabled, specific version number to load
-* `fromDataGrid`: Whether or not this link was built for data grid (can be used to direct back to grid, rather than edit/view record)
-
-e.g.
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function buildTranslateRecordLink( event, rc, prc, args={} ) {
- var recordId = args.recordId ?: "";
- var language = args.language ?: "";
- var version = Val( args.version ?: "" );
- var qs = "id=#recordId#&language=#language#";
-
- if ( version ) {
- qs &= "&version=" & version;
- }
-
- if ( Len( Trim( args.queryString ?: "" ) ) {
- qs &= "&" & args.queryString;
- }
-
- return event.buildAdminLink( linkto="admin.blogmanager.translate", queryString=qs );
- }
-
-}
-```
-
-
-
----
-id: datamanager-customization-prerenderlisting
-title: "Data Manager customization: preRenderListing"
----
-
-## Data Manager customization: preRenderListing
-
-The `preRenderListing` customization allows you to add your own output _above_ the default object listing screen.
-
-The customization handler should return a string of the rendered viewlet and is supplied an args structure with an `objectName` key.
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/sensitive_data.cfc
-component {
-
- private string function preRenderListing( event, rc, prc, args={} ) {
- return '
Warning: use this listing with extreme caution.
';
- }
-
-}
-```
-
----
-id: datamanager-customization-getclonerecordactionbuttons
-title: "Data Manager customization: getCloneRecordActionButtons"
----
-
-## Data Manager customization: getCloneRecordActionButtons
-
-The `getCloneRecordActionButtons` customization allows you to _completely override_ the set of buttons and links that appears below the clone record form. It must _return an array_ of structs that describe the buttons to display and is provided `objectName` and `recordId` in the `args` struct.
-
-Note, if you simply want to add, or tweak, the buttons, you may wish to use [[datamanager-customization-getextraclonerecordactionbuttons]].
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private array function getCloneRecordActionButtons( event, rc, prc, args={} ) {
- var actions = [{
- type = "link"
- , href = event.buildAdminLink( objectName="blog" )
- , class = "btn-default"
- , globalKey = "c"
- , iconClass = "fa-reply"
- , label = translateResource( uri="cms:cancel.btn" )
- }];
-
- actions.append({
- type = "button"
- , class = "btn-info"
- , iconClass = "fa-save"
- , name = "_saveAction"
- , value = "publish"
- , label = translateResource( uri="cms:datamanager.addrecord.btn", data=[ prc.objectTitle ?: "" ] )
- } );
-
- actions.append({
- type = "button"
- , class = "btn-plus"
- , iconClass = "fa-save"
- , name = "_saveAction"
- , value = "publishAndClone"
- , label = translateResource( uri="cms:presideobjects.blog:addrecord.and.clone.btn", data=[ prc.objectTitle ?: "" ] )
- } );
-
- return actions;
- }
-
-}
-```
-
->>> See [[datamanager-customization-actionbuttons]] for detailed documentation on the format of the action items.
-
----
-id: datamanager-customization-getextraeditrecordactionbuttons
-title: "Data Manager customization: getExtraEditRecordActionButtons"
----
-
-## Data Manager customization: getExtraEditRecordActionButtons
-
-The `getExtraEditRecordActionButtons` customization allows you to modify the set of buttons and links that appears below the edit record form. It is expected _not_ to return a value and receives the following in the `args` struct:
-
-* `objectName`: The name of the object
-* `recordId`: The id of the current record
-* `actions`: the array of button "actions"
-
-Note, if you want to completely override the buttons, you may wish to use [[datamanager-customization-geteditrecordactionbuttons]].
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private array function getExtraEditRecordActionButtons( event, rc, prc, args={} ) {
- var actions = args.actions ?: [];
-
- actions.append({
- type = "button"
- , class = "btn-plus"
- , iconClass = "fa-save"
- , name = "_saveAction"
- , value = "publishAndEdit"
- , label = translateResource( uri="cms:presideobjects.blog:editrecord.and.edit.btn", data=[ prc.objectTitle ?: "" ] )
- } );
- }
-
-}
-```
-
->>> See [[datamanager-customization-actionbuttons]] for detailed documentation on the format of the action items.
-
----
-id: datamanager-customization-buildexportdataactionlink
-title: "Data Manager customization: buildExportDataActionLink"
----
-
-## Data Manager customization: buildExportDataActionLink
-
-The `buildExportDataActionLink` customization allows you to customize the URL used to submit data export forms. It is expected to return the URL as a string and is provided the `objectName` in the `args` struct along with any other arguments passed to `event.buildAdminLink()`. e.g.
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function buildExportDataActionLink( event, rc, prc, args={} ) {
- var queryString = args.queryString ?: "";
-
- return event.buildAdminLink( linkto="admin.blogmanager.dataExportAction", queryString=queryString );
- }
-
-}
-```
-
----
-id: datamanager-customization-getquickaddrecordformname
-title: "Data Manager customization: getQuickAddRecordFormName"
----
-
-## Data Manager customization: getQuickAddRecordFormName
-
->>> This customization was added in Preside 10.13.0
-
-The `getQuickAddRecordFormName` customization allows you to use a different form name than the Data Manager default for "quick adding" records. The method should return the form name (see [[presideforms]]) and is provided `args.objectName` should you need to use it. For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function getQuickAddRecordFormName( event, rc, prc, args={} ) {
- return "admin.blogs.addblog";
- }
-
-}
-```
-
----
-id: datamanager-customization-postQuickEditrecordaction
-title: "Data Manager customization: postQuickEditRecordAction"
----
-
-## Data Manager customization: postQuickEditRecordAction
-
-The `postQuickEditRecordAction` customization allows you to run logic _after_ the core Data Manager add record logic is run. It is not expected to return a value and is supplied the following in the `args` struct:
-
-* `objectName`: name of the object
-* `formData`: struct containing the form submission
-* `validationResult`: validation result from general form validation
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function postQuickEditRecordAction( event, rc, prc, args={} ) {
- var newId = args.newId ?: "";
-
- // redirect to a different than default page
- setNextEvent( url=event.buildAdminLink(
- objectName = "blog"
- , recordId = newId
- , operation = "preview"
- ) );
- }
-}
-```
-
-See also: [[datamanager-customization-prequickeditrecordaction|preQuickEditRecordAction]] and [[datamanager-customization-quickeditrecordaction|quickEditRecordAction]].
-
-
----
-id: datamanager-customization-getlistingmultiactions
-title: "Data Manager customization: getListingMultiActions"
----
-
-## Data Manager customization: getListingMultiActions
-
-The `getListingMultiActions` customization allows you to completely override the array of buttons that gets rendered as part of the listing screen (displayed when a user selects rows from the grid). It should return an array of button definitions as defined in [[datamanager-customization-multi-action-buttons]].
-
-Note, if you only want to modify the buttons, or add / remove to them, look at: [[datamanager-customization-getextralistingmultiactions|getExtraListingMultiActions]]. Overriding the generated buttons string entirely can be achieved with: [[datamanager-customization-listingmultiactions|listingMultiActions]].
-
-
-For example:
-
-
-```luceescript
-// /application/handlers/admin/datamanager/GlobalDefaults.cfc
-component {
-
- private array function getListingMultiActions( event, rc, prc, args={} ) {
- return [{
- label = "Archive selected entities"
- , name = "archive"
- , prompt = "Archive the selected entities"
- , globalKey = "d"
- , class = "btn-danger"
- , iconClass = "fa-trash-o"
- }];
- }
-
-}
-```---
-id: datamanager-customization-deleterecordaction
-title: "Data Manager customization: deleteRecordAction"
----
-
-## Data Manager customization: deleteRecordAction
-
-The `deleteRecordAction` allows you to override the core action logic for deleting a record through the Data Manager. The core will have already checked permissions for deleting records, but all other logic will be up to you to implement (including audit trails, etc.).
-
-The method is not expected to return a value and is provided with `args.objectName` and `args.recordId`. _The expectation is that the method will redirect the user after processing the request._
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- property name="blogService" inject="blogService";
- property name="messageBox" inject="messagebox@cbmessagebox";
-
- private void function deleteRecordAction( event, rc, prc, args={} ) {
- blogService.archiveBlog( args.recordId ?: "" );
-
- messageBox.info( translateResource( uri="preside-objects.blog:archived.message", data=[ prc.recordLabel ?: "" ] ) );
-
- setNextEvent( url=event.buildAdminLink( objectName = "blog" ) );
- }
-
-}
-```---
-id: datamanager-customization-editrecordaction
-title: "Data Manager customization: editRecordAction"
----
-
-## Data Manager customization: editRecordAction
-
-The `editRecordAction` allows you to override the core action logic for adding a record when a form is submitted. The core will have already checked permissions for editing records, but all other logic will be up to you to implement (including audit trails, validation, etc.).
-
-The method is not expected to return a value and is provided with `args.objectName` and `args.recordId`. _The expectation is that the method will redirect the user after processing the request._
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- property name="blogService" inject="blogService";
-
- private void function editRecordAction( event, rc, prc, args={} ) {
- var formName = "my.custom.editrecord.form";
- var recordId = args.recordId ?: "";
- var formData = event.getDataForForm( formName );
- var validationResult = validateForm( formName, formData );
-
- if ( validationResult.validated ) {
- blogService.saveBlog( argumentCollection=formData, id=recordId );
-
- setNextEvent( url=event.buildAdminLink(
- objectName = "blog"
- , recordId = recordId
- ) );
- }
-
- var persist = formData;
- persist.validationResult = validationResult;
-
- setNextEvent( url=event.buildAdminLink(
- objectName = "blog"
- , operation = "editRecord"
- , recordId = recordId
- ), persistStruct=persist );
-
- }
-
-}
-
-
-```
-
->>> If you wish to still use core logic for editing records but need to add additional logic to the process, use [[datamanager-customization-preeditrecordaction|preEditRecordAction]] or [[datamanager-customization-posteditrecordaction|postEditRecordAction]] instead.
-
----
-id: datamanager-customization-postrenderrecordleftcol
-title: "Data Manager customization: postRenderRecordLeftCol"
----
-
-## Data Manager customization: postRenderRecordLeftCol
-
-The `postRenderRecordLeftCol` customization allows you to add custom HTML _below_ the left-hand column of the core view record screen for your object (see [[adminrecordviews]]). The action is expected to return a string containing the HTML and is provided the following in the `args` struct:
-
-* `objectName`: The object name
-* `recordId`: ID of the record
-* `version`: Version number of the record (if the object is versioned)
-
->>>>>> You can also make use of variables in the `prc` scope, such as `prc.record`, that will allow you to potentially not duplicate calls to the database.
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function postRenderRecordLeftCol() {
- args.blog = prc.record ?: QueryNew('');
-
- return renderView( view="/admin/blogs/auditTrail", args=args );
- }
-
-}
-```
-
----
-id: datamanager-customization-gettoprightbuttonsforeditrecord
-title: "Data Manager customization: getTopRightButtonsForEditRecord"
----
-
-## Data Manager customization: getTopRightButtonsForEditRecord
-
-The `getTopRightButtonsForEditRecord` customization allows you to _completely override_ the set of buttons that appears at the top right hand side of the edit record screen. It must _return an array_ of structs that describe the buttons to display and is provided the `objectName` in the `args` struct.
-
-Note, if you simply want to add, or tweak, the top right buttons, you may wish to use [[datamanager-customization-extratoprightbuttonsforeditrecord]].
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private array function getTopRightButtonsForEditRecord( event, rc, prc, args={} ) {
- var actions = [];
- var objectName = args.objectName ?: "";
- var recordId = prc.recordId ?: "";
-
- actions.append({
- link = event.buildAdminLink( objectName=objectName, operation="reports", recordId=recordId )
- , btnClass = "btn-default"
- , iconClass = "fa-bar-chart"
- , globalKey = "r"
- , title = translateResource( "preside-objects.blog:reports.btn" )
- } );
-
- return actions;
- }
-
-}
-```
-
->>> See [[datamanager-customization-toprightbuttonsformat]] for detailed documentation on the format of the action items.---
-id: datamanager-customization-editrecordform
-title: "Data Manager customization: editRecordForm"
----
-
-## Data Manager customization: editRecordForm
-
-The `editRecordForm` customization allows you to completely overwrite the view for rendering the edit record form page. The crumb trail, permissions checks and page title will be taken care of, but the rest is up to you.
-
-The handler should return a string (the rendered edit record form page) is provided the following in the `args` struct.
-
-* `objectName`: The name of the object
-* `recordId`: The ID of the record being edited
-* `record`: Struct of the record being edited
-* `editRecordAction`: URL for submitting the form
-* `useVersioning`: Whether or not to use versioning
-* `version`: Version number (for versioning only) of the record in `args.record`
-* `draftsEnabled`: Whether or not drafts are enabled
-* `canSaveDraft`: Whether or not the current user can save drafts (for drafts only)
-* `canPublish`: Whether or not the current user can publish (for drafts only)
-* `cancelAction`: URL that any rendered 'cancel' link should use
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-component {
-
- private string function editRecordForm( event, rc, prc, args={} ) {
- return renderView( view="/admin/my/custom/editrecordForm", args=args );
- }
-
-}
-```
-
-
----
-id: datamanager-customization-prerenderrecordrightcol
-title: "Data Manager customization: preRenderRecordRightCol"
----
-
-## Data Manager customization: preRenderRecordRightCol
-
-The `preRenderRecordRightCol` customization allows you to add custom HTML _above_ the right-hand column of the core view record screen for your object (see [[adminrecordviews]]). The action is expected to return a string containing the HTML and is provided the following in the `args` struct:
-
-* `objectName`: The object name
-* `recordId`: ID of the record
-* `version`: Version number of the record (if the object is versioned)
-
->>>>>> You can also make use of variables in the `prc` scope, such as `prc.record`, that will allow you to potentially not duplicate calls to the database.
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function preRenderRecordRightCol() {
- args.blog = prc.record ?: QueryNew('');
-
- return renderView( view="/admin/blogs/auditTrail", args=args );
- }
-
-}
-```
-
----
-id: datamanager-customization-extratoprightbuttonsforaddrecord
-title: "Data Manager customization: extraTopRightButtonsForAddRecord"
----
-
-## Data Manager customization: extraTopRightButtonsForAddRecord
-
-The `extraTopRightButtonsForAddRecord` customization allows you to add to, or modify, the set of buttons that appears at the top right hand side of the add record screen. It is provided an `actions` array along with the `objectName` in the `args` struct and is not expected to return a value.
-
-Modifying `args.actions` is required to make changes to the top right buttons.
-
-
->>> See [[datamanager-customization-toprightbuttonsformat]] for detailed documentation on the format of the action items.
-
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function extraTopRightButtonsForAddRecord( event, rc, prc, args={} ) {
- var objectName = args.objectName ?: "";
-
- args.actions = args.actions ?: [];
-
- args.actions.append({
- link = event.buildAdminLink( objectName=objectName, operation="reports" )
- , btnClass = "btn-default"
- , iconClass = "fa-bar-chart"
- , globalKey = "r"
- , title = translateResource( "preside-objects.blog:reports.btn" )
- } );
- }
-
-}
-```
-
-
-
-
-
----
-id: datamanager-customization-postbatchdeleterecordsaction
-title: "Data Manager customization: postBatchDeleteRecordsAction"
----
-
-## Data Manager customization: postBatchDeleteRecordsAction
-
-As of **Preside 10.16.0**, the `postBatchDeleteRecordsAction` customization allows you to run logic _after_ the core Data Manager logic batch deletes a number of records. It is not expected to return a value and is supplied the following in the `args` struct:
-
-* `object`: name of the object
-* `records`: query containing the records that will be deleted
-* `logger`: logger object - used to output logs to an end user following the batch delete process
-* `progress`: progress object - used to update progress bar for the end user following the batch delete process
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- property name="blogService" inject="blogService";
-
- private void function postBatchDeleteRecordsAction( event, rc, prc, args={} ) {
- var canLog = StructKeyExists( args, "logger" );
- var canInfo = canLog && args.logger.canInfo();
-
- for( var record in records ) {
- blogService.notifyServicesOfDeletedBlog( record.id );
- if ( canInfo ) {
- args.logger.info( "Did something with [#record.label#]" );
- }
- }
- }
-}
-
-```
-
-See also: [[datamanager-customization-prebatchdeleterecordsaction|preBatchDeleteRecordsAction]]
-
-
-
----
-id: datamanager-customization-postQuickaddrecordaction
-title: "Data Manager customization: postQuickAddRecordAction"
----
-
-## Data Manager customization: postQuickAddRecordAction
-
-The `postQuickAddRecordAction` customization allows you to run logic _after_ the core Data Manager add record logic is run. It is not expected to return a value and is supplied the following in the `args` struct:
-
-* `objectName`: name of the object
-* `formData`: struct containing the form submission
-* `newId`: ID of the newly created record
-
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function postQuickAddRecordAction( event, rc, prc, args={} ) {
- var newId = args.newId ?: "";
-
- // redirect to a different than default page
- setNextEvent( url=event.buildAdminLink(
- objectName = "blog"
- , recordId = newId
- , operation = "preview"
- ) );
- }
-}
-```
-
-See also: [[datamanager-customization-prequickaddrecordaction|preQuickAddRecordAction]] and [[datamanager-customization-quickaddrecordaction|quickAddRecordAction]].
-
-
----
-id: datamanager-customization-prelayoutrender
-title: "Data Manager customization: preLayoutRender"
----
-
-## Data Manager customization: preLayoutRender
-
-The `preLayoutRender` customization allows you fire off code just before the full admin page layout is rendered for a Data manager based page. The customization is **not** expected to return a value and can be used to set variables that effect the layout such as `prc.pageTitle`, `prc.pageIcon` and the breadcrumbs for the request.
-
-In addition to this global customization, you can also implement customizations with the convention `preLayoutRenderFor{actionName}`, where `{actionName}` is the name of the current data manager action. For example `preLayoutRenderForViewRecord`.
-
-The following attributes are available in the `args` struct but examining the `prc` scope is also useful for getting at already generated content such as `prc.record`, `prc.recordLabel`, etc.
-
-* `objectName`: the name of the object
-* `action`: the current coldbox action, e.g. `editRecord`, `viewRecord`, `object`, etc.
-
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private void function preLayoutRender( event, rc, prc, args={} ) {
- prc.pageTitle = translateResource(
- uri = "preside-objects.blog:#args.action#.page.title"
- , defaultValue = prc.pageTitle ?: ""
- );
- }
-
- private void function preLayoutRenderForEditRecord( event, rc, prc, args={} ) {
- prc.pageTitle = translateResource(
- uri = "preside-objects.blog:editRecord.page.title"
- , data = [ prc.recordLabel ?: "" ]
- );
-
- // modify the title of the last breadcrumb
- var breadCrumbs = event.getAdminBreadCrumbs();
- breadCrumbs[ breadCrumbs.len() ].title = prc.pageTitle;
- }
-}
-```---
-id: datamanager-customization-buildgetnodesfortreeviewlink
-title: "Data Manager customization: buildGetNodesForTreeViewLink"
----
-
-## Data Manager customization: buildGetNodesForTreeViewLink
-
-The `buildGetNodesForTreeViewLink` customization allows you to customize the ajax URL for fetching child nodes for tree view. It is expected to return the listing URL as a string and is provided the `objectName` in the `args` struct along with any other arguments passed to `event.buildAdminLink()`. e.g.
-
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- private string function buildGetNodesForTreeViewLink( event, rc, prc, args={} ) {
- var queryString = args.queryString ?: "";
-
- return event.buildAdminLink( linkto="admin.blogmanager.ajaxTreeViewNodes", queryString=queryString );
- }
-
-}
-```
-
->>> See [[datamanagerbasics]] for information regarding setting up a tree view for your object.
----
-id: datamanager-customization-preQuickeditrecordaction
-title: "Data Manager customization: preQuickEditRecordAction"
----
-
-## Data Manager customization: preQuickEditRecordAction
-
-The `preQuickEditRecordAction` customization allows you to run logic _before_ the core Data Manager edit record logic is run. It is not expected to return a value and is supplied the following in the `args` struct:
-
-* `objectName`: name of the object
-* `formData`: struct containing the form submission
-
-For example:
-
-```luceescript
-// /application/handlers/admin/datamanager/blog.cfc
-
-component {
-
- property name="blogService" inject="blogService";
-
- private void function preQuickEditRecordAction( event, rc, prc, args={} ) {
- rc.clearance_level = blogService.calculateClearanceLevel( argumentCollection=args.formData ?: {} );
- }
-}
-
-```
-
-See also: [[datamanager-customization-postquickeditrecordaction|postQuickEditRecordAction]] and [[datamanager-customization-quickeditrecordaction|quickEditRecordAction]].
-
-
----
-id: datamanager-customization-clonerecordactionbuttons
-title: "Data Manager customization: cloneRecordActionButtons"
----
-
-## Data Manager customization: cloneRecordActionButtons
-
-The `cloneRecordActionButtons` customization allows you to completely override the form action buttons (e.g. "Cancel", "Add record") for the clone record form. The handler is expected to return a string that is the rendered HTML and is provided the following in the `args` struct:
-
-* `objectName`: The name of the object
-* `recordId`: The ID of the record being cloneed
-* `record`: Struct of the record being cloneed
-* `cloneRecordAction`: URL for submitting the form
-* `draftsEnabled`: Whether or not drafts are enabled
-* `canSaveDraft`: Whether or not the current user can save drafts (for drafts only)
-* `canPublish`: Whether or not the current user can publish (for drafts only)
-* `cancelAction`: URL that any rendered 'cancel' link should use
-
-For example:
-
-
-```luceescript
-// /application/handlers/admin/datamanager/GlobalDefaults.cfc
-component {
-
- private string function cloneRecordActionButtons( event, rc, prc, args={} ) {
- var objectName = args.objectName ?: "";
-
- args.cancelAction = event.buildAdminLink( objectName=objectName );
-
- return renderView( view="/admin/datamanager/globaldefaults/cloneRecordActionButtons", args=args );
- }
-
-}
-```
-
-```lucee
-
-
-
-
-
-```
-
->>>> The core implementation has logic for showing different buttons for drafts and dynamically building labels for buttons, etc. Be sure to know what you're missing out on when overriding this (or any) customization!
-
----
-id: customizing-deletion-prompt-matches
-title: "Customizing the delete record prompt and match text"
----
-
-## Summary
-
-As of **10.16.0**, you are able to configure objects to use a "match" text in the delete prompt. You can configure both application-wide default behaviour, and object-level overrides for the default.
-
-![Screenshot of a delete record prompt](images/screenshots/deleteprompt.png)
-
-## Configuring application defaults
-
-### Enabling/disabling the match text
-
-There are two **Config.cfc** settings that control whether or not match text must be input:
-
-```luceescript
-// default values supplied by Preside
-settings.dataManager.defaults.typeToConfirmDelete = false;
-settings.dataManager.defaults.typeToConfirmBatchDelete = true;
-```
-
-So by _default_, we _will_ prompt to enter a matching text when _batch_ deleting records, but _not_ while deleting _single_ records. Update the settings above to change this behaviour.
-
-### Customizing the global match text
-
-Two i18n entries are used for the match text. To change them, supply your own application/extension overrides of the properties:
-
-```properties
-# /i18n/cms.properties
-datamanager.delete.record.match=delete
-datamanager.batch.delete.records.match=delete
-```
-
-## Per object customisation
-
-### Enabling/disabling the match text
-
-To have an object use a non-default behaviour, annotate the object cfc file with the `datamanagerTypeToConfirmDelete` and/or `datamanagerTypeToConfirmBatchDelete` flags:
-
-```luceescript
-/**
- * @datamanagerTypeToConfirmDelete true
- * @datamanagerTypeToConfirmBatchDelete true
- *
- */
-component {
- // ...
-}
-
-```
-
-### Customizing per-object match text
-
-You have two approaches available here, static i18n match text and dynamically generated text for single record deletes.
-
-#### Static i18n
-
-In your object's `.properties` file (i.e. `/i18n/preside-objects/my_object.propertes`), implement the property keys `delete.record.match` and/or `batch.delete.records.match`. i.e.
-
-```properties
-# ...
-
-delete.record.match=CONFIRM
-batch.delete.records.match=DO IT
-```
-
-#### Dynamic match text for single record deletes
-
-To create dynamic match text per record, use the datamanager customisation: [[datamanager-customization-getrecorddeletionpromptmatch|getRecordDeletionPromptMatch]] (see guide for more details).
-
-
-
-
----
-id: datamanager-customization-prerendereditrecordform
-title: "Data Manager customization: preRenderEditRecordForm"
----
-
-## Data Manager customization: preRenderEditRecordForm
-
-The `preRenderEditRecordForm` customization allows you to add rendered HTML _before_ the rendering of the core edit record form. The HTML will live _inside_ the html `