Jifty::View::Declare::CRUD - Provides typical CRUD views to a model


Jifty documentation Contained in the Jifty distribution.

Index


Code Index:

NAME

Top

Jifty::View::Declare::CRUD - Provides typical CRUD views to a model

SYNOPSIS

Top

  package App::View::User;
  use Jifty::View::Declare -base;
  use base qw/ Jifty::View::Declare::CRUD /;

  template 'view' => sub {
      # customize the view
  };

  1;

  package App::View::Tag;
  use Jifty::View::Declare -base;
  use base qw/ Jifty::View::Declare::CRUD /;

  template 'view' => sub {
      # customize the view
  };

  1;

  package App::View;
  use Jifty::View::Declare -base;

  use Jifty::View::Declare::CRUD;

  # If you have customizations, this is a good way...
  Jifty::View::Declare::CRUD->mount_view('User');
  Jifty::View::Declare::CRUD->mount_view('Category', 'App::View::Tag', '/tag');

  # Another way to do the above, good for quick and dirty
  alias Jifty::View::Declare::CRUD under '/admin/blog', {
      object_type => 'BlogPost',
  };

DESCRIPTION

Top

This class provides a set of views that may be used by a model to display Create/Read/Update/Delete views using the Template::Declare templating language.

Basically, you can use this class to do most (and maybe all) of the work you need to manipulate and view your records.

METHODS

Top

mount_view MODELCLASS VIEWCLASS /path

Call this method in your application's view class to add the CRUD views you're looking for. Only the first argument is required.

Arguments:

MODELCLASS

This is the name of the model that you want to generate the CRUD views for. This is the only required parameter. Leave off the parts of the class name prior to and including the "Model" part. (I.e., App::Model::User should be passed as just User).

VIEWCLASS

This is the name of the class that will be generated to hold the CRUD views of your model. If not given, it will be set to: App::View::MODELCLASS. If given, it should be the full name of the view class.

/path

This is the path where you can reach the CRUD views for this model in your browser. If not given, this will be set to the model class name in lowercase letters. (I.e., User would be found at /user if not passed explicitly).

object_type

This method returns the type of object this CRUD view has been generated for. This is normally the model class parameter that was passed to mount_view.

record_class

This is the full name of the model class these CRUD views are for. The default implementation returns:

  Jifty->app_class('Model', $self->object_type);

You will want to override this (in addition to object_type) if you want to provide CRUD views in a plugin, or from an external model class, or for one of the Jifty built-in models.

fragment_for

This is a helper that returns the path to a given fragment. The only argument is the name of the fragment. It returns a absolute base path to the fragment page.

This will attempt to lookup a method named fragment_for_FRAGMENT, where FRAGMENT is the argument passed. If that method exists, it's result is used as the returned path.

Otherwise, the fragment_base_path is joined to the passed fragment name to create the return value.

If you really want to mess with this, you may need to read the source code of this class.

fragment_base_path

This is a helper for fragment_for. It looks up the current template using current_template in Template::Declare::Tags, finds it's parent path and then returns that.

If you really want to mess with this, you may need to read the source code of this class.

_get_record $id

Given an $id, returns a record object for the CRUD view's model class.

display_columns [ACTION]

Returns a list of all the column names that this REST view should display. Defaults to all argument names for the provided ACTION. If there is no action provided, returns the record_class's readable_attributes.

edit_columns ACTION

Returns a list of all the columns that this REST view should display for update. Defaults to the display_columns, without id.

create_columns ACTION

Returns a list of all of the columns that this REST view should display for create. Defaults to edit_columns.

render_field mode => $mode, field => $field, action => $action

Renders a particular field in a given mode (read, create, edit). This attempts to dispatch directly to a method with the given field name. For example, if the subclass has, say, an edit_field_post method, then it will be preferred over the generic edit_field method.

view_field action => $action_object, field => $field_name

Displays the column as read-only.

create_field action => $action_object, field => $field_name

Displays the column for a create form.

edit_field action => $action_object, field => $field_name

Displays the column for an edit form.

page_title

The title for the CRUD page

TEMPLATES

Top

index.html

Contains the master form and page region containing the list of items. This is mainly a wrapper for the list fragment.

The search fragment displays a search screen connected to the search action of the module. If your subclass can search_fields, then that method will be called and the return value (which should be a list) will be used as a list of fields to render. Otherwise, all the fields of the Search action are displayed.

See Jifty::Action::Record::Search.

view

This fragment displays the data held by a single model record.

private template view_item_controls

Used by the view fragment to show the edit link for each record.

update

The update fragment displays a form for editing the data held within a single model record.

See Jifty::Action::Record::Update.

edit_item_controls $record $action

The controls we should be rendering in the 'edit' region for a given fragment

list

The list template provides an interactive list for showing a list of records in the record collection, adding new records, deleting records, and updating records.

per_page

This routine returns how many items should be shown on each page of a listing. The default is 25.

sort_header

Sort by field toolbar

The private template makes use of the predefined_search constant, which contains a list of hashref, each defines a collection in the format:

  { name => 'my_list',
    label => "My List",
    collection => defer {
      # ... construct and return the collection
    }
  },
  { name => 'my_list2',
    label => "My List2",
    condition => [
       { column => 'foo' value => 'bar' },
       # ... and your other Jifty::DBI::Collection limit args
    ]
  }




search_region

This private template renders a region to show an expandable region for a search widget.

new_item_region

This private template renders a region to show a the new_item template.

no_items_found

Prints "No items found."

list_items $collection $item_path

Renders a div of class list with a region per item.

paging_top $collection $page_number

Paging for your list, rendered at the top of the list

paging_bottom $collection $page_number

Paging for your list, rendered at the bottom of the list

create_item $action

Renders the action $Action, handing it the array ref returned by display_columns.

edit_item $action

Renders the action $Action, handing it the array ref returned by display_columns.

new_item

Top

The new_item template provides a form for creating new model records. See Jifty::Action::Record::Create.

SEE ALSO

Top

Jifty::Action::Record::Create, Jifty::Action::Record::Search, Jifty::Action::Record::Update, Jifty::Action::Record::Delete, Template::Declare, Jifty::View::Declare::Helpers, Jifty::View::Declare

LICENSE

Top

Jifty is Copyright 2005-2010 Best Practical Solutions, LLC. Jifty is distributed under the same terms as Perl itself.


Jifty documentation Contained in the Jifty distribution.
use warnings;
use strict;

package Jifty::View::Declare::CRUD;
use Jifty::View::Declare -base;
use Scalar::Defer 'force';

# XXX: should register 'template type' handler, so the
# client_cache_content & the TD sub here agrees with the arguments.
use Attribute::Handlers;
my %VIEW;
sub CRUDView :ATTR(CODE,BEGIN) {
    $VIEW{$_[2]}++;
}

sub mount_view {
    my ($class, $model, $vclass, $path) = @_;
    my $caller = caller(0);

    # Sanitize the arguments
    $model = ucfirst($model);
    $vclass ||= $caller.'::'.$model;
    $path ||= '/'.lc($model);

    # Load the view class, alias it, and define its object_type method
    Jifty::Util->require($vclass);
    eval qq{package $caller;
                        alias $vclass under '$path'; 1} or die $@;

    # Override object_type
    no strict 'refs';
    my $object_type = $vclass."::object_type";

    # Avoid the override if object_type() is already defined
    *{$object_type} = sub { $model } unless defined *{$object_type};
}

# XXX TODO FIXME This is related to the trimclient branch and performs some
# magic related to that or that was once related to that. This is also related
# to the CRUDView attribute above. This is a little unfinished, but I'll leave
# it up to clkao to figure out what needs to happen here.
sub _dispatch_template {
    my $class = shift;
    my $code  = shift;
    if ($VIEW{$code} && !UNIVERSAL::isa($_[0], 'Evil')) {
        my ( $object_type, $id ) = ( $class->object_type, get('id') );
        @_ = ($class, $class->_get_record($id), @_);
    }
    else {
        unshift @_, $class;
    }
    goto $code;
}

sub object_type {
    my $self = shift;
    my $object_type = $self->package_variable('object_type') || get('object_type');

    warn "No object type found for $self"
        if !$object_type;

    return $object_type;
}

# NB: We don't just create the record class here and return it. Why? Because
# the mount_view() method is generally called very early in the Jifty
# lifecycle. As such, Jifty->app_class() might not work yet since it requires
# the Jifty singleton to be built and the configuration to be loaded. So, this
# implementation caches the record class after the first calculation, which
# should happen during the request dispatch process, which always happens after
# Jifty is completely initialized.
sub record_class {
    my $self = shift;

    # If object_type is set via set, don't cache
    if (!$self->package_variable('object_type') && get('object_type')) {
        return Jifty->app_class('Model', $self->object_type);
    }

    # Otherwise, assume object_type is permanent
    else {
        return ($self->package_variable('record_class') 
            or ($self->package_variable( record_class =>
                    Jifty->app_class('Model', $self->object_type))));
    }

}

sub fragment_for {
    my $self     = shift;
    my $fragment = shift;

    # Check for fragment_for_$fragment and use that if it exists
    if ( my $coderef = $self->can( 'fragment_for_' . $fragment ) ) {
        return $coderef->($self);
    }

    # Otherwise return the fragment_base_path/$fragment
    return $self->package_variable( 'fragment_for_' . $fragment )
        || $self->fragment_base_path . "/" . $fragment;
}

sub fragment_base_path {
    my $self = shift;

    # Rip it apart
    my @parts = split('/', current_template());

    # Remove the last element
    pop @parts;

    # Put it back together again
    my $path = join('/', @parts);

    # And serve
    return $path;
}

sub _get_record {
    my ( $self, $id ) = @_;

    # Load the model, create an empty object, load the object by ID
    my $record_class = $self->record_class;
    my $record = $record_class->new();
    $record->load($id);

    return $record;
}

sub display_columns {
    my $self = shift;
    my $action = shift;

    return $action->argument_names if $action;

    return $self->record_class->readable_attributes;
}

sub edit_columns {
    my $self = shift; 
    return grep { $_ ne 'id' } $self->display_columns(@_);
}

sub create_columns {
    my $self = shift; 
    return $self->edit_columns(@_);
}

sub render_field {
    my $self = shift;
    my %args = @_;

    my $mode  = $args{mode};
    my $field = $args{field};

    my $render_method = "${mode}_field";

    $render_method = "${mode}_field_${field}"
        if $self->can("${mode}_field_${field}");

    $self->$render_method(%args);
}

sub view_field {
    my $self = shift;
    my %args = @_;

    my $action = delete $args{action};
    my $field  = delete $args{field};

    render_param($action => $field, render_mode => 'read', %args);
}

sub create_field {
    my $self = shift;
    my %args = @_;

    my $action = delete $args{action};
    my $field  = delete $args{field};

    render_param($action => $field, %args);
}

sub edit_field {
    my $self = shift;
    my %args = @_;

    my $action = delete $args{action};
    my $field  = delete $args{field};

    render_param($action => $field, %args);
}

sub page_title {
    my $self = shift;
    $self->object_type;
}

template 'index.html' => page { title => shift->page_title } content {
    my $self = shift;
    form {
        render_region(
            name     => $self->object_type.'-list',
            path     => $self->fragment_base_path.'/list'
        );
    }

};

template 'search' => sub {
    my $self          = shift;
    my ($object_type) = ( $self->object_type );
    my $search        = $self->record_class->as_search_action(
        moniker           => 'search',
        sticky_on_success => 1,
    );

    div {
        { class is "jifty_admin crud-search-form" };

        if ( $self->can('search_fields') ) {
            render_param( $search => $_ )
                for $self->search_fields;
        }
        else {
            render_action($search);
        }

        $search->button(
            label   => _('Search'),
            onclick => [
                { submit => $search },
                {
                    refresh => Jifty->web->current_region->parent,
                    args    => { page => 1 },
                },
            ],
        );
    }
};

template 'view' => sub :CRUDView {
    my $self   = shift;
    my $record = $self->_get_record( get('id') );

    return unless $record->id;
    my $update = $record->as_update_action(
        moniker => "update-" . Jifty->web->serial,
    );

    my @fields = $self->display_columns($update);
    for my $field (@fields) {
        div { { class is 'crud-field view-argument-'.$field};
            $self->render_field(
                mode   => 'view',
                action => $update,
                field  => $field,
                label  => '',
            );
        };
    }
    div { { class is 'crud-field view-item-controls'};
        show ('./view_item_controls', $record, $update);
    };
};

private template view_item_controls  => sub {
    my $self = shift;
    my $record = shift;

    if ( $record->current_user_can('update') ) {
        hyperlink(
            label   => _("Edit"),
            class   => "editlink",
            onclick => {
                popout => $self->fragment_for('update'),
                args   => { id => $record->id },
            },
        );
    }
};

template 'update' => sub {
    my $self = shift;
    my ( $object_type, $id ) = ( $self->object_type, get('id') );

    my $record_class = $self->record_class;
    my $record = $record_class->new();
    $record->load($id);
    my $update = $record->as_update_action(
        moniker => "update-" . Jifty->web->serial,
    );

    div {
        { class is "crud-field update item " . $object_type }

        show('./edit_item', $update);
        show('./edit_item_controls', $record, $update);
    }
};

private template edit_item_controls => sub {
    my $self = shift;
    my $record = shift;
    my $update = shift;

    my $object_type = $self->object_type;
    my $id = $record->id;

    my $delete = $record->as_delete_action(
        moniker => 'delete-' . Jifty->web->serial,
    );
    my $view_region = Jifty->web->qualified_parent_region;

    div {
        { class is 'crud editlink' };
        hyperlink(
            label   => _("Save"),
            onclick => [
                { submit => $update },
                { refresh => $view_region },
            ],
        );
        if ( $record->current_user_can('delete') ) {
            $delete->button(
                label   => _('Delete'),
                onclick => [
                    {
                        submit  => $delete,
                        confirm => _('Really delete?'),
                    },
                    {
                        region => $view_region,
                        replace_with => '/__jifty/empty',
                    },
                ],
                class => 'delete',
            );
        }
    };
};

template 'list' => sub {
    my $self = shift;

    my ( $page ) = get('page');
    my $item_path = get('item_path') || $self->fragment_for("view");
    my $sort_by = get ('sort_by') || '';
    my $order = get ('order') || '';
    my $collection =  $self->_current_collection();

    div {
        {class is 'crud-ui crud-'.$self->object_type };
        show( './search_region');
        show( './paging_top',    $collection, $page );

        div {
            { class is 'crud-table' };
            show( './sort_header',   $item_path, $sort_by, $order );
            show( './list_items',    $collection, $item_path );
        };

        show( './paging_bottom', $collection, $page );
        show( './new_item_region');
    };
};

sub per_page { 25 }

# This method just does a whole lot of sanitizing to try and get a valid
# collection out the other end based upon either the current search or an
# unlimited collection if there is no current search.
sub _current_collection {
    my $self = shift; 
    my ( $page ) = get('page') || 1;
    my ( $sort_by ) = get('sort_by');
    my ( $order ) = get('order');
    my $collection_class = $self->record_class->collection_class;
    my $search = ( Jifty->web->response->result('search') ? Jifty->web->response->result('search')->content('search') : undef );
    my $collection;
    if ( $search ) {
        $collection = $search;
    } elsif (my $predefined = get('predefined')) {
        my ($entry) = grep { $_->{name} eq $predefined } $self->predefined_search;
        $collection = force $entry->{collection} || $collection_class->new();
        for (@{$entry->{condition} || []}) {
            $collection->limit(%$_);
        }
    } else {
        $collection = $collection_class->new();
        $collection->find_all_rows();
        $collection->order_by(column => $sort_by, order=>'ASC') if ($sort_by && !$order);
        $collection->order_by(column => $sort_by, order=>'DESC') if ($sort_by && $order);
    }

    $collection->set_page_info( current_page => $page, per_page => $self->per_page );

    return $collection;    
}

template 'sort_header' => sub {
    my $self = shift;
    my $item_path = shift;
    my $sort_by = shift;
    my $order = shift;
    my $record_class = $self->record_class;
    my $update = $record_class->as_update_action();

    div {
        { class is "crud-column-headers" };
        for my $argument ($self->display_columns($update)) {
            my $column = $record_class->column($argument);
            unless ($column) {

                # in case we want to show a field but it's not a real column
                div {
                    { class is 'crud-column-header' };
                    $argument;
                };
                next;
            }

            div {
                { class is 'crud-column-header' };
                ul { attr { class => 'crud-sort-menu', style => 'display:none;' };
                    li {
                        my $imgdown ="<img height='16' width='16' src='/images/silk/bullet_arrow_down.png' alt='down' name='down'>";
                        hyperlink(
                            label => $imgdown,
                            escape_label => 0,
                            onclick =>
                                { args => { sort_by => $argument, order => undef } },
                        );
                    } if (!($sort_by && !$order && $argument eq $sort_by));
                    li {
                        my $imgup ="<img height='16' width='16' src='/images/silk/bullet_arrow_up.png' alt='up' name='up'>";
                        hyperlink(
                            label => $imgup,
                            escape_label => 0,
                            onclick =>
                                { args => { sort_by => $argument, order => 'D' } },
                        );
                    } if (!($sort_by && $order && $argument eq $sort_by));
                    li {
                        my $imgup ="<img height='16' width='16' rc='/images/silk/cancel_grey.png' alt='del' name='del'>";
                        hyperlink(
                            label => $imgup,
                            escape_label => 0,
                            onclick =>
                                { args => { sort_by =>'', order => '' } },
                        );
                    } if ($sort_by && $argument eq $sort_by);
                };
                span{
                    {class is "field"};
                    my $label = $record_class->column($argument)->label || $argument;
                    if ( $sort_by && $argument eq $sort_by ) {
                        div { class is 'crud-sort-selected';
                        hyperlink ( label =>$label );
                        my $img = ($order eq 'D')?'up':'down';
                        img { attr {
                            height => 16,
                            width  => 16,
                            src    => '/images/silk/bullet_arrow_'.$img.'.png' }; };
                        };
                    }
                    else {
                        hyperlink(label => $label);
                    };
                };
            }
        }
    };
    outs_raw("<script type=\"text/javascript\">
        jQuery(document).ready(function() {
            jQuery('.crud-sort-menu').each(function(){
                jQuery(this).parent().hover(
                function(){
                jQuery(this).children('.crud-sort-menu').show();
                },
                function(){
                        jQuery(this).children('.crud-sort-menu').hide();
                });
            });
        });
        </script>");

};


use constant predefined_search => ();

private template 'predefined_search' => sub {
    my $self = shift;
    my @predefined = $self->predefined_search or return;

    ul { { class is 'predefined-search' };
        li { hyperlink( label => _("Default"),
                onclick => [ {
                        refresh => Jifty->web->current_region,
                        args => { predefined => undef }
                } ] ) };

        for (@predefined) {
            li {
                hyperlink( label => $_->{label},
                    onclick => [ {
                            refresh => Jifty->web->current_region,
                            args => { predefined => $_->{name} }
                        } ]
                );
            }
        }
    };
    div { { class is 'clear' } };
};

private template 'search_region' => sub {
    my $self        = shift;
    my $object_type = $self->object_type;

    div {
        attr { class is 'crud-search' };

        show('predefined_search');

        my $search_region = Jifty::Web::PageRegion->new(
            name => 'search',
            path => '/__jifty/empty'
        );

        hyperlink(
            onclick => {
                popout => $self->fragment_for('search'),
                args   => { object_type => $object_type }
            },
            label => _('Search'),
        );

        outs( $search_region->render );
    }
};

private template 'new_item_region' => sub {
    my $self        = shift;
    my $fragment_for_new_item = get('fragment_for_new_item') || $self->fragment_for('new_item');
    my $object_type = $self->object_type;

    return unless $self->record_class->new->current_user_can('create');

    if ($fragment_for_new_item) {
        render_region(
            name     => 'new_item',
            path     => $fragment_for_new_item,
            defaults => {
                        object_type => $object_type },
        );
    }
};

template 'no_items_found' => sub {
    div {
        { class is 'no_items' };
        outs( _("No items found.") );
    }
};

private template 'list_items' => sub {
    my $self        = shift;
    my $collection  = shift;
    my $item_path   = shift;
    my $callback    = shift;
    my $object_type = $self->object_type;
    $collection->_do_search(); # we're going to need the results. 
    # XXX TODO, should use a real API to force the search

    div {
        { class is 'crud-list list' };
        if ( $collection->count == 0 ) {
            render_region(
                name => 'no_items_found',
                path => $self->fragment_for('no_items_found'),
            );
        }

        my $i = 0;
        while ( my $item = $collection->next ) {

            render_region(
                name     => 'item-' . $item->id,
                path     => $item_path,
                class    => 'crud-item ' . ($i++ % 2 ? 'odd' : 'even'),
                defaults => {
                    id => $item->id,
                    object_type => $object_type,
                }
            );

            $callback->($i) if $callback;
        }
    };
};

private template 'paging_top' => sub {
    my $self       = shift;
    my $collection = shift;
    my $page       = shift;

    render_mason '/_elements/paging' => {
        collection => $collection,
        page       => $page,
        allow_all  => 1,
    };
};

private template paging_bottom => sub {
    my $self       = shift;
    my $collection = shift;
    my $page       = shift;

    render_mason '/_elements/paging' => {
        collection => $collection,
        page       => $page,
        allow_all  => 1,
    };
};


private template 'create_item' => sub {
    my $self = shift;
    my $action = shift;

    for my $field ($self->create_columns($action)) {
        div { 
            { class is 'create-argument-'.$field};
            $self->render_field(
                mode   => 'create',
                action => $action,
                field  => $field,
            );
        }
    }
};

private template 'edit_item' => sub {
    my $self = shift;
    my $action = shift;
    for my $field ($self->edit_columns($action)) {
        div {
            { class is 'update-argument-'.$field};
            $self->render_field(
                mode   => 'edit',
                action => $action,
                field  => $field,
            );
        }
    }
};

template 'new_item' => sub {
    my $self = shift;
    my ( $object_type ) = ( $self->object_type );

    my $record_class = $self->record_class;
    my $create = $record_class->as_create_action;

    div {
        { class is 'crud-create crud create item inline' };
        show('./create_item', $create);
        show('./new_item_controls', $create);
    }
};

private template 'new_item_controls' => sub {
    my $self = shift;
    my $create = shift;
    my ( $object_type ) = ( $self->object_type);

    outs(
        Jifty->web->form->submit(
            label   => _('Create'),
            onclick => [
                { submit       => $create },
                { refresh_self => 1 },
                { delete => Jifty->web->qualified_parent_region('no_items_found') },
                {   element => Jifty->web->current_region->parent->get_element( 'div.crud-list'),
                    append => $self->fragment_for('view'),
                    args   => {
                        object_type => $object_type,
                        id => { result_of => $create, name => 'id' },
                    },
                },
            ]
        )
    )
};




1;