Work a bit on subforum viewing/thread listing

This commit is contained in:
ngoomie 2023-05-16 22:18:25 -06:00
parent 45343a87fd
commit fd585edc80
17 changed files with 223 additions and 150 deletions

12
.vscode/settings.json vendored
View File

@ -10,6 +10,7 @@
"CharmBoard", "CharmBoard",
"Facepunch", "Facepunch",
"listsubf", "listsubf",
"origp",
"passchk", "passchk",
"passgen", "passgen",
"pgsql", "pgsql",
@ -54,5 +55,14 @@
"perl-toolbox.lint.useProfile": true, "perl-toolbox.lint.useProfile": true,
"perl-toolbox.syntax.includePaths": [ "perl-toolbox.syntax.includePaths": [
"$workspaceRoot/libs" "$workspaceRoot/libs"
] ],
"sqltools.connections": [
{
"previewLimit": 50,
"driver": "SQLite",
"name": "CharmBoard",
"database": "${workspaceFolder:CharmBoard}/charmboard.db"
}
],
"sqltools.useNodeRuntime": true
} }

View File

@ -1,5 +1,6 @@
{ {
board_name => '', board_name => '',
board_root => '', # this doesn't do anything yet
database => { database => {
type => '', # 'sqlite' or 'mariadb' type => '', # 'sqlite' or 'mariadb'

View File

@ -1,5 +1,5 @@
-- --
-- File generated with SQLiteStudio v3.4.4 on Mon. May 8 03:12:22 2023 -- File generated with SQLiteStudio v3.4.4 on Tue. May 16 22:16:54 2023
-- --
-- Text encoding used: UTF-8 -- Text encoding used: UTF-8
-- --
@ -8,43 +8,14 @@ BEGIN TRANSACTION;
-- Table: categories -- Table: categories
DROP TABLE IF EXISTS categories; DROP TABLE IF EXISTS categories;
CREATE TABLE categories (cat_id INTEGER NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK, cat_rank INTEGER NOT NULL, cat_name TEXT, PRIMARY KEY (cat_id AUTOINCREMENT));
CREATE TABLE categories (
cat_id INTEGER NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK,
cat_name TEXT,
PRIMARY KEY (
cat_id AUTOINCREMENT
)
);
-- Table: posts -- Table: posts
DROP TABLE IF EXISTS posts; DROP TABLE IF EXISTS posts;
CREATE TABLE posts (post_id INTEGER NOT NULL ON CONFLICT ROLLBACK UNIQUE ON CONFLICT ROLLBACK, user_id INTEGER NOT NULL ON CONFLICT ROLLBACK, thread_id INTEGER NOT NULL ON CONFLICT ROLLBACK, post_date INTEGER NOT NULL ON CONFLICT ROLLBACK, post_body TEXT NOT NULL, PRIMARY KEY (post_id AUTOINCREMENT), FOREIGN KEY (user_id) REFERENCES users (user_id), FOREIGN KEY (thread_id) REFERENCES threads (thread_id));
CREATE TABLE posts (
post_id INTEGER NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK,
user_id INTEGER NOT NULL ON CONFLICT ROLLBACK,
thread_id INTEGER NOT NULL ON CONFLICT ROLLBACK,
post_date INTEGER NOT NULL ON CONFLICT ROLLBACK,
PRIMARY KEY (
post_id AUTOINCREMENT
),
FOREIGN KEY (
user_id
)
REFERENCES users (user_id),
FOREIGN KEY (
thread_id
)
REFERENCES threads (thread_id)
);
-- Table: sessions -- Table: sessions
DROP TABLE IF EXISTS sessions; DROP TABLE IF EXISTS sessions;
CREATE TABLE sessions ( CREATE TABLE sessions (
session_key TEXT NOT NULL session_key TEXT NOT NULL
UNIQUE UNIQUE
@ -57,25 +28,12 @@ CREATE TABLE sessions (
bound_ip TEXT bound_ip TEXT
); );
-- Table: subforums -- Table: subforums
DROP TABLE IF EXISTS subforums; DROP TABLE IF EXISTS subforums;
CREATE TABLE subforums (subf_id INTEGER PRIMARY KEY UNIQUE ON CONFLICT ROLLBACK NOT NULL ON CONFLICT ROLLBACK, subf_cat INTEGER REFERENCES categories (cat_id) NOT NULL, subf_rank INTEGER NOT NULL, subf_name TEXT NOT NULL ON CONFLICT ROLLBACK, subf_desc TEXT);
CREATE TABLE subforums (
subf_id INTEGER PRIMARY KEY
UNIQUE ON CONFLICT ROLLBACK
NOT NULL ON CONFLICT ROLLBACK,
subf_cat INTEGER REFERENCES categories (cat_id)
UNIQUE ON CONFLICT ROLLBACK
NOT NULL ON CONFLICT ROLLBACK,
subf_name TEXT NOT NULL ON CONFLICT ROLLBACK,
subf_desc
);
-- Table: threads -- Table: threads
DROP TABLE IF EXISTS threads; DROP TABLE IF EXISTS threads;
CREATE TABLE threads ( CREATE TABLE threads (
thread_id INTEGER NOT NULL ON CONFLICT ROLLBACK, thread_id INTEGER NOT NULL ON CONFLICT ROLLBACK,
thread_title TEXT NOT NULL ON CONFLICT ROLLBACK, thread_title TEXT NOT NULL ON CONFLICT ROLLBACK,
@ -85,10 +43,8 @@ CREATE TABLE threads (
) )
); );
-- Table: users -- Table: users
DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS users;
CREATE TABLE users ( CREATE TABLE users (
user_id INTEGER NOT NULL ON CONFLICT ROLLBACK user_id INTEGER NOT NULL ON CONFLICT ROLLBACK
UNIQUE ON CONFLICT ROLLBACK, UNIQUE ON CONFLICT ROLLBACK,
@ -105,6 +61,5 @@ CREATE TABLE users (
ON CONFLICT ABORT ON CONFLICT ABORT
); );
COMMIT TRANSACTION; COMMIT TRANSACTION;
PRAGMA foreign_keys = on; PRAGMA foreign_keys = on;

View File

@ -20,10 +20,10 @@ sub startup {
{file => 'charmboard.conf'}); {file => 'charmboard.conf'});
# set this specific forum's name # set this specific forum's name
$self->helper(boardName => sub {$config->{board_name}}); $self->helper(board_name => sub {$config->{board_name}});
# load dev env only stuff, if applicable # load dev env only stuff, if applicable
if ( $config->{environment} eq 'dev' ) { if ($config->{environment} eq 'dev') {
$self->plugin('Renderer::WithoutCache'); $self->plugin('Renderer::WithoutCache');
$self->renderer->cache->max_keys(0)}; $self->renderer->cache->max_keys(0)};
@ -31,18 +31,19 @@ sub startup {
$self->secrets($config->{secrets}); $self->secrets($config->{secrets});
# import password pepper value # import password pepper value
$self->helper(pepper => sub {$config->{pass_crypt}->{pepper}}); $self->helper(
pepper => sub {$config->{pass_crypt}->{pepper}});
## database setup ## database setup
# ? this could maybe be a given/when # ? this could maybe be a given/when
my ($dsn, $dbUnicode); { my ($_dsn, $_unicode);
if ($self->config->{database}->{type} ~~ 'sqlite') { if ($self->config->{database}->{type} ~~ 'sqlite') {
$dsn = "dbi:SQLite:" . $config->{database}->{name}; $_dsn = "dbi:SQLite:" . $config->{database}->{name};
$dbUnicode = "sqlite_unicode"} $_unicode = "sqlite_unicode"}
elsif ($self->config->{database}->{type} ~~ 'mariadb') { elsif ($self->config->{database}->{type} ~~ 'mariadb') {
$dsn = "dbi:mysql:" . $config->{database}->{name}; $_dsn = "dbi:mysql:" . $config->{database}->{name};
$dbUnicode = "mysql_enable_utf"} $_unicode = "mysql_enable_utf"}
else {die "\nUnknown, unsupported, or empty database type else {die "\nUnknown, unsupported, or empty database type
in charmboard.conf. If you're sure you've set it to in charmboard.conf. If you're sure you've set it to
@ -50,16 +51,22 @@ sub startup {
\n\n\t \n\n\t
Valid options: 'sqlite', 'mariadb'"}; Valid options: 'sqlite', 'mariadb'"};
my $schema = CharmBoard::Schema->connect( our $schema = CharmBoard::Schema->connect(
$dsn, $_dsn,
$config->{database}->{user}, $config->{database}->{user},
$config->{database}->{pass}, $config->{database}->{pass},
{$dbUnicode => 1}); {$_unicode => 1});
$self->helper(schema => sub {$schema});
$self->helper(schema => sub {$schema})}
# router # router
my $r = $self->routes; my $r = $self->routes;
# view subforum
$r->get('/subforum/:id')->to(
controller => 'Controller::ViewSubf',
action => 'subf_view');
# controller routes # controller routes
## index page ## index page
$r->get('/')->to( $r->get('/')->to(
@ -89,5 +96,24 @@ sub startup {
} }
1; 1;
__END__ __END__
=pod
=head1 NAME
CharmBoard - revive the fun posting experience!
=head1 NOTES
This documentation is intended for prospective code
contributors. If you're looking to set CharmBoard up,
look for the Markdown format (.md) documentation instead.
CharmBoard uses a max line length of 60 chars and a tab
size of two spaces.
=head1 DESCRIPTION
CharmBoard is forum software written in Perl with
Mojolicious, intended to be a more fun alternative to the
bigger forum suites available today, inspired by older
forum software like AcmlmBoard, while also being more
modernized in terms of security practices than they are.
=cut

View File

@ -12,34 +12,34 @@ sub index {
my $self = shift; my $self = shift;
# fetch a list of all categories # fetch a list of all categories
my @allCat = my @all_cat =
$self->schema->resultset('Categories')->fetch_all; $self->schema->resultset('Categories')->fetch_all;
# create a Tree::Simple object that will contain the list # create a Tree::Simple object that will contain the list
# of categories and the subforums that belong to them # of categories and the subforums that belong to them
my $tree = my $tree =
Tree::Simple->new("ROOT", Tree::Simple->ROOT); Tree::Simple->new("subf_list", Tree::Simple->ROOT);
my (@fetchSubf, $catBranch); my (@fetch_subf, $cat_branch);
foreach my $iterCat (@allCat) { foreach my $iter_cat (@all_cat) {
# create branch of ROOT for the current category # create branch of subf_list for the current category
$catBranch = $cat_branch =
Tree::Simple->new($iterCat, $tree); Tree::Simple->new($iter_cat, $tree);
# fetch all subforums that belong to this category # fetch all subforums that belong to this category
@fetchSubf = @fetch_subf =
$self->schema->resultset('Subforums') $self->schema->resultset('Subforums')
->fetch_by_cat($iterCat); ->fetch_by_cat($iter_cat);
# add each fetched subforum as children of the branch # add each fetched subforum as children of the branch
# for the current category # for the current category
foreach my $iterSubf (@fetchSubf) { foreach my $iter_subf (@fetch_subf) {
Tree::Simple->new($iterSubf, $catBranch)}} Tree::Simple->new($iter_subf, $cat_branch)}}
$self->render( $self->render(
template => 'index', template => 'index',
categoryTree => $tree)} category_tree => $tree)}
1; 1;
__END__ __END__

View File

@ -22,54 +22,54 @@ sub login_do {
my $username = $self->param('username'); my $username = $self->param('username');
my $password = $self->pepper . ':' . $self->param('password'); my $password = $self->pepper . ':' . $self->param('password');
my $catchError; my $catch_error;
# declare vars used through multiple try/catch blocks with # declare vars used through multiple try/catch blocks with
# 'our' so they work throughout the entire subroutine # 'our' so they work throughout the entire subroutine
our ($userInfo, $passCheck, $userID, $sessionKey); our ($user_info, $pass_check, $user_id, $session_key);
try { # check user credentials first try { # check user credentials first
# check to see if user by entered username exists # check to see if user by entered username exists
$userInfo = $self->schema->resultset('Users')->search( $user_info = $self->schema->resultset('Users')->search(
{username => $username}); {username => $username});
$userInfo or die; $user_info or die;
# now check password validity # now check password validity
$passCheck = passchk($userInfo->get_column('salt')->first, $pass_check = passchk($user_info->get_column('salt')->first,
$userInfo->get_column('password')->first, $password); $user_info->get_column('password')->first, $password);
$passCheck or die;} $pass_check or die;}
catch ($catchError) { # redirect to login page on fail catch ($catch_error) { # redirect to login page on fail
print $catchError; print $catch_error;
$self->flash(error => 'Username or password incorrect.'); $self->flash(error => 'Username or password incorrect.');
$self->redirect_to('login');} $self->redirect_to('login');}
try { # now attempt to create session try { # now attempt to create session
# get user ID for session creation # get user ID for session creation
$userID = $userInfo->get_column('user_id')->first; $user_id = $user_info->get_column('user_id')->first;
# gen session key # gen session key
$sessionKey = seasoning(16); $session_key = seasoning(16);
# add session to database # add session to database
$self->schema->resultset('Session')->create({ $self->schema->resultset('Session')->create({
session_key => $sessionKey, session_key => $session_key,
user_id => $userID, user_id => $user_id,
session_expiry => time + 604800, session_expiry => time + 604800,
is_ip_bound => 0, is_ip_bound => 0,
bound_ip => undef }) or die; bound_ip => undef }) or die;
# now create session cookie for user # now create session cookie for user
$self->session(is_auth => 1); $self->session(is_auth => 1);
$self->session(user_id => $userID); $self->session(user_id => $user_id);
$self->session(session_key => $sessionKey); $self->session(session_key => $session_key);
$self->session(expiration => 604800); $self->session(expiration => 604800);
# redirect to index upon success # redirect to index upon success
$self->redirect_to('/')} $self->redirect_to('/')}
catch ($catchError) { # redirect to login page on fail catch ($catch_error) { # redirect to login page on fail
print $catchError; print $catch_error;
$self->flash(error => 'Your username and password were correct, $self->flash(error => 'Your username and password were correct,
but a server error prevented you from logging in. This has been but a server error prevented you from logging in. This has been
logged so the administrator can fix it.'); logged so the administrator can fix it.');

View File

@ -0,0 +1,32 @@
package CharmBoard::Controller::ViewSubf;
use utf8;
use strict;
use warnings;
use experimental qw(try smartmatch);
use Mojo::Base 'Mojolicious::Controller', -signatures;
sub subf_view {
my $self = shift;
my $subf_id = $self->param('id');
my $subf_cat =
$self->schema->resultset('Subforums')->cat_from_id($subf_id);
my $cat_title =
$self->schema->resultset('Categories')
->title_from_id($subf_cat);
my @thread_list =
$self->schema->resultset('Threads')->fetch_by_subf($subf_id);
$self->render(
template => 'subf',
subf_id => $subf_id,
cat_title => $cat_title,
subf_title =>
$self->schema->resultset('Subforums')
->title_from_id($subf_id),
thread_list => \@thread_list)}
1;

View File

@ -8,21 +8,29 @@ use experimental qw(try smartmatch);
use base 'DBIx::Class::ResultSet'; use base 'DBIx::Class::ResultSet';
sub fetch_all { sub fetch_all {
my $set = shift; my $_set = shift;
my $_fetch = my $_fetch =
$set->search({}, $_set->search({},
{order_by => 'cat_rank'}); {order_by => 'cat_rank'});
return($_fetch->get_column('cat_id')->all)} return($_fetch->get_column('cat_id')->all)}
sub title_from_id { sub title_from_id {
my $set = shift; my $_set = shift;
return( return(
$set->search({'cat_id' => $_[0]})-> $_set->search({'cat_id' => $_[0]})->
get_column('cat_name')->first)} get_column('cat_name')->first)}
1; 1;
__END__ __END__
=pod
=head1 NAME
CharmBoard::Schema::Set::Categories - DBIC ResultSet for
the categories table
=head1 SYNOPSIS
=head1 DESCRIPTION
=cut

View File

@ -8,20 +8,27 @@ use experimental qw(try smartmatch);
use base 'DBIx::Class::ResultSet'; use base 'DBIx::Class::ResultSet';
sub fetch_by_cat { sub fetch_by_cat {
my $set = shift; my $_set = shift;
my $fetch = my $_fetch =
$set->search( $_set->search(
{'subf_cat' => $_[0] }, {'subf_cat' => $_[0] },
{order_by => 'subf_rank'}); {order_by => 'subf_rank'});
return($fetch->get_column('subf_id')->all)} return($_fetch->get_column('subf_id')->all)}
sub title_from_id { sub cat_from_id {
my $set = shift; my $_set = shift;
return( return(
$set->search({'subf_id' => $_[0]})-> $_set->search({'subf_id' => $_[0]})->
get_column('subf_cat')->first)}
sub title_from_id {
my $_set = shift;
return(
$_set->search({'subf_id' => $_[0]})->
get_column('subf_name')->first)} get_column('subf_name')->first)}
1; 1;

View File

@ -0,0 +1,26 @@
package CharmBoard::Schema::Set::Threads;
use utf8;
use strict;
use warnings;
use experimental qw(try smartmatch);
use base 'DBIx::Class::ResultSet';
sub fetch_by_subf {
my $_set = shift;
my $_fetch =
$_set->search({'thread_subf' => $_[0]});
return($_fetch->get_column('thread_id')->all)}
sub title_from_id {
my $_set = shift;
return(
$_set->search({'thread_id' => $_[0]})->
get_column('thread_title')->first)}
1;
__END__

View File

@ -16,6 +16,10 @@ __PACKAGE__->add_columns(
thread_title => { thread_title => {
data_type => 'text', data_type => 'text',
is_nullable => 0, }, is_nullable => 0, },
thread_op => {
data_type => 'integer',
is_foreign_key => 1,
is_nullable => 0, },
thread_subf => { thread_subf => {
data_type => 'integer', data_type => 'integer',
is_foreign_key => 1, is_foreign_key => 1,
@ -27,6 +31,9 @@ __PACKAGE__->belongs_to(
thread_subf => thread_subf =>
'CharmBoard::Schema::Source::Subforums', 'CharmBoard::Schema::Source::Subforums',
{'foreign.subf_id' => 'self.thread_subf'}); {'foreign.subf_id' => 'self.thread_subf'});
__PACKAGE__->belongs_to(
thread_op => 'CharmBoard::Schema::Source::Posts',
{'foreign.post_id' => 'self.thread_op'});
1; 1;
__END__ __END__

View File

@ -14,25 +14,4 @@ Mojolicious::Commands->start_app('CharmBoard');
__END__ __END__
=pod
=head1 NAME
CharmBoard - revive the fun posting experience!
=head1 NOTES
This documentation is intended for prospective code
contributors. If you're looking to set CharmBoard up,
look for the Markdown format (.md) documentation instead.
CharmBoard uses a max line length of 60 chars and a tab
size of two spaces.
=head1 DESCRIPTION
CharmBoard is forum software written in Perl with
Mojolicious, intended to be a more fun alternative to the
bigger forum suites available today, inspired by older
forum software like AcmlmBoard, while also being more
modernized in terms of security practices than they are.
Customization ability is another important goal next to
making software that feels fun for the end user to use.
=cut

View File

@ -1,31 +1,30 @@
% layout 'default', title => $self->boardName; % layout 'default', title => $self->board_name;
<% my $catHeader = begin %> <% my $cat_header = begin %>
% my $_catID = shift; my $_name = shift; % my $_cat_id = shift; my $_name = shift;
<div class="category-header category-<%= $_catID %>"> <div class="category-header category-<%= $_cat_id %>">
<b><%= $_name %></b> <b><%= $_name %></b>
</div> </div>
<% end %> <% end %>
<% my $subfItem = begin %> <% my $subf_item = begin %>
% my $_subfID = shift; my $_catID = shift; % my $_subf_id = shift; my $_cat_id = shift;
% my $_name = shift; % my $_name = shift;
<div class=" <div class="
subforum-item subforum-<%= $_subfID %> subforum-item subforum-<%= $_subf_id %>
category-<%= $_catID %> category-<%= $_cat_id %>
"><%= $_name %></div> "><a href="/subforum/<%= $_subf_id %>"><%= $_name %></a></div>
<% end %> <% end %>
<% <%
foreach my $category ($categoryTree->getAllChildren) { %> foreach my $category ($category_tree->getAllChildren) { %>
<%= $catHeader->( <%= $cat_header->(
$category->getNodeValue, $category->getNodeValue,
$self->schema->resultset('Categories')-> $self->schema->resultset('Categories')->
title_from_id($category->getNodeValue)) %> title_from_id($category->getNodeValue)) %>
<% say ('$category: ' . $category); %>
<% <%
foreach my $subforum ($category->getAllChildren) { %> foreach my $subforum ($category->getAllChildren) { %>
<%= $subfItem->( <%= $subf_item->(
$subforum->getNodeValue, $subforum->getNodeValue,
$category->getNodeValue, $category->getNodeValue,
$self->schema->resultset('Subforums')-> $self->schema->resultset('Subforums')->

View File

@ -10,5 +10,5 @@ else {
"<a href=\"/login\">login</a> | "<a href=\"/login\">login</a> |
<a href=\"/register\">register</a>"}; <a href=\"/register\">register</a>"};
%> %>
<a href="/"><h2><%== $self->boardName %></h2></a> <a href="/"><h2><%== $self->board_name %></h2></a>
<%== $userControls %><br /><br /> <%== $userControls %><br /><br />

View File

@ -1,4 +1,4 @@
% layout 'default', title => $self->boardName . ' - Login'; % layout 'default', title => $self->board_name . ' - Login';
% if ($error) { % if ($error) {
<p style="color: red"><%= $error %></p> <p style="color: red"><%= $error %></p>
%}; %};

View File

@ -1,4 +1,4 @@
% layout 'default', title => $self->boardName . ' - Registration'; % layout 'default', title => $self->board_name . ' - Registration';
% if ($error) { % if ($error) {
<p style="color: red"><%= $error %></p> <p style="color: red"><%= $error %></p>
%}; %};

23
templates/subf.html.ep Normal file
View File

@ -0,0 +1,23 @@
% layout 'default', title => $subf_title . ' - ' . $self->board_name;
% my @thread_list = @{stash('thread_list')};
<% my $thread_item = begin %>
% my $_thread_id = shift; my $_thread_title = shift;
<div class=" thread-item thread-<%= $_thread_id %>">
<a href="/thread/<%= $_thread_id %>"></a>
</div>
<% end %>
<a href="/"><%= $self->board_name %></a> » <%= $cat_title %> » <%= $subf_title %>
<br /><br />
<% if (! @thread_list) { %>
Oops! Looks like there's no threads here yet. Maybe you'd
like to <a href="/thread/new/<%= $subf_id %>">make one?</a>
<% } else {
foreach my $thread_id (@thread_list) { %>
<%= $thread_item->(
$thread_id,
$self->schema->resultset('Threads')->
title_from_id($thread_id)) %>
<% }} %>