From: Damyan Ivanov Date: Wed, 9 Feb 2022 06:31:09 +0000 (+0000) Subject: first take at a Dancer2 app, some functions work X-Git-Url: https://git.ktnx.net/?a=commitdiff_plain;h=b28adff9a0ae8c3359e2bcf315479b46ad90306b;p=lsl.git first take at a Dancer2 app, some functions work --- diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..87eb03b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM perl:latest +WORKDIR /srv/App::REST::LazyShoppingList +COPY . . +RUN cpanm --installdeps --notest --with-feature=accelerate . +EXPOSE 4000 +CMD plackup -p 4000 bin/app.psgi diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..ac12130 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,25 @@ +MANIFEST +MANIFEST.SKIP +.dancer +config.yml +Makefile.PL +cpanfile +environments/production.yml +environments/development.yml +public/favicon.ico +public/dispatch.fcgi +public/404.html +public/500.html +public/dispatch.cgi +t/001_base.t +t/002_index_route.t +lib/App/REST/LazyShoppingList.pm +views/index.tt +bin/app.psgi +public/javascripts/jquery.js +public/images/perldancer-bg.jpg +public/images/perldancer.jpg +public/css/error.css +public/css/style.css +views/layouts/main.tt +Dockerfile diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP new file mode 100644 index 0000000..c6f32b2 --- /dev/null +++ b/MANIFEST.SKIP @@ -0,0 +1,17 @@ +^\.git\/ +maint +^tags$ +.last_cover_stats +Makefile$ +^blib +^pm_to_blib +^.*.bak +^.*.old +^t.*sessions +^cover_db +^.*\.log +^.*\.swp$ +MYMETA.* +^.gitignore +^.svn\/ +^App::REST::LazyShoppingList- diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..db39efd --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,26 @@ +use strict; +use warnings; +use ExtUtils::MakeMaker; + +# Normalize version strings like 6.30_02 to 6.3002, +# so that we can do numerical comparisons on it. +my $eumm_version = $ExtUtils::MakeMaker::VERSION; +$eumm_version =~ s/_//; + +WriteMakefile( + NAME => 'App::REST::LazyShoppingList', + AUTHOR => q{YOUR NAME }, + VERSION_FROM => 'lib/App/REST/LazyShoppingList.pm lib/App/REST/LazyShoppingList.pm', + ABSTRACT => 'YOUR APPLICATION ABSTRACT', + ($eumm_version >= 6.3001 + ? ('LICENSE'=> 'perl') + : ()), + PL_FILES => {}, + PREREQ_PM => { + 'Test::More' => 0, + 'YAML' => 0, + 'Dancer2' => 0.301004, + }, + dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, + clean => { FILES => 'App-REST-LazyShoppingList-*' }, +); diff --git a/bin/app.psgi b/bin/app.psgi new file mode 100755 index 0000000..820846c --- /dev/null +++ b/bin/app.psgi @@ -0,0 +1,44 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use FindBin; +use lib "$FindBin::Bin/../lib"; + + +=begin comment +# use this block if you don't need middleware, and only have a single target Dancer app to run here +use App::LazyShoppingList::API; + +App::LazyShoppingList::API->to_app; + +=end comment + +=begin comment +# use this block if you want to include middleware such as Plack::Middleware::Deflater + +use App::LazyShoppingList::API; +use Plack::Builder; + +builder { + enable 'Deflater'; + App::LazyShoppingList::API->to_app; +} + +=end comment + +=cut + +# use this block if you want to mount several applications on different path + +use App::LazyShoppingList::API::v1; +#use App::LazyShoppingList::WEB; + +use Plack::Builder; + +builder { + mount '/api/v1' => App::LazyShoppingList::API::v1->to_app; + # mount '/' => App::REST::LazyShoppingList::WEB->to_app; +} + + diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..3eedfb6 --- /dev/null +++ b/config.yml @@ -0,0 +1,56 @@ +# === Basic configuration === + +# Your application's name +appname: "App::REST::LazyShoppingList" + +# The default layout to use for your application (located in +# views/layouts/main.tt) +layout: "main" + +# === Engines === +# +# NOTE: All the engine configurations need to be under a single "engines:" +# key. If you uncomment engine configurations below, make sure to delete +# all "engines:" lines except the first. Otherwise, only the last +# "engines:" block will take effect. + +# template engine +# simple: default and very basic template engine +# template_toolkit: TT + +template: "simple" + +# template: "template_toolkit" +# engines: +# template: +# template_toolkit: +# # Note: start_tag and end_tag are regexes +# start_tag: '<%' +# end_tag: '%>' + +# session engine +# +# Simple: in-memory session store - Dancer2::Session::Simple +# YAML: session stored in YAML files - Dancer2::Session::YAML +# +# Check out metacpan for other session storage options: +# https://metacpan.org/search?q=Dancer2%3A%3ASession&search_type=modules +# +# Default value for 'cookie_name' is 'dancer.session'. If you run multiple +# Dancer apps on the same host then you will need to make sure 'cookie_name' +# is different for each app. +# +#engines: +# session: +# Simple: +# cookie_name: testapp.session +# +#engines: +# session: +# YAML: +# cookie_name: eshop.session +# is_secure: 1 +# is_http_only: 1 + +db: + dsn: "dbi:Pg:dbname=lsl" diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..7c2192b --- /dev/null +++ b/cpanfile @@ -0,0 +1,34 @@ +requires "Dancer2" => "0.301004"; + +recommends "YAML" => "0"; +recommends "URL::Encode::XS" => "0"; +recommends "CGI::Deurl::XS" => "0"; +recommends "CBOR::XS" => "0"; +recommends "YAML::XS" => "0"; +recommends "Class::XSAccessor" => "0"; +recommends "Crypt::URandom" => "0"; +recommends "HTTP::XSCookies" => "0"; +recommends "HTTP::XSHeaders" => "0"; +recommends "Math::Random::ISAAC::XS" => "0"; +recommends "MooX::TypeTiny" => "0"; +recommends "Type::Tiny::XS" => "0"; + +feature 'accelerate', 'Accelerate Dancer2 app performance with XS modules' => sub { + requires "URL::Encode::XS" => "0"; + requires "CGI::Deurl::XS" => "0"; + requires "YAML::XS" => "0"; + requires "Class::XSAccessor" => "0"; + requires "Cpanel::JSON::XS" => "0"; + requires "Crypt::URandom" => "0"; + requires "HTTP::XSCookies" => "0"; + requires "HTTP::XSHeaders" => "0"; + requires "Math::Random::ISAAC::XS" => "0"; + requires "MooX::TypeTiny" => "0"; + requires "Type::Tiny::XS" => "0"; +}; + +on "test" => sub { + requires "Test::More" => "0"; + requires "HTTP::Request::Common" => "0"; +}; + diff --git a/environments/development.yml b/environments/development.yml new file mode 100644 index 0000000..bf826db --- /dev/null +++ b/environments/development.yml @@ -0,0 +1,20 @@ +# configuration file for development environment + +# the logger engine to use +# console: log messages to STDOUT (your console where you started the +# application server) +# file: log message to a file in log/ +logger: "console" + +# the log level for this environment +# core is the lowest, it shows Dancer2's core log messages as well as yours +# (debug, info, warning and error) +log: "core" + +# should Dancer2 show a stacktrace when an 5xx error is caught? +# if set to yes, public/500.html will be ignored and either +# views/500.tt, 'error_template' template, or a default error template will be used. +show_errors: 1 + +# print the banner +startup_info: 1 diff --git a/environments/production.yml b/environments/production.yml new file mode 100644 index 0000000..d86c30c --- /dev/null +++ b/environments/production.yml @@ -0,0 +1,13 @@ +# configuration file for production environment + +# only log warning and error messsages +log: "warning" + +# log message to a file in logs/ +logger: "file" + +# hide errors +show_errors: 0 + +# disable server tokens in production environments +no_server_tokens: 1 diff --git a/lib/App/LazyShoppingList/API.pm b/lib/App/LazyShoppingList/API.pm new file mode 100644 index 0000000..c828974 --- /dev/null +++ b/lib/App/LazyShoppingList/API.pm @@ -0,0 +1,74 @@ +use v5.26; +use warnings; +use utf8; + +package App::LazyShoppingList::API; +use Dancer2; +use Exporter qw(import); + +our $VERSION = '0.1'; + +our @EXPORT = qw( &get_database &get_lists_version &increment_lists_version + &invalid_input ); + +use Dancer2::Plugin::DBIC; + +use experimental 'signatures'; + +my %fixed_db_settings = ( + RaiseError => 1, + AutoCommit => 1, + PrintError => 0, +); +#my @on_db_connect_do = ( +# "SET NAMES 'utf8'", +#); +# +#hook database_connected => sub { +# my $dbh = shift; +# +# $dbh->do($_) for @on_db_connect_do; +#}; + +sub get_database { + $ENV{DBIC_TRACE}=1; + my %cfg = %{ config->{db} }; + DBICx::Sugar::config( { default => \%cfg } ); + $cfg{schema_class} = 'App::LazyShoppingList::Schema'; + while(my($k,$v) = each %fixed_db_settings ) { + $cfg{options}{$k} = $v; + } + my $dbh = DBICx::Sugar::schema(); + + warn to_json(DBICx::Sugar::get_config()); + + $dbh->connect; + + return $dbh; +} + +sub get_lists_version($dbh) { + my $g = $dbh->resultset('Globals')->single; + return $g->lists_version; +} + +sub increment_lists_version($dbh) { + $dbh->storage->dbh_do( + sub { + my ( $storage, $dbh ) = @_; + $dbh->do( + 'UPDATE globals SET lists_version = lists_version + 1'); + } + ); + + my $g = $dbh->resultset('Globals')->single; + return $g->lists_version; +} + +sub invalid_input($details = undef) { + status 400; + return "Invalid input" . ( defined($details) ? " ($details)" : '' ); +} + + +1; diff --git a/lib/App/LazyShoppingList/API/v1.pm b/lib/App/LazyShoppingList/API/v1.pm new file mode 100644 index 0000000..6e1cd31 --- /dev/null +++ b/lib/App/LazyShoppingList/API/v1.pm @@ -0,0 +1,231 @@ +use v5.26; +use warnings; +use utf8; + +package App::LazyShoppingList::API::v1; +use App::LazyShoppingList::API; +use Dancer2; + +our $VERSION = '0.1'; + +use JSON(); + +use experimental 'signatures'; + +set charset => 'UTF-8'; +set serializer => 'JSON'; +set content_type => 'application/json'; + + +# get the URI for the list of shopping lists +get '/' => sub { + return { lists => uri_for('/list') }; +}; + +# get the list of shopping lists +get '/list' => sub { + my $dbh = get_database; + $dbh->txn_begin; + + my %r = ( lists_version => get_lists_version($dbh), lists => [] ); + for my $list ( + $dbh->resultset('ShoppingList') + ->search( undef, + { order_by => { -asc => 'name' } } )->all + ) + { + push @{ $r{lists} }, uri_for( "/list/" . $list->id ); + } + + $dbh->txn_commit; + return \%r; +}; + +# create shopping list +post '/list' => sub { + my $req = decode_json(request_data); + + my $name = $req->{name}; + unless ($name) { + status 400; + content_type 'text/plain'; + return "Missing list name"; + } + + my $dbh = get_database; + $dbh->txn_begin; + my $list = $dbh->resultset('ShoppingList')->create({ + name => $name}); + $list->discard_changes; + + my $lists_ver = increment_lists_version($dbh); + + $dbh->txn_commit; + + return { + uri => uri_for( '/list/' . $list->id ), + version => $list->version, + lists_version => $lists_ver + }; +}; + +# get shopping list items +get '/list/:list_id' => sub { + my $list_id = route_parameters->get('list_id'); + length($list_id) and $list_id =~ /^\d{1,18}$/ or return invalid_input; + + my $dbh = get_database; + + warn $dbh; + my %r = ( items => [] ); + $dbh->txn_begin; + + $r{lists_version} = get_lists_version($dbh); + + my $list = $dbh->resultset('ShoppingList')->find($list_id); + + unless ($list) { + $dbh->txn_commit; + status 404; + content_type 'text/plain'; + return "No list with that ID found"; + } + + $r{version} = $list->version; + + my @items = $dbh->resultset('ShoppingListItem') + ->search( undef, { order_by => { -asc => 'id' } } )->all; + for my $item (@items) { + push @{ $r{items} }, + { uri => + uri_for( sprintf( "/list/%s/%s", $list->id, $item->id ) ), + description => $item->description, + done => JSON->boolean( $item->done ), + version => $item->version, + }; + } + + $dbh->txn_commit; + + return \%r; +}; + +# create shopping list item +post '/list/:id' => sub { + my $list_id = route_parameters->get('id'); + length($list_id) and $list_id =~ /^\d{1,18}$/ + or return invalid_input('bad list ID'); + + my $req = decode_json(request_data); + + my $descr = $req->{description}; + my $done = JSON->boolean( $req->{done} // 0 ); + + my $dbh = get_database; + $dbh->txn_begin; + + my %r = ( + lists_version => get_lists_version($dbh), + ); + + my $list = $dbh->resultset('ShoppingList')->find($list_id); + + unless ($list) { + $dbh->txn_commit; + status 404; + content_type 'text/plain'; + return "No such list"; + } + + my $item = $dbh->resultset('ShoppingListItem')->create( + { shopping_list => $list->id, + description => $descr, + done => $done, + } + ); + $item->discard_changes; + + $list->update( { version => $list->version + 1 } ); + + $r{uri} = uri_for( sprintf( "list/%s/%s", $list->id, $item->id ) ); + $r{version} = $item->version; + $r{list_version} = $list->version; + + $dbh->txn_commit; + + return \%r; +}; + +# modify shopping list item +put '/list/:list_id/:item_id' => sub { + my $list_id = route_parameters->get('list_id'); + length($list_id) and $list_id =~ /^\d{1,18}$/ + or return invalid_input('bad list ID'); + + my $item_id = route_parameters->get('item_id'); + length($item_id) and $item_id =~ /^\d{1,18}$/ + or return invalid_input('bad item ID'); + + my $req = decode_json(request_data); + + my $descr = $req->{description}; + my $done = JSON->boolean( $req->{done} // 0 ); + my $version = $req->{version}; + + length($version) and $version =~ /^\d{1,18}$/ + or return invalid_input('bad version'); + + my $dbh = get_database; + $dbh->txn_begin; + + my %r = ( + lists_version => get_lists_version($dbh), + ); + + my $list = $dbh->resultset('ShoppingList')->find($list_id); + unless ($list) { + $dbh->txn_commit; + status 404; + content_type 'text/plain'; + return "No such list"; + } + + my $item = $dbh->resultset('ShoppingListItem')->find({shopping_list => $list->id, id => $item_id}); + unless ($item) { + $dbh->txn_commit; + status 404; + content_type 'text/plain'; + return "No such item"; + } + + # in case no real changes are needed will return the current state + if ( defined($descr) and $descr ne ( $item->description // '' ) + or defined($done) and $done != $item->done ) + { + unless ($version == $item->version) { + $dbh->txn_commit; + status 409; + content_type 'text/plain'; + return + sprintf( 'Outdated version (current is %d)', $item->version ); + } + + $item->update({ + defined($descr) ? (description => $descr) : (), + defined($done) ? (done => $done) : (), + version => $version + 1, + }); + + $list->update( { version => $list->version + 1 } ); + } + + $r{uri} = uri_for( sprintf( "list/%s/%s", $list->id, $item->id ) ); + $r{version} = $item->version; + $r{list_version} = $list->version; + + $dbh->txn_commit; + + return \%r; +}; + +true; diff --git a/lib/App/LazyShoppingList/Schema.pm b/lib/App/LazyShoppingList/Schema.pm new file mode 100644 index 0000000..2062174 --- /dev/null +++ b/lib/App/LazyShoppingList/Schema.pm @@ -0,0 +1,11 @@ +use v5.28; +use warnings; +use utf8; + +package App::LazyShoppingList::Schema; + +use base qw(DBIx::Class::Schema); + +__PACKAGE__->load_namespaces; + +1; diff --git a/lib/App/LazyShoppingList/Schema/Result/Globals.pm b/lib/App/LazyShoppingList/Schema/Result/Globals.pm new file mode 100644 index 0000000..1ec31ed --- /dev/null +++ b/lib/App/LazyShoppingList/Schema/Result/Globals.pm @@ -0,0 +1,17 @@ +use v5.28; +use warnings; +use utf8; + +package App::LazyShoppingList::Schema::Result::Globals; + +use parent qw( DBIx::Class::Core ); + +__PACKAGE__->table('globals'); + +__PACKAGE__->add_columns( + lists_version => { data_type => 'integer' }, + db_revision => { data_type => 'integer' }, +); + + +1; diff --git a/lib/App/LazyShoppingList/Schema/Result/ShoppingList.pm b/lib/App/LazyShoppingList/Schema/Result/ShoppingList.pm new file mode 100644 index 0000000..bc8784e --- /dev/null +++ b/lib/App/LazyShoppingList/Schema/Result/ShoppingList.pm @@ -0,0 +1,25 @@ +use v5.28; +use warnings; +use utf8; + +package App::LazyShoppingList::Schema::Result::ShoppingList; + +use parent qw( DBIx::Class::Core ); + +__PACKAGE__->table('shopping_lists'); + +__PACKAGE__->add_columns( + id => { data_type => 'integer', + is_auto_increment => 1, + }, + name => { data_type => 'text' }, + version => { data_type => 'integer' }, +); + +__PACKAGE__->set_primary_key('id'); + +__PACKAGE__->has_many( + items => 'App::LazyShoppingList::Schema::Result::ShoppingListItem' => + 'shopping_list' ); + +1; diff --git a/lib/App/LazyShoppingList/Schema/Result/ShoppingListItem.pm b/lib/App/LazyShoppingList/Schema/Result/ShoppingListItem.pm new file mode 100644 index 0000000..95b45a8 --- /dev/null +++ b/lib/App/LazyShoppingList/Schema/Result/ShoppingListItem.pm @@ -0,0 +1,36 @@ +use v5.28; +use warnings; +use utf8; + +package App::LazyShoppingList::Schema::Result::ShoppingListItem; + +use parent qw( DBIx::Class::Core ); + +__PACKAGE__->table('shopping_list_items'); + +__PACKAGE__->add_columns( + id => { + data_type => 'integer', + is_auto_increment => 1, + }, + shopping_list => { + data_type => 'integer', + }, + description => { + data_type => 'text', + }, + done => { + data_type => 'boolean', + }, + version => { + data_type => 'integer', + }, +); + +__PACKAGE__->set_primary_key('id'); + +__PACKAGE__->belongs_to( shopping_list => + 'App::LazyShoppingList::Schema::Result::ShoppingList' => + 'shopping_list' ); + +1; diff --git a/lib/App/LazyShoppingList/Web.pm b/lib/App/LazyShoppingList/Web.pm new file mode 100644 index 0000000..b2ecbf1 --- /dev/null +++ b/lib/App/LazyShoppingList/Web.pm @@ -0,0 +1,21 @@ +use v5.26; +use warnings; +use utf8; + +package App::LazyShoppingList::Web; +use Dancer2; + +our $VERSION = '0.1'; + +use JSON(); + +use experimental 'signatures'; + +set charset => 'UTF-8'; + +# main web-application +get '/' => sub { + template 'index' => { 'title' => 'App::REST::LazyShoppingList' }; +}; + +true; diff --git a/lib/Dancer2/Plugin/DBIC.pm b/lib/Dancer2/Plugin/DBIC.pm new file mode 100644 index 0000000..d063fc3 --- /dev/null +++ b/lib/Dancer2/Plugin/DBIC.pm @@ -0,0 +1,266 @@ +package Dancer2::Plugin::DBIC; + +our $VERSION = '0.0100'; # VERSION + +use strict; +use warnings; +use utf8; +use Dancer2::Plugin; +use DBICx::Sugar; + +sub _schema { + my ($dsl, $name, $cfg) = @_; + my $config; + # ugly switch needed to support plugin2 plugins which use this plugin + # whilst still working for plugin1 + if ( $dsl->app->can('with_plugin') ) { + $config = $dsl->config; + } + else { + $config = plugin_setting; + } + DBICx::Sugar::config( $config ); + return DBICx::Sugar::schema($name, $cfg); +} + +sub _rset { + my ($dsl, $rset_name) = @_; + return schema($dsl)->resultset($rset_name); +} + +register schema => \&_schema; +register resultset => \&_rset; +register rset => \&_rset; +register_plugin; + +# ABSTRACT: DBIx::Class interface for Dancer2 applications + + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Dancer2::Plugin::DBIC - DBIx::Class interface for Dancer2 applications + +=head1 VERSION + +version 0.0100 + +=head1 SYNOPSIS + + use Dancer2; + use Dancer2::Plugin::DBIC; + + get '/users/:user_id' => sub { + my $user = schema('default')->resultset('User')->find(param 'user_id'); + + # If you are accessing the 'default' schema, then all the following + # are equivalent to the above: + $user = schema->resultset('User')->find(param 'user_id'); + $user = resultset('User')->find(param 'user_id'); + $user = rset('User')->find(param 'user_id'); + + template user_profile => { + user => $user + }; + }; + + dance; + +=head1 DESCRIPTION + +This plugin makes it very easy to create L applications that interface +with databases. +It automatically exports the keyword C which returns a +L object. +It also exports the keywords C and C. +You just need to configure your database connection information. +For performance, schema objects are cached in memory +and are lazy loaded the first time they are accessed. + +This plugin is a thin wrapper around L. + +=head1 CONFIGURATION + +Configuration can be done in your L config file. +This is a minimal example. It defines one database named C: + + plugins: + DBIC: + default: + dsn: dbi:SQLite:dbname=some.db + +In this example, there are 2 databases configured named C and C: + + plugins: + DBIC: + default: + dsn: dbi:SQLite:dbname=some.db + schema_class: MyApp::Schema + foo: + dsn: dbi:mysql:foo + schema_class: Foo::Schema + user: bob + password: secret + options: + RaiseError: 1 + PrintError: 1 + +Each database configured must at least have a dsn option. +The dsn option should be the L driver connection string. +All other options are optional. + +If you only have one schema configured, or one of them is named +C, you can call C without an argument to get the only +or C schema, respectively. + +If a schema_class option is not provided, then L +will be used to dynamically load the schema by introspecting the database +corresponding to the dsn value. +Remember that you need L installed to take +advantage of that. + +The schema_class option, should be a proper Perl package name that +Dancer2::Plugin::DBIC will use as a L class. +Optionally, a database configuration may have user, password, and options +parameters as described in the documentation for C in L. + +You may also declare your connection information in the following format +(which may look more familiar to DBIC users): + + plugins: + DBIC: + default: + connect_info: + - dbi:mysql:foo + - bob + - secret + - + RaiseError: 1 + PrintError: 1 + +=head1 FUNCTIONS + +=head2 schema + + my $user = schema->resultset('User')->find('bob'); + +The C keyword returns a L object ready for you to +use. +If you have configured only one database, then you can simply call C +with no arguments. +If you have configured multiple databases, +you can still call C with no arguments if there is a database +named C in the configuration. +With no argument, the C schema is returned. +Otherwise, you B provide C with the name of the database: + + my $user = schema('foo')->resultset('User')->find('bob'); + +=head2 resultset + +This is a convenience method that will save you some typing. +Use this B when accessing the C schema. + + my $user = resultset('User')->find('bob'); + +is equivalent to: + + my $user = schema->resultset('User')->find('bob'); + +=head2 rset + + my $user = rset('User')->find('bob'); + +This is simply an alias for C. + +=head1 SCHEMA GENERATION + +There are two approaches for generating schema classes. +You may generate your own L classes and set +the corresponding C setting in your configuration as shown above. +This is the recommended approach for performance and stability. + +It is also possible to have schema classes dynamically generated +if you omit the C configuration setting. +This requires you to have L installed. +The C naming scheme will be used for naming the auto generated classes. +See L for more information about +naming. + +For generating your own schema classes, +you can use the L command line tool provided by +L to help you. +For example, if your app were named Foo, then you could run the following +from the root of your project directory: + + dbicdump -o dump_directory=./lib Foo::Schema dbi:SQLite:/path/to/foo.db + +For that example, your C setting would be C. + +=head1 SEE ALSO + +=over 4 + +=item * + +L + +=back + +=head1 CONTRIBUTORS + +=over 4 + +=item * + +Alexis Sukrieh + +=item * + +Dagfinn Ilmari Mannsåker > + +=item * + +David Precious + +=item * + +ennio > + +=item * + +Fabrice Gabolde > + +=item * + +Franck Cuny + +=item * + +Steven Humphrey > + +=item * + +Yanick Champoux > + +=back + +=head1 AUTHOR + +Naveed Massjouni + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2013 by Naveed Massjouni. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +=cut diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..c2f4032 --- /dev/null +++ b/public/404.html @@ -0,0 +1,18 @@ + + + + + + Error 404 + + + +

Error 404

+
+

Page Not Found

Sorry, this is the void.

+
+ + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..d84ebbc --- /dev/null +++ b/public/500.html @@ -0,0 +1,18 @@ + + + + + + Error 500 + + + +

Error 500

+
+

Internal Server Error

Wooops, something went wrong

+
+ + + diff --git a/public/css/error.css b/public/css/error.css new file mode 100644 index 0000000..a39e596 --- /dev/null +++ b/public/css/error.css @@ -0,0 +1,86 @@ +body { + font-family: Lucida,sans-serif; +} + +h1 { + color: #AA0000; + border-bottom: 1px solid #444; +} + +h2 { color: #444; } + +pre { + font-family: "lucida console","monaco","andale mono","bitstream vera sans mono","consolas",monospace; + font-size: 12px; + border-left: 2px solid #777; + padding-left: 1em; +} + +footer { + font-size: 10px; +} + +span.key { + color: #449; + font-weight: bold; + width: 120px; + display: inline; +} + +span.value { + color: #494; +} + +/* these are for the message boxes */ + +pre.content { + background-color: #eee; + color: #000; + padding: 1em; + margin: 0; + border: 1px solid #aaa; + border-top: 0; + margin-bottom: 1em; + overflow-x: auto; +} + +div.title { + font-family: "lucida console","monaco","andale mono","bitstream vera sans mono","consolas",monospace; + font-size: 12px; + background-color: #aaa; + color: #444; + font-weight: bold; + padding: 3px; + padding-left: 10px; +} + +table.context { + border-spacing: 0; +} + +table.context th, table.context td { + padding: 0; +} + +table.context th { + color: #889; + font-weight: normal; + padding-right: 15px; + text-align: right; +} + +.errline { + color: red; +} + +pre.error { + background: #334; + color: #ccd; + padding: 1em; + border-top: 1px solid #000; + border-left: 1px solid #000; + border-right: 1px solid #eee; + border-bottom: 1px solid #eee; + overflow-x: auto; +} + diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..80f94eb --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,189 @@ + +body { +margin: 0; +margin-bottom: 25px; +padding: 0; +background-color: #ddd; +background-image: url("/images/perldancer-bg.jpg"); +background-repeat: no-repeat; +background-position: top left; + +font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana"; +font-size: 13px; +color: #333; +} + +h1 { +font-size: 28px; +color: #000; +} + +a {color: #03c} +a:hover { +background-color: #03c; +color: white; +text-decoration: none; +} + +#page { +background-color: #ddd; +width: 750px; +margin: auto; +margin-left: auto; +padding-left: 0px; +margin-right: auto; +} + +#content { +background-color: white; +border: 3px solid #aaa; +border-top: none; +padding: 25px; +width: 500px; +} + +#sidebar { +float: right; +width: 175px; +} + +#header, #about, #getting-started { +padding-left: 75px; +padding-right: 30px; +} + + +#header { +background-image: url("/images/perldancer.jpg"); +background-repeat: no-repeat; +background-position: top left; +height: 64px; +} +#header h1, #header h2 {margin: 0} +#header h2 { +color: #888; +font-weight: normal; +font-size: 16px; +} + +#about h3 { +margin: 0; +margin-bottom: 10px; +font-size: 14px; +} + +#about-content { +background-color: #ffd; +border: 1px solid #fc0; +margin-left: -11px; +} +#about-content table { +margin-top: 10px; +margin-bottom: 10px; +font-size: 11px; +border-collapse: collapse; +} +#about-content td { +padding: 10px; +padding-top: 3px; +padding-bottom: 3px; +} +#about-content td.name {color: #555} +#about-content td.value {color: #000} + +#about-content.failure { +background-color: #fcc; +border: 1px solid #f00; +} +#about-content.failure p { +margin: 0; +padding: 10px; +} + +#getting-started { +border-top: 1px solid #ccc; +margin-top: 25px; +padding-top: 15px; +} +#getting-started h1 { +margin: 0; +font-size: 20px; +} +#getting-started h2 { +margin: 0; +font-size: 14px; +font-weight: normal; +color: #333; +margin-bottom: 25px; +} +#getting-started ol { +margin-left: 0; +padding-left: 0; +} +#getting-started li { +font-size: 18px; +color: #888; +margin-bottom: 25px; +} +#getting-started li h2 { +margin: 0; +font-weight: normal; +font-size: 18px; +color: #333; +} +#getting-started li p { +color: #555; +font-size: 13px; +} + +#search { +margin: 0; +padding-top: 10px; +padding-bottom: 10px; +font-size: 11px; +} +#search input { +font-size: 11px; +margin: 2px; +} +#search-text {width: 170px} + +#sidebar ul { +margin-left: 0; +padding-left: 0; +} +#sidebar ul h3 { +margin-top: 25px; +font-size: 16px; +padding-bottom: 10px; +border-bottom: 1px solid #ccc; +} +#sidebar li { +list-style-type: none; +} +#sidebar ul.links li { +margin-bottom: 5px; +} + +h1, h2, h3, h4, h5 { +font-family: sans-serif; +margin: 1.2em 0 0.6em 0; +} + +p { +line-height: 1.5em; +margin: 1.6em 0; +} + +code, .filepath, .app-info { + font-family: 'Andale Mono', Monaco, 'Liberation Mono', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', monospace; +} + +#footer { +clear: both; +padding-top: 2em; +text-align: center; +padding-right: 160px; +font-family: sans-serif; +font-size: 10px; +} diff --git a/public/dispatch.cgi b/public/dispatch.cgi new file mode 100755 index 0000000..706ba0c --- /dev/null +++ b/public/dispatch.cgi @@ -0,0 +1,16 @@ +#!/usr/bin/env perl +BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';} +use Dancer2; +use FindBin '$RealBin'; +use Plack::Runner; + +# For some reason Apache SetEnv directives don't propagate +# correctly to the dispatchers, so forcing PSGI and env here +# is safer. +set apphandler => 'PSGI'; +set environment => 'production'; + +my $psgi = path($RealBin, '..', 'bin', 'app.psgi'); +die "Unable to read startup script: $psgi" unless -r $psgi; + +Plack::Runner->run($psgi); diff --git a/public/dispatch.fcgi b/public/dispatch.fcgi new file mode 100755 index 0000000..ad42deb --- /dev/null +++ b/public/dispatch.fcgi @@ -0,0 +1,18 @@ +#!/usr/bin/env perl +BEGIN { $ENV{DANCER_APPHANDLER} = 'PSGI';} +use Dancer2; +use FindBin '$RealBin'; +use Plack::Handler::FCGI; + +# For some reason Apache SetEnv directives don't propagate +# correctly to the dispatchers, so forcing PSGI and env here +# is safer. +set apphandler => 'PSGI'; +set environment => 'production'; + +my $psgi = path($RealBin, '..', 'bin', 'app.psgi'); +my $app = do($psgi); +die "Unable to read startup script: $@" if $@; +my $server = Plack::Handler::FCGI->new(nproc => 5, detach => 1); + +$server->run($app); diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..96c7465 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/perldancer-bg.jpg b/public/images/perldancer-bg.jpg new file mode 100644 index 0000000..6ee6b77 Binary files /dev/null and b/public/images/perldancer-bg.jpg differ diff --git a/public/images/perldancer.jpg b/public/images/perldancer.jpg new file mode 100644 index 0000000..3f9e718 Binary files /dev/null and b/public/images/perldancer.jpg differ diff --git a/public/javascripts/jquery.js b/public/javascripts/jquery.js new file mode 100644 index 0000000..8661e38 --- /dev/null +++ b/public/javascripts/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(T,e){"use strict";function g(e){return"function"==typeof e&&"number"!=typeof e.nodeType}function y(e){return null!=e&&e===e.window}var t=[],n=Object.getPrototypeOf,s=t.slice,m=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,r={},o=r.toString,v=r.hasOwnProperty,a=v.toString,l=a.call(Object),x={},C=T.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function h(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?r[o.call(e)]||"object":typeof e}var f="3.5.1",E=function(e,t){return new E.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=h(e);return!g(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0>10|55296,1023&e|56320))}function p(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}function r(){T()}var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,y,s,m,v,S="sizzle"+ +new Date,x=n.document,k=0,A=0,N=ue(),D=ue(),j=ue(),q=ue(),L=function(e,t){return e===t&&(l=!0),0},H={}.hasOwnProperty,t=[],O=t.pop,P=t.push,R=t.push,M=t.slice,I=function(e,t){for(var n=0,r=e.length;n+~]|"+F+")"+F+"*"),G=new RegExp(F+"|>"),Y=new RegExp(_),Q=new RegExp("^"+B+"$"),J={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+$),PSEUDO:new RegExp("^"+_),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+F+"*(even|odd|(([+-]|)(\\d*)n|)"+F+"*(?:([+-]|)"+F+"*(\\d+)|))"+F+"*\\)|)","i"),bool:new RegExp("^(?:"+W+")$","i"),needsContext:new RegExp("^"+F+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+F+"*((?:-\\d)?\\d*)"+F+"*\\)|)(?=[^-]|$)","i")},K=/HTML$/i,Z=/^(?:input|select|textarea|button)$/i,ee=/^h\d$/i,te=/^[^{]+\{\s*\[native \w/,ne=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,re=/[+~]/,ie=new RegExp("\\\\[\\da-fA-F]{1,6}"+F+"?|\\\\([^\\r\\n\\f])","g"),oe=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ae=ve(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{R.apply(t=M.call(x.childNodes),x.childNodes),t[x.childNodes.length].nodeType}catch(e){R={apply:t.length?function(e,t){P.apply(e,M.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c=e&&e.ownerDocument,f=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==f&&9!==f&&11!==f)return n;if(!r&&(T(e),e=e||C,E)){if(11!==f&&(s=ne.exec(t)))if(l=s[1]){if(9===f){if(!(o=e.getElementById(l)))return n;if(o.id===l)return n.push(o),n}else if(c&&(o=c.getElementById(l))&&v(e,o)&&o.id===l)return n.push(o),n}else{if(s[2])return R.apply(n,e.getElementsByTagName(t)),n;if((l=s[3])&&d.getElementsByClassName&&e.getElementsByClassName)return R.apply(n,e.getElementsByClassName(l)),n}if(d.qsa&&!q[t+" "]&&(!y||!y.test(t))&&(1!==f||"object"!==e.nodeName.toLowerCase())){if(l=t,c=e,1===f&&(G.test(t)||V.test(t))){(c=re.test(t)&&ge(e.parentNode)||e)===e&&d.scope||((a=e.getAttribute("id"))?a=a.replace(oe,p):e.setAttribute("id",a=S)),i=(u=h(t)).length;while(i--)u[i]=(a?"#"+a:":scope")+" "+me(u[i]);l=u.join(",")}try{return R.apply(n,c.querySelectorAll(l)),n}catch(e){q(t,!0)}finally{a===S&&e.removeAttribute("id")}}}return g(t.replace(U,"$1"),e,n,r)}function ue(){var n=[];function r(e,t){return n.push(e+" ")>b.cacheLength&&delete r[n.shift()],r[e+" "]=t}return r}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t)}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function he(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,e=(e.ownerDocument||e).documentElement;return!K.test(t||e&&e.nodeName||"HTML")},T=se.setDocument=function(e){var t,e=e?e.ownerDocument||e:x;return e!=C&&9===e.nodeType&&e.documentElement&&(a=(C=e).documentElement,E=!i(C),x!=C&&(t=C.defaultView)&&t.top!==t&&(t.addEventListener?t.addEventListener("unload",r,!1):t.attachEvent&&t.attachEvent("onunload",r)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=te.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(ie,c);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){e=t.getElementById(e);return e?[e]:[]}}):(b.filter.ID=function(e){var t=e.replace(ie,c);return function(e){e="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return e&&e.value===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"!==e)return o;while(n=o[i++])1===n.nodeType&&r.push(n);return r},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=te.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+F+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+F+"*(?:value|"+W+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+F+"*name"+F+"*="+F+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+F+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=te.test(m=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),s.push("!=",_)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=te.test(a.compareDocumentPosition),v=t||te.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,t=t&&t.parentNode;return e===t||!(!t||1!==t.nodeType||!(n.contains?n.contains(t):e.compareDocumentPosition&&16&e.compareDocumentPosition(t)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},L=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==x&&v(x,e)?-1:t==C||t.ownerDocument==x&&v(x,t)?1:u?I(u,e)-I(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?I(u,e)-I(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==x?-1:s[r]==x?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!q[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=m.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){q(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(ie,c),e[3]=(e[3]||e[4]||e[5]||"").replace(ie,c),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return J.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&Y.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(ie,c).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=new RegExp("(^|"+F+")"+e+"("+F+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(t,n,r){return function(e){e=se.attr(e,t);return null==e?"!="===n:!n||(e+="","="===n?e===r:"!="===n?e!==r:"^="===n?r&&0===e.indexOf(r):"*="===n?r&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return g(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){if(!e)return this;if(n=n||q,"string"!=typeof e)return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this);if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:j.exec(e))||!r[1]&&t)return(!t||t.jquery?t||n:this.constructor(t)).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),N.test(r[1])&&E.isPlainObject(t))for(var r in t)g(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(e=C.getElementById(r[2]))&&(this[0]=e,this.length=1),this}).prototype=E.fn;var q=E(C),L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;f=C.createDocumentFragment().appendChild(C.createElement("div")),(d=C.createElement("input")).setAttribute("type","radio"),d.setAttribute("checked","checked"),d.setAttribute("name","t"),f.appendChild(d),x.checkClone=f.cloneNode(!0).cloneNode(!0).lastChild.checked,f.innerHTML="",x.noCloneChecked=!!f.cloneNode(!0).lastChild.defaultValue,f.innerHTML="",x.option=!!f.lastChild;var de={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function he(e,t){var n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&A(e,t)?E.merge([e],n):n}function ge(e,t){for(var n=0,r=e.length;n",""]);var ye=/<|&#?\w+;/;function me(e,t,n,r,i){for(var o,a,s,u,l,c=t.createDocumentFragment(),f=[],p=0,d=e.length;p\s*$/g;function De(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function je(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o;if(1===t.nodeType){if(Y.hasData(e)&&(o=Y.get(e).events))for(i in Y.remove(t,"handle events"),o)for(n=0,r=o[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Qt=[],Jt=/(=)\?(?=&|$)|\?\?/;E.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Qt.pop()||E.expando+"_"+Dt.guid++;return this[e]=!0,e}}),E.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Jt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Jt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=g(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Jt,"$1"+r):!1!==e.jsonp&&(e.url+=(jt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||E.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=T[r],T[r]=function(){o=arguments},n.always(function(){void 0===i?E(T).removeProp(r):T[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Qt.push(r)),o&&g(i)&&i(o[0]),o=i=void 0}),"script"}),x.createHTMLDocument=((f=C.implementation.createHTMLDocument("").body).innerHTML="
",2===f.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(x.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),r=!n&&[],(n=N.exec(e))?[t.createElement(n[1])]:(n=me([e],t,r),r&&r.length&&E(r).remove(),E.merge([],n.childNodes)));var r},E.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(E.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},E.expr.pseudos.animated=function(t){return E.grep(E.timers,function(e){return t===e.elem}).length},E.offset={setOffset:function(e,t,n){var r,i,o,a,s=E.css(e,"position"),u=E(e),l={};"static"===s&&(e.style.position="relative"),o=u.offset(),r=E.css(e,"top"),a=E.css(e,"left"),a=("absolute"===s||"fixed"===s)&&-1<(r+a).indexOf("auto")?(i=(s=u.position()).top,s.left):(i=parseFloat(r)||0,parseFloat(a)||0),null!=(t=g(t)?t.call(e,n,E.extend({},o)):t).top&&(l.top=t.top-o.top+i),null!=t.left&&(l.left=t.left-o.left+a),"using"in t?t.using.call(e,l):("number"==typeof l.top&&(l.top+="px"),"number"==typeof l.left&&(l.left+="px"),u.css(l))}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n=this[0];return n?n.getClientRects().length?(e=n.getBoundingClientRect(),n=n.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return $(this,function(e,t,n){var r;return y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n?r?r[i]:e[t]:void(r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n)},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=Qe(x.pixelPosition,function(e,t){if(t)return t=Ye(e,n),Ue.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){E.fn[t]=function(e){return this.on(t,e)}}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){E.fn[n]=function(e,t){return 0 1; +use_ok 'App::REST::LazyShoppingList'; diff --git a/t/002_index_route.t b/t/002_index_route.t new file mode 100644 index 0000000..8262d7f --- /dev/null +++ b/t/002_index_route.t @@ -0,0 +1,16 @@ +use strict; +use warnings; + +use App::REST::LazyShoppingList; +use Test::More tests => 2; +use Plack::Test; +use HTTP::Request::Common; +use Ref::Util qw; + +my $app = App::REST::LazyShoppingList->to_app; +ok( is_coderef($app), 'Got app' ); + +my $test = Plack::Test->create($app); +my $res = $test->request( GET '/' ); + +ok( $res->is_success, '[GET /] successful' ); diff --git a/views/index.tt b/views/index.tt new file mode 100644 index 0000000..7b8eb8e --- /dev/null +++ b/views/index.tt @@ -0,0 +1,151 @@ + + + +
+ + +
+ + +
+

Getting started

+

Here’s how to get dancing:

+ +

About your application's environment

+ + + + + + +
    +
  1. +

    Tune your application

    + +

    + Your application is configured via a global configuration file, + config.yml and an "environment" configuration file, + environments/development.yml. Edit those files if you + want to change the settings of your application. +

    +
  2. + +
  3. +

    Add your own routes

    + +

    + The default route that displays this page can be removed, + it's just here to help you get started. The template used to + generate this content is located in + views/index.tt. + You can add some routes to lib/App/REST/LazyShoppingList.pm lib/App/REST/LazyShoppingList.pm. +

    +
  4. + +
  5. +

    Enjoy web development again

    + +

    + Once you've made your changes, restart your standalone server + (bin/app.psgi) and you're ready + to test your web application. +

    +
  6. + +
+
+
+
diff --git a/views/layouts/main.tt b/views/layouts/main.tt new file mode 100644 index 0000000..0f3de6f --- /dev/null +++ b/views/layouts/main.tt @@ -0,0 +1,23 @@ + + + + + + <% title %> + + + + + + + + +<% content %> + + +