Merge branch 'search'
This commit is contained in:
commit
3342ad40c6
|
@ -61,7 +61,7 @@ tests locally:
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
1. Full text search + archive old posts
|
1. Winter/Christmas/New Years background
|
||||||
1. "All new posts flagged" mode (require approval for new posts)
|
1. "All new posts flagged" mode (require approval for new posts)
|
||||||
1. Tests for mod-only user?
|
1. Tests for mod-only user?
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,8 @@ body {
|
||||||
/* background-image: url('/images/topwwbackground.gif'); */
|
/* background-image: url('/images/topwwbackground.gif'); */
|
||||||
/* Spooky time! */
|
/* Spooky time! */
|
||||||
background-image: url('/images/halloween_background_1.gif');
|
background-image: url('/images/halloween_background_1.gif');
|
||||||
|
/* Christmas/New Years-ish? */
|
||||||
|
/* background-image: url('/images/windowstars.gif'); */
|
||||||
width: 95vmin;
|
width: 95vmin;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-family: 'w95fa', sans-serif;
|
font-family: 'w95fa', sans-serif;
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
{
|
{
|
||||||
|
max_thread_pages => 10,
|
||||||
threads_per_page => 5,
|
threads_per_page => 5,
|
||||||
remarks_per_page => 5,
|
remarks_per_page => 5,
|
||||||
|
results_per_page => 5,
|
||||||
body_max_length => 8_000,
|
body_max_length => 8_000,
|
||||||
secrets => ['t0p_s3cr3t'],
|
secrets => ['t0p_s3cr3t'],
|
||||||
development => {
|
development => {
|
||||||
pg_string =>
|
pg_string =>
|
||||||
'postgresql://post_text:t0p_s3cr3t@127.0.0.1/post_text'
|
'postgresql://post_text:t0p_s3cr3t@127.0.0.1/post_text'
|
||||||
},
|
},
|
||||||
|
production => {
|
||||||
|
pg_string =>
|
||||||
|
'postgresql://post_text:t0p_s3cr3t@127.0.0.1/post_text'
|
||||||
|
},
|
||||||
'TagHelpers-Pagination', {
|
'TagHelpers-Pagination', {
|
||||||
separator => ' ',
|
separator => ' ',
|
||||||
current => '<strong><u>{current}</u></strong>',
|
current => '<strong><u>{current}</u></strong>',
|
||||||
|
|
|
@ -13,6 +13,7 @@ use HTML::Restrict;
|
||||||
use PostText::Model::Thread;
|
use PostText::Model::Thread;
|
||||||
use PostText::Model::Remark;
|
use PostText::Model::Remark;
|
||||||
use PostText::Model::Moderator;
|
use PostText::Model::Moderator;
|
||||||
|
use PostText::Model::Page;
|
||||||
|
|
||||||
sub startup($self) {
|
sub startup($self) {
|
||||||
$self->plugin('Config');
|
$self->plugin('Config');
|
||||||
|
@ -59,6 +60,10 @@ sub startup($self) {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$self->helper(page => sub ($c) {
|
||||||
|
state $moderator = PostText::Model::Page->new(pg => $c->pg)
|
||||||
|
});
|
||||||
|
|
||||||
$self->helper(truncate_text => sub ($c, $input_text) {
|
$self->helper(truncate_text => sub ($c, $input_text) {
|
||||||
my $truncated_text = 299 < length($input_text)
|
my $truncated_text = 299 < length($input_text)
|
||||||
? substr($input_text, 0, 299) . '…' : $input_text;
|
? substr($input_text, 0, 299) . '…' : $input_text;
|
||||||
|
@ -85,7 +90,7 @@ sub startup($self) {
|
||||||
# Finish configuring some things
|
# Finish configuring some things
|
||||||
$self->secrets($self->config->{'secrets'}) || die $@;
|
$self->secrets($self->config->{'secrets'}) || die $@;
|
||||||
|
|
||||||
$self->pg->migrations->from_dir('migrations')->migrate(14);
|
$self->pg->migrations->from_dir('migrations')->migrate(15);
|
||||||
|
|
||||||
if (my $threads_per_page = $self->config->{'threads_per_page'}) {
|
if (my $threads_per_page = $self->config->{'threads_per_page'}) {
|
||||||
$self->thread->per_page($threads_per_page)
|
$self->thread->per_page($threads_per_page)
|
||||||
|
@ -95,6 +100,14 @@ sub startup($self) {
|
||||||
$self->remark->per_page($remarks_per_page)
|
$self->remark->per_page($remarks_per_page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (my $results_per_page = $self->config->{'results_per_page'}) {
|
||||||
|
$self->page->per_page($results_per_page)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (my $max_thread_pages = $self->config->{'max_thread_pages'}) {
|
||||||
|
$self->thread->max_pages($max_thread_pages)
|
||||||
|
}
|
||||||
|
|
||||||
$self->asset->process;
|
$self->asset->process;
|
||||||
|
|
||||||
push @{$self->commands->namespaces}, 'PostText::Command';
|
push @{$self->commands->namespaces}, 'PostText::Command';
|
||||||
|
@ -116,7 +129,7 @@ sub startup($self) {
|
||||||
|
|
||||||
return $c->redirect_to(
|
return $c->redirect_to(
|
||||||
captcha_page => return_url =>
|
captcha_page => return_url =>
|
||||||
b64_encode gzip $c->url_for->to_abs->to_string
|
b64_encode gzip $c->url_with->to_abs->to_string
|
||||||
), undef;
|
), undef;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -137,6 +150,9 @@ sub startup($self) {
|
||||||
|
|
||||||
$r->get('/feeds')->to('page#feeds')->name('feeds_page');
|
$r->get('/feeds')->to('page#feeds')->name('feeds_page');
|
||||||
|
|
||||||
|
# Not-so-static but I mean they're all 'pages' c'mon
|
||||||
|
$human->get('/search')->to('page#search')->name('search_page');
|
||||||
|
|
||||||
$r->any([qw{GET POST}], '/captcha/*return_url')
|
$r->any([qw{GET POST}], '/captcha/*return_url')
|
||||||
->to('page#captcha')
|
->to('page#captcha')
|
||||||
->name('captcha_page');
|
->name('captcha_page');
|
||||||
|
|
|
@ -7,7 +7,7 @@ sub flagged($self) {
|
||||||
my @post_links = map {
|
my @post_links = map {
|
||||||
$self->url_for(
|
$self->url_for(
|
||||||
'hidden_' . $_->{'type'}, $_->{'type'} . '_id' => $_->{'id'}
|
'hidden_' . $_->{'type'}, $_->{'type'} . '_id' => $_->{'id'}
|
||||||
)
|
)
|
||||||
} @{$flagged_posts};
|
} @{$flagged_posts};
|
||||||
|
|
||||||
$self->stash(post_links => \@post_links);
|
$self->stash(post_links => \@post_links);
|
||||||
|
@ -20,7 +20,7 @@ sub hidden($self) {
|
||||||
my @post_links = map {
|
my @post_links = map {
|
||||||
$self->url_for(
|
$self->url_for(
|
||||||
'hidden_' . $_->{'type'}, $_->{'type'} . '_id' => $_->{'id'}
|
'hidden_' . $_->{'type'}, $_->{'type'} . '_id' => $_->{'id'}
|
||||||
)
|
)
|
||||||
} @{$hidden_posts};
|
} @{$hidden_posts};
|
||||||
|
|
||||||
$self->stash(post_links => \@post_links);
|
$self->stash(post_links => \@post_links);
|
||||||
|
@ -57,7 +57,7 @@ sub login($self) {
|
||||||
author => $mod_name,
|
author => $mod_name,
|
||||||
is_admin => $admin_status
|
is_admin => $admin_status
|
||||||
);
|
);
|
||||||
$self->flash(info => "Hello, $mod_name 😎");
|
$self->flash(info => "Hello, $mod_name. 😎");
|
||||||
$self->moderator->login_timestamp($mod_id);
|
$self->moderator->login_timestamp($mod_id);
|
||||||
|
|
||||||
return $self->redirect_to('flagged_list');
|
return $self->redirect_to('flagged_list');
|
||||||
|
@ -77,7 +77,7 @@ sub login($self) {
|
||||||
sub logout($self) {
|
sub logout($self) {
|
||||||
delete $self->session->%{qw(mod_id is_admin)};
|
delete $self->session->%{qw(mod_id is_admin)};
|
||||||
|
|
||||||
$self->flash(info => 'Logged out successfully 👋');
|
$self->flash(info => 'Logged out successfully. 👋');
|
||||||
|
|
||||||
$self->redirect_to('threads_list');
|
$self->redirect_to('threads_list');
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ sub create($self) {
|
||||||
my $password = $v->param('password');
|
my $password = $v->param('password');
|
||||||
|
|
||||||
$self->moderator->create($name, $email, $password);
|
$self->moderator->create($name, $email, $password);
|
||||||
$self->stash(info => "Created moderator account for $name 🧑🏭");
|
$self->stash(info => "Created moderator account for $name. 🧑🏭");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ sub admin_reset($self) {
|
||||||
my $password = $v->param('password');
|
my $password = $v->param('password');
|
||||||
|
|
||||||
$self->moderator->admin_reset($email, $password);
|
$self->moderator->admin_reset($email, $password);
|
||||||
$self->stash(info => "Reset password for $email 🔐");
|
$self->stash(info => "Reset password for $email. 🔐");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ sub mod_reset($self) {
|
||||||
my $mod_id = $self->session->{'mod_id'};
|
my $mod_id = $self->session->{'mod_id'};
|
||||||
|
|
||||||
$self->moderator->mod_reset($mod_id, $password);
|
$self->moderator->mod_reset($mod_id, $password);
|
||||||
$self->flash(info => "Password has been reset 🔐");
|
$self->flash(info => "Password has been reset. 🔐");
|
||||||
|
|
||||||
return $self->redirect_to('flagged_list');
|
return $self->redirect_to('flagged_list');
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@ sub lock_acct($self) {
|
||||||
my $email = $v->param('email');
|
my $email = $v->param('email');
|
||||||
|
|
||||||
$self->moderator->lock_acct($email);
|
$self->moderator->lock_acct($email);
|
||||||
$self->stash(info => "Account $email has been locked 🔒");
|
$self->stash(info => "Account $email has been locked. 🔒");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ sub unlock_acct($self) {
|
||||||
my $email = $v->param('email');
|
my $email = $v->param('email');
|
||||||
|
|
||||||
$self->moderator->unlock_acct($email);
|
$self->moderator->unlock_acct($email);
|
||||||
$self->stash(info => "Account $email has been unlocked 🔓");
|
$self->stash(info => "Account $email has been unlocked. 🔓");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@ sub promote($self) {
|
||||||
my $email = $v->param('email');
|
my $email = $v->param('email');
|
||||||
|
|
||||||
$self->moderator->promote($email);
|
$self->moderator->promote($email);
|
||||||
$self->stash(info => "Account $email has been promoted to admin 🧑🎓");
|
$self->stash(info => "Account $email has been promoted to admin. 🧑🎓");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ sub demote($self) {
|
||||||
my $email = $v->param('email');
|
my $email = $v->param('email');
|
||||||
|
|
||||||
$self->moderator->demote($email);
|
$self->moderator->demote($email);
|
||||||
$self->stash(info => "Account $email has been demoted to mod 🧒");
|
$self->stash(info => "Account $email has been demoted to mod. 🧒");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ sub thread_by_id($self) {
|
||||||
|
|
||||||
$self->stash(thread => $thread);
|
$self->stash(thread => $thread);
|
||||||
|
|
||||||
$self->stash(status => 404, error => 'Thread not found 🤷')
|
$self->stash(status => 404, error => 'Thread not found. 🤷')
|
||||||
unless keys %{$thread};
|
unless keys %{$thread};
|
||||||
|
|
||||||
$self->render;
|
$self->render;
|
||||||
|
@ -338,7 +338,7 @@ sub remark_by_id($self) {
|
||||||
|
|
||||||
$self->stash(remark => $remark);
|
$self->stash(remark => $remark);
|
||||||
|
|
||||||
$self->stash(status => 404, error => 'Remark not found 🤷')
|
$self->stash(status => 404, error => 'Remark not found. 🤷')
|
||||||
unless keys %{$remark};
|
unless keys %{$remark};
|
||||||
|
|
||||||
$self->render;
|
$self->render;
|
||||||
|
|
|
@ -50,4 +50,38 @@ sub captcha($self) {
|
||||||
$self->render;
|
$self->render;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub search($self) {
|
||||||
|
my $v = $self->validation;
|
||||||
|
my $search_results = [];
|
||||||
|
my ($search_query, $this_page, $last_page, $base_path);
|
||||||
|
|
||||||
|
if ($v->has_data) {
|
||||||
|
$v->required('q' )->size(1, 2_047);
|
||||||
|
$v->optional('page');
|
||||||
|
|
||||||
|
if ($v->has_error) {
|
||||||
|
$self->stash(status => 400)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$search_query = $v->param('q');
|
||||||
|
$this_page = $v->param('page') || 1;
|
||||||
|
$last_page = $self->page->last_page_for($search_query);
|
||||||
|
$base_path = $self->url_for->query(q => $search_query);
|
||||||
|
$search_results = $self->page->search($search_query, $this_page);
|
||||||
|
|
||||||
|
$self->stash(status => 404, error => 'No posts found. 🔎')
|
||||||
|
unless scalar @{$search_results};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$self->stash(
|
||||||
|
this_page => $this_page,
|
||||||
|
last_page => $last_page,
|
||||||
|
base_path => $base_path,
|
||||||
|
search_results => $search_results
|
||||||
|
);
|
||||||
|
|
||||||
|
$self->render;
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -10,7 +10,7 @@ sub by_id($self) {
|
||||||
|
|
||||||
$self->stash(remark => $remark);
|
$self->stash(remark => $remark);
|
||||||
|
|
||||||
$self->stash(status => 404, error => 'Remark not found 🤷')
|
$self->stash(status => 404, error => 'Remark not found. 🤷')
|
||||||
unless keys %{$remark};
|
unless keys %{$remark};
|
||||||
|
|
||||||
# Set filename for right-click & save-as behavior
|
# Set filename for right-click & save-as behavior
|
||||||
|
@ -79,7 +79,7 @@ sub create($self) {
|
||||||
body_limit => $body_limit
|
body_limit => $body_limit
|
||||||
);
|
);
|
||||||
|
|
||||||
$self->stash(status => 404, error => 'Thread not found 🤷')
|
$self->stash(status => 404, error => 'Thread not found. 🤷')
|
||||||
unless keys %{$thread};
|
unless keys %{$thread};
|
||||||
|
|
||||||
return $self->render;
|
return $self->render;
|
||||||
|
|
|
@ -67,12 +67,12 @@ sub by_id($self) {
|
||||||
remarks => $remarks
|
remarks => $remarks
|
||||||
);
|
);
|
||||||
|
|
||||||
$self->stash(status => 404, error => 'Thread not found 🤷')
|
$self->stash(status => 404, error => 'Thread not found. 🤷')
|
||||||
unless keys %{$thread};
|
unless keys %{$thread};
|
||||||
|
|
||||||
# Check for remarks or thread page number to make sure
|
# Check for remarks or thread page number to make sure
|
||||||
# remark->by_page_for did its job
|
# remark->by_page_for did its job
|
||||||
$self->stash(status => 404, error => 'Page not found 🕵️')
|
$self->stash(status => 404, error => 'Page not found. 🕵️')
|
||||||
unless scalar @{$remarks} || $this_page == $last_page;
|
unless scalar @{$remarks} || $this_page == $last_page;
|
||||||
|
|
||||||
# Set filename for right-click & save-as behavior
|
# Set filename for right-click & save-as behavior
|
||||||
|
@ -98,7 +98,7 @@ sub by_page($self) {
|
||||||
base_path => $base_path
|
base_path => $base_path
|
||||||
);
|
);
|
||||||
|
|
||||||
$self->stash(status => 404, error => 'Page not found 🕵️')
|
$self->stash(status => 404, error => 'Page not found. 🕵️')
|
||||||
unless scalar @{$threads};
|
unless scalar @{$threads};
|
||||||
|
|
||||||
$self->render;
|
$self->render;
|
||||||
|
|
64
lib/PostText/Model/Page.pm
Normal file
64
lib/PostText/Model/Page.pm
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package PostText::Model::Page;
|
||||||
|
|
||||||
|
use Mojo::Base -base, -signatures;
|
||||||
|
|
||||||
|
has 'pg';
|
||||||
|
|
||||||
|
has per_page => 5;
|
||||||
|
|
||||||
|
has date_format => 'Dy, FMDD Mon YYYY HH24:MI:SS TZHTZM';
|
||||||
|
|
||||||
|
sub search($self, $search_query, $this_page = 1) {
|
||||||
|
my $date_format = $self->date_format;
|
||||||
|
my $row_count = $self->per_page;
|
||||||
|
my $offset = ($this_page - 1) * $row_count;
|
||||||
|
my @data = ($date_format, $search_query, $row_count, $offset);
|
||||||
|
|
||||||
|
$self->pg->db->query(<<~'END_SQL', @data)->hashes;
|
||||||
|
SELECT 'thread' AS post_type,
|
||||||
|
thread_id AS post_id,
|
||||||
|
TO_CHAR(thread_date, $1) AS post_date,
|
||||||
|
thread_author AS post_author,
|
||||||
|
thread_body AS post_body,
|
||||||
|
TS_RANK(search_tokens, PLAINTO_TSQUERY('english', $2)) AS search_rank
|
||||||
|
FROM threads
|
||||||
|
WHERE search_tokens @@ PLAINTO_TSQUERY('english', $2)
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'remark',
|
||||||
|
remark_id,
|
||||||
|
TO_CHAR(remark_date, $1),
|
||||||
|
remark_author,
|
||||||
|
remark_body,
|
||||||
|
TS_RANK(search_tokens, PLAINTO_TSQUERY('english', $2))
|
||||||
|
FROM remarks
|
||||||
|
WHERE search_tokens @@ PLAINTO_TSQUERY('english', $2)
|
||||||
|
ORDER BY search_rank DESC, post_date DESC
|
||||||
|
LIMIT $3 OFFSET $4;
|
||||||
|
END_SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
sub count_for($self, $search_query) {
|
||||||
|
$self->pg->db->query(<<~'END_SQL', $search_query)->hash->{'post_tally'}
|
||||||
|
SELECT COUNT(*) AS post_tally
|
||||||
|
FROM (SELECT thread_date AS post_date
|
||||||
|
FROM threads
|
||||||
|
WHERE search_tokens @@ PLAINTO_TSQUERY('english', $1)
|
||||||
|
UNION ALL
|
||||||
|
SELECT remark_date
|
||||||
|
FROM remarks
|
||||||
|
WHERE search_tokens @@ PLAINTO_TSQUERY('english', $1))
|
||||||
|
AS posts;
|
||||||
|
END_SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
sub last_page_for($self, $search_query) {
|
||||||
|
my $post_count = $self->count_for($search_query);
|
||||||
|
my $last_page = int($post_count / $self->per_page);
|
||||||
|
|
||||||
|
# Add a page for 'remainder' posts
|
||||||
|
$last_page++ if $post_count % $self->per_page;
|
||||||
|
|
||||||
|
return $last_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
1;
|
|
@ -2,7 +2,7 @@ package PostText::Model::Thread;
|
||||||
|
|
||||||
use Mojo::Base -base, -signatures;
|
use Mojo::Base -base, -signatures;
|
||||||
|
|
||||||
has [qw{pg hr}];
|
has [qw{pg hr max_pages}];
|
||||||
|
|
||||||
has per_page => 5;
|
has per_page => 5;
|
||||||
|
|
||||||
|
@ -53,11 +53,16 @@ sub by_page($self, $this_page = 1) {
|
||||||
sub last_page($self) {
|
sub last_page($self) {
|
||||||
my $thread_count = $self->count;
|
my $thread_count = $self->count;
|
||||||
my $last_page = int($thread_count / $self->per_page);
|
my $last_page = int($thread_count / $self->per_page);
|
||||||
|
my $max_pages = $self->max_pages;
|
||||||
|
|
||||||
# Add a page for 'remainder' posts
|
# Add a page for 'remainder' posts
|
||||||
$last_page++ if $thread_count % $self->per_page;
|
$last_page++ if $thread_count % $self->per_page;
|
||||||
|
|
||||||
$last_page;
|
if ($max_pages) {
|
||||||
|
$last_page = $max_pages if $last_page > $max_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
return $last_page;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub count($self) {
|
sub count($self) {
|
||||||
|
|
7
migrations/15/down.sql
Normal file
7
migrations/15/down.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
DROP EXTENSION pg_trgm;
|
||||||
|
|
||||||
|
ALTER TABLE threads
|
||||||
|
DROP COLUMN search_tokens;
|
||||||
|
|
||||||
|
ALTER TABLE remarks
|
||||||
|
DROP COLUMN search_tokens;
|
27
migrations/15/up.sql
Normal file
27
migrations/15/up.sql
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
-- Fuzzy search
|
||||||
|
-- https://hevodata.com/blog/postgresql-full-text-search-setup/#Fuzzy_Search_vs_Full_Text_Search
|
||||||
|
CREATE EXTENSION pg_trgm;
|
||||||
|
|
||||||
|
-- Create column for seearch tokens
|
||||||
|
ALTER TABLE threads
|
||||||
|
ADD COLUMN search_tokens TSVECTOR
|
||||||
|
GENERATED ALWAYS AS
|
||||||
|
(TO_TSVECTOR('english', thread_author) ||
|
||||||
|
TO_TSVECTOR('english', thread_title ) ||
|
||||||
|
TO_TSVECTOR('english', thread_body )) STORED;
|
||||||
|
|
||||||
|
-- Create GIN index for search tokens
|
||||||
|
CREATE INDEX threads_search_idx
|
||||||
|
ON threads
|
||||||
|
USING GIN(search_tokens);
|
||||||
|
|
||||||
|
-- Same for remarks
|
||||||
|
ALTER TABLE remarks
|
||||||
|
ADD COLUMN search_tokens TSVECTOR
|
||||||
|
GENERATED ALWAYS AS
|
||||||
|
(TO_TSVECTOR('english', remark_author) ||
|
||||||
|
TO_TSVECTOR('english', remark_body )) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX remarks_search_idx
|
||||||
|
ON remarks
|
||||||
|
USING GIN(search_tokens);
|
BIN
public/images/windowstars.gif
Normal file
BIN
public/images/windowstars.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 127 KiB |
34
t/search.t
Normal file
34
t/search.t
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use Mojo::Base -strict;
|
||||||
|
use Test::More;
|
||||||
|
use Test::Mojo;
|
||||||
|
|
||||||
|
my $t = Test::Mojo->new('PostText');
|
||||||
|
my $invalid_query = 'aaaaaaaa' x 300;
|
||||||
|
my %good_human = (answer => 1, number => 'Ⅰ');
|
||||||
|
my $search_url =
|
||||||
|
'/captcha/H4sIABJ8PGUAA8soKSmw0tfPyU9OzMnILy6xMjYwMNDPKM1NzNMvTk0sSs4AAPrUR3kiAAAA%0A';
|
||||||
|
|
||||||
|
subtest 'Search before CAPTCHA', sub {
|
||||||
|
$t->get_ok('/human/search')->status_is(302)
|
||||||
|
->header_like(Location => qr/captcha/);
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest 'Search after CAPTCHA', sub {
|
||||||
|
$t->post_ok($search_url, form => \%good_human)
|
||||||
|
->status_is(302)
|
||||||
|
->header_like(Location => qr{human/search});
|
||||||
|
|
||||||
|
$t->get_ok('/human/search')->status_is(200)
|
||||||
|
->text_like(h2 => qr/Search Posts/);
|
||||||
|
|
||||||
|
$t->get_ok('/human/search?q=aaaaaaaaaa')->status_is(404)
|
||||||
|
->text_like(p => qr/No posts found/);
|
||||||
|
|
||||||
|
$t->get_ok('/human/search?q=lmao')->status_is(200)
|
||||||
|
->text_like(h3 => qr/Results/);
|
||||||
|
|
||||||
|
$t->get_ok("/human/search?q=$invalid_query")->status_is(400)
|
||||||
|
->text_like(p => qr/Must be between/);
|
||||||
|
};
|
||||||
|
|
||||||
|
done_testing;
|
60
templates/page/search.html.ep
Normal file
60
templates/page/search.html.ep
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
% layout 'default';
|
||||||
|
% title 'Search Posts';
|
||||||
|
<% content_for open_graph => begin %>
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="<%= title %>">
|
||||||
|
<meta property="og:description" content="Search for posts.">
|
||||||
|
<% end %>
|
||||||
|
<% content_for twitter_card => begin %>
|
||||||
|
<meta name="twitter:title" content="<%= title %>">
|
||||||
|
<meta name="twitter:description" content="Search for posts.">
|
||||||
|
<% end %>
|
||||||
|
<h2 class="page-title"><%= title %></h2>
|
||||||
|
<form method="get" class="form-body">
|
||||||
|
<div class="form-field">
|
||||||
|
<% if (my $error = validation->error('q')) { =%>
|
||||||
|
<p class="field-with-error">Must be between <%= $error->[2] %>
|
||||||
|
and <%= $error->[3] %> characters.</p>
|
||||||
|
<% } =%>
|
||||||
|
<%= label_for search => 'Search' %>
|
||||||
|
<%= text_field q => (
|
||||||
|
id => 'search',
|
||||||
|
maxlength => 2047,
|
||||||
|
minlength => 1,
|
||||||
|
required => undef,
|
||||||
|
autofocus => undef
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="form-button">Search</button>
|
||||||
|
</form>
|
||||||
|
<% if (scalar @{$search_results}) { =%>
|
||||||
|
<main class="pager" id="results">
|
||||||
|
<h3 class="pager__title">Results</h3>
|
||||||
|
<% for my $result (@{$search_results}) { =%>
|
||||||
|
<article class="post">
|
||||||
|
<h4 class="post__title">
|
||||||
|
<span>
|
||||||
|
<%= $result->{'post_date'} %>
|
||||||
|
</span>
|
||||||
|
<% if ($result->{'post_type'} eq 'thread') { =%>
|
||||||
|
<%= link_to "#$result->{'post_id'}", single_thread =>
|
||||||
|
{thread_id => $result->{'post_id'}}, (class => 'post__id') %>
|
||||||
|
<% } else { =%>
|
||||||
|
<%= link_to "#$result->{'post_id'}", single_remark =>
|
||||||
|
{remark_id => $result->{'post_id'}}, (class => 'post__id') %>
|
||||||
|
<% } =%>
|
||||||
|
</h4>
|
||||||
|
<h5 class="post__author"><%= $result->{'post_author'} %></h5>
|
||||||
|
<div class="post__body">
|
||||||
|
<%== markdown $result->{'post_body'} =%>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<% } =%>
|
||||||
|
<% if ($last_page && $last_page != 1) { =%>
|
||||||
|
<nav class="pager__nav">
|
||||||
|
<%= pagination $this_page, $last_page,
|
||||||
|
($base_path . '&page={page}#results') %>
|
||||||
|
</nav>
|
||||||
|
<% } =%>
|
||||||
|
</main>
|
||||||
|
<% } =%>
|
|
@ -12,6 +12,22 @@
|
||||||
<h2 class="page-title"><%= title %></h2>
|
<h2 class="page-title"><%= title %></h2>
|
||||||
<% if (scalar @{$threads}) { =%>
|
<% if (scalar @{$threads}) { =%>
|
||||||
<main class="pager">
|
<main class="pager">
|
||||||
|
<%= form_for search_page => (class => 'form-body'), begin %>
|
||||||
|
<div class="form-field">
|
||||||
|
<% if (my $error = validation->error('q')) { =%>
|
||||||
|
<p class="field-with-error">Must be between <%= $error->[2] %>
|
||||||
|
and <%= $error->[3] %> characters.</p>
|
||||||
|
<% } =%>
|
||||||
|
<%= label_for search => 'Search' %>
|
||||||
|
<%= text_field q => (
|
||||||
|
id => 'search',
|
||||||
|
maxlength => 2047,
|
||||||
|
minlength => 1,
|
||||||
|
required => undef
|
||||||
|
) %>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="form-button">Search</button>
|
||||||
|
<% end %>
|
||||||
<% for my $thread (@{$threads}) { =%>
|
<% for my $thread (@{$threads}) { =%>
|
||||||
<article class="post">
|
<article class="post">
|
||||||
<h3 class="post__title">
|
<h3 class="post__title">
|
||||||
|
|
Loading…
Reference in a new issue