big kahuna
This commit is contained in:
@@ -2,6 +2,6 @@ package CW::Controller;
|
||||
|
||||
use Moo;
|
||||
|
||||
has dbh => ( is => 'ro' );
|
||||
has dbh => ( is => 'ro', required => 1 );
|
||||
|
||||
1;
|
||||
|
||||
@@ -2,19 +2,17 @@ package CW::Controller::Index;
|
||||
|
||||
use Moo;
|
||||
|
||||
with 'CW::Controller';
|
||||
extends 'CW::Controller';
|
||||
|
||||
sub index {
|
||||
my ( $self, $request, $response ) = @_;
|
||||
|
||||
$response->template(
|
||||
[
|
||||
head => [],
|
||||
body => [
|
||||
h1 => ["Foo Bar!"]
|
||||
]
|
||||
]
|
||||
);
|
||||
my $boards =
|
||||
$self->dbh->selectall_arrayref( q[SELECT * FROM boards ORDER BY id],
|
||||
{ Slice => {} } );
|
||||
|
||||
$response->template( 'Index',
|
||||
{ title => 'compiled.world | home', boards => $boards } );
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
18
lib/CW/Layout.pm
Normal file
18
lib/CW/Layout.pm
Normal file
@@ -0,0 +1,18 @@
|
||||
package CW::Layout;
|
||||
|
||||
use Moo;
|
||||
use Types::Standard qw(HashRef InstanceOf);
|
||||
|
||||
has template => (
|
||||
is => 'ro',
|
||||
required => 1,
|
||||
isa => InstanceOf ['CW::Template']
|
||||
);
|
||||
|
||||
has ctx => (
|
||||
is => 'ro',
|
||||
required => 1,
|
||||
isa => HashRef
|
||||
);
|
||||
|
||||
1;
|
||||
134
lib/CW/Layout/Default.pm
Normal file
134
lib/CW/Layout/Default.pm
Normal file
@@ -0,0 +1,134 @@
|
||||
package CW::Layout::Default;
|
||||
|
||||
use Moo;
|
||||
|
||||
extends 'CW::Layout';
|
||||
|
||||
my $HACKERSPACE_LINK = "https://en.wikipedia.org/wiki/Hackerspace";
|
||||
|
||||
sub render {
|
||||
my ($self) = @_;
|
||||
|
||||
my $ctx = $self->ctx // {};
|
||||
|
||||
if ( !$ctx->{nav} ) {
|
||||
$ctx->{nav} = [];
|
||||
my @path = split '/', $ctx->{env}->{PATH_INFO};
|
||||
|
||||
if (@path) {
|
||||
my $nav = [];
|
||||
for ( 0 .. $#path ) {
|
||||
my $text = $path[$_];
|
||||
my @prev = @path[ 0 .. $_ ];
|
||||
push @$nav,
|
||||
{
|
||||
text => $text,
|
||||
path => '/' . join( '/', ( @prev, $text ) )
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my @additional_links;
|
||||
if ( !$ctx->{user} ) {
|
||||
push @additional_links, $self->_make_nav_link("login");
|
||||
}
|
||||
else {
|
||||
if ( $ctx->{user}->is_admin ) {
|
||||
push @additional_links, $self->_make_nav_link("admin");
|
||||
}
|
||||
|
||||
push @additional_links, $self->_make_nav_link("logout");
|
||||
}
|
||||
|
||||
my $board = $ctx->{board} // 'cw';
|
||||
|
||||
return [
|
||||
head => [
|
||||
title => [ $self->ctx->{title} ],
|
||||
meta => { charset => 'UTF-8' },
|
||||
meta => {
|
||||
name => 'viewport',
|
||||
content => 'width=device-width, initial-scale=1.0'
|
||||
},
|
||||
meta => {
|
||||
name => 'description',
|
||||
content => (
|
||||
$self->ctx->{description}
|
||||
// 'compiled.world, a gated hackerspace '
|
||||
. 'for humans to interact with humans'
|
||||
)
|
||||
},
|
||||
meta => {
|
||||
name => 'robots',
|
||||
content => 'index, follow'
|
||||
},
|
||||
link => {
|
||||
href => '/static/css/shared.css',
|
||||
rel => 'stylesheet'
|
||||
},
|
||||
link => {
|
||||
href => '/static/css/' . $board . '.css',
|
||||
rel => 'stylesheet'
|
||||
}
|
||||
],
|
||||
body => [
|
||||
header => [
|
||||
div => [
|
||||
div => {
|
||||
class => "header-wrapper"
|
||||
} => [
|
||||
img => {
|
||||
src => "/static/images/$board.svg",
|
||||
class => "logo"
|
||||
},
|
||||
div => [
|
||||
a => {
|
||||
href => "/",
|
||||
style => "text-decoration: none"
|
||||
} => [
|
||||
h1 => { style => "margin: 0" } =>
|
||||
["compiled.world"]
|
||||
],
|
||||
p => { style => "margin-top: 0" } => [
|
||||
"A digital ",
|
||||
a => {
|
||||
href => $HACKERSPACE_LINK
|
||||
} => ["hackerspace"],
|
||||
" for human beings"
|
||||
],
|
||||
nav => { class => "navbar" } => [
|
||||
(
|
||||
map { $self->_make_nav_link($_) }
|
||||
qw(recent popular badges about)
|
||||
),
|
||||
@additional_links,
|
||||
]
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
main => [
|
||||
nav => { class => "quicknav" } => [
|
||||
map {
|
||||
(
|
||||
li => [
|
||||
"/",
|
||||
a => { href => $_->{path} } => [ $_->{text} ]
|
||||
]
|
||||
)
|
||||
} @{ $ctx->{nav} }
|
||||
],
|
||||
div => $self->template->render
|
||||
],
|
||||
footer => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
sub _make_nav_link {
|
||||
my ( $self, $text ) = @_;
|
||||
return a => { class => "navbar-link", href => "/$text" } => [$text];
|
||||
}
|
||||
|
||||
1;
|
||||
72
lib/CW/Migration.pm
Normal file
72
lib/CW/Migration.pm
Normal file
@@ -0,0 +1,72 @@
|
||||
package CW::Migration;
|
||||
|
||||
use Moo;
|
||||
use feature qw(state say);
|
||||
use File::Find;
|
||||
|
||||
has dbh => (
|
||||
is => 'ro',
|
||||
required => 1
|
||||
);
|
||||
|
||||
sub migrate {
|
||||
my ($self) = @_;
|
||||
|
||||
return $self->_perform_migrations(pop)
|
||||
if !ref($self) && $self eq __PACKAGE__;
|
||||
|
||||
$self->dbh->do(<<'SQL');
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id VARCHAR(4),
|
||||
migration_sql TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
SQL
|
||||
|
||||
my $id = $self->id;
|
||||
my $already_migrated = $self->already_migrated;
|
||||
|
||||
if ($already_migrated) {
|
||||
say "[Migration] $id already migrated";
|
||||
return;
|
||||
}
|
||||
|
||||
$self->dbh->do( $self->sql );
|
||||
$self->dbh->do( "INSERT INTO migrations (id, migration_sql) VALUES (?, ?)",
|
||||
undef, $id, $self->sql );
|
||||
say "[Migration] $id migrated successfully";
|
||||
}
|
||||
|
||||
sub already_migrated {
|
||||
my ($self) = @_;
|
||||
|
||||
my ($exists_check) =
|
||||
$self->dbh->selectrow_array(
|
||||
"SELECT 1 FROM migrations WHERE id = ? LIMIT 1",
|
||||
undef, $self->id );
|
||||
|
||||
return $exists_check;
|
||||
}
|
||||
|
||||
# static
|
||||
|
||||
sub _perform_migrations {
|
||||
my ( $class, $dbh ) = @_;
|
||||
|
||||
find(
|
||||
{
|
||||
wanted => sub {
|
||||
return unless /\.pm$/;
|
||||
my @parts = split( /\/+/, $File::Find::name );
|
||||
my $file = $parts[-1];
|
||||
my $class = "CW::Migration::" . ( $file =~ s/\.pm$//r );
|
||||
eval "use $class";
|
||||
my $migration = $class->new( dbh => $dbh );
|
||||
$migration->migrate;
|
||||
}
|
||||
},
|
||||
'lib/CW/Migration'
|
||||
);
|
||||
}
|
||||
|
||||
1;
|
||||
21
lib/CW/Migration/01.pm
Normal file
21
lib/CW/Migration/01.pm
Normal file
@@ -0,0 +1,21 @@
|
||||
package CW::Migration::01;
|
||||
use Moo;
|
||||
|
||||
use FindBin;
|
||||
|
||||
extends 'CW::Migration';
|
||||
|
||||
sub id {
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub sql {
|
||||
return <<'SQL';
|
||||
CREATE TABLE sessions (
|
||||
id CHAR(72) PRIMARY KEY,
|
||||
session_data TEXT
|
||||
)
|
||||
SQL
|
||||
}
|
||||
|
||||
1;
|
||||
21
lib/CW/Migration/02.pm
Normal file
21
lib/CW/Migration/02.pm
Normal file
@@ -0,0 +1,21 @@
|
||||
package CW::Migration::02;
|
||||
|
||||
use Moo;
|
||||
|
||||
extends 'CW::Migration';
|
||||
|
||||
sub id {
|
||||
return 2;
|
||||
}
|
||||
|
||||
sub sql {
|
||||
return <<'SQL';
|
||||
CREATE TABLE boards (
|
||||
id VARCHAR(3) PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
description TEXT
|
||||
)
|
||||
SQL
|
||||
}
|
||||
|
||||
1;
|
||||
60
lib/CW/Migration/03.pm
Normal file
60
lib/CW/Migration/03.pm
Normal file
@@ -0,0 +1,60 @@
|
||||
package CW::Migration::03;
|
||||
|
||||
use Moo;
|
||||
|
||||
extends 'CW::Migration';
|
||||
|
||||
sub id {
|
||||
return 3;
|
||||
}
|
||||
|
||||
sub sql {
|
||||
my $boards = [
|
||||
{
|
||||
id => 't',
|
||||
name => 'Technology',
|
||||
description => 'Programming, computing, hardware discussion'
|
||||
},
|
||||
{
|
||||
id => 'b',
|
||||
name => 'Money',
|
||||
description => 'Finance, money, and business discussion'
|
||||
},
|
||||
{
|
||||
id => 'a',
|
||||
name => 'Art',
|
||||
description => 'Human made art (all forms welcome) discussion'
|
||||
},
|
||||
{
|
||||
id => 's',
|
||||
name => 'Math & Science',
|
||||
description => 'Math and science discussion'
|
||||
},
|
||||
{
|
||||
id => 'g',
|
||||
name => 'Games',
|
||||
description => 'Video, Tabletop, TCG, gaming discussion'
|
||||
},
|
||||
{
|
||||
id => 'm',
|
||||
name => 'Meta',
|
||||
description => 'Discussion about compiled.world'
|
||||
}
|
||||
];
|
||||
|
||||
my $values = join(
|
||||
",",
|
||||
map {
|
||||
"('"
|
||||
. $_->{id} . "', '"
|
||||
. $_->{name} . "', '"
|
||||
. $_->{description} . "')"
|
||||
} @$boards
|
||||
);
|
||||
my $sql =
|
||||
'INSERT INTO boards (id, name, description) VALUES ' . $values . ';';
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
1;
|
||||
25
lib/CW/Migration/04.pm
Normal file
25
lib/CW/Migration/04.pm
Normal file
@@ -0,0 +1,25 @@
|
||||
package CW::Migration::04;
|
||||
|
||||
use Moo;
|
||||
|
||||
extends 'CW::Migration';
|
||||
|
||||
sub id {
|
||||
return 4;
|
||||
}
|
||||
|
||||
sub sql {
|
||||
return q[CREATE TABLE users (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(24),
|
||||
password VARCHAR(62),
|
||||
admin BOOLEAN DEFAULT false,
|
||||
active BOOLEAN DEFAULT false,
|
||||
denied BOOLEAN DEFAULT false,
|
||||
denied_reason TEXT,
|
||||
join_request JSON NOT NULL
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -10,13 +10,18 @@ has h => (
|
||||
has _request => (
|
||||
is => 'ro',
|
||||
init_arg => 'request',
|
||||
required => 1
|
||||
required => 1,
|
||||
handles => [qw(env)]
|
||||
);
|
||||
|
||||
sub new_response {
|
||||
my ( $self, @args ) = @_;
|
||||
my $response = $self->_request->new_response(@args);
|
||||
return CW::Plack::Response->new( response => $response, h => $self->h );
|
||||
return CW::Plack::Response->new(
|
||||
request => $self,
|
||||
response => $response,
|
||||
h => $self->h
|
||||
);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package CW::Plack::Response;
|
||||
|
||||
use Moo;
|
||||
use Carp ();
|
||||
|
||||
has _response => (
|
||||
is => 'ro',
|
||||
@@ -9,15 +10,53 @@ has _response => (
|
||||
handles => [qw(finalize status body headers header)]
|
||||
);
|
||||
|
||||
has request => (
|
||||
is => 'ro',
|
||||
required => 1,
|
||||
handles => [qw(env)]
|
||||
);
|
||||
|
||||
has h => (
|
||||
is => 'ro',
|
||||
required => 1
|
||||
);
|
||||
|
||||
sub template {
|
||||
my ( $self, $template ) = @_;
|
||||
my $html = $self->h->html($template);
|
||||
my ( $self, $template, $ctx ) = @_;
|
||||
|
||||
my $html;
|
||||
|
||||
# If rendering inline
|
||||
if ( ref($template) && ref($template) eq 'ARRAY' ) {
|
||||
$html = $self->h->html($template);
|
||||
}
|
||||
elsif ( !ref($template) ) {
|
||||
$ctx //= {};
|
||||
$ctx->{env} = $self->env;
|
||||
|
||||
## no strict [BuiltinFunctions::ProhibitStringyEval]
|
||||
my $template_pkg = "CW::Template::$template";
|
||||
eval "use $template_pkg";
|
||||
|
||||
my $layout_pkg =
|
||||
"CW::Layout::" . ( delete( $ctx->{layout} ) // 'Default' );
|
||||
## no strict [BuiltinFunctions::ProhibitStringyEval]
|
||||
eval "use $layout_pkg";
|
||||
|
||||
my $layout = $layout_pkg->new(
|
||||
template => $template_pkg->new( ctx => $ctx ),
|
||||
ctx => $ctx
|
||||
);
|
||||
|
||||
$html = $self->h->html( $layout->render );
|
||||
}
|
||||
else {
|
||||
Carp::croak("Invalid value passed to template call: $template");
|
||||
}
|
||||
|
||||
$self->_response->header( 'Content-Type' => 'text/html; encoding=utf-8' );
|
||||
$self->_response->body($html);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
|
||||
20
lib/CW/Template.pm
Normal file
20
lib/CW/Template.pm
Normal file
@@ -0,0 +1,20 @@
|
||||
package CW::Template;
|
||||
|
||||
use Moo;
|
||||
use Types::Standard qw(HashRef);
|
||||
|
||||
has ctx => (
|
||||
is => 'ro',
|
||||
required => 1,
|
||||
isa => HashRef
|
||||
);
|
||||
|
||||
sub render {
|
||||
my ($self) = @_;
|
||||
return [
|
||||
h1 => ["Undefined Template"],
|
||||
p => [ "You should probably implement this template! " . ref($self) ]
|
||||
];
|
||||
}
|
||||
|
||||
1;
|
||||
11
lib/CW/Template/Boards.pm
Normal file
11
lib/CW/Template/Boards.pm
Normal file
@@ -0,0 +1,11 @@
|
||||
package CW::Template::Boards;
|
||||
|
||||
use Moo;
|
||||
|
||||
extends 'CW::Template';
|
||||
|
||||
sub render {
|
||||
my ($self) = @_;
|
||||
}
|
||||
|
||||
1;
|
||||
27
lib/CW/Template/Index.pm
Normal file
27
lib/CW/Template/Index.pm
Normal file
@@ -0,0 +1,27 @@
|
||||
package CW::Template::Index;
|
||||
|
||||
use Moo;
|
||||
|
||||
extends 'CW::Template';
|
||||
|
||||
sub render {
|
||||
my ($self) = @_;
|
||||
|
||||
my @boards = @{ $self->ctx->{boards} };
|
||||
return [
|
||||
map {
|
||||
(
|
||||
li => [
|
||||
a => {
|
||||
href => '/' . $_->{id} . '/',
|
||||
class => 'index-board-link'
|
||||
} => [ '/' . $_->{id} . '/ - ' . $_->{name} ],
|
||||
p => { class => 'index-board-description' } =>
|
||||
[ $_->{description} ]
|
||||
]
|
||||
)
|
||||
} @boards
|
||||
];
|
||||
}
|
||||
|
||||
1;
|
||||
Reference in New Issue
Block a user