From b28adff9a0ae8c3359e2bcf315479b46ad90306b Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Wed, 9 Feb 2022 06:31:09 +0000 Subject: [PATCH] first take at a Dancer2 app, some functions work --- Dockerfile | 6 + MANIFEST | 25 ++ MANIFEST.SKIP | 17 ++ Makefile.PL | 26 ++ bin/app.psgi | 44 +++ config.yml | 56 ++++ cpanfile | 34 +++ environments/development.yml | 20 ++ environments/production.yml | 13 + lib/App/LazyShoppingList/API.pm | 74 +++++ lib/App/LazyShoppingList/API/v1.pm | 231 +++++++++++++++ lib/App/LazyShoppingList/Schema.pm | 11 + .../LazyShoppingList/Schema/Result/Globals.pm | 17 ++ .../Schema/Result/ShoppingList.pm | 25 ++ .../Schema/Result/ShoppingListItem.pm | 36 +++ lib/App/LazyShoppingList/Web.pm | 21 ++ lib/Dancer2/Plugin/DBIC.pm | 266 ++++++++++++++++++ public/404.html | 18 ++ public/500.html | 18 ++ public/css/error.css | 86 ++++++ public/css/style.css | 189 +++++++++++++ public/dispatch.cgi | 16 ++ public/dispatch.fcgi | 18 ++ public/favicon.ico | Bin 0 -> 1406 bytes public/images/perldancer-bg.jpg | Bin 0 -> 7125 bytes public/images/perldancer.jpg | Bin 0 -> 2240 bytes public/javascripts/jquery.js | 2 + sql/schema/init.sql | 17 ++ t/001_base.t | 5 + t/002_index_route.t | 16 ++ views/index.tt | 151 ++++++++++ views/layouts/main.tt | 23 ++ 32 files changed, 1481 insertions(+) create mode 100644 Dockerfile create mode 100644 MANIFEST create mode 100644 MANIFEST.SKIP create mode 100644 Makefile.PL create mode 100755 bin/app.psgi create mode 100644 config.yml create mode 100644 cpanfile create mode 100644 environments/development.yml create mode 100644 environments/production.yml create mode 100644 lib/App/LazyShoppingList/API.pm create mode 100644 lib/App/LazyShoppingList/API/v1.pm create mode 100644 lib/App/LazyShoppingList/Schema.pm create mode 100644 lib/App/LazyShoppingList/Schema/Result/Globals.pm create mode 100644 lib/App/LazyShoppingList/Schema/Result/ShoppingList.pm create mode 100644 lib/App/LazyShoppingList/Schema/Result/ShoppingListItem.pm create mode 100644 lib/App/LazyShoppingList/Web.pm create mode 100644 lib/Dancer2/Plugin/DBIC.pm create mode 100644 public/404.html create mode 100644 public/500.html create mode 100644 public/css/error.css create mode 100644 public/css/style.css create mode 100755 public/dispatch.cgi create mode 100755 public/dispatch.fcgi create mode 100644 public/favicon.ico create mode 100644 public/images/perldancer-bg.jpg create mode 100644 public/images/perldancer.jpg create mode 100644 public/javascripts/jquery.js create mode 100644 sql/schema/init.sql create mode 100644 t/001_base.t create mode 100644 t/002_index_route.t create mode 100644 views/index.tt create mode 100644 views/layouts/main.tt 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 0000000000000000000000000000000000000000..96c746534efbc258304d25171912ebf5493bc5ed GIT binary patch literal 1406 zcmdVZ_g9lw7zgmLZEevms-gmFt;np(3`4-k3QJtHT191V$`B-g2!af$0wP*y!9tNG z2!uU?U;?DPs310s$Sf(tf`S}RwbmrP`2+mPpZebOx!?DB?oZFT=K?Fpl9dJ7W#F?C zSPwvrAT4Yqr2V&jGBT?mw{9)uH^@OzVIwvx%45qW1t@Rd1{Fe8SxO0NDoRjSRfYzk zuBMEg8miFJ+5uf{P3TGK>>@NlBlLB(K%?ovh^7q#eO(wy8R){;P!Dty1MD#|BxwY? zsnl**(2ZebZVHA49oCj~*bvs1dtu8k$37cNIM`Vc3^>>`aL~aPhxgmT@qm;)jvqaQ zlgAw4>~sX4u1@fBJpnIwXZU)Yg13hYd_A4v-~ zNBAQm)E~^y07QlbfEgA7=9wUzWdXJ#LKKn{;*gRQkMxu{q+d=(W@-YFh%2cVaXmc=8EKc0otc8%tTYtlWTNOsCW`a2 zP*RYMQlhLd7v;sds4UJwRcRhrxAMU%%SY|)n`o#iLL;k~C_!^|3F>Pqz^*L?yRHmP zbroo>FGpKL1)3YG!D+4_D#2~3MhBY(F1s3?tt^7ntu^4bHG$V&hwi%#=;1Vi;xyqN zm;84$qmS2u{%$rNQg^`bX~p2ZHazI*0{=b-0}t8>E{1xEJ}!p)I!JaBT@dsW1HG6S zx{p!8?+`xj#h8GP*|CS1938^+*dvm|m>uuO?8GC?5mV#C5Kaj2WRj1+{@_D6IfD7A z0n7^rut3ZU2T2a%>7PTG5sqU{IDu!=LwGtpOj3ZwnPDu=N(u0sSekuIjN_W#eV<41I8=(2vC+PtC=QRJC|>vV4cccD5`)y0n>xv9cZk9zR`QwS z7pvk|XLj;9d5SfK#VyrQ&S|%^KWXDqo6>d79Nz!-vsEgVcHufy*3JeC>(!c{nx5FI z^N!1jCW zE%iS3jqfehh+spu%-YtC^(FRUH#Hj7zNUY8W2OAn^_3N0bWz(fl+;Z&UwBV0>GRst zdK9O8O-%I9uCd|$TiI7T0y3_+Z`pN6T1w~5 RaWvTN)J&1_sCT`<{{RlP*^2-G literal 0 HcmV?d00001 diff --git a/public/images/perldancer-bg.jpg b/public/images/perldancer-bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6ee6b77394657a70de11dbda4f0c8ceffb07bd92 GIT binary patch literal 7125 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 literal 0 HcmV?d00001 diff --git a/public/images/perldancer.jpg b/public/images/perldancer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f9e718c1cde297047ece800fdd8252f9a26e2c7 GIT binary patch literal 2240 zcmb7^XHe7W7RLV}2?=F^5Q;<+x^%o01tCCyg-|roq=YCUgt7?IF+eOxQ(3?emMTR< z1f;i6br%#|T+kp*nv|s%r7Ec8#(QV>e!kC{IWuQ|=RD86UtZ1_X9f_lwX(4SKp+rc z%`Jd)1RyQK$X*d-AKB=D2!C0;%{hC{1YiL`!4L=p4COW`6v_(|gu}QJ77*YQLNc>x>;03PsG6eje7486m zA^={Ps2m(AuWcso;1zmXOhQq|-0^{eZdi%M74LZNp(F_S@BTloKT^0r8u0^g1OEp= zJP1)at`%n-5aco*1Q-FB0`FrgCt1VC!%Dsew!=Tk{fqVlL-A6zK6n`0y1CZ80GVia zho|!&SBqSd2ocTDiG09#o!MizaP5*T@swrVqYk7yztipJm?Kx|H zmXWthKloGo+kDl8`I_Swdel_Tb>=t)o!JT~HtIFGW4gCDB<{TbYa8ewf9<`kacoQ0 zR`>NU(P`o7v<#t~i7;Qb-!+@;W~~b2Y*C$~!5v{GGe8;pIz*ZQ@U89az8#oqyC3vR zig<@vSp#j$>=%t#Of*(yYX+RTP!KmzRw_@*hrJMD@2Y;ByLMue5>t*0%vI@aKNQfC zdF{41Tm|jT#>;3j}OuGHU^Z69so)Js3pUBF19*dq} zC4s9_>+3zCyF@E25{&WBzuwTKjO{sgXTGO?{Wk zOG?fKc!rYt6MdS=FDY_CZG&C3;QS?T&7><}q3H(Z} zw~wh}sPK++fQ{=iCZ0FSj68>}O4nEKy(%;JxDt1nVg+)&uY53csIc*DG`219NP(ze zi8CSH&PH z7F{aST|h9J1CU*C{CkTFJyc2^Q`q}NizoS_$NJ&Oz`@g0)}pnhrjm+NK`#FrRRbbE zyE^Z9%{6DoPsoT~#*_Py=gs6SmQ+SsWKP8{rgiX5{fWbI_S*8kVbs*n#h!VB+(BP_ z0-k7o?-OZhaT~mQuxuyw>ncKD*|&m~61q-w%5Qo-qY74Wh~NMhNM96~CKyVO-r8JX zRM3`g!@Fp*ZKFkolU~{1+1;tpxnFktY}bsdbbR~gPJMF|mLDjJXj0QH$(#-oZ^`Tn z9r9s(=gpOlnF_^xD3G0IVxP>Jh2Gs6Pi2RWV9$1VI8r71@Ch?}D9=BN-r+wIkF{q9 z*_F||Fhb1~(UQYjE0a$ADDGhz3)1qS=6ZntcEQcyXM*AydnA3B?#*6@ZB_P7A^j@C zw~5t`(kp+Y{7F44*6-aKo+~R#35&U&Oj-$Q#@88asOX9hoY~j|eVP>kMxr=FlbgGm=GWLl~dQ##X#= zL%qpvyAXvCV(=xDS$zwoT-R}DYm)xXi!XD^kO9M?v1YQnE5-dS)TRkdmRlG5qVwT& z?~3tEfN7tH&EaZqLUh>#7D(93o<*10eX5;1!{VTdTf7+eyTk`xc?C=EmwqD&wOZ}oTGs1qwhz5 zgHAn<(D#pW8KW(#Rf!)+q+5-|(dO`H3!tF%(2?iEgtdu?d}i)p?BUqHh$lhf-79_> z?M+j(Z6N>9vfOz;o{?i&7nNA!vdGtq1z&q}Mx|Us+*JceeNL6-FO=!=#URBjO3umU>Lf9?@S?}jRr)(B; zfD}-Z_hP9`T4j}v7K(3{{po0xM6YX}@6HrwIkN0u4WMlTEQf1nn!zep^n3>=&M_`4$zJ{`&fe+5+hJ|j^UxIg YF`UKK`df5{a0sfXY&amGz?Ad;-=8MrLjV8( literal 0 HcmV?d00001 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 %> + + + -- 2.39.2