File Coverage

blib/lib/App/AutoCRUD.pm
Criterion Covered Total %
statement 113 114 99.1
branch 10 14 71.4
condition 6 13 46.1
subroutine 36 36 100.0
pod 6 10 60.0
total 171 187 91.4


line stmt bran cond sub pod time code
1             package App::AutoCRUD;
2              
3 2     2   73038 use 5.010;
  2         6  
4 2     2   8 use strict;
  2         2  
  2         36  
5 2     2   6 use warnings;
  2         5  
  2         36  
6              
7 2     2   972 use Moose;
  2         600993  
  2         11  
8 2     2   10074 use MooseX::NonMoose;
  2         1286  
  2         6  
9             extends 'Plack::Component';
10              
11 2     2   87973 use Plack::Request;
  2         68269  
  2         60  
12 2     2   866 use Plack::Util;
  2         3870  
  2         44  
13 2     2   9 use Carp;
  2         3  
  2         101  
14 2     2   804 use Scalar::Does qw/does/;
  2         122236  
  2         21  
15 2     2   1586 use Clone qw/clone/;
  2         3842  
  2         113  
16 2     2   10 use Try::Tiny;
  2         2  
  2         91  
17 2     2   668 use YAML::Any qw/Dump/;
  2         1523  
  2         7  
18 2     2   12018 use Data::Reach qw/reach/;
  2         1525  
  2         7  
19              
20 2     2   81 use namespace::clean -except => 'meta';
  2         2  
  2         14  
21              
22              
23             our $VERSION = '0.13';
24              
25             has 'config' => (is => 'bare', isa => 'HashRef', required => 1);
26             has 'dir' => (is => 'ro', isa => 'Str',
27             builder => '_dir', lazy => 1);
28             has 'name' => (is => 'ro', isa => 'Str',
29             builder => '_name', lazy => 1);
30             has 'title' => (is => 'ro', isa => 'Str',
31             builder => '_title', lazy => 1);
32             has 'datasources' => (is => 'ro',
33             isa => 'HashRef',
34             builder => '_datasources', lazy => 1);
35             has 'share_paths' => (is => 'ro',
36             isa => 'ArrayRef',
37             builder => '_share_paths', lazy => 1, auto_deref => 1);
38             has 'readonly' => (is => 'ro', isa => 'Bool',
39             builder => '_readonly', lazy => 1);
40              
41              
42              
43             #======================================================================
44             # LAZY ATTRIBUTE CONSTRUCTORS
45             #======================================================================
46              
47              
48              
49             sub _dir {
50 2     2   6 my $self = shift;
51 2   50     8 return $self->config('dir') || '.';
52             }
53              
54             sub _name {
55 2     2   3 my $self = shift;
56 2   50     7 return $self->config(qw/app name/) || 'ANONYMOUS_AutoCRUD';
57             }
58              
59             sub _title {
60 2     2   3 my $self = shift;
61 2   50     8 return $self->config(qw/app title/) || 'Welcome to the wonders of AutoCRUD';
62             }
63              
64             sub _readonly {
65 2     2   3 my $self = shift;
66 2         6 return $self->config(qw/app readonly/);
67             }
68              
69             sub _datasources {
70 2     2   3 my $self = shift;
71              
72 2         7 my $source_class = $self->find_class('DataSource');
73 2         11 my $config_sources = $self->config('datasources');
74 2         43 return {map {($_ => $source_class->new(name => $_, app => $self))}
  2         18  
75             sort keys %$config_sources};
76             }
77              
78             sub _share_paths {
79 2     2   4 my ($self) = @_;
80              
81             # NOTE : we don't use L<File::ShareDir> because of its lack of support for
82             # a development environment. L<File::Share> doesn't help either because
83             # you need to know the distname; here we only know classnames. So in the end,
84             # we put share directories directly under the modules files, which works in
85             # any environment.
86 2         4 my @paths;
87 2         8 foreach my $class ($self->meta->linearized_isa) {
88 6         51 $class =~ s[::][/]g;
89 6         15 my $path = $INC{$class . ".pm"};
90 6         17 $path =~ s[\.pm$][/share];
91 6 100       139 push @paths, $path if -d $path;
92             }
93 2         53 return \@paths;
94             }
95              
96              
97             sub BUILD {
98 2     2 0 2809 my $self = shift;
99 2         5 $self->_check_config;
100             }
101              
102              
103             sub _check_config {
104 2     2   3 my $self = shift;
105 2         6 my $config_domain_class = $self->find_class("ConfigDomain");
106 2         14 my $domain = $config_domain_class->Config;
107 2         3893 my $msgs = $domain->inspect($self->{config});
108 2 50       5212 die Dump({"ERROR IN CONFIG" => $msgs}) if $msgs;
109             }
110              
111              
112              
113              
114              
115             #======================================================================
116             # METHODS
117             #======================================================================
118              
119             sub datasource {
120 24     24 1 36 my ($self, $name) = @_;
121 24         523 return $self->datasources->{$name};
122             }
123              
124              
125             sub call { # request dispatcher (see L<Plack::Component>)
126 24     24 1 70914 my ($self, $env) = @_;
127              
128             try {
129 24     24   468 $self->respond($env);
130             }
131             catch {
132 8     8   1031 return [500, ['Content-type' => 'text/html'], [$self->show_error($_)]];
133 24         114 };
134             }
135              
136              
137              
138              
139             sub respond { # request dispatcher (see L<Plack::Component>)
140 24     24 0 35 my ($self, $env) = @_;
141              
142 24         21 my $controller_name;
143              
144             # build context object
145 24   50     44 my $request_class = $self->find_class("Request") || 'Plack::Request';
146 24         150 my $req = $request_class->new($env);
147 24         195 my $context_class = $self->find_class("Context");
148 24         154 my $context = $context_class->new(app => $self, req => $req);
149              
150             # see if a specific view was required in the URL
151 24         25783 $context->maybe_set_view_from_path;
152              
153             # setup datasource from initial path segment
154 24 50       63 if (my $source_name = $context->extract_path_segments(1)) {
155 24 100       59 if (my $datasource = $self->datasource($source_name)) {
156             # integrate datasource into the context
157 22         76 $datasource->prepare_for_request($req);
158 22         3990 $context->set_datasource($datasource);
159              
160             # setup controller from initial path segment
161 22   50     65 $controller_name = ucfirst($context->extract_path_segments(1))
162             || 'Schema'; # default
163             }
164             else {
165 2         8 $controller_name = ucfirst($source_name);
166             }
167             }
168             else {
169 0         0 $controller_name = 'Home';
170             }
171              
172             # call controller
173 24 50       97 my $controller_class = $self->find_class("Controller::$controller_name")
174             or die "no such controller : $controller_name";
175 24         178 my $controller = $controller_class->new(context => $context);
176 24         9216 $controller->respond;
177             }
178              
179              
180             sub config {
181 15     15 1 20 my $self = shift;
182 15         39 my $config = $self->{config};
183 15         59 return reach $config, @_;
184             }
185              
186              
187             sub find_class {
188 92     92 1 130 my ($self, $name) = @_;
189              
190             # try to find $name within namespace of current class, then within parents
191 92         326 foreach my $namespace ($self->meta->linearized_isa) {
192 140         2391 my $class = $self->try_load_class($name, $namespace);
193 140 100       461 return $class if $class;
194             }
195              
196 24         111 return; # not found
197             }
198              
199              
200             sub default {
201 3     3 0 8 my ($self, @path) = @_;
202              
203             # convenience function, returns default value from config (if any)
204 3         11 return $self->config(default => @path);
205             }
206              
207              
208             sub try_load_class {
209 140     140 1 166 my ($self, $name, $namespace) = @_;
210              
211             # return classname if loaded successfully;
212             # return undef if not found;
213             # raise exception if found but there is a compilation error
214 140     140   3223 my $class = try {Plack::Util::load_class($name, $namespace)}
215 140 50   72   647 catch {die $_ if $_ !~ /^Can't locate(?! object method)/};
  72         12840  
216 140         1896 return $class;
217             }
218              
219              
220             sub is_class_loaded {
221 2     2 1 4 my ($self, $class) = @_;
222              
223             # deactivate strict refs because we'll be looking into symbol tables
224 2     2   2673 no strict 'refs';
  2         5  
  2         260  
225              
226             # looking at %{$class."::"} is not enough (it may contain other namespaces);
227             # so we consider a class loaded if it has at least an ISA or a VERSION
228 2   33     3 return @{$class."::ISA"} || ${$class."::VERSION"};
229              
230             }
231              
232              
233             sub show_error {
234 8     8 0 12 my ($self, $msg) = @_;
235              
236 8         69 return <<__EOHTML__;
237             <!doctype html>
238             <html>
239             <head><title>500 Server Error</title></head>
240             <body><h1>500 Server Error</h1>
241             <pre>
242             $msg
243             </pre>
244              
245             <!--
246             512 bytes of padding to suppress Internet Explorer's "Friendly error messages"
247              
248             From: HOW TO: Turn Off the Internet Explorer 5.x and 6.x
249             "Show Friendly HTTP Error Messages" Feature on the Server Side"
250             http://support.microsoft.com/kb/294807
251              
252             Several frequently-seen status codes have "friendly" error messages
253             that Internet Explorer 5.x displays and that effectively mask the
254             actual text message that the server sends.
255             However, these "friendly" error messages are only displayed if the
256             response that is sent to the client is less than or equal to a
257             specified threshold.
258             For example, to see the exact text of an HTTP 500 response,
259             the content length must be greater than 512 bytes.
260             -->
261             </body>
262             </html>
263             __EOHTML__
264             }
265              
266              
267             1; # End of App::AutoCRUD
268              
269             __END__
270              
271             =head1 NAME
272              
273             App::AutoCRUD - A Plack application for browsing and editing databases
274              
275             =head1 SYNOPSIS
276              
277             =head2 Quick demo
278              
279             To see the demo distributed with this application :
280              
281             cd examples/Chinook
282             plackup app.psgi
283              
284             Then point your browser to L<http://localhost:5000>.
285              
286             =head2 General startup
287              
288             Create a configuration file, for example in L<YAML> format, like this :
289              
290             app:
291             name: Test AutoCRUD
292              
293             datasources :
294             Source1 :
295             dbh:
296             connect:
297             # arguments that will be passed to DBI->connect(...)
298             # for example :
299             - dbi:SQLite:dbname=some_file
300             - "" # user
301             - "" # password
302             - RaiseError : 1
303             sqlite_unicode: 1
304              
305             Create a file F<crud.psgi> like this :
306              
307             use App::AutoCRUD;
308             use YAML qw/LoadFile/;
309             my $config = LoadFile "/path/to/config.yaml";
310             my $crud = App::AutoCRUD->new(config => $config);
311             my $app = $crud->to_app;
312              
313             Then run the app
314              
315             plackup crud.psgi
316              
317             or mount the app in Apache
318              
319             <Location /crud>
320             SetHandler perl-script
321             PerlResponseHandler Plack::Handler::Apache2
322             PerlSetVar psgi_app /path/to/crud.psgi
323             </Location>
324              
325             and use your favorite web browser to navigate through your database.
326              
327              
328             =head1 DESCRIPTION
329              
330             This module embodies a web application for Creating, Retrieving,
331             Updating and Deleting records in relational databases (hence the
332             'CRUD' acronym). The 'C<Auto>' part of the name is because the
333             application automatically generates and immediately uses the
334             components needed to work with your data -- you don't have to edit
335             scaffolding code. The 'C<Plack>' part of the name comes from the
336             L<Plack middleware framework|Plack> used to implement this application.
337              
338             To connect to one or several databases, just supply a configuration
339             file with the connnection information, and optionally some
340             presentation information, and then you can directly work with the
341             data. Optionally, the configuration file can also specify many
342             additional details, like table groups, column groups, data
343             descriptions, etc. If more customization is needed, then you can
344             modify the presentation templates, or even subclass some parts of the
345             framework.
346              
347             This application was designed to be easy to integrate with other web
348             resources in your organization : every table, every record, every
349             search form has its own URL which can be linked from other sources,
350             can be bookmarked, etc. This makes it a great tool for example
351             for adding an admin interface to an existing application : just
352             install AutoCRUD at a specific location within your Web server
353             (with appropriate access control :-).
354              
355             Some distinctive features of this module, in comparison with other
356             CRUD applications, are :
357              
358             =over
359              
360             =item *
361              
362             Hyperlinks between records, corresponding to foreign key
363             relationships in the database.
364              
365             =item *
366              
367             Support for update or delete of several records at once.
368              
369             =item *
370              
371             Support for reordering, masking, documenting tables and columns
372             through configuration files -- a cheap way to provide reasonable
373             user experience without investing into a full-fledged custom application.
374              
375             =item *
376              
377             Data export in Excel, YAML, JSON, XML formats
378              
379             =item *
380              
381             Extensibility through inheritance
382              
383             =back
384              
385              
386             This application is also meant as an example for showing the
387             power of "Modern Perl", assembling several advanced frameworks
388             such as L<Moose>, L<Plack> and L<DBIx::DataModel>.
389              
390              
391             B<Disclaimer> : this application is already usable, but is still at an
392             early design stage; many improvements need to be made, as can be seen from
393             the long L</TODO> section at the bottom. The global architecture and URL
394             space is unlikely to change, but many presentation details will probably
395             different in future versions.
396              
397              
398             =head1 CONFIGURATION
399              
400             The bare minimum for this application to run is to
401             get some configuration information about how to connect
402             to datasources. This can be done directly in Perl, like in
403             the test file F<t/00_autocrud.t> :
404              
405             my $connect_options = {
406             RaiseError => 1,
407             sqlite_unicode => 1,
408             };
409             my $config = {
410             app => {
411             name => "SomeName"
412             },
413             datasources => {
414             SomeDatabase => {
415             dbh => {
416             connect => [$dbi_connect_string, $user, $passwd, $connect_options],
417             },
418             },
419             },
420             };
421              
422             # instantiate the app
423             my $crud = App::AutoCRUD->new(config => $config);
424             my $app = $crud->to_app;
425              
426             With this minimal information, the application will just display
427             tables and columns in alphabetical order. However, the configuration
428             may also specify many details about grouping and ordering tables
429             and columns; in that case, it is more convenient to use an external
430             format like L<YAML>, L<XML> or L<AppConfig>. Here is an excerpt from the
431             YAML configuration for L<Chinook|http://chinookdatabase.codeplex.com>, a
432             sample database distributed with this application (see the complete
433             example under the F<examples/Chinook> directory within this distribution) :
434              
435             datasources :
436             Chinook :
437             dbh:
438             connect:
439             - "dbi:SQLite:dbname=Chinook_Sqlite_AutoIncrementPKs.sqlite"
440             - ""
441             - ""
442             - RaiseError: 1
443             sqlite_unicode: 1
444              
445             tablegroups :
446             - name: Music
447             descr: Tables describing music content
448             node: open
449             tables :
450             - Artist
451             - Album
452             - Track
453              
454             - name: Playlist
455             descr: Tables for structuring playlists
456             node: open
457             tables :
458             - Playlist
459             - PlaylistTrack
460             ...
461             tables:
462             Track:
463             colgroups:
464             - name: keys
465             columns:
466             - name: TrackId
467             descr: Primary key
468             - name: AlbumId
469             descr: foreign key to the album where this track belongs
470             - name: GenreId
471             descr: foreign key to the genre of this track
472             - name: MediaTypeId
473             descr: foreign key to the media type of this track
474             - name: Textual information
475             columns:
476             - name: Name
477             descr: name of this track
478             - name: Composer
479             descr: name of composer of this track
480             - name: Technical details
481             columns:
482             - name: Bytes
483             - name: Milliseconds
484             - name: Commercial details
485             columns:
486             - name: UnitPrice
487              
488             The full datastructure for configuration information is documented
489             in L<App::AutoCRUD::ConfigDomain>.
490              
491             =head1 USAGE
492              
493             =head2 Generalities
494              
495             All pages are presented with a
496             L<Tree navigator|Alien::GvaScript::TreeNavigator>.
497             Tree sections can be folded/unfolded either through the mouse or
498             through navigation keys LEFT and RIGHT. Keys DOWN and UP navigate
499             to the next/previous sections. Typing the initial characters of a
500             section title directly jumps to that section.
501              
502              
503              
504             =head2 Homepage
505              
506             The homepage displays the application short name, title, and the list
507             of available datasources.
508              
509             =head2 Schema
510              
511             The schema page, for a given datasource, displays the list of
512             tables, grouped and ordered according to the configuration (if any).
513              
514             Each table has an immediate hyperlink to its search form; in addition,
515             another link points to the I<description page> for this table.
516              
517             =head2 Table description
518              
519             The description page for a given table presents the list of columns,
520             with typing information as obtained from the database, and hyperlinks
521             to other tables for which this table has foreign keys.
522              
523             =head2 Search form
524              
525             The search form allows users to enter I<search criteria> and
526             I<presentation parameters>.
527              
528             =head3 Search criteria
529              
530             Within a column input field, one may enter a constant value,
531             a list of values separated by commas, a partial word with an
532             ending star (which will be interpreted as a SQL "LIKE" clause),
533             a comparison operator (ex C<< > 2013 >>), or a BETWEEN clause
534             (ex C<< BETWEEN 2 AND 6 >>).
535              
536             The full syntax accepted for such criteria is documented
537             in L<SQL::Abstract::FromQuery>. That syntax is customizable,
538             so if you want to support additional fancy operators for your
539             database, you might do so by augmenting or subclassing the grammar.
540              
541             =head3 Columns to display
542              
543             On the right of each column input field is a checkbox to decide
544             if this column should be displayed in the results or not.
545             If the configuration specifies column groups, each column group
546             also has a checkbox to simultaneously check all columns in that group.
547             Finally, there is also a global checkbox to check/uncheck everything.
548             If nothing is checked (which is the default), this will be implicitly
549             interpreted as "SELECT *", i.e. showing everything.
550              
551             =head3 Presentation parameters
552              
553             Presentation parameters include :
554              
555             =over
556              
557             =item *
558              
559             pagination information (page size / page index)
560              
561             =item *
562              
563             output format, which is one of :
564              
565             =over
566              
567             =item html
568              
569             Default presentation view
570              
571             =item xlsx
572              
573             Export to Excel
574              
575             =item yaml
576              
577             L<YAML> format
578              
579             =item json
580              
581             L<JSON> format
582              
583             =item xml
584              
585             C<XML> format
586              
587             =back
588              
589             =item *
590              
591             Flag for total page count (this is optional because it is not always
592             important, and on many databases it has an additional cost as
593             it requires an additional call to the database to know the total
594             number of records).
595              
596             =back
597              
598             =head2 List page
599              
600             The list page displays a list of records resulting from a search.
601             The generated SQL is shown for information.
602             For columns that related to other tables, there are hyperlinks
603             to the related lists.
604              
605             Each record has a checkbox for marking this record for update or delete.
606              
607             Hyperlinks to the next/previous page are provided, but navigation through
608             pages can also be performed with the LEFT/RIGHT arrow keys.
609              
610             =head2 Single record display
611              
612             The single record page is very similar to the list page, but only
613             displays one single record. The only difference is in the hyperlinks
614             to update/delete/clone operations.
615              
616              
617             =head2 Update
618              
619             The update page has two modes : single-record or multiple-records
620              
621             =head2 Single-record update
622              
623             The form shows current values on the right, and has input fields
624             on the left. Only fields with some user input will be sent for update
625             to the database.
626              
627             =head2 Multiple-records update
628              
629             This form is reached from the L</List page>, when several records
630             were checked, or when updating the whole result set.
631              
632             Input fields on the left correspond to the SQL "C<SET>" clause,
633             i.e. they specify values that will be updated within I<several records>
634             simultaneously.
635              
636             Input fields on the right, labelled "where/and", specify some criteria
637             for the SQL "C<WHERE>" clause.
638              
639             Needless to say, this is quite a powerful operation which if misused
640             could easily corrupt your data.
641              
642             =head2 Delete
643              
644             Like updates, delete forms can be either single-record or
645             multiple-records.
646              
647              
648             =head2 Insert
649              
650             The insert form is very much like the single-record update form, except
651             that there are no "current values"
652              
653             =head2 Clone
654              
655             The clone form is like an insert form, but pre-filled with the data to clone,
656             except the primary key which is always empty.
657              
658              
659              
660             =head1 ARCHITECTURE
661              
662             [to be developed]
663              
664              
665             =head2 Classes
666              
667             Modules are organized in a classical Model-View-Controller structure.
668              
669             =head2 Inheritance and customization
670              
671             All classes can be subclassed, and the application will automatically
672             discover and load appropriate modules on demand.
673             Presentation templates can also be overridden in sub-applications.
674              
675             =head2 DataModel
676              
677             This application requires a L<DBIx::DataModel::Schema> subclass
678             for every datasource. If none is supplied, a subclass will be
679             generated and loaded on the fly; but this incurs an additional
680             startup cost, and does not exploit all possibilities of
681             L<DBIx::DataModel>; so apart from short demos and experiments,
682             it is better to statically generate a schema and store it in a
683             file.
684              
685             An initial schema class can be built, either from a L<DBI> database
686             handle, or from an existing L<DBIx::Class> schema; see
687             L<DBIx::DataModel::Schema::Generator>.
688              
689              
690             =head1 ATTRIBUTES
691              
692             =head2 config
693              
694             A datatree of information, whose structure should comply with
695             L<App::AutoCRUD::ConfigDomain>.
696              
697             =head2 name
698              
699             The application name (displayed in most pages).
700             This attribute defaults to the value of the C<app/name> entry in config.
701              
702             =head2 datasources
703              
704             A hashref of the datasources served by this application.
705             Hash keys are unique identifiers for the datasources (these names will also
706             be used to generate URIs); hash values are instances of the
707             L<App::AutoCRUD::DataSource> class.
708              
709             =head2 dir
710              
711             The root directory where some application components could be
712             placed (like for example some presentation templates).
713              
714             This attribute defaults to the value of the C<dir> entry in config,
715             or, if absent, to the current directory.
716              
717             This directory is associated with the application I<instance>.
718             When components are not found in this directory, they are searched
719             in the directories associated with the application I<classes>
720             (see the C<share_path> attribute below).
721              
722              
723             =head2 share_paths
724              
725             An arrayref to a list of directories corresponding
726             to the hierarchy of application classes. These directories are searched
727             as second resort, when components are not found in the application instance
728             directory.
729              
730              
731             =head2 readonly
732              
733             A boolean to restrict actions available to only read from the database.
734             The value of readonly boolean is set in YAML configuration file.
735              
736              
737             =head1 METHODS
738              
739             =head2 new
740              
741             my $crud_app = App::AutoCRUD->new(%options);
742              
743             Creates a new instance of the application.
744             All attributes described above may be supplied as
745             C<%options>.
746              
747             =head2 datasource
748              
749             my $datasource = $app->datasource($name);
750              
751             Returnes the the datasource registered under the given name.
752              
753              
754             =head2 call
755              
756             This method implements request dispatch, as required by
757             the L<Plack> middleware.
758              
759              
760             =head2 config
761              
762             my $data = $app->config(@path);
763              
764             Walks through the configuration tree, following node names
765             as specified in C<@path>, and returns whatever is found at
766             the end of this path ( either a subtree, or scalar data, or
767             C<undef> if the path leads to nothing ).
768              
769              
770             =head2 try_load_class
771              
772             my $class = $self->try_load_class($name, $namespace);
773              
774             Invokes L<Plack::Util/load_class>; returns the loaded class in case
775             of success, or C<undef> in case of failure.
776              
777              
778             =head2 find_class
779              
780             my $class = $app->find_class($subclass_name);
781              
782             Tries to find the given C<$subclass_name> within the namespaces
783             of the application classes.
784              
785             =head2 is_class_loaded
786              
787             Checks if the given class is already loaded in memory or not.
788              
789              
790             =head1 CAVEATS
791              
792             In the current implementation, the slash charater (C<'/'>) is interpreted
793             as a separator for primary keys over multiple columns. This means that
794             an embedded slash in a column name or in the value of a primary key
795             could yield unexpected results. This is definitely something to be
796             improved in a future versions, but at the moment I still don't know how
797             it will be solved.
798              
799              
800              
801             =head1 ACKNOWLEDGEMENTS
802              
803             Some design aspects were borrowed from
804              
805             =over
806              
807             =item L<Catalyst>
808              
809             =item L<Catalyst::Helper::View::TTSite>
810              
811             =back
812              
813              
814              
815             =head1 AUTHOR
816              
817             Laurent Dami, C<< <dami at cpan.org> >>
818              
819             =head1 BUGS
820              
821             Please report any bugs or feature requests to C<bug-app-autocrud at rt.cpan.org>, or through
822             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-AutoCRUD>. I will be notified, and then you'll
823             automatically be notified of progress on your bug as I make changes.
824              
825              
826             =head1 SUPPORT
827              
828             You can find documentation for this module with the perldoc command.
829              
830             perldoc App::AutoCRUD
831              
832              
833             You can also look for information at:
834              
835             =over 4
836              
837             =item * RT: CPAN's request tracker (report bugs here)
838              
839             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-AutoCRUD>
840              
841             =item * AnnoCPAN: Annotated CPAN documentation
842              
843             L<http://annocpan.org/dist/App-AutoCRUD>
844              
845             =item * CPAN Ratings
846              
847             L<http://cpanratings.perl.org/d/App-AutoCRUD>
848              
849             =item * Search CPAN
850              
851             L<http://search.cpan.org/dist/App-AutoCRUD/>
852              
853             =back
854              
855              
856             The source code is at
857             L<https://github.com/damil/App-AutoCRUD>.
858              
859              
860             =head1 SEE ALSO
861              
862             L<Catalyst::Plugin::AutoCRUD>,
863             L<Plack>,
864             L<http://www.codeplex.com/ChinookDatabase>.
865              
866              
867             =head1 TODO
868              
869             - column properties
870             - noinsert, noupdate, nosearch, etc.
871              
872             - edit: select or autocompleter for foreign keys
873              
874             - internationalisation
875             -
876              
877             - View:
878             - default view should be defined in config
879             - overridable content-type & headers
880              
881             - search form, show associations => link to join search
882              
883             - list foreign keys even if not in DBIDM schema
884              
885             - change log
886              
887             - quoting problem (FromQuery: "J&B")
888              
889             - readonly fields: tabindex -1 (can be done by CSS?)
890             in fact, current values should NOT be input fields, but plain SPANs
891              
892             - NULL in updates
893             - Update form, focus problem (focus in field should deactivate TreeNav)
894             - add insert link in table descr
895              
896             - deal with Favicon.ico
897              
898             - declare in http://www.sqlite.org/cvstrac/wiki?p=ManagementTools
899              
900             - multicolumns : if there is an association over a multicolumns key,
901             it is not displayed as a hyperlink in /list. To do so, we would need
902             to add a line in the display, corresponding to the multicolumn.
903              
904             =head1 LICENSE AND COPYRIGHT
905              
906             Copyright 2014-2016 Laurent Dami.
907              
908             This program is free software; you can redistribute it and/or modify it
909             under the terms of the the Artistic License (2.0). You may obtain a
910             copy of the full license at:
911              
912             L<http://www.perlfoundation.org/artistic_license_2_0>
913              
914              
915             =cut
916              
917