Group
Extension

Mojolicious-Plugin-Tables/lib/Mojolicious/Plugin/Tables.pm

package Mojolicious::Plugin::Tables;
use Mojo::Base 'Mojolicious::Plugin';

use File::Basename 'dirname';
use File::Spec::Functions 'catdir';

use Mojolicious::Plugin::Tables::Model;

our $VERSION = '1.06';

sub register {
    my ($self, $app, $conf) = @_;

    my $log = $app->log;

    $app->helper(add_stash => sub {
        my ($c, $slot, $val) = @_;
        push @{$c->stash->{$slot} ||= []}, $val;
    });

    $app->helper(add_flash => sub {
        my ($c, $slot, $val) = @_;
        push @{$c->session->{new_flash}{$slot} ||= []}, $val;
    });

    $app->helper(shipped => sub {
        # special stash slot for sending json-able structs to js;
        # get/set logic here cloned from Mojo::Util::_stash.
        my $c = shift;
        my $shipped = $c->stash->{shipped} ||= {};
        return $shipped unless @_;
        return $shipped->{$_[0]} unless @_>1 || ref $_[0];
        my $values = ref $_[0]? $_[0] : {@_};
        @$shipped{keys %$values} = values %$values;
    });

    $app->config(default_theme=>'redmond');
    $app->defaults(layout => $conf->{layout} || 'tables');

    my $model_class = $conf->{model_class} ||= 'Mojolicious::Plugin::Tables::Model';
    eval "require $model_class" or die;

    $model_class->log($log);
    my $schema = $model_class->setup($conf);
    my $model = $schema->model;
    $app->config(model=>$model);

    $app->hook(before_dispatch => sub {
        my $c = shift;
        # Move first part and slash from path to base path when deployed under a path
        if ($c->req->headers->to_string =~ /X-Forwarded/) {
            my $part0 = shift @{$c->req->url->path->leading_slash(0)};
            push @{$c->req->url->base->path->trailing_slash(0)}, $part0;
            $c->shipped(urlbase => $c->req->url->base);
        } else {
            $c->shipped(urlbase => '');
        }
        # capture https into base
        if ($c->req->headers->header('X-Forwarded-HTTPS')
        || ($c->req->headers->header('X-Forwarded-Proto')||'') eq 'https') {
            $c->req->url->base->scheme('https')
        }
    });
 
    my $plugin_resources = catdir dirname(__FILE__), 'Tables', 'resources';
    push @{$app->routes->namespaces}, 'Mojolicious::Plugin::Tables::Controller';
    push @{$app->renderer->paths}, catdir($plugin_resources, 'templates');
    push @{$app->static->paths},   catdir($plugin_resources, 'public');

    # Arrange for custom table-specific template overrides, when present.
    # We do this by wrapping the standard ep-handler with a
    # pre-processor to test for existence of the custom template.
    # Any internal caching by EP renderer is preserved even for overrides.

    my $handlers = $app->renderer->handlers;
    my $base_ep  = $handlers->{ep};
    $handlers->{ep} = sub {
        my ($renderer, $c, $output, $options) = @_;
        {
            my $table           = $c->stash('table') || last;
            my $custom_template = "$table/$options->{template}";
            my $custom_path     = $renderer->template_path ({%$options,
                                             template=>$custom_template}) || last;
            $options->{template} = $custom_template;
            $log->debug("custom 'tables' template at $custom_path") if $ENV{TABLES_DEBUG};
        }
        $base_ep->(@_)
    };

    my $r = $app->routes;
    $r->get('/' => sub{shift->redirect_to('tables')}) unless $conf->{nohome};

    my @crud_gets  = (qw/view edit del nuke navigate/);
    my @crud_posts = (qw/save/);
    my $fmts       = [format=>[qw/html json/]];

    for ($r->under('tables')                          ->to('auth#ok'   )) {
        for ($_->under()                              ->to('tables#ok' )) {
            $_->get                                   ->to('#page'     );
            for ($_->under(':table')                  ->to('#table_ok' )) {
                $_->any('/'=>$fmts)                   ->to('#table', format=>'html' );
                $_->get('add')                        ->to('#add'      );
                $_->post('save')                      ->to('#save'     );
                for ($_->under(':id')                 ->to('#id_ok'    )) {
                    my $r = $_;
                    $r->get( $_=>$fmts)               ->to("#$_", format=>'html') for @crud_gets;
                    $r->post($_=>$fmts)               ->to("#$_", format=>'html') for @crud_posts;
                    $r->get('add_child/:child'=>$fmts)->to('#view', format=>'html'     );
                    $r->any(':children'=>$fmts)       ->to('#children', format=>'html' );
                }
            }
        }
    }

    my @tablist = @{$model->{tablist}};
    $log->info ("'Tables' Framework enabled.");
    $log->debug("Route namespaces are..");
    $log->debug("--> $_") for @{$app->routes->namespaces};
    $log->debug("Renderer paths are..");
    $log->debug("--> $_") for @{$app->renderer->paths};
    $log->debug("Static paths are..");
    $log->debug("--> $_") for @{$app->static->paths};
    $log->info ("/tables routes are in place. ".scalar(@tablist)." tables available");
    $log->debug("--> $_") for @tablist;

    $log->debug($app->dumper($model)) if $ENV{TABLES_DEBUG};

}

1;
__END__

=encoding utf8

=head1 NAME

Mojolicious::Plugin::Tables -- Quickstart and grow a Tables-Maintenance application

=head1 SYNOPSIS

    # in a new Mojolicious app..
 
    $app->plugin( Tables => {connect_info=>['dbi:Pg:dbname="mydatabase"', 'uid', 'pwd']} );

    # when ready to grow..

    $app->plugin( Tables => {model_class => 'StuffDB'} );

=head1 DESCRIPTION

L<Mojolicious::Plugin::Tables> is a L<Mojolicious> plugin which helps you grow a high-featured web app by
starting with basic table / relationship maintenance and growing by overriding default behaviours.

By supplying 'connect_info' (DBI-standard connect parameters) you get a presentable database maintenance
web app, featuring full server-side support for paged lists of large tables using
'Datatables' (datatables.net), plus ajax handling for browsing 1-many relationships, plus
JQueryUI-styled select lists for editing many-to-1 picklists.

By supplying your own Model Class you can override most of the default behaviours
and build an enterprise-ready rdbms-based web app.

By supplying your own templates for context-specific calls you can start to give your app a truly 
specialised look and feel.

=head1 STATUS

This is an early release.  Guides and more override-hooks coming Real Soon Now.

=head1 GROWTH PATH

=head2 Ground Zero Startup

    tables dbi-connect-params..

The 'tables' script is supplied with this distribution.  Give it standard DBI-compatible parameters on the 
commandline and it will run a minimal web-server at localhost:3000 to allow maintenance on the named database.

=head2 Day One

In your Mojolicious 'startup'..

    $self->plugin(Tables => { connect_info => [ connect-param, connect-param, .. ] };

Add this line to a new Mojolicious app then run it using any Mojo-based server (morbo, prefork, hypnotoad..) to achieve exactly the same functionality as the 'tables' script.

=head2 Day Two

    # templates/:table/{view,edit,dml,page}.html.ep
    # e.g. 
    # templates/artist/view.html.ep
    <h1>Artist: <%= $row %></h1>

For any :table in your database, create override-templates as required.
e.g. The above code will give a very different look-and-feel when viewing a single Artist, but all other
pages are unchanged.  For better examples and to see which stash-variables are available, start by 
copying the distribution templates from ../Plugin/Tables/resources/templates into your own template area.

=head2 Infinity and Beyond

    $self->plugin(Tables => { model_class => 'MyDB' } );

Prepare your own model_class to override the default database settings which "Tables" has determined from the 
database.  This class (and its per-table descendants) can be within or without your Mojolicious application.
C<model_class> implements the specification given in L<Mojolicious::Plugin::Tables::Model>.
This lets you start to customise and grow your web app.

=head1 CONFIGURATION

    $app->plugin(Tables => $conf) 

Where the full list of configuration options is:

=head3 layout

Provide an alternative 'wrapper' layout; typically the one from your own application.  If you prepare one of these you will need
to include most of the layout arrangements in the Day-One layout, i.e. the one at resources/templates/layouts/tables.html.ep.
The easiest approach is to start by copying the packaged version into your own application and then change its look and feel to
suit your requirements.

=head3 nohome

Unless you supply a true value for this, an automatic redirection will be in place from the root of your app to the '/tables' path.
The redirection is there to make day-one functionality easy to find, but once your app grows you will not want this redirection.

=head3 model_class

See 'Customising Model Class'

=head3 connect_info

See 'Day One'

=head3 default_theme

To experiment with completely different colour themes, choose any standard JQueryUI theme name or "roll" your own as described here 
L<http://jqueryui.com/themeroller/>.   Our default is 'Redmond'.

=head1 DEBUGGING

To generate detailed trace info into the server log, export TABLES_DEBUG=1.

=head1 CAVEAT

We use dynamically-generated DBIx::Class classes.  This technique does not scale well for very large numbers
of tables.  Previous (private) incarnations of this Framework used specially prepared high-performance versions of 
Class::DBI::Loader to speed this up.  So that speeding-up at start-time is a TODO for this DBIx::Class-based release.

=head1 SOURCE and ISSUES REPOSITORY

Open-Sourced at Github: L<https://github.com/frank-carnovale/Mojolicious-Plugin-Tables>.  Please use the Issues register there.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2016, Frank Carnovale <frankc@cpan.org>

This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.

=head1 SEE ALSO

L<Mojolicious>, L<DBIx::Class::Schema::Loader>, L<Mojolicious::Plugin::Tables::Model>

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.