The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Rose::DBx::Object::Renderer - Web UI Rendering for Rose::DB::Object

SYNOPSIS

  use Rose::DBx::Object::Renderer;

  use CGI;
  my $query = new CGI;
  print $query->header();

  # Load a database, for instance, called 'company', which has two tables: 'employee' and 'position' where employee has a position
  load_database('company', {db_username => 'root', db_password => 'root'});

  # Render a form to add employees
  Company::Employee->render_as_form();

  # Load an object and render a customised form
  my $e = Company::Employee->new(id => 1);
  $e->load;
  $e->render_as_form(template => 'custom_template.tt');
  
  
  # Render a link to google map for the 'address' column
  print $e->address_for_view();


  # Render a table
  Company::Employee::Manager->render_as_table();

  # Render a table for all the employees who love 'Coding' with create, edit, and delete access
  Company::Employee::Manager->render_as_table(
    get => {query => [hobby => 'Coding']}
    order => ['first_name', 'email', 'address', 'phone'],
    create => 1, 
    edit => 1,
    delete => 1,
    searchable => ['first_name', 'address']
  );

  # Render a menu
  my $menu = Company::Employee::Manager->render_as_menu (
    order => ['Company::Employee', 'Company::Position']
  );


  # Render a pie chart via Google Chart API
  Company::Employee::Manager->render_as_chart(
    type => 'pie',
    values => ['Coding', 'Cooking'],
    column => 'hobby',
  );

  # Render a bar chart
  Company::Employee::Manager->render_as_chart(
    type => 'bar',
    title => 'The Employee Bar Chart',
    description => 'A useful bar chart.',
    columns => ['salary', 'tax'],
    objects => [1, 2, 3],
        options => {chco => 'ff6600,ffcc00'} # the color for each bar
  );
        

DESCRIPTION

Rose::DBx::Object::Renderer generates web UIs for Rose::DB::Object. It encapsulates many web conventions in the generated UIs as default behaviours. For example, email addresses are by default rendered as mailto links in tables and appropiate validation is enforced automatically in forms. These behaviours are highly configurable and extensible.

Renderer uses CGI::FormBuilder to generate forms and the Google Chart API to render charts. Template::Toolkit is used for template processing, however, Renderer can dynamically generate the full set of UIs without any templates.

RESTRICTIONS

  • The database table must follow the conventions in Rose::DB::Object.

  • Support for database tables with multiple primary keys is limited.

CONFIGURATION

Renderer exports a global config hash

  $Rose::DBx::Object::Renderer::CONFIG

in which the database connection, template path, and column definitions are defined.

Database Connection

We can configure the database connection settings used by the load_database method:

  # Use the DBD for PostgreSQL (defaulted to 'mysql')
  $Rose::DBx::Object::Renderer::CONFIG->{db}->{type} = 'Pg'; 

  $Rose::DBx::Object::Renderer::CONFIG->{db}->{port} = '5543';
  $Rose::DBx::Object::Renderer::CONFIG->{db}->{username} = 'admin';
  $Rose::DBx::Object::Renderer::CONFIG->{db}->{password} = 'password';

  # Change the Rose::DB::Object convention such that database table names are singular 
  $Rose::DBx::Object::Renderer::CONFIG->{db}->{tables_are_singular} = 1;

Paths

The default Template Toolkit INCLUDE_PATH is './template', which can be configured in:

  $Rose::DBx::Object::Renderer::CONFIG->{template}->{path} = '../templates:../alternative';

We can also specify the default URL to static contents, such as javascript libraries or images, templates:

  $Rose::DBx::Object::Renderer::CONFIG->{template}->{url} = '../docs/';

Renderer also needs a directory with write access to upload files. The default file upload path is './upload', which can be configured in:

  $Rose::DBx::Object::Renderer::CONFIG->{upload}->{path} = '../uploads';

We can also update the corresponding url for the upload directory:

  $Rose::DBx::Object::Renderer::CONFIG->{upload}->{url} = '../uploads';

Default Settings for Rendering Methods

The global config also defines the specific options available for each of the rendering methods, i.e. render_as_form, render_as_table, render_as_menu, and render_as_chart. For example:

  # Keep old upload files
  $Rose::DBx::Object::Renderer::CONFIG->{form}->{keep_old_file} = 1;

  # Change the default number of rows per page to 25 in tables
  $Rose::DBx::Object::Renderer::CONFIG->{table}->{per_page} = '25';

  # Use 'ilike' to perform case-insensitive searches in PostgreSQL
  $Rose::DBx::Object::Renderer::CONFIG->{table}->{search_operator} = 'ilike'; # defaulted to 'like'

Column Definitions

In order to encapsulate web-oriented behaviours, Renderer maintains a list of built-in column types, such as email, address, photo, document, and media, which are defined in:

  $Rose::DBx::Object::Renderer::CONFIG->{columns}

Except for the format, unsortable, and stringify options, other options in each column type are in fact CGI::FormBuilder field options.

format

load_database injects the coderefs defined inside the format hashref as object methods, for example:

  # Prints the serialised DateTime object in 'DD/MM/YYYY' format
  print $object->date_for_view;

  # Prints the image column in formatted HTML
  print $object->image_for_view;

  # Prints the url of the image
  print $object->image_url;

  # Prints the file path of the image
  print $object->image_path;

These extended object methods take preference over the the default object methods. The for_edit and for_update methods are used by render_as_form. The for_edit methods are triggered to format column values during form rendering, while the for_update methods are triggered to update column values during form submission. On the other hand, the for_view, for_search, and for_filter methods are used by render_as_table. The for_view methods are used to format column values during table rendering, while the for_filter and for_search methods are respectively triggered for column filtering and keyword searches.

We can easily overwrite the existing formatting methods or create new ones. For instance, we would like to use the HTML::Strip module to strip out HTML for the 'description' column type:

  use HTML::Strip;
  ...
  $Rose::DBx::Object::Renderer::CONFIG->{columns}->{description}->{format}->{for_update} = sub{
    my ($self, $column, $value) = @_;
    return unless $value;
    my $hs = HTML::Strip->new(emit_spaces => 0);
    my $clean_text = $hs->parse($value);
    return $self->$column($clean_text);  
  };

  load_namespace('company');
  my $p = Company::Product->new(id => 1);
  $p->load;
  
  $p->description_for_update('<html>The Lightweight UI Generator.</html>');
  print $p->description;
  # which prints 'The Lightweight UI Generator.'
  
  $p->save();

Similarly, we can create a new method for the 'first_name' column type so that users can click on a link to search the first name in CPAN:

  $Rose::DBx::Object::Renderer::CONFIG->{columns}->{first_name}->{format}->{in_cpan} = sub{
  my ($self, $column) = @_; 
  my $value = $self->$column; 
  return qq(<a href="http://search.cpan.org/search?query=$value&mode=all">$value</a>) if $value;
  };
  ...
  load_namespace('company');
  my $e = Company::Employee->new(id => 1);
  $e->load;
  print $e->first_name_in_cpan;

Of course, we can always define new column types, for example:

  $Rose::DBx::Object::Renderer::CONFIG->{columns}->{hobby} = {
    label => 'Your Favourite Hobby',
    sortopts => 'LABELNAME',
    required => 1,
    options => ['Reading', 'Coding', 'Shopping']
  };
unsortable

This option defines whether a column is a sortable column in tables. For example, the 'password' column type is by default unsortable, i.e.:

  $Rose::DBx::Object::Renderer::CONFIG->{columns}->{password}->{unsortable} = 1;

Custom columns are always unsortable.

stringify

This option specifies which columns are stringified. This is used by the exported stringify_me object method.

  $Rose::DBx::Object::Renderer::CONFIG->{columns}->{first_name}->{stringify} = 1;

METHODS

load_database

load_database loads database tables into classes using Rose::DB::Object::Loader. In order to eliminate the need for manually mapping column type definitions to database table columns, load_database also tries to auto-assign a column type to each column by matching the column definition name with the database table column name.

load_database accepts three parameters. The first parameter is the database name, the second parameter is a hashref that gets passed directly to the Rose::DB::Object::Loader constructor, while the last parameter is passed to its make_classes method. load_database by default uses the title case of the database name provided as the class_prefix unless the option is specified. For instance:

  load_database(
    'company',
    {db_username => 'admin', db_password => 'password'},
    {include_tables => ['employee','position']}
  );
  
  Company::Employee->render_as_form;
  
  Company::Employee::Manager->render_as_table;

load_database returns an array of the loaded classes via the make_classes method in Rose::DB::Object::Loader. However, if the Rose::DB::Object base_class for the particular database already exists, which most likely happens in a persistent environment, load_database will simply skip the loading process and return nothing.

Common Parameters in Rendering Methods

Here is a list of parameters that are applicable for all the rendering methods:

template

A string to define the name of the TT template for rendering the UI. When it is set to 1, it will try to find the default template based on the rendering method name. For example:

  Company::Employee->render_as_form(template => 1);
  # tries to use the template 'form.tt'

  Company::Employee::Manager->render_as_table(template => 1);
  # tries to use the template 'table.tt'
prefix

A string to set a prefix for a UI. prefix is for preventing CGI param conflicts when rendering multiple UIs on the same web page.

title

A string to set the title of the UI.

description

A string to set the description of the UI.

no_head

When set to 1, rendering methods will not include the default DOCTYPE and CSS styles defined in

  $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}

This is useful when rendering multiple UIs in the same page.

output

When set to 1, the rendering methods would return the rendered UI instead of printing it directly. For example:

  my $form = Company::Employee->render_as_form(output => 1);
  print $form->{output};
extra

A hashref of additional template variables. For example:

  Company::Employee->render_as_form(extra => {hobby => 'basketball'});

  # to access it within a template:
  [% extra.hobby %]
template_options

Optional parameters to be passed to template toolkit. This is not applicable to render_as_form.

render_as_form

render_as_form renders forms and handles its submission.

  # Render a form for creating a new object instance
  Company::Employee->render_as_form();
  
  # Render a form for updating an existing object instance
  my $e = Company::Employee->new(id => 1);
  $e->load;
  $e->render_as_form();
order

render_as_form by default sorts all fields based on the column order of the underlying database table. order accepts an arrayref to define the order of the form fields to be shown.

fields

Accepts a hashref to overwrite the CGI::FormBuilder field options auto-initialised by render_as_form. Any custom fields must be included to the order arrayref in order to be shown.

  Company::Employee->render_as_form(
    order => ['username', 'password', 'confirm_password', 'favourite_cuisine'],
    fields => {
    password => {required => 1, class=> 'password_css'},
  });

Please note that Renderer has a built-in column type called 'confirm_password', where its default validation tries to match a field named 'password' in the form.

queries

An arrayref of query parameters to be converted as hidden fields.

  Company::Employee->render_as_form(
    queries => {
    'rm' => 'edit',
    'favourite_cuisine' => ['French', 'Japanese']
  });

Please note that when a prefix is used, all fields are renamed to 'prefix_fieldname'.

controllers and controller_order

Controllers are essentially callbacks. We can add multiple custom controllers to a form. They are rendered as submit buttons. controller_order defines the order of the controllers, in other words, the order of the submit buttons.

  my $form = Employee::Company->render_as_form(
    output => 1,
    controller_order => ['Hello', 'Good Bye'],
    controllers => {
      'Hello' => {
        create => sub {
          return if DateTime->now->day_name eq 'Sunday';
          return 1;
        },
        callback => sub {
          my $self = shift;
          if (ref $self)
          {
            return 'Hello ' . $self->first_name;
          }
          else
          {
            return 'No employee has been created'.
          }
      },
      'Good Bye' => \&say_goodbye
    });

  if (exists $form->{controller})
  {
    print $form->{controller};
  }
  else
  {
    print $form->{output};
  }

  sub say_goodbye
  {
    return 'Good Bye';
  }

Within the controllers hashref, we can set the create parameter to 1 so that the object is always inserted into the database before running the custom callback. We can also point create to a coderef, in which case, the object is inserted into the database only if the coderef returns true.

Similarly, when rendering an object instance as a form, we can update the object before running the custom callback:

  ...
  $e->render_as_form(
    controllers => {
      'Hello' => {
        update => 1,
        callback => sub{...};
      }
  );

Another parameter within the controllers hashref is hide_form, which informs render_as_form not to render the form after executing the controller.

cancel

render_as_form has a built-in controller called 'Cancel'. cancel is a string for renaming the default 'Cancel' controller in case it clashes with custom controllers.

form

Parameters for the CGI::FormBuilder constructor.

validate

Parameters for the CGI::FormBuilder's validate method.

jserror

When a template is used, render_as_form sets CGI::FormBuilder's jserror function name to 'notify_error' so that we can always customise the error alert mechanism within the template (see the included 'form.tt' template).

show_id

Shows the ID column (primary key) of the table as a form field when it is set to 1. This is generally not a very good idea except for debugging purposes.

javascript_code

A string with javascript code to be added to the template

render_as_form passes the following list of variables to a template:

  [% self %] - the calling object instance or class
  [% form %] - CGI::FormBuilder's form object
  [% field_order %] - The order of the form fields
  [% form_id %] - the form id
  [% title %] - the form title
  [% description %] - the form description
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option
  [% wait_message %] - the text defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{wait_message}
  [% extra %] - custom variables
  [% cancel %] - the name of the 'Cancel' controller
  [% javascript_code %] - javascript code 
  [% template_url %] - The template url defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}

render_as_table

render_as_table renders tables for CRUD operations.

or_filter

render_as_table allows columns to be filtered via URL. For example:

  http://www.yoursite.com/yourscript.pl?first_name=Danny&last_name=Liang

returns the records where 'first_name' is 'Danny' and 'Last_name' is 'liang'. By default, column queries are joined by "AND", unless or_filter is set to 1.

columns

The columns parameter can be used to define custom columns, which do not exist in the underlying database table

  Company::Employee::Manager->render_as_table(
    columns => {'custom_column' => 
      label => 'Total',
      value => {
        1 => '100', # the 'Total' is 100 for object ID 1
        2 => '50'
      },
  });
order

order accepts an arrayref to define the order of the columns to be shown. The order parameter also determines which columns are allowed to be filtered via url.

searchable

The searchable option enables keyword search in multiple columns, including the columns of foreign objects:

  Company::Employee::Manager->render_as_table(
    get => {with_objects => [ 'position' ]},
    searchable => ['first_name', 'last_name', 'position.title'],
  );

A search box will be shown in rendered table. The CGI param of the search box is called 'q', in other words,

  http://www.yoursite.com/yourscript.pl?q=danny
get

get accepts a hashref to construct database queries. get is directly passed to the get method of the manager class.

  Company::Employee::Manager->render_as_table(
    get => {
          per_page = 5,
      require_objects => [ 'position' ],
      query => ['position.title' => 'Manager'],
    });
controllers and controller_order

The controllers parameter works very similar to render_as_form. controller_order defines the order of the controllers.

  Company::Employee::Manager->render_as_table(
        controller_order => ['edit', 'Review', 'approve'],
    controllers => {
      'Review' => sub{my $self = shift; do_something_with($self);}
      'approve' => {
            label => 'Approve',
        hide_table => 1,
        queries => {approve => '1'}, 
        callback => sub {my $self = shift; do_something_else_with($self);
      }
    }
  );

Within the controllers hashref, the queries parameter allows us to define custom query strings for the controller. The hide_table parameter informs render_as_table not to render the table after executing the controller.

create

This enables the built-in 'create' controller when set to 1.

  Company::Employee::Manager->render_as_table(create => 1);

Since render_as_form is used to render the form, we can also pass a hashref to manipulate the generated form.

  Company::Employee::Manager->render_as_table(
    create => {title => 'Add New Employee', fields => {...}}
  );
edit

Similar to create, edit enables the built-in 'edit' controller for updating objects.

delete

When set to 1, delete enables the built-in 'delete' controller for removing objects.

queries

Similar to the queries parameter in render_as_form, queries is an arrayref of query parameters, which will be converted to query strings. Please note that when a prefix is used, all query strings are renamed to 'prefix_querystring'.

url

Unless a url is specified in url, render_as_table will resolve the self url using CGI.

show_id

Shows the id column (primary key) of the table when it is set to 1. This can be also achieved using the order parameter.

javascript_code

A string with javascript code to be added to the template

ajax and ajax_template

These two parameters are designed for rendering Ajax-enabled tables. When ajax is set to 1, render_as_table tries to use the template 'table_ajax.tt' for rendering, unless the name of the template is defined in ajax_template. render_as_table also passes a variable called 'ajax' to the template and sets it to 1 when a CGI param named 'ajax' is set. We can use this variable in the template to differentiate whether the current CGI request is an ajax request or not.

no_pagination

The pagination will not be rendered if this option is set to 1.

Within a template, we can loop through objects using the [% table %] variable. Alternatively, we can use the [% objects %] variable.

render_as_table passes the following list of variables to a template:

  [% table %] - the hash for the formatted table, see the sample template 'table.tt' 
  [% objects %] - the raw objects returned by the 'get_object' method
  [% column_order %] - the order of the columns
  [% template_url %] - The template URL defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}
  [% table_id %] - the table id
  [% title %] - the table title
  [% description %] - the table description
  [% class_label %] - title case of the calling package name
  [% no_pagination %] - the 'no_pagination' option
  [% query_string %] - a hash of URL encoded query strings
  [% query_hidden_fields %] - CGI queries converted into hidden fields; it is used by the keyword search form
  [% param_list %] - a list of CGI param names with the table prefix, e.g. the name of the keyword search box is [% param_list.q %]
  [% searchable %] - the 'searchable' option
  [% sort_by_column %] - the column to be sorted 
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option
  [% wait_message %] - the text defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{wait_message}
  [% extra %] - custom variables
  [% javascript_code %] - javascript code
  [% ajax %] - the ajax variable for checking whether the current CGI request is a ajax request
  [% url %] - the base url

render_as_menu

render_as_menu generates a menu with the given list of classes and renders a table for the current class. We can have fine-grained control over each table within the menu. For example, we can alter the 'date_of_birth' field inside the 'create' form of the 'Company::Employee' table inside the menu:

  Company::Employee::Manager->render_as_menu (
    order => ['Company::Employee', 'Company::Position'],
    items => {
    'Company::Employee' => {
      create => {
            fields => {date_of_birth => {required => 1}}
          }
    }
    'Company::Position' => {
          title => 'Current Positions',
      description => 'important positions in the company'
    }},
    create => 1,
    edit => 1,
    delete => 1,
  );
order

The order parameter defines the list of classes to be shown in the menu as well as their order. The current item of the menu is always the calling class, i.e. Company::Employee::Manager in the example.

items

The items parameter is a hashref of parameters to control each table within the menu.

create, edit, delete, and ajax

These parameters are shortcuts which get passed to all the underlying tables rendered by the menu.

render_as_menu passes the following list of variables to a template:

  [% template_url %] - The template URL defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}
  [% menu_id %] - the menu id
  [% title %] - the menu title
  [% description %] - the menu description
  [% items %] - the hash for the menu items
  [% item_order %] - the order of the menu items
  [% current %] - the current menu item
  [% content %] - the output of the table
  [% extra %] - custom variables
  [% hide %] - whether the menu should be hidden
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option

render_as_chart

render_as_chart renders pie, line, and vertical bar charts via the Google Chart API.

type

This can be 'pie', 'bar', or 'line', which maps to the Google chart type (cht) 'p', 'bvg', and 'ls' respectively.

column and values

These two parameters are only applicable to pie charts. column defines the column of the table in which the values are compared. The values parameter is a list of values to be compared in that column, i.e. the slices.

columns and objects

These two parameters are only applicable to bar and line charts. columns defines the columns of the object to be compared. The objects parameter is a list of object IDs representing the objects to be compared.

options

A hashref for specifying Google Chart API options, such as the chart type, size, labels, or data. This hashref is serialised into a query string.

engine

Accepts a coderef to plug in your own charting engine.

render_as_chart passes the following list of variables to a template:

  [% template_url %] - The template URL defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}
  [% chart_id %] - the chart id
  [% title %] - the chart title
  [% description %] - the chart description
  [% chart %] - the chart
  [% options %] - the 'options' hash
  [% extra %] - custom variables
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option

OBJECT METHODS

Apart from the formatting methods injected by load_namespace, there are several lesser-used object methods:

delete_with_file

This is a wrapper of the object's delete method to remove any uploaded files associated:

  $object->delete_with_file();

stringify_me

The default stringify_me method return a string by joining all the matching columns with the stringify parameter set to true. The default stringify delimiter is comma.

  # Change the stringify delimiter to a space
  $Rose::DBx::Object::Renderer::CONFIG->{misc}->{stringify_delimiter} = ' '; 
  ...
  $object->title('Mr');
  $object->first_name('Rose');
  ...
  print $object->stringify_me();
  # prints 'Mr Rose';

This method is used internally to stringify foreign objects as form field values.

stringify_package_name

This method stringifies the package name:

  print Company::Employee->stringify_package_name(); 
  # Prints 'company_employee'

OTHER CONFIGURATIONS

Other miscellaneous configurations are defined in:

  $Rose::DBx::Object::Renderer::CONFIG->{misc}

By default, column types, such as 'date', 'phone', and 'mobile', are localised for Australia.

The default CSS class for the 'address' column type is 'disable_editor'. This is for excluding the TinyMCE editor with this setup: editor_deselector : "disable_editor".

Sample Templates

There are four sample templates: 'form.tt', 'table.tt', 'menu.tt', and 'chart.tt' in the 'templates' folder of the TAR archive.

SEE ALSO

Rose::DB::Object, CGI::FormBuilder, Template::Toolkit, http://code.google.com/apis/chart/

AUTHOR

Xufeng (Danny) Liang (danny.glue@gmail.com)

COPYRIGHT & LICENSE

Copyright 2008 Xufeng (Danny) Liang, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.