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 1     1   100977 use 5.010;
  1         4  
4 1     1   6 use strict;
  1         2  
  1         22  
5 1     1   4 use warnings;
  1         2  
  1         27  
6              
7 1     1   675 use Moose;
  1         486482  
  1         8  
8 1     1   8266 use MooseX::NonMoose;
  1         1051  
  1         3  
9             extends 'Plack::Component';
10              
11 1     1   73248 use Plack::Request;
  1         80464  
  1         40  
12 1     1   523 use Plack::Util;
  1         2969  
  1         42  
13 1     1   7 use Carp;
  1         3  
  1         63  
14 1     1   562 use Scalar::Does qw/does/;
  1         109562  
  1         16  
15 1     1   705 use Clone qw/clone/;
  1         3  
  1         55  
16 1     1   6 use Try::Tiny;
  1         2  
  1         52  
17 1     1   487 use YAML::Any qw/Dump/;
  1         1204  
  1         4  
18 1     1   4144 use Data::Reach qw/reach/;
  1         1193  
  1         6  
19              
20 1     1   59 use namespace::clean -except => 'meta';
  1         2  
  1         7  
21              
22              
23             our $VERSION = '0.14';
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 1     1   3 my $self = shift;
51 1   50     4 return $self->config('dir') || '.';
52             }
53              
54             sub _name {
55 1     1   3 my $self = shift;
56 1   50     5 return $self->config(qw/app name/) || 'ANONYMOUS_AutoCRUD';
57             }
58              
59             sub _title {
60 1     1   3 my $self = shift;
61 1   50     8 return $self->config(qw/app title/) || 'Welcome to the wonders of AutoCRUD';
62             }
63              
64             sub _readonly {
65 1     1   4 my $self = shift;
66 1         4 return $self->config(qw/app readonly/);
67             }
68              
69             sub _datasources {
70 1     1   3 my $self = shift;
71              
72 1         5 my $source_class = $self->find_class('DataSource');
73 1         6 my $config_sources = $self->config('datasources');
74 1         37 return {map {($_ => $source_class->new(name => $_, app => $self))}
  1         13  
75             sort keys %$config_sources};
76             }
77              
78             sub _share_paths {
79 1     1   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 1         2 my @paths;
87 1         6 foreach my $class ($self->meta->linearized_isa) {
88 3         50 $class =~ s[::][/]g;
89 3         11 my $path = $INC{$class . ".pm"};
90 3         13 $path =~ s[\.pm$][/share];
91 3 100       126 push @paths, $path if -d $path;
92             }
93 1         37 return \@paths;
94             }
95              
96              
97             sub BUILD {
98 1     1 0 2352 my $self = shift;
99 1         5 $self->_check_config;
100             }
101              
102              
103             sub _check_config {
104 1     1   3 my $self = shift;
105 1         3 my $config_domain_class = $self->find_class("ConfigDomain");
106 1         8 my $domain = $config_domain_class->Config;
107 1         3236 my $msgs = $domain->inspect($self->{config});
108 1 50       4015 die Dump({"ERROR IN CONFIG" => $msgs}) if $msgs;
109             }
110              
111              
112              
113              
114              
115             #======================================================================
116             # METHODS
117             #======================================================================
118              
119             sub datasource {
120 17     17 1 45 my ($self, $name) = @_;
121 17         490 return $self->datasources->{$name};
122             }
123              
124              
125             sub call { # request dispatcher (see L<Plack::Component>)
126 17     17 1 77068 my ($self, $env) = @_;
127              
128             try {
129 17     17   685 $self->respond($env);
130             }
131             catch {
132 4     4   877 return [500, ['Content-type' => 'text/html'], [$self->show_error($_)]];
133 17         122 };
134             }
135              
136              
137              
138              
139             sub respond { # request dispatcher (see L<Plack::Component>)
140 17     17 0 48 my ($self, $env) = @_;
141              
142 17         29 my $controller_name;
143              
144             # build context object
145 17   50     52 my $request_class = $self->find_class("Request") || 'Plack::Request';
146 17         180 my $req = $request_class->new($env);
147 17         236 my $context_class = $self->find_class("Context");
148 17         147 my $context = $context_class->new(app => $self, req => $req);
149              
150             # see if a specific view was required in the URL
151 17         31486 $context->maybe_set_view_from_path;
152              
153             # setup datasource from initial path segment
154 17 50       97 if (my $source_name = $context->extract_path_segments(1)) {
155 17 100       92 if (my $datasource = $self->datasource($source_name)) {
156             # integrate datasource into the context
157 16         99 $datasource->prepare_for_request($req);
158 16         4118 $context->set_datasource($datasource);
159              
160             # setup controller from initial path segment
161 16   50     67 $controller_name = ucfirst($context->extract_path_segments(1))
162             || 'Schema'; # default
163             }
164             else {
165 1         5 $controller_name = ucfirst($source_name);
166             }
167             }
168             else {
169 0         0 $controller_name = 'Home';
170             }
171              
172             # call controller
173 17 50       77 my $controller_class = $self->find_class("Controller::$controller_name")
174             or die "no such controller : $controller_name";
175 17         174 my $controller = $controller_class->new(context => $context);
176 17         11586 $controller->respond;
177             }
178              
179              
180             sub config {
181 8     8 1 20 my $self = shift;
182 8         29 my $config = $self->{config};
183 8         42 return reach $config, @_;
184             }
185              
186              
187             sub find_class {
188 66     66 1 171 my ($self, $name) = @_;
189              
190             # try to find $name within namespace of current class, then within parents
191 66         313 foreach my $namespace ($self->meta->linearized_isa) {
192 100         2425 my $class = $self->try_load_class($name, $namespace);
193 100 100       492 return $class if $class;
194             }
195              
196 17         128 return; # not found
197             }
198              
199              
200             sub default {
201 2     2 0 10 my ($self, @path) = @_;
202              
203             # convenience function, returns default value from config (if any)
204 2         10 return $self->config(default => @path);
205             }
206              
207              
208             sub try_load_class {
209 100     100 1 237 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 100     100   4305 my $class = try {Plack::Util::load_class($name, $namespace)}
215 100 50   51   623 catch {die $_ if $_ !~ /^Can't locate(?! object method)/};
  51         12824  
216 100         2205 return $class;
217             }
218              
219              
220             sub is_class_loaded {
221 1     1 1 4 my ($self, $class) = @_;
222              
223             # deactivate strict refs because we'll be looking into symbol tables
224 1     1   1648 no strict 'refs';
  1         3  
  1         145  
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 1   33     2 return @{$class."::ISA"} || ${$class."::VERSION"};
229              
230             }
231              
232              
233             sub show_error {
234 4     4 0 12 my ($self, $msg) = @_;
235              
236 4         57 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             =head1 CONFIGURATION
392              
393             The bare minimum for this application to run is to
394             get some configuration information about how to connect
395             to datasources. This can be done directly in Perl, like in
396             the test file F<t/00_autocrud.t> :
397              
398             my $connect_options = {
399             RaiseError => 1,
400             sqlite_unicode => 1,
401             };
402             my $config = {
403             app => {
404             name => "SomeName"
405             },
406             datasources => {
407             SomeDatabase => {
408             dbh => {
409             connect => [$dbi_connect_string, $user, $passwd, $connect_options],
410             },
411             },
412             },
413             };
414              
415             # instantiate the app
416             my $crud = App::AutoCRUD->new(config => $config);
417             my $app = $crud->to_app;
418              
419             With this minimal information, the application will just display
420             tables and columns in alphabetical order. However, the configuration
421             may also specify many details about grouping and ordering tables
422             and columns; in that case, it is more convenient to use an external
423             format like L<YAML>, L<XML> or L<AppConfig>. Here is an excerpt from the
424             YAML configuration for L<Chinook|http://chinookdatabase.codeplex.com>, a
425             sample database distributed with this application (see the complete
426             example under the F<examples/Chinook> directory within this distribution) :
427              
428             datasources :
429             Chinook :
430             dbh:
431             connect:
432             - "dbi:SQLite:dbname=Chinook_Sqlite_AutoIncrementPKs.sqlite"
433             - ""
434             - ""
435             - RaiseError: 1
436             sqlite_unicode: 1
437              
438             tablegroups :
439             - name: Music
440             descr: Tables describing music content
441             node: open
442             tables :
443             - Artist
444             - Album
445             - Track
446              
447             - name: Playlist
448             descr: Tables for structuring playlists
449             node: open
450             tables :
451             - Playlist
452             - PlaylistTrack
453             ...
454             tables:
455             Track:
456             colgroups:
457             - name: keys
458             columns:
459             - name: TrackId
460             descr: Primary key
461             - name: AlbumId
462             descr: foreign key to the album where this track belongs
463             - name: GenreId
464             descr: foreign key to the genre of this track
465             - name: MediaTypeId
466             descr: foreign key to the media type of this track
467             - name: Textual information
468             columns:
469             - name: Name
470             descr: name of this track
471             - name: Composer
472             descr: name of composer of this track
473             - name: Technical details
474             columns:
475             - name: Bytes
476             - name: Milliseconds
477             - name: Commercial details
478             columns:
479             - name: UnitPrice
480              
481             The full datastructure for configuration information is documented
482             in L<App::AutoCRUD::ConfigDomain>.
483              
484             =head1 USAGE
485              
486             =head2 Generalities
487              
488             All pages are presented with a
489             L<Tree navigator|Alien::GvaScript::TreeNavigator>.
490             Tree sections can be folded/unfolded either through the mouse or
491             through navigation keys LEFT and RIGHT. Keys DOWN and UP navigate
492             to the next/previous sections. Typing the initial characters of a
493             section title directly jumps to that section.
494              
495              
496              
497             =head2 Homepage
498              
499             The homepage displays the application short name, title, and the list
500             of available datasources.
501              
502             =head2 Schema
503              
504             The schema page, for a given datasource, displays the list of
505             tables, grouped and ordered according to the configuration (if any).
506              
507             Each table has an immediate hyperlink to its search form; in addition,
508             another link points to the I<description page> for this table.
509              
510             =head2 Table description
511              
512             The description page for a given table presents the list of columns,
513             with typing information as obtained from the database, and hyperlinks
514             to other tables for which this table has foreign keys.
515              
516             =head2 Search form
517              
518             The search form allows users to enter I<search criteria> and
519             I<presentation parameters>.
520              
521             =head3 Search criteria
522              
523             Within a column input field, one may enter a constant value,
524             a list of values separated by commas, a partial word with an
525             ending star (which will be interpreted as a SQL "LIKE" clause),
526             a comparison operator (ex C<< > 2013 >>), or a BETWEEN clause
527             (ex C<< BETWEEN 2 AND 6 >>).
528              
529             The full syntax accepted for such criteria is documented
530             in L<SQL::Abstract::FromQuery>. That syntax is customizable,
531             so if you want to support additional fancy operators for your
532             database, you might do so by augmenting or subclassing the grammar.
533              
534             =head3 Columns to display
535              
536             On the right of each column input field is a checkbox to decide
537             if this column should be displayed in the results or not.
538             If the configuration specifies column groups, each column group
539             also has a checkbox to simultaneously check all columns in that group.
540             Finally, there is also a global checkbox to check/uncheck everything.
541             If nothing is checked (which is the default), this will be implicitly
542             interpreted as "SELECT *", i.e. showing everything.
543              
544             =head3 Presentation parameters
545              
546             Presentation parameters include :
547              
548             =over
549              
550             =item *
551              
552             pagination information (page size / page index)
553              
554             =item *
555              
556             output format, which is one of :
557              
558             =over
559              
560             =item html
561              
562             Default presentation view
563              
564             =item xlsx
565              
566             Export to Excel
567              
568             =item yaml
569              
570             L<YAML> format
571              
572             =item json
573              
574             L<JSON> format
575              
576             =item xml
577              
578             C<XML> format
579              
580             =back
581              
582             =item *
583              
584             Flag for total page count (this is optional because it is not always
585             important, and on many databases it has an additional cost as
586             it requires an additional call to the database to know the total
587             number of records).
588              
589             =back
590              
591             =head2 List page
592              
593             The list page displays a list of records resulting from a search.
594             The generated SQL is shown for information.
595             For columns that related to other tables, there are hyperlinks
596             to the related lists.
597              
598             Each record has a checkbox for marking this record for update or delete.
599              
600             Hyperlinks to the next/previous page are provided, but navigation through
601             pages can also be performed with the LEFT/RIGHT arrow keys.
602              
603             =head2 Single record display
604              
605             The single record page is very similar to the list page, but only
606             displays one single record. The only difference is in the hyperlinks
607             to update/delete/clone operations.
608              
609              
610             =head2 Update
611              
612             The update page has two modes : single-record or multiple-records
613              
614             =head2 Single-record update
615              
616             The form shows current values on the right, and has input fields
617             on the left. Only fields with some user input will be sent for update
618             to the database.
619              
620             =head2 Multiple-records update
621              
622             This form is reached from the L</List page>, when several records
623             were checked, or when updating the whole result set.
624              
625             Input fields on the left correspond to the SQL "C<SET>" clause,
626             i.e. they specify values that will be updated within I<several records>
627             simultaneously.
628              
629             Input fields on the right, labelled "where/and", specify some criteria
630             for the SQL "C<WHERE>" clause.
631              
632             Needless to say, this is quite a powerful operation which if misused
633             could easily corrupt your data.
634              
635             =head2 Delete
636              
637             Like updates, delete forms can be either single-record or
638             multiple-records.
639              
640              
641             =head2 Insert
642              
643             The insert form is very much like the single-record update form, except
644             that there are no "current values"
645              
646             =head2 Clone
647              
648             The clone form is like an insert form, but pre-filled with the data to clone,
649             except the primary key which is always empty.
650              
651              
652              
653             =head1 ARCHITECTURE
654              
655             [to be developed]
656              
657              
658             =head2 Classes
659              
660             Modules are organized in a classical Model-View-Controller structure.
661              
662             =head2 Inheritance and customization
663              
664             All classes can be subclassed, and the application will automatically
665             discover and load appropriate modules on demand.
666             Presentation templates can also be overridden in sub-applications.
667              
668             =head2 DataModel
669              
670             This application requires a L<DBIx::DataModel::Schema> subclass
671             for every datasource. If none is supplied, a subclass will be
672             generated and loaded on the fly; but this incurs an additional
673             startup cost, and does not exploit all possibilities of
674             L<DBIx::DataModel>; so apart from short demos and experiments,
675             it is better to statically generate a schema and store it in a
676             file.
677              
678             An initial schema class can be built, either from a L<DBI> database
679             handle, or from an existing L<DBIx::Class> schema; see
680             L<DBIx::DataModel::Schema::Generator>.
681              
682              
683             =head1 ATTRIBUTES
684              
685             =head2 config
686              
687             A datatree of information, whose structure should comply with
688             L<App::AutoCRUD::ConfigDomain>.
689              
690             =head2 name
691              
692             The application name (displayed in most pages).
693             This attribute defaults to the value of the C<app/name> entry in config.
694              
695             =head2 datasources
696              
697             A hashref of the datasources served by this application.
698             Hash keys are unique identifiers for the datasources (these names will also
699             be used to generate URIs); hash values are instances of the
700             L<App::AutoCRUD::DataSource> class.
701              
702             =head2 dir
703              
704             The root directory where some application components could be
705             placed (like for example some presentation templates).
706              
707             This attribute defaults to the value of the C<dir> entry in config,
708             or, if absent, to the current directory.
709              
710             This directory is associated with the application I<instance>.
711             When components are not found in this directory, they are searched
712             in the directories associated with the application I<classes>
713             (see the C<share_path> attribute below).
714              
715              
716             =head2 share_paths
717              
718             An arrayref to a list of directories corresponding
719             to the hierarchy of application classes. These directories are searched
720             as second resort, when components are not found in the application instance
721             directory.
722              
723              
724             =head2 readonly
725              
726             A boolean to restrict actions available to only read from the database.
727             The value of readonly boolean is set in YAML configuration file.
728              
729              
730             =head1 METHODS
731              
732             =head2 new
733              
734             my $crud_app = App::AutoCRUD->new(%options);
735              
736             Creates a new instance of the application.
737             All attributes described above may be supplied as
738             C<%options>.
739              
740             =head2 datasource
741              
742             my $datasource = $app->datasource($name);
743              
744             Returnes the the datasource registered under the given name.
745              
746              
747             =head2 call
748              
749             This method implements request dispatch, as required by
750             the L<Plack> middleware.
751              
752              
753             =head2 config
754              
755             my $data = $app->config(@path);
756              
757             Walks through the configuration tree, following node names
758             as specified in C<@path>, and returns whatever is found at
759             the end of this path ( either a subtree, or scalar data, or
760             C<undef> if the path leads to nothing ).
761              
762              
763             =head2 try_load_class
764              
765             my $class = $self->try_load_class($name, $namespace);
766              
767             Invokes L<Plack::Util/load_class>; returns the loaded class in case
768             of success, or C<undef> in case of failure.
769              
770              
771             =head2 find_class
772              
773             my $class = $app->find_class($subclass_name);
774              
775             Tries to find the given C<$subclass_name> within the namespaces
776             of the application classes.
777              
778             =head2 is_class_loaded
779              
780             Checks if the given class is already loaded in memory or not.
781              
782              
783             =head1 CAVEATS
784              
785             In the current implementation, the slash charater (C<'/'>) is interpreted
786             as a separator for primary keys over multiple columns. This means that
787             an embedded slash in a column name or in the value of a primary key
788             could yield unexpected results. This is definitely something to be
789             improved in a future versions, but at the moment I still don't know how
790             it will be solved.
791              
792              
793              
794             =head1 ACKNOWLEDGEMENTS
795              
796             Some design aspects were borrowed from
797              
798             =over
799              
800             =item L<Catalyst>
801              
802             =item L<Catalyst::Helper::View::TTSite>
803              
804             =back
805              
806              
807              
808             =head1 AUTHOR
809              
810             Laurent Dami, C<< <dami at cpan.org> >>
811              
812              
813             =head1 SUPPORT
814              
815             You can find documentation for this module with the perldoc command.
816              
817             perldoc App::AutoCRUD
818              
819              
820             You can also look for information at:
821              
822             =over 4
823              
824             =item * github's request tracker (report bugs here)
825              
826             L<https://github.com/damil/App-AutoCRUD/issues>
827              
828             =item * MetaCPAN
829              
830             L<https://metacpan.org/pod/App::AutoCRUD>
831              
832             =back
833              
834              
835             The source code is at
836             L<https://github.com/damil/App-AutoCRUD>.
837              
838              
839             =head1 SEE ALSO
840              
841             L<Catalyst::Plugin::AutoCRUD>,
842             L<WebAPI::DBIC>,
843             L<Plack>,
844             L<http://www.codeplex.com/ChinookDatabase>.
845              
846              
847             =head1 TODO
848              
849             - column properties
850             - noinsert, noupdate, nosearch, etc.
851              
852             - edit: select or autocompleter for foreign keys
853              
854             - internationalisation
855             -
856              
857             - View:
858             - default view should be defined in config
859             - overridable content-type & headers
860              
861             - search form, show associations => link to join search
862              
863             - list foreign keys even if not in DBIDM schema
864              
865             - change log
866              
867             - quoting problem (FromQuery: "J&B")
868              
869             - readonly fields: tabindex -1 (can be done by CSS?)
870             in fact, current values should NOT be input fields, but plain SPANs
871              
872             - NULL in updates
873             - Update form, focus problem (focus in field should deactivate TreeNav)
874             - add insert link in table descr
875              
876             - deal with Favicon.ico
877              
878             - declare in http://www.sqlite.org/cvstrac/wiki?p=ManagementTools
879              
880             - multicolumns : if there is an association over a multicolumns key,
881             it is not displayed as a hyperlink in /list. To do so, we would need
882             to add a line in the display, corresponding to the multicolumn.
883              
884             =head1 LICENSE AND COPYRIGHT
885              
886             Copyright 2014-2021 Laurent Dami.
887              
888             This program is free software; you can redistribute it and/or modify it
889             under the terms of the the Artistic License (2.0). You may obtain a
890             copy of the full license at:
891              
892             L<http://www.perlfoundation.org/artistic_license_2_0>
893              
894              
895             =cut
896              
897