Initial commit master 0.000_001
authorMarius Gavrilescu <marius@ieval.ro>
Thu, 31 Mar 2016 22:15:09 +0000 (01:15 +0300)
committerMarius Gavrilescu <marius@ieval.ro>
Thu, 31 Mar 2016 22:15:09 +0000 (01:15 +0300)
Changes [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
Makefile.PL [new file with mode: 0644]
README [new file with mode: 0644]
app.psgi [new file with mode: 0644]
db.sql [new file with mode: 0644]
lib/App/Web/Comstock.pm [new file with mode: 0644]
t/App-Web-Comstock.t [new file with mode: 0644]
tmpl/index.html [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..3614f84
--- /dev/null
+++ b/Changes
@@ -0,0 +1,4 @@
+Revision history for Perl extension App::Web::Comstock.
+
+0.000_001 2016-04-01T01:15+03:00
+ - Initial release
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..d79af47
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,9 @@
+app.psgi
+Changes
+db.sql
+lib/App/Web/Comstock.pm
+Makefile.PL
+MANIFEST
+README
+t/App-Web-Comstock.t
+tmpl/index.html
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..cb371fe
--- /dev/null
@@ -0,0 +1,26 @@
+use 5.010000;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+       NAME              => 'App::Web::Comstock',
+       VERSION_FROM      => 'lib/App/Web/Comstock.pm',
+       ABSTRACT_FROM     => 'lib/App/Web/Comstock.pm',
+       AUTHOR            => 'Marius Gavrilescu <marius@ieval.ro>',
+       MIN_PERL_VERSION  => '5.10.0',
+       LICENSE           => 'perl',
+       SIGN              => 1,
+       PREREQ_PM         => {
+               qw/DateTime               0
+                  DBIx::Simple           0
+                  HTML::TreeBuilder      0
+                  HTML::Element::Library 0
+                  Plack::Builder         0
+                  Plack::Request         0/,
+       },
+       META_ADD         => {
+               dynamic_config => 0,
+               resources      => {
+                       repository => 'https://git.ieval.ro/?p=app-web-comstock.git',
+               },
+       }
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..8632262
--- /dev/null
+++ b/README
@@ -0,0 +1,35 @@
+App-Web-Comstock version 0.000_001
+==================================
+
+Comstock is an unfinished website for managing bookings of generic
+items. Users will be able to see the availability of existing items,
+book them for some periods and view existing bookings.
+
+INSTALLATION
+
+To install this module type the following:
+
+   perl Makefile.PL
+   make
+   make test
+   make install
+
+DEPENDENCIES
+
+This module requires these other modules and libraries:
+
+* DateTime
+* DBIx::Simple
+* HTML::Element
+* HTML::Element::Library
+* Plack
+
+COPYRIGHT AND LICENCE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.22.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
diff --git a/app.psgi b/app.psgi
new file mode 100644 (file)
index 0000000..ff22fb4
--- /dev/null
+++ b/app.psgi
@@ -0,0 +1,12 @@
+#!/usr/bin/perl
+use 5.010000;
+use warnings;
+
+use App::Web::Comstock;
+use Plack::App::File;
+use Plack::Builder;
+
+builder {
+       mount '/static/' => Plack::App::File->new(root => 'static')->to_app,
+       mount '/' => App::Web::Comstock::app,
+}
diff --git a/db.sql b/db.sql
new file mode 100644 (file)
index 0000000..d94c518
--- /dev/null
+++ b/db.sql
@@ -0,0 +1,18 @@
+CREATE TABLE items (
+       item        SERIAL PRIMARY KEY,
+       title       TEXT   NOT NULL,
+       category    TEXT   NOT NULL,
+       description TEXT,
+
+       begin_hour  INT    NOT NULL DEFAULT 5,
+       end_hour    INT    NOT NULL DEFAULT 23,
+       min_hours   INT    NOT NULL DEFAULT 1,
+       max_hours   INT    NOT NULL DEFAULT 24
+);
+
+CREATE TABLE bookings (
+       item       INT     NOT NULL REFERENCES items,
+       name       TEXT    NOT NULL,
+       begin_time BIGINT  NOT NULL,
+       end_time   BIGINT  NOT NULL
+);
diff --git a/lib/App/Web/Comstock.pm b/lib/App/Web/Comstock.pm
new file mode 100644 (file)
index 0000000..db4cd32
--- /dev/null
@@ -0,0 +1,190 @@
+package App::Web::Comstock;
+
+use 5.010000;
+use strict;
+use warnings;
+our $VERSION = '0.000_001';
+
+use DateTime;
+use DBIx::Simple;
+use HTML::TreeBuilder;
+use HTML::Element::Library;
+use Plack::Builder;
+use Plack::Request;
+use POSIX qw/strftime/;
+
+sub HTML::Element::iter3 {
+       my ($self, $data, $code) = @_;
+       my $orig = $self;
+       my $prev = $orig;
+       for my $el (@$data) {
+               my $current = $orig->clone;
+               $code->($el, $current);
+               $prev->postinsert($current);
+               $prev = $current;
+       }
+       $orig->detach;
+}
+
+sub HTML::Element::fid    { shift->look_down(id    => shift) }
+sub HTML::Element::fclass { shift->look_down(class => qr/\b$_[0]\b/) }
+
+##################################################
+
+my ($index);
+
+{
+       sub parse_html {
+               my $builder = HTML::TreeBuilder->new;
+               $builder->ignore_unknown(0);
+               $builder->parse_file("tmpl/$_[0].html");
+               $builder
+       }
+
+       $index = parse_html 'index';
+}
+
+sub db : lvalue {
+       shift->{'comstock.db'}
+}
+
+sub nav_li {
+       my ($data, $li) = @_;
+       $li->find('a')->replace_content($data->{title});
+       $li->find('a')->attr(href => '?item='.$data->{item});
+}
+
+sub nav_ul {
+       my ($data, $ul) = @_;
+       $ul->find('li')->iter3($data, \&nav_li);
+}
+
+sub display_app {
+       my ($env) =  @_;
+       my $req = Plack::Request->new($env);
+       my $tree = $index->clone;
+       my @items = db($env)->select(items => '*')->hashes;
+       my %items;
+
+       for my $item (@items) {
+               $items{$item->{category}} //= [];
+               push @{$items{$item->{category}}}, $item
+       }
+
+       my @data = sort { $a->[0]{category} cmp $b->[0]{category} } values %items; #map { $items{$_} } sort keys %items;
+       $tree->fid('comstock_nav')->find('ul')->iter3(\@data, \&nav_ul);
+       my $item = $req->param('item');
+       if ($item) {
+               $tree->look_down(name => 'item')->attr(value => $item);
+               my ($begin, $end) = db($env)->select(items => [qw/begin_hour end_hour/], {item => $item})->list;
+               for my $name (qw/begin_hour end_hour/) {
+                       my $select = $tree->look_down(name => $name);
+                       $select->iter($select->find('option') => $begin .. $end)
+               }
+       } else {
+               $tree->fid('book_div')->detach
+       }
+       $tree
+}
+
+sub error {
+       'Error: ' . $_[0]
+}
+
+sub book_app {
+       my ($env) = @_;
+       my $req = Plack::Request->new($env);
+       my ($begin_year, $begin_month, $begin_day) = split '/', $req->param('begin');
+       my ($end_year, $end_month, $end_day) = split '/', $req->param('end');
+       my $begin_hour = $req->param('begin_hour');
+       my $end_hour = $req->param('end_hour');
+       my $begin = DateTime->new(year => $begin_year, month => $begin_month, day => $begin_day, hour => $begin_hour)->epoch;
+       my $end = DateTime->new(year => $end_year, month => $end_month, day => $end_day, hour => $end_hour)->epoch;
+       my $item = 0+$req->param('item');
+       my ($begin_range, $end_range, $min_hours, $max_hours) = db($env)->select(items => [qw/begin_hour end_hour min_hours max_hours/], {item => $item})->list or return error 'No such item';
+
+       return error 'End time is not later than begin time' if $end <= $begin;
+       return error 'Begin/end hour not in allowed range' if $begin_hour < $begin_range || $begin_hour > $end_range || $end_hour < $begin_range || $end_hour > $end_range;
+       return error 'Bookings must last for at least $min_hours hours' if (($end - $begin) / 3600 < $min_hours);
+       return error 'Bookings must last for at most $max_hours hours' if (($end - $begin) / 3600 > $max_hours);
+       return error 'Item is not available for the selected period' if db($env)->query('SELECT item FROM bookings WHERE item = ? AND (end_time - begin_time + ? - ?) > GREATEST(end_time, ?) - LEAST(begin_time, ?)', $item, $end, $begin, $end, $begin)->list;
+       db($env)->insert(bookings => {
+               item => $item,
+               name => scalar $req->param('name'),
+               begin_time => $begin,
+               end_time => $end,
+       });
+       return [200, ['Content-Type' => 'text/plain'], ['Booking was successful']];
+}
+
+sub view_app {
+       my ($env) = @_;
+       my $req = Plack::Request->new($env);
+       my $item = $req->param('item');
+       my $time = time;
+       $time -= $time % 86400;
+       my @bookings = db($env)->select(bookings => '*', {item => $item, begin_time => {'>', $time}}, 'begin_time')->hashes;
+       my $ans;
+       for my $booking (@bookings) {
+               $booking->{name} =~ y/\n//d;
+               $ans .= sprintf "%s -> %s  %s\n", strftime ('%c', gmtime $booking->{begin_time}), strftime ('%c', gmtime $booking->{end_time}), $booking->{name};
+       }
+       [200, ['Content-type' => 'text/plain'], [$ans]]
+}
+
+sub app {
+       builder {
+               enable 'ContentLength';
+               enable sub {
+                       my $app = shift;
+                       my $db = DBIx::Simple->connect($ENV{COMSTOCK_DSN} // 'dbi:Pg:');
+                       sub {
+                               my ($env) = @_;
+                               db($env) = $db;
+                               my $res = $app->($env);
+                               return $res if ref $res eq 'ARRAY';
+                               return [200, ['Content-type' => 'text/html; charset=utf-8'], [$res->as_HTML]]
+                                 if ref $res;
+                               return [500, ['Content-type' => 'text/plain'], ["$res"]]
+                       }
+               };
+               mount '/book' => \&book_app;
+               mount '/view' => \&view_app;
+               mount '/'     => \&display_app;
+       }
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+App::Web::Comstock - Website for managing bookings of generic items
+
+=head1 SYNOPSIS
+
+  use App::Web::Comstock;
+  App::Web::Comstock->app
+
+=head1 DESCRIPTION
+
+Comstock is an unfinished website for managing bookings of generic
+items. Users will be able to see the availability of existing items,
+book them for some periods and view existing bookings.
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2016 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.22.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
diff --git a/t/App-Web-Comstock.t b/t/App-Web-Comstock.t
new file mode 100644 (file)
index 0000000..3a88590
--- /dev/null
@@ -0,0 +1,5 @@
+use strict;
+use warnings;
+
+use Test::More tests => 1;
+BEGIN { use_ok('App::Web::Comstock') };
diff --git a/tmpl/index.html b/tmpl/index.html
new file mode 100644 (file)
index 0000000..d784084
--- /dev/null
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Comstock</title>
+<meta charset="utf-8">
+
+<style>
+ul {
+       display: block;
+}
+
+li {
+       display: inline-block;
+       list-item-style: none;
+}
+
+li::after {
+       content: ", ";
+}
+</style>
+
+<div id="comstock_nav">
+<ul><li><a href="#">Item</a></li></ul>
+</div>
+
+<div id="book_div">
+<form action="/book" method="POST">
+<input type="hidden" name="item">
+<label>Name:<br> <input type="text" name="name"></label><br>
+<label>Start date:<br> <input type="date" name="begin" placeholder="yyyy/mm/dd"></label><br>
+<label>Start hour:<br> <select name="begin_hour"><option>1</option></select></label><br>
+<label>End date:<br> <input type="date" name="end" placeholder="yyyy/mm/dd"></label><br>
+<label>End hour:<br> <select name="end_hour"><option>1</option></select></label><br>
+<input type="submit" value="Book">
+</div>
This page took 0.019625 seconds and 4 git commands to generate.