Initial commit 0.000_001
authorMarius Gavrilescu <marius@ieval.ro>
Sat, 13 Feb 2016 18:48:25 +0000 (18:48 +0000)
committerMarius Gavrilescu <marius@ieval.ro>
Sat, 13 Feb 2016 18:48:25 +0000 (18:48 +0000)
15 files changed:
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/.#Oof.pm [new symlink]
lib/App/Web/Oof.pm [new file with mode: 0644]
static/Gravity-UltraLight.otf [new file with mode: 0644]
static/pattern.png [new file with mode: 0644]
static/style.css [new file with mode: 0644]
t/App-Web-Oof.t [new file with mode: 0644]
tmpl/continue.html [new file with mode: 0644]
tmpl/form.html [new file with mode: 0644]
tmpl/order.html [new file with mode: 0644]

diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..d83f15c
--- /dev/null
+++ b/Changes
@@ -0,0 +1,4 @@
+Revision history for Perl extension App::Web::Oof.
+
+0.000_001 2016-02-13T18:48+00:00
+ - Initial release
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..3f7cc43
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,14 @@
+app.psgi
+Changes
+db.sql
+lib/App/Web/Oof.pm
+Makefile.PL
+MANIFEST
+README
+static/Gravity-UltraLight.otf
+static/pattern.png
+static/style.css
+t/App-Web-Oof.t
+tmpl/continue.html
+tmpl/form.html
+tmpl/order.html
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644 (file)
index 0000000..278e388
--- /dev/null
@@ -0,0 +1,26 @@
+use 5.014000;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+       NAME              => 'App::Web::Oof',
+       VERSION_FROM      => 'lib/App/Web/Oof.pm',
+       ABSTRACT_FROM     => 'lib/App/Web/Oof.pm',
+       AUTHOR            => 'Marius Gavrilescu <marius@ieval.ro>',
+       MIN_PERL_VERSION  => '5.14.0',
+       LICENSE           => 'perl',
+       SIGN              => 1,
+       PREREQ_PM         => {
+               qw/Plack::App::File       0
+                  Plack::Builder         0
+                  DBIx::Simple           0
+                  HTML::TreeBuilder      0
+                  HTML::Element::Library 0
+                  JSON::MaybeXS          0/,
+       },
+       META_ADD         => {
+               dynamic_config => 0,
+               resources      => {
+                       repository => 'https://git.ieval.ro/?p=app-web-oof.git',
+               },
+       }
+);
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..d7e690a
--- /dev/null
+++ b/README
@@ -0,0 +1,38 @@
+App-Web-Oof version 0.000_001
+=============================
+
+Oof (Oversimplified order form) is a very simple ecommerce website.
+As of this moment it is incomplete (although functional), hence the
+version number.
+
+To use just edit the templates in the "tmpl/" directory and then run
+plackup -Ilib.
+
+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:
+
+* Plack
+* DBIx::Simple
+* HTML::Element
+* HTML::Element::Library
+* JSON::MaybeXS
+
+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..785f9b3
--- /dev/null
+++ b/app.psgi
@@ -0,0 +1,12 @@
+#!/usr/bin/perl
+use 5.014000;
+use warnings;
+
+use App::Web::Oof;
+use Plack::App::File;
+use Plack::Builder;
+
+builder {
+       mount '/static/' => Plack::App::File->new(root => 'static')->to_app,
+       mount '/' => App::Web::Oof::app,
+}
diff --git a/db.sql b/db.sql
new file mode 100644 (file)
index 0000000..56edc09
--- /dev/null
+++ b/db.sql
@@ -0,0 +1,37 @@
+CREATE TABLE IF NOT EXISTS products (
+       product  serial PRIMARY KEY,
+       title    TEXT   NOT NULL,
+       subtitle TEXT   NOT NULL,
+       summary  TEXT   NOT NULL,
+       pictures TEXT[]     NULL,
+       price    INT    NOT NULL,
+       stock    INT    NOT NULL,
+       CONSTRAINT positive_stock CHECK (stock >= 0)
+);
+
+CREATE TABLE IF NOT EXISTS discounts (
+       discount VARCHAR(20) PRIMARY KEY,
+       fraction DECIMAL,
+       flat     INT,
+       CONSTRAINT fraction_xor_flat CHECK ((fraction IS NULL AND flat IS NOT NULL) OR (fraction IS NOT NULL AND flat IS NULL))
+);
+
+CREATE TABLE IF NOT EXISTS orders (
+       id       TEXT PRIMARY KEY,
+       products JSON NOT NULL,
+       total    INT NOT NULL,
+       discount VARCHAR(20) REFERENCES discounts UNIQUE,
+
+       -- DELIVERY
+       first_name   VARCHAR(20) NOT NULL,
+       last_name    VARCHAR(20) NOT NULL,
+       email        VARCHAR(80) NOT NULL,
+       phone        VARCHAR(20),
+       postcode     VARCHAR(10) NOT NULL,
+       address1     VARCHAR(32) NOT NULL,
+       address2     VARCHAR(32),
+       address3     VARCHAR(32),
+       address4     VARCHAR(32),
+       safe_place   VARCHAR(32),
+       instructions VARCHAR(32)
+);
diff --git a/lib/App/Web/.#Oof.pm b/lib/App/Web/.#Oof.pm
new file mode 120000 (symlink)
index 0000000..2792ff4
--- /dev/null
@@ -0,0 +1 @@
+marius@mgvx.1727:1455362169
\ No newline at end of file
diff --git a/lib/App/Web/Oof.pm b/lib/App/Web/Oof.pm
new file mode 100644 (file)
index 0000000..b8fbaf5
--- /dev/null
@@ -0,0 +1,202 @@
+package App::Web::Oof;
+
+use 5.014000;
+use strict;
+use warnings;
+use utf8;
+use parent qw/Plack::Component/;
+
+our $VERSION = '0.000_001';
+
+use DBIx::Simple;
+use HTML::TreeBuilder;
+use HTML::Element::Library;
+use JSON::MaybeXS qw/encode_json decode_json/;
+use Plack::Builder;
+use Plack::Request;
+
+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 $db;
+my ($form, $continue, $order);
+
+{
+       sub parse_html {
+               my $builder = HTML::TreeBuilder->new;
+               $builder->ignore_unknown(0);
+               $builder->parse_file("tmpl/$_[0].html");
+               $builder
+       }
+
+       $form     = parse_html 'form';
+       $continue = parse_html 'continue';
+       $order    = parse_html 'order';
+}
+
+sub stringify_money { sprintf "£%.2f", $_[0] / 100 }
+
+sub form_table_row {
+       my ($data, $tr) = @_;
+       $tr->fclass($_)->replace_content($data->{$_}) for qw/title subtitle stock/;
+       $tr->fclass('price')->replace_content(stringify_money $data->{price});
+       $tr->fclass('title')->attr('data-product', $data->{product});
+       $tr->fclass('title')->attr('data-summary', $data->{summary});
+       $tr->look_down(_tag => 'input')->attr(max => $data->{stock});
+       $tr->look_down(_tag => 'input')->attr(name => 'quant'.$data->{product});
+}
+
+sub form_app {
+       my ($env) = @_;
+       $db //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+
+       my $data = $db->select(products => '*', {}, 'product')->hashes;
+       my $tree = $form->clone;
+       $tree->find('tbody')->find('tr')->iter3($data, \&form_table_row);
+
+       [200, ['Content-type' => 'text/html; charset=utf-8'], [$tree->as_HTML]]
+}
+
+sub continue_table_row {
+       my ($data, $tr) = @_;
+       $tr->fclass($_)->replace_content($data->{$_}) for qw/title subtitle quantity/;
+       $tr->fclass('price')->replace_content(stringify_money $data->{subtotal});
+       $tr->fclass('title')->attr('data-product', $data->{product});
+}
+
+sub continue_app {
+       my ($env) = @_;
+       $db //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       my $tree = $continue->clone;
+       my $req = Plack::Request->new($env);
+       my $params = $req->body_parameters;
+
+       my ($quant, $total, @data, @notes);
+       for (sort keys %$params) {
+               next unless /^quant/;
+               next unless $params->{$_};
+               my $data = $db->select(products => '*', {product => substr $_, 5})->hash;
+               $data->{quantity} = $params->{$_};
+               if ($data->{stock} == 0) {
+                       push @notes, 'Item is out of stock and was removed from order: '.$data->{title};
+                       next
+               }
+               if ($data->{quantity} > $data->{stock}) {
+                       $data->{quantity} = $data->{stock};
+                       push @notes, 'Not enough units of "'.$data->{title}.'" available. Quantity reduced to '.$data->{quantity}
+               }
+               $data->{subtotal} = $data->{price} * $data->{quantity};
+               $quant += $data->{quantity};
+               $total += $data->{subtotal};
+               push @data, $data
+       }
+
+       $tree->fid('subtotal')->replace_content(stringify_money $total);
+       my $dvalue;
+       if ($params->{discount}) {
+               my $discount = $db->select(discounts => '*', {discount => $params->{discount}})->hash;
+               if (!defined $discount) {
+                       push @notes, 'Discount code incorrect. No discount applied.'
+               } elsif ($db->select(orders => 'COUNT(*)', {discount => $params->{discount}})->list) {
+                       push @notes, 'Discount code already used once. No discount applied.'
+               } else {
+                       $dvalue = int (0.5 + $discount->{fraction} * $total) if $discount->{fraction};
+                       $dvalue = $discount->{flat}                          if $discount->{flat};
+                       $tree->fid('discount')->replace_content('-'.stringify_money $dvalue);
+                       $total -= $dvalue;
+                       $tree->look_down(name => 'discount')->attr(value => $params->{discount});
+                       push @notes, 'Discount applied.'
+               }
+       }
+       $tree->look_down(name => 'discount')->detach unless $dvalue;
+       $tree->fid('discount_tr')->detach unless $dvalue;
+       my $postage = 220 + 50 * $quant;
+       $tree->fid('postage')->replace_content(stringify_money $postage);
+       $total += $postage;
+       $tree->fid('total')->replace_content(stringify_money $total);
+
+       $tree->fid('order')->find('tbody')->find('tr')->iter3(\@data, \&continue_table_row);
+       $tree->iter($tree->fid('notes')->find('li') => @notes);
+
+       $tree->look_down(name => 'products')->attr(value => encode_json \@data);
+       $tree->look_down(name => 'total')->attr(value => $total);
+
+       [200, ['Content-type' => 'text/html; charset=utf-8'], [$tree->as_HTML]]
+}
+
+sub order_app {
+       my ($env) = @_;
+       $db //= DBIx::Simple->connect($ENV{OOF_DSN} // 'dbi:Pg:');
+       my $tree = $order->clone;
+       my $req = Plack::Request->new($env);
+       my $id = sprintf "%X", time; # Not good enough!
+
+       $db->begin_work;
+       $db->insert(orders => {id => $id, %{$req->body_parameters}});
+       my $products = decode_json $req->body_parameters->{products};
+       for my $prod (@$products) {
+               my $stock = $db->select(products => 'stock', {product => $prod->{product}})->list;
+               die "Not enough of " .$prod->{title}."\n" if $prod->{quantity} > $stock;
+               $db->update(products => {stock => $stock - $prod->{quantity}}, {product => $prod->{product}});
+       }
+       $db->commit;
+
+       $tree->fid('orderid')->replace_content($id);
+       [200, ['Content-type' => 'text/html; charset=utf-8'], [$tree->as_HTML]]
+}
+
+sub app {
+       builder {
+               mount '/' => sub { [301, [Location => '/form'], []] };
+               mount '/form'     => \&form_app;
+               mount '/continue' => \&continue_app;
+               mount '/order'    => \&order_app;
+       }
+}
+
+1;
+__END__
+
+=head1 NAME
+
+App::Web::Oof - Oversimplified order form / ecommerce website
+
+=head1 SYNOPSIS
+
+  use App::Web::Oof;
+
+=head1 DESCRIPTION
+
+Oof (Oversimplified order form) is a very simple ecommerce website.
+As of this moment it is incomplete (although functional), hence the
+version number.
+
+=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/static/Gravity-UltraLight.otf b/static/Gravity-UltraLight.otf
new file mode 100644 (file)
index 0000000..31b8567
Binary files /dev/null and b/static/Gravity-UltraLight.otf differ
diff --git a/static/pattern.png b/static/pattern.png
new file mode 100644 (file)
index 0000000..720549e
Binary files /dev/null and b/static/pattern.png differ
diff --git a/static/style.css b/static/style.css
new file mode 100644 (file)
index 0000000..ba7af92
--- /dev/null
@@ -0,0 +1,56 @@
+@font-face {
+       font-family: "Gravity Ultra Light";
+       src: url("/static/Gravity-UltraLight.otf");
+}
+
+body {
+       background: url("/static/pattern.png");
+       padding: 0.3em 1em;
+       line-height: 1.4;
+}
+
+#title {
+       font-family: "Gravity Ultra Light";
+       text-align: center;
+       font-size: 5em;
+       font-weight: normal;
+       margin: 0;
+       margin-bottom: 5px;
+}
+
+#items, #order {
+       width: 100%;
+}
+
+table,th,td {
+       border-collapse: collapse;
+}
+
+th,td {
+       padding: 0.2em;
+}
+
+a.title {
+       font-weight: bold;
+       text-decoration: none;
+}
+
+#continue, #place_order{
+       font-size: 1.2em;
+       font-weight: bold;
+       padding: 1em;
+       background-color: lightgreen;
+       border-radius: 2em;
+       border-style: solid;
+       border-width: medium;
+       margin: auto;
+       display: block;
+}
+
+.stock {
+       text-align: center;
+}
+
+#total {
+       font-weight: bold;
+}
\ No newline at end of file
diff --git a/t/App-Web-Oof.t b/t/App-Web-Oof.t
new file mode 100644 (file)
index 0000000..39453fe
--- /dev/null
@@ -0,0 +1,18 @@
+# Before 'make install' is performed this script should be runnable with
+# 'make test'. After 'make install' it should work as 'perl App-Web-Oof.t'
+
+#########################
+
+# change 'tests => 1' to 'tests => last_test_to_print';
+
+use strict;
+use warnings;
+
+use Test::More tests => 1;
+BEGIN { use_ok('App::Web::Oof') };
+
+#########################
+
+# Insert your test code below, the Test::More module is use()ed here so read
+# its man page ( perldoc Test::More ) for help writing this test script.
+
diff --git a/tmpl/continue.html b/tmpl/continue.html
new file mode 100644 (file)
index 0000000..9f32342
--- /dev/null
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="stylesheet" href="/static/style.css">
+<title>Order details</title>
+
+<h1 id="title">ledparts4you</h1>
+
+<ul id="notes"><li>Note</ul>
+
+<h2>Your order</h2>
+<form action="/order" method="POST">
+<table id="order">
+<thead><tr><th>Item<th>Quantity<th>Price</thead>
+<tbody><tr><td class="item"><a href="#" class="title"></a><br><span class="subtitle"></span><td class="quantity"><td class="price"></tbody>
+</table>
+
+<h2>Totals</h2>
+<table id="totals">
+<tbody><tr><td>Subtotal<td id="subtotal">
+<tr id="discount_tr"><td>Discount<td id="discount">
+<tr><td>Postage and packaging<td id="postage">
+<tr><td><strong>Total</strong><td id="total"></tbody>
+</table>
+
+<h2>Your details</h2>
+<label>First name<br>   <input name="first_name" autocomplete="given-name"  type="text"  maxlength="20" required></label><br>
+<label>Last name<br>    <input name="last_name"  autocomplete="family-name" type="text"  maxlength="20" required></label><br>
+<label>Email address<br><input name="email"      autocomplete="email"       type="email" maxlength="80" required></label><br>
+<label>Phone number <em>(optional)</em><br><input name="phone" autocomplete="tel" type="tel" maxlength="20"></label><br>
+<label>Postcode<br>     <input name="postcode"   autocomplete="postal-code" type="text"  maxlength="10" required></label><br>
+<label>Address line 1<br><input name="address1" autocomplete="address-line1" type="text" maxlength="32" required></label><br>
+<label>Address line 2 <em>(optional)</em><br><input name="address2" autocomplete="address-line2" type="text" maxlength="32"></label><br>
+<label>Address line 3 <em>(optional)</em><br><input name="address3" autocomplete="address-line3" type="text" maxlength="32"></label><br>
+<label>Address line 4 <em>(optional)</em><br><input name="address4" type="text" maxlength="32"></label><br>
+<label>Safe place <em>(optional)</em><br><input name="safe_place" type="text" maxlength="32"></label><br>
+<label>Delivery instructions <em>(optional)</em><br><input name="instructions" type="text" maxlength="32"></label><br>
+
+<input type="hidden" name="discount">
+<input type="hidden" name="products">
+<input type="hidden" name="total">
+<input type="submit" value="Place order" id="place_order">
+</form>
diff --git a/tmpl/form.html b/tmpl/form.html
new file mode 100644 (file)
index 0000000..b5da6fc
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="stylesheet" href="/static/style.css">
+<title>Order form</title>
+
+<h1 id="title">ledparts4you</h1>
+
+<div id="info"></div>
+<form action="continue" method="POST">
+<table id="items">
+<thead><tr><th>Item<th>Price<th>Stock<th>Quantity</thead>
+<tbody><tr><td class="item"><a href="#" class="title"></a><br><span class="subtitle"></span><td class="price"><td class="stock"><td><input class="quantity" type="number" min="0" value="0"></tbody>
+</table>
+
+<label>Discount code (optional)<br><input name="discount" type="text"></label><br>
+<input type="submit" value="Continue&raquo;" id="continue">
+</form>
diff --git a/tmpl/order.html b/tmpl/order.html
new file mode 100644 (file)
index 0000000..f105b14
--- /dev/null
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="stylesheet" href="/static/style.css">
+<title>Order placed</title>
+
+<h1 id="title">ledparts4you</h1>
+
+<h2>Success</h2>
+Order <strong id="orderid">FAKEID</strong> has been placed successfully. You will receive an email when the order has been dispatched.
This page took 0.022144 seconds and 4 git commands to generate.