File Coverage

blib/lib/App/AutoCRUD.pm
Criterion Covered Total %
statement 117 120 97.5
branch 16 24 66.6
condition 8 16 50.0
subroutine 36 36 100.0
pod 6 10 60.0
total 183 206 88.8


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