The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

App::ZofCMS::Plugin::CRUD - Generic "Create Read Update Delete List" functionality

SYNOPSIS

In your ZofCMS Template:

    plugins => [
        qw/CRUD/,
    ],

    plug_crud => {
        table       => 'information_packages',
        file_dir    => 'files/information-packages/',
        items       => [
            'Item',
            { Description => [ 'textarea', optional => 1 ] },
            { File        => 'file'                        },
            { Time        => sub { time(); }               },
        ],
    },

Create a SQL table for the plugin to use:

    CREATE TABLE `information_packages` (
        `crud_id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
        `item` TEXT,
        `description` TEXT,
        `file` TEXT,
        `time` INT(10) UNSIGNED
    );

In your HTML::Template Template:

    <h2>Information Packages Admin</h2>

    <tmpl_var name='crud_success_message'>
    <tmpl_var name='crud_error'>

    <h3>Add New Item</h3>

    <tmpl_var name='crud_form'>

    <h3>Items In The Database</h3>

    <tmpl_if name='crud_has_items'>
        <table>
        <thead>
            <tr>
                <td>File</td>
                <td>Description</td>
                <td>Add Time</td>
            </tr>
        </thead>
        <tbody>
            <tmpl_loop name='crud_items'>
                <tr>
                    <td>
                    <tmpl_var name='crud_d_form'>
                    <tmpl_var name='crud_u_form'>
                    <a href="<tmpl_var escape='html' name='file'>"
                        target="_blank"><tmpl_var
                        escape='html' name='item'></a></td>
                    <td><tmpl_var name='description'></td>
                    <td><tmpl_var name='foo1'></td>
                    <td><tmpl_var name='foo2'></td>
                    <td><tmpl_var escape='html' name='time'></td>
                </tr>
            </tmpl_loop>
        </tbody>
        </table>
    <tmpl_else>
        <p>Currently there are no items in the database.</p>
    </tmpl_if>

DESCRIPTION

The plugin provides a generic "Create Read Update Delete List" functionality. (Currently, READ is not implemented). In conjunction with this plugin, you might find these plugins useful App::ZofCMS::Plugin::DBIPPT and App::ZofCMS::Plugin::FormChecker.

ZofCMS TEMPLATE/MAIN CONFIG FILE FIRST LEVEL KEYS

The keys can be set either in ZofCMS template or in Main Config file, if same keys are set in both, then the one in ZofCMS template takes precedence.

plugins

    plugins => [ qw/CRUD/ ],

You obviously would want to include the plugin in the list of plugins to execute.

plug_crud

    ### Mandatory fields without defaults: dsn, user, items

    plug_crud => {
        dsn             => 'DBI:mysql:database=zofdb;host=localhost',
        user            => 'db_login',
        pass            => 'db_pass',
        opt             => {
            RaiseError        => 1,
            AutoCommit        => 1,
            mysql_enable_utf8 => 1,
        },
        items       => [
            'Item',
            { Description => [ 'textarea', optional => 1 ] },
            { File        => 'file'                        },
            { Time        => sub { time(); }               },
        ],
        list_sub        => # sub for processing the list of output items
        table           => 'products',
        file_dir        => 'files/',
        can             => 'CRUDL',
        no_time_sort    => 0,
    }

Mandatory. Takes either a hashref or a subref as a value. If a subref is specified, its return value will be assigned to plug_crud as if it were already there. If the sub returns an undef, then the plugin will stop further processing. The @_ of the subref will contain (in that order): ZofCMS Template hashref, query parameters hashref, and App::ZofCMS::Config object. Possible keys/values for the hashref are as follows:

dsn

    dsn => "DBI:mysql:database=test;host=localhost",

Mandatory. Specifies the "DSN" for DBI module. See DBI's docs for connect_cached() method for more info on this one.

user

    user => 'test',

Mandatory. Specifies your username for the SQL database.

pass

    pass => 'test',

Optional. Specifies your password for the SQL database. If not specified, behaviour will be akin to not having a set password.

opt

    opt => {
        RaiseError => 1,
        AutoCommit => 1,
        mysql_enable_utf8 => 1,
    }

Optional. Takes a hashref as a value. Specifies the additional options for DBI's connect_cached() method. See DBI's docs for connect_cached() method for more info on this one. Defaults to: { RaiseError => 1, AutoCommit => 1, mysql_enable_utf8 => 1, }

table

    table => 'products',

Optional. Takes a string as a value that represents the name of the table in which to store the data. Defaults to: products

items

    items => [
        'Item',
        { Description => [ 'textarea', optional => 1 ] },
        { File        => 'file'                        },
        { Time        => sub { time(); }               },
    ],

Mandatory. Takes an arrayref as a value that must contain at least one item in it. Specifies the list of items that comprise a single record that the plugin will create/read/update/delete. Important note: if you're not using time item to store time, see no_time_sort option below; you'll likely want it set to a true value, to avoid the plugin from erroring out. The items in a list can be of these types:

String

    items => [
        'Item',
        q|Employee's Performance Record #|,
    ],

A simple string will be represented as a <input type="text"> in the Create/Update HTML form. The string will become the <label> text for the form element. Then everything in it that doesn't match \w will be converted into underscores, and lc() will be run, and that new string will be used for:

  • SQL column name for that field

  • HTML <input>'s name="" attribute in the Create/Update form

  • HTML id="" attribute in the Create/Update form (prefix crud_ will be added)

  • <tmpl_var> name for the List function of the plugin.

An example:

We have two items in our record that we'll manipulate with this plugin:

    items => [
        'Item',
        q|Employee's Performance Record #|,
    ],

We'll create a table, where field `crud_id` is mandatory and must contain the unique ID of the record (this example shows MySQL taking care of that automatically):

    CREATE TABLE `example` (
        `crud_id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
        `item` TEXT,
        `employee_s_performance_record__` TEXT,
    );

Let's say we have App::ZofCMS::Plugin::FormChecker checking the input given to the Create form. The trigger will be crud_create and the fields it will be checking are item and employee_s_performance_record__:

    plug_form_checker => {
        trigger     => 'crud_create',
        fail_code   => sub { my $t = shift; delete $t->{plug_crud} },
        rules       => {
            item                            => { max => 200 },
            employee_s_performance_record__ => 'num',
        },
    },

Lastly, when displaying the list of records, we'll use this code in the HTML::Template template:

    <tmpl_if name='crud_has_items'>
        <table>
        <thead>
            <tr>
                <td>Item</td>
                <td>Employee's Performance Record #</td>
            </tr>
        </thead>
        <tbody>
            <tmpl_loop name='crud_items'>
                <tr>
                    <td><tmpl_var escape='html' name='item'></td>
                    <td><tmpl_var escape='html'
                        name='employee_s_performance_record__'></td>
                </tr>
            </tmpl_loop>
        </tbody>
        </table>
    <tmpl_else>
        <p>Currently there are no items in the database.</p>
    </tmpl_if>

hashref

    items => [
        { File        => 'file'                        },
        { Description => [ 'textarea', optional => 1 ] },
        { Time        => sub { time(); }               },
    ]

The key of the hashref functions the same as described above for String (i.e. it'll be the <label> and the base for the name="", etc.). The value of the hashref can be a string, an arrayref, or a subref:

A string

    items => [
        { File => 'file' },
    ],

When the value is a string, it will specify the type of HTML element to use for this record item in the Create/Update form. Currently supported values are text for <input type="text">, textarea for <textarea>, and file for <input type="file">. File inputs are currently not editable in the Update form.

An arrayref

    items => [
        { Description => [ 'textarea', optional => 1 ] },
    ]

The first item in the arrayref will be the type of HTML element to use for this record item (see A string section right above). The rest of the items are in key/value pairs and specify options for this record. The currently available option is optional that, when set to a true value, will cause the item to be optional: an asterisk will not be prepended to its label and an error will not show if the user leaves this item blank in the Create/Update form.

A subref

    items => [
        { Time => sub { time(); } },
    ]

The key will function the same as described for A string above. The only difference is that this item will not be shown in the Create/Update form. The sub will be executed and its value will be assigned to the item as if it were specified by the user. On Update, this item will not be editable and the sub will NOT be executed. The @_ of the sub will contain (in that order): ZofCMS Template hashref and query parameters hashref where keys are params' names and values are their values.

list_sub

    list_sub => sub {
        my ( $d, $t, $q ) = @_;

        for ( @$d ) {
            $_->{time} = localtime $_->{time};
            $_->{is_good_employee}
            = $t->{d}{records}{ $_->{employee_s_performance_record__} }
                ? 1 : 0;

            $_->{sort} = $q->{sort};
        }
    },

Optional. By default is not specified. Takes a subref as a value. Allows you to make modifications to the list of records in the List output. The @_ of the sub will contain (in that order): the arrayref of hashrefs where each hashref is a record in the database, ZofCMS Template hashref, and query parameters hashref where keys are params' names and values are their values. The hashref for each record will contain all the columns from the database for that specific record, as well as:

  • crud_can_ud => 1 if the user can either update or delete records (see can parameter below),

  • crud_can_u => 1 if the user can update records

  • crud_can_d => 1 if the user can delete records

  • crud_d_form containing HTML of the form used for deleting this record

  • crud_u_form containing HTML of the form used for updating this record

file_dir

    file_dir => 'files',

Optional. Defaults to files. Takes a string as a value that specifies the directory (relative to index.pl) where the plugin will store files uploaded by the user (that is for any records for which <input type="file"> was used in the Create form).

can

    can => 'CRUDL',

    can => 'RL',

    can => 'CUL',

Optional. Defaults to CRUDL. Takes a string of letters (in any order) as the value. Each letter specifies what the current user is allowed to do: C => Create, R => Read, U => Update, D => Delete, L => List. Note: if L is specified, plugin will automatically load all records into {t}{crud_list}.

no_time_sort

    no_time_sort => 0,

Optional. Takes true or false value. Defaults to false. In 99% of my CRUD pages, I've had a time item in the record that stored the time of when the record was added, and when the records were listed they were sorted by "most recent first." This is exactly what this plugin does automatically and it expects time item to be present and set to a value of Unix time (output of time()). If you don't have such a column or don't want your records sorted by time, set no_time_sort to a true value.

HTML::Template TEMPLATE VARIABLES

    <tmpl_var name='crud_success_message'>
    <tmpl_var name='crud_error'>

    <h3>Add New Item</h3>

    <tmpl_var name='crud_form'>

    <h3>Items In The Database</h3>

    <tmpl_if name='crud_has_items'>
        <table>
        <thead>
            <tr>
                <td>File</td>
                <td>Description</td>
                <td>Add Time</td>
            </tr>
        </thead>
        <tbody>
            <tmpl_loop name='crud_items'>
                <tr>
                    <td>
                    <tmpl_var name='crud_d_form'>
                    <tmpl_var name='crud_u_form'>
                    <a href="<tmpl_var escape='html' name='file'>"
                        ><tmpl_var escape='html' name='item'></a></td>
                    <td><tmpl_var name='description'></td>
                    <td><tmpl_var name='foo1'></td>
                    <td><tmpl_var name='foo2'></td>
                    <td><tmpl_var escape='html' name='time'></td>
                </tr>
            </tmpl_loop>
        </tbody>
        </table>
    <tmpl_else>
        <p>Currently there are no items in the database.</p>
    </tmpl_if>

crud_success_message

    <tmpl_var name='crud_success_message'>

This variable will contain <p class="success-message">Item was successfully deleted.</p> when Delete action succeeds.

crud_error

    <tmpl_var name='crud_error'>

This variable will contain an error message, if any, except for messages generated during submission of the Create/Update forms, as those will be stuffed inside the form, but same HTML code will be wrapping the error message. The code will be <p class="error">$error</p> where $error is the text of the error, which currently will be either Couldn't find the item to update or Couldn't find the item to delete.

crud_form

    <tmpl_var name='crud_form'>

This variable will contain either Create or Update form, depending on whether is the user is trying to update a record. See the source code of this module or the output of crud_form to find HTML code for the form. This variable will be empty if the user doesn't have Create or Update permissions (see can configuration variable).

crud_has_items

    <tmpl_if name='crud_has_items'>
        ... output the record list here
    <tmpl_else>
        <p>Currently there are no items in the database.</p>
    </tmpl_if>

Contains true or false values. If true, it means the plugin retrieved at least one record with the List operation. This variable will always be false if the user isn't allowed to List (see can configuration argument).

crud_items

   <tmpl_loop name='crud_items'>
        <tr>
            <td>
            <tmpl_var name='crud_d_form'>
            <tmpl_var name='crud_u_form'>
            <a href="<tmpl_var escape='html' name='file'>"
                ><tmpl_var escape='html' name='item'></a></td>
            <td><tmpl_var name='description'></td>
            <td><tmpl_var name='foo1'></td>
            <td><tmpl_var name='foo2'></td>
            <td><tmpl_var escape='html' name='time'></td>
        </tr>
    </tmpl_loop>

A loop containing records returned by the List operation. This variable will be empty if the user isn't allowed to List (see can configuration argument). The variables in the loop are as follows:

All items from items configuration argument

    <a href="<tmpl_var escape='html' name='file'>"
        ><tmpl_var escape='html' name='item'></a></td>
    <td><tmpl_var name='description'></td>
    <td><tmpl_var name='foo1'></td>
    <td><tmpl_var name='foo2'></td>
    <td><tmpl_var escape='html' name='time'></td>

All the items you specified in the items configuration argument will be present here, even if that item was set as a subref in the items. You can also add extra keys here through list_sub sub specified in the configuration. Note: any file items will contain filename and directory specified in the file_dir argument. You can modify that using list_sub sub.

crud_can_d

    <tmpl_if name="crud_can_d">
        Can delete!
    </tmpl_if>

True or false value. If true, then the user is allowed to delete records (see can configuration argument).

crud_can_u

    <tmpl_if name="crud_can_u">
        Can update!
    </tmpl_if>

True or false value. If true, then the user is allowed to update records (see can configuration argument).

crud_can_ud

    <tmpl_if name="crud_can_ud">
        Can update or delete!
    </tmpl_if>

True or false value. If true, then the user is allowed to delete or update records (see can configuration argument).

crud_u_form

    <tmpl_var name='crud_u_form'>

Contains HTML code for the "Update Record" form. This HTML might change in the future or be configurable, as currently it's highly specific to what I use in a specific Web app. You can easily use your own form by including crud_update parameter set to a true value and crud_id paramater set to the ID of the record (that will be in the <tmpl_var name='crud_id>).

crud_d_form

    <tmpl_var name='crud_d_form'>

Contains HTML code for the "Delete Record" form. This HTML might change in the future or be configurable, as currently it's highly specific to what I use in a specific Web app. You can easily use your own form by including crud_delete parameter set to a true value and crud_id paramater set to the ID of the record (that will be in the <tmpl_var name='crud_id>).

TODO AND LIMITATIONS

Currently, the module doesn't actually implement the "READ" functionality. Instead, it only does "LIST" (i.e. list all records instead of a chosen one) and it doesn't support pagination. If you expect a whole ton of records in the database, heed the "L" flag in the can option; if it's present, the plugin will always load all records into {t}

Along with the "READ" optional and pagination, this plugin could make use of absorbing some of the HTML for the LIST feature; so you'd have to just type <tmpl_var name="crud_list"> instead of doing HTML by hand, but so far I haven't found a flexible solution that doesn't drown the user of the plugin in settings options.

In addition, the plugin needs an option to add a variable amount of files, all stored in a single database field, which in turn would allow updating the file field during record editing.

Lastly, the plugin currently doesn't support selects, checkboxes, or radio boxes in the Create/Update form.

A NOTE ON FORM INPUT ERROR CHECKING

This plugin only checks for whether or not a mandatory field is present when creating/updating records. If you need more advanced error checking, see App::ZofCMS::Plugin::FormChecker that can (read "should") work together with this plugin (run FormChecker first).

REPOSITORY

Fork this module on GitHub: https://github.com/zoffixznet/App-ZofCMS

BUGS

To report bugs or request features, please use https://github.com/zoffixznet/App-ZofCMS/issues

If you can't access GitHub, you can email your request to bug-App-ZofCMS at rt.cpan.org

AUTHOR

Zoffix Znet <zoffix at cpan.org> (http://zoffix.com/, http://haslayout.net/)

LICENSE

You can use and distribute this module under the same terms as Perl itself. See the LICENSE file included in this distribution for complete details.