From 1659638a8fc585734fe6893f2700f40f9488c2b4 Mon Sep 17 00:00:00 2001
From: Marius Gavrilescu <marius@ieval.ro>
Date: Fri, 1 Apr 2016 01:15:09 +0300
Subject: [PATCH 1/1] Initial commit

---
 Changes                 |   4 +
 MANIFEST                |   9 ++
 Makefile.PL             |  26 ++++++
 README                  |  35 ++++++++
 app.psgi                |  12 +++
 db.sql                  |  18 ++++
 lib/App/Web/Comstock.pm | 190 ++++++++++++++++++++++++++++++++++++++++
 t/App-Web-Comstock.t    |   5 ++
 tmpl/index.html         |  33 +++++++
 9 files changed, 332 insertions(+)
 create mode 100644 Changes
 create mode 100644 MANIFEST
 create mode 100644 Makefile.PL
 create mode 100644 README
 create mode 100644 app.psgi
 create mode 100644 db.sql
 create mode 100644 lib/App/Web/Comstock.pm
 create mode 100644 t/App-Web-Comstock.t
 create mode 100644 tmpl/index.html

diff --git a/Changes b/Changes
new file mode 100644
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
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
index 0000000..cb371fe
--- /dev/null
+++ b/Makefile.PL
@@ -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
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
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
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
index 0000000..db4cd32
--- /dev/null
+++ b/lib/App/Web/Comstock.pm
@@ -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
index 0000000..3a88590
--- /dev/null
+++ b/t/App-Web-Comstock.t
@@ -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
index 0000000..d784084
--- /dev/null
+++ b/tmpl/index.html
@@ -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>
-- 
2.39.5