post comments, refactor post loading, hide load more btn

This commit is contained in:
Freya Murphy 2024-03-30 16:36:54 -04:00
parent 2968ee5280
commit 1f647374a8
Signed by: freya
GPG key ID: 744AB800E383AE52
22 changed files with 647 additions and 98 deletions

View file

@ -0,0 +1,15 @@
CREATE VIEW api.comment AS
SELECT
c.id,
c.user_id,
c.post_id,
c.content,
c.date
FROM
admin.comment c
ORDER BY id ASC;
GRANT SELECT ON TABLE api.comment
TO rest_anon, rest_user;
GRANT SELECT ON TABLE admin.comment
TO rest_anon, rest_user;

View file

@ -0,0 +1,31 @@
CREATE FUNCTION _api.comment_delete()
RETURNS TRIGGER
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_user_id INTEGER;
BEGIN
_user_id = _api.get_user_id();
IF OLD.user_id <> _user_id THEN
PERFORM _api.raise_deny();
END IF;
DELETE FROM admin.comment
WHERE user_id = _user_id
AND id = OLD.id;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.comment_delete()
TO rest_user;
GRANT DELETE ON TABLE api.comment
TO rest_user;
GRANT DELETE ON TABLE admin.comment
TO rest_user;
CREATE TRIGGER api_comment_delete_trgr
INSTEAD OF DELETE
ON api.comment
FOR EACH ROW
EXECUTE PROCEDURE _api.comment_delete();

View file

@ -0,0 +1,56 @@
CREATE FUNCTION _api.comment_insert()
RETURNS TRIGGER
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_user_id INTEGER;
BEGIN
_user_id = _api.get_user_id();
NEW.content := _api.trim(NEW.content);
PERFORM _api.validate_text(
_text => NEW.content,
_column => 'content',
_min => 1,
_max => 1024
);
PERFORM TRUE
FROM admin.post
WHERE id = NEW.post_id;
IF NOT FOUND THEN
PERFORM _api.raise(
_msg => 'api_null_post',
_err => 400
);
END IF;
INSERT INTO admin.comment (
user_id,
post_id,
content
) VALUES (
_user_id,
NEW.post_id,
NEW.content
);
RETURN NEW;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.comment_insert()
TO rest_user;
GRANT INSERT ON TABLE api.comment
TO rest_user;
GRANT INSERT ON TABLE admin.comment
TO rest_user;
GRANT UPDATE ON TABLE sys.comment_id_seq
TO rest_user;
CREATE TRIGGER api_comment_insert_trgr
INSTEAD OF INSERT
ON api.comment
FOR EACH ROW
EXECUTE PROCEDURE _api.comment_insert();

View file

@ -0,0 +1,50 @@
CREATE FUNCTION _api.comment_update()
RETURNS TRIGGER
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_user_id INTEGER;
_changed BOOLEAN;
BEGIN
_user_id = _api.get_user_id();
_changed = FALSE;
IF OLD.user_id <> _user_id THEN
PERFORM _api.raise_deny();
END IF;
NEW.content = COALESCE(NEW.content, OLD.content);
NEW.content := _api.trim(NEW.content);
PERFORM _api.validate_text(
_text => NEW.content,
_column => 'content',
_min => 1,
_max => 1024
);
IF NEW.content IS DISTINCT FROM OLD.content THEN
_changed = TRUE;
END IF;
IF _changed THEN
UPDATE admin.comment
SET content = NEW.content
WHERE id = OLD.id;
END IF;
RETURN NEW;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.comment_update()
TO rest_user;
GRANT UPDATE ON TABLE api.comment
TO rest_user;
GRANT UPDATE ON TABLE admin.comment
TO rest_user;
CREATE TRIGGER api_comment_update_trgr
INSTEAD OF UPDATE
ON api.comment
FOR EACH ROW
EXECUTE PROCEDURE _api.comment_update();

View file

@ -3,10 +3,21 @@ CREATE VIEW api.post AS
p.id,
p.user_id,
p.content,
p.date
p.date,
COALESCE(c.cc, 0)
AS comment_count
FROM
admin.post p
ORDER BY id DESC;
LEFT JOIN (
SELECT
COUNT(c.id) as cc,
c.post_id
FROM
admin.comment c
GROUP BY
c.post_id
) c ON p.id = c.post_id
ORDER BY p.id DESC;
GRANT SELECT ON TABLE api.post
TO rest_anon, rest_user;

View file

@ -7,6 +7,8 @@ DECLARE
BEGIN
_user_id = _api.get_user_id();
NEW.content := _api.trim(NEW.content);
PERFORM _api.validate_text(
_text => NEW.content,
_column => 'content',

View file

@ -3,13 +3,45 @@ RETURNS TRIGGER
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_length INTEGER;
_user_id INTEGER;
_changed BOOLEAN;
BEGIN
_user_id = _api.get_user_id();
_changed = FALSE;
IF OLD.user_id <> _user_id THEN
PERFORM _api.raise_deny();
END IF;
NEW.content = COALESCE(NEW.content, OLD.content);
NEW.content := _api.trim(NEW.content);
PERFORM _api.validate_text(
_text => NEW.content,
_column => 'content',
_min => 1,
_max => 4096
);
IF NEW.content IS DISTINCT FROM OLD.content THEN
_changed = TRUE;
END IF;
IF _changed THEN
UPDATE admin.post
SET content = NEW.content
WHERE id = OLD.id;
END IF;
RETURN NEW;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.post_update() TO rest_user;
GRANT EXECUTE ON FUNCTION _api.post_update()
TO rest_user;
GRANT UPDATE ON TABLE api.post
TO rest_user;
GRANT UPDATE ON TABLE admin.post
TO rest_user;
CREATE TRIGGER api_post_update_trgr
INSTEAD OF UPDATE

View file

@ -14,6 +14,7 @@ GRANT USAGE ON SCHEMA api TO rest_anon, rest_user;
GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
-- util
\i /db/rest/util/_api_trim.sql;
\i /db/rest/util/_api_serve_media.sql;
\i /db/rest/util/_api_raise.sql;
\i /db/rest/util/_api_raise_null.sql;
@ -34,6 +35,12 @@ GRANT USAGE ON SCHEMA _api TO rest_anon, rest_user;
\i /db/rest/post/api_post_update.sql;
\i /db/rest/post/api_post_delete.sql;
-- comment
\i /db/rest/comment/api_comment.sql;
\i /db/rest/comment/api_comment_insert.sql;
\i /db/rest/comment/api_comment_update.sql;
\i /db/rest/comment/api_comment_delete.sql;
-- login
\i /db/rest/login/_api_sign_jwt.sql;
\i /db/rest/login/_api_verify_jwt.sql;

View file

@ -5,6 +5,16 @@ AS $BODY$
DECLARE
_length INTEGER;
BEGIN
NEW.username := _api.trim(NEW.username);
NEW.password := _api.trim(NEW.password);
NEW.first_name := _api.trim(NEW.first_name);
NEW.last_name := _api.trim(NEW.last_name);
NEW.middle_name := _api.trim(NEW.middle_name);
NEW.email := _api.trim(NEW.email);
NEW.gender := _api.trim(NEW.gender);
NEW.profile_bio := _api.trim(NEW.profile_bio);
PERFORM _api.validate_text(
_text => NEW.username,
_column => 'username',
@ -22,6 +32,7 @@ BEGIN
PERFORM _api.validate_text(
_text => NEW.password,
_column => 'password',
_min => 1,
_max => 256
);
@ -82,8 +93,6 @@ BEGIN
email,
gender,
birth_date,
profile_avatar,
profile_banner,
profile_bio
) VALUES (
NEW.username,
@ -94,8 +103,6 @@ BEGIN
NEW.email,
NEW.gender,
NEW.birth_date,
NEW.profile_avatar,
NEW.profile_banner,
NEW.profile_bio
);

View file

@ -3,15 +3,161 @@ RETURNS TRIGGER
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_length INTEGER;
_user_id INTEGER;
_changed BOOLEAN;
BEGIN
_user_id = _api.get_user_id();
_changed = FALSE;
IF OLD.id <> _user_id THEN
PERFORM _api.raise_deny();
END IF;
-- username
NEW.username = COALESCE(NEW.username, OLD.username);
NEW.username := _api.trim(NEW.username);
PERFORM _api.validate_text(
_text => NEW.username,
_column => 'username',
_min => 1,
_max => 24
);
IF NEW.username IS DISTINCT FROM OLD.username THEN
PERFORM TRUE FROM admin.user
WHERE username = NEW.username;
IF FOUND THEN
PERFORM _api.raise_unique('username');
END IF;
_changed = TRUE;
END IF;
-- password
SELECT password
INTO OLD.password
FROM admin.user
WHERE id = OLD.id;
NEW.password = COALESCE(NEW.password, OLD.password);
NEW.password := _api.trim(NEW.password);
PERFORM _api.validate_text(
_text => NEW.password,
_column => 'password',
_min => 1,
_max => 256
);
IF NEW.password IS DISTINCT FROM OLD.password THEN
_changed = TRUE;
END IF;
-- first name
NEW.first_name = COALESCE(NEW.first_name, OLD.first_name);
NEW.first_name := _api.trim(NEW.first_name);
PERFORM _api.validate_text(
_text => NEW.first_name,
_column => 'first_name',
_max => 256
);
IF NEW.first_name IS DISTINCT FROM OLD.first_name THEN
_changed = TRUE;
END IF;
-- last name
NEW.last_name = COALESCE(NEW.last_name, OLD.last_name);
NEW.last_name := _api.trim(NEW.last_name);
PERFORM _api.validate_text(
_text => NEW.last_name,
_column => 'last_name',
_max => 256
);
IF NEW.last_name IS DISTINCT FROM OLD.last_name THEN
_changed = TRUE;
END IF;
-- middle name
NEW.middle_name = COALESCE(NEW.middle_name, OLD.middle_name);
NEW.middle_name := _api.trim(NEW.middle_name);
PERFORM _api.validate_text(
_text => NEW.middle_name,
_column => 'middle_name',
_max => 256
);
IF NEW.middle_name IS DISTINCT FROM OLD.middle_name THEN
_changed = TRUE;
END IF;
-- email
NEW.email = COALESCE(NEW.email, OLD.email);
NEW.email := _api.trim(NEW.email);
PERFORM _api.validate_text(
_text => NEW.email,
_column => 'email',
_max => 256
);
IF NEW.email IS DISTINCT FROM OLD.email THEN
_changed = TRUE;
END IF;
-- gender
NEW.gender = COALESCE(NEW.gender, OLD.gender);
NEW.gender := _api.trim(NEW.gender);
PERFORM _api.validate_text(
_text => NEW.gender,
_column => 'gender',
_max => 256
);
IF NEW.gender IS DISTINCT FROM OLD.gender THEN
_changed = TRUE;
END IF;
-- birth date
NEW.birth_date = COALESCE(NEW.birth_date, OLD.birth_date);
IF NEW.birth_date IS DISTINCT FROM OLD.birth_date THEN
_changed = TRUE;
END IF;
-- profile bio
NEW.profile_bio = COALESCE(NEW.profile_bio, OLD.profile_bio);
NEW.profile_bio := _api.trim(NEW.profile_bio);
PERFORM _api.validate_text(
_text => NEW.profile_bio,
_column => 'profile_bio',
_max => 2048
);
IF NEW.profile_bio IS DISTINCT FROM OLD.profile_bio THEN
_changed = TRUE;
END IF;
IF _changed THEN
UPDATE admin.user SET
username = NEW.username,
password = NEW.password,
first_name = NEW.first_name,
last_name = NEW.last_name,
middle_name = NEW.middle_name,
email = NEW.email,
gender = NEW.gender,
birth_date = NEW.birth_date,
profile_bio = NEW.profile_bio
WHERE id = OLD.id;
END IF;
RETURN NEW;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.user_update()
TO rest_user;
GRANT DELETE ON TABLE api.user
GRANT UPDATE ON TABLE api.user
TO rest_user;
GRANT UPDATE ON TABLE admin.user
TO rest_user;
CREATE TRIGGER api_user_update_trgr

View file

@ -0,0 +1,25 @@
CREATE FUNCTION _api.trim(
_text TEXT
)
RETURNS TEXT
LANGUAGE plpgsql VOLATILE
AS $BODY$
DECLARE
_new TEXT;
BEGIN
IF _text IS NULL THEN
RETURN NULL;
END IF;
_new = _text;
_new = TRIM(_new);
_new = REGEXP_REPLACE(_new, '^(?: |\r|\n)*', '');
_new = REGEXP_REPLACE(_new, '(?: |\r|\n)*$', '');
RETURN _new;
END
$BODY$;
GRANT EXECUTE ON FUNCTION _api.trim(TEXT)
TO rest_anon, rest_user;

View file

@ -33,6 +33,7 @@ BEGIN
_detail => _column,
_hint => _min || ''
);
RETURN FALSE;
END IF;
IF _max IS NOT NULL AND _length > _max THEN
@ -41,6 +42,7 @@ BEGIN
_detail => _column,
_hint => _max || ''
);
RETURN FALSE;
END IF;
RETURN TRUE;

View file

@ -0,0 +1,129 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Post_controller extends Controller {
// the request model
private $request_model;
// the caceh model
private $cache_model;
// page size
private $page_size;
function __construct($load) {
parent::__construct($load);
$this->request_model = $this->load->model('request');
$this->cache_model = $this->load->model('cache');
$this->page_size = 10;
}
public function index(): void {
$this->view('template/posts');
}
/**
* @return array<string,mixed>
*/
public function posts(): array {
$page = $this->request_model->get_int('page', 0);
$max = $this->request_model->get_int('max');
$offset = $page * $this->page_size;
$user = $this->main->user();
$query = $this->db;
if ($user) {
$query = $query->select('p.*, l.post_id IS NOT NULL as liked');
} else {
$query = $query->select('p.*, FALSE as liked');
}
$query = $query->from('api.post p');
if ($user) {
$query = $query->join('admin.like l', 'p.id = l.post_id AND l.user_id')
->eq($user['id']);
}
if ($max) {
$query = $query
->where('id')->le($max);
}
$posts = $query
->limit($this->page_size)
->offset($offset)
->rows();
$users = $this->cache_model->get_users($posts);
$max = 0;
foreach ($posts as $post) {
$max = max($max, $post['id']);
$data = array();
$data['page_size'] = $this->page_size;
$data['user'] = $users[$post['user_id']];
$data['post'] = $post;
$this->view('template/post', $data);
}
$pc = $this->db
->select('COUNT(p.id) as pc')
->from('api.post p')
->row()['pc'];
return array(
'loaded' => count($posts),
'total' => $pc,
'page_size' => $this->page_size,
'max' => $max,
);
}
/**
* @return array<string,mixed>
*/
public function comments(): array {
$page = $this->request_model->get_int('page', 0);
$max = $this->request_model->get_int('max');
$id = $this->request_model->get_int('id', 0);
$offset = $page * $this->page_size;
$query = $this->db
->select('*')
->from('api.comment')
->where('post_id')
->eq($id);
if ($max) {
$query = $query
->and()
->where('id')
->le($max);
}
$comments = $query
->limit($this->page_size)
->offset($offset)
->rows();
$users = $this->cache_model->get_users($comments);
$max = 0;
foreach ($comments as $comment) {
$max = max($max, $comment['id']);
$data = array();
$data['user'] = $users[$comment['user_id']];
$data['comment'] = $comment;
$this->view('template/comment', $data);
}
return array(
'loaded' => count($comments),
'page_size' => $this->page_size,
'max' => $max,
);
}
}

View file

@ -4,17 +4,13 @@ class Home_controller extends Controller {
// the home model
private $home_model;
// the request model
private $request_model;
// the caceh model
private $cache_model;
// the post controller
protected $post_controller;
function __construct($load) {
parent::__construct($load);
$this->home_model = $this->load->model('apps/home');
$this->request_model = $this->load->model('request');
$this->cache_model = $this->load->model('cache');
$this->post_controller = $this->load->controller('_util/post');
}
public function index(): void {
@ -24,66 +20,6 @@ class Home_controller extends Controller {
$this->view('apps/home/main', $data);
}
public function posts(): void {
$page = $this->request_model->get_int('page', 0);
$page_size = 20;
$offset = $page * $page_size;
$user = $this->main->user();
$query = $this->db;
if ($user) {
$query = $query->select('p.*, l.post_id IS NOT NULL as liked');
} else {
$query = $query->select('p.*, FALSE as liked');
}
$query = $query->from('api.post p');
if ($user) {
$query = $query->join('admin.like l', 'p.id = l.post_id')
->where('l.user_id')->eq($user['id'])
->or()->where('l.user_id IS NULL');
}
$posts = $query->limit($page_size)
->offset($offset)
->rows();
$users = $this->cache_model->get_users($posts);
foreach ($posts as $post) {
$data = array();
$data['user'] = $users[$post['user_id']];
$data['post'] = $post;
$this->view('template/post', $data);
}
}
public function comments(): void {
$page = $this->request_model->get_int('page', 0);
$id = $this->request_model->get_int('id');
$page_size = 20;
$offset = $page * $page_size;
$comments = $this->db
->select('*')
->from('admin.comment')
->limit($page_size)
->offset($offset)
->rows();
$users = $this->cache_model->get_users($comments);
foreach ($comments as $comment) {
$data = array();
$data['user'] = $users[$comment['user_id']];
$data['comment'] = $comment;
$this->view('template/comment', $data);
}
}
}
?>

View file

@ -1,5 +1,5 @@
<?php /* Copyright (c) 2024 Freya Murphy */
class Format_model extends Modal {
class Format_model extends Model {
function __construct($load) {
parent::__construct($load);

View file

@ -23,8 +23,5 @@
</script>
</div>
<?php endif; ?>
<div id="post-container">
<?=$this->posts()?>
</div>
<?=ilang('action_load_posts', id: 'action-load-posts', class: 'btn btn-line')?>
<?php $this->post_controller->index(); ?>
</div>

View file

@ -1,11 +1,14 @@
<?php /* Copyright (c) 2024 Freya Murphy */ ?>
<?php /* vi: syntax=php */ ?>
<?php
$format_model = $this->load->model('format');
?>
<div class="comment row mt">
<?php $this->view('template/pfp', array('user' => $user))?>
<div class="ml col sub-card">
<div class="row">
<strong><?=$this->main->display_name($user)?></strong>
<span class="dim ml"><?=$this->main->display_date($comment['date'])?></span>
<strong><?=$format_model->name($user)?></strong>
<span class="dim ml"><?=$format_model->date($comment['date'])?></span>
</div>
<?=$comment['content']?>
</div>

View file

@ -29,25 +29,40 @@
<div class="col comments">
<?php
$_GET['id'] = $post['id'];
$this->comments();
$cdata = $this->comments();
$loaded = $cdata['loaded'];
$max = $cdata['max'];
$page_size = $cdata['page_size'];
$total = $post['comment_count'];
if ($loaded >= $page_size && $page_size < $total) {
ilang('action_load_comments',
class: 'action-load-comments btn btn-line mt',
attrs: array('postId' => $post['id'])
attrs: array(
'postId' => $post['id'],
'loaded' => $loaded,
'pageSize' => $page_size,
'commentCount' => $total,
'commentMax' => $max,
)
);
}
?>
</div>
<?php if ($self): ?>
<div class="row grow mt">
<?php $this->view('template/pfp', array('user' => $user))?>
<form class="ml">
<form class="ml action-new-comment-form">
<input
type="hidden"
name="id"
value="<?=$post['id']?>"
>
<input
id="new-comment-<?=$post['id']?>"
class="input"
class="action-new-comment input"
postId="<?=$post['id']?>"
autocomplete="off"
type="text"
name="text"

View file

@ -0,0 +1,23 @@
<div id="post-container">
<?php
$pdata = $this->posts();
$loaded = $pdata['loaded'];
$page_size = $pdata['page_size'];
$total = $pdata['total'];
$max = $pdata['max'];
if ($loaded >= $page_size && $page_size < $total) {
ilang('action_load_posts',
id: 'action-load-posts',
class: 'btn btn-line mb',
attrs: array(
'loaded' => $loaded,
'pageSize' => $page_size,
'postCount' => $total,
'postMax' => $max,
)
);
}
?>
</div>

View file

@ -69,6 +69,18 @@ class DatabaseQuery {
return $this;
}
public function lt($item) {
$this->query .= "< ?\n";
array_push($this->param, $item);
return $this;
}
public function le($item) {
$this->query .= "<= ?\n";
array_push($this->param, $item);
return $this;
}
public function where_in($column, $array) {
if (!$this->where) {
$this->where = TRUE;

View file

@ -10,3 +10,7 @@
.action-load-comments {
margin-left: 4rem;
}
#action-load-posts {
justify-content: center;
}

View file

@ -5,14 +5,28 @@ observe('#main-content', '.action-load-comments', function(me) {
page = '1';
}
let newPage = Number(page) + 1;
let id = me.attr('postId');
me.attr('page', newPage + '');
let url = '/home/comments?page=' + page + '&id=' + id;
let postId = me.attr('postId');
let loaded = Number(me.attr('loaded'));
let pageSize = Number(me.attr('pageSize'));
let commmentCount = Number(me.attr('commentCount'));
let commentMax = Number(me.attr('commentMax'));
let url = '/_util/post/comments?page=' + page + '&id=' + postId + '&max' + commentMax;
$.get(url, function (data) {
if (data === '') {
me.remove();
return;
}
$(data).insertBefore(me);
loaded += pageSize;
if (loaded >= commmentCount) {
me.remove();
} else {
$(me).prepend(data);
me.attr('loaded', loaded + '');
}
});
});
@ -26,13 +40,45 @@ observe('#main-content', '#action-load-posts', function(me) {
}
let newPage = Number(page) + 1;
me.attr('page', newPage + '');
let url = '/home/posts?page=' + page;
let loaded = Number(me.attr('loaded'));
let pageSize = Number(me.attr('pageSize'));
let postCount = Number(me.attr('postCount'));
let postMax = Number(me.attr('postMax'));
let url = '/_util/post/posts?page=' + page + '&max=' + postMax;
$.get(url, function (data) {
if (data === '') {
me.remove();
return;
}
$(data).insertBefore(me);
loaded += pageSize;
if (loaded >= postCount) {
me.remove();
} else {
$('#post-container').append(data);
me.attr('loaded', loaded + '');
}
});
});
});
observe('#main-content', '.action-new-comment-form', function(me) {
me.on('submit', function(e) {
e.preventDefault();
let input = me.find('.action-new-comment');
let content = input.val();
let post_id = input.attr('postId');
$.ajax({
url: '/api/comment',
method: 'POST',
data: JSON.stringify({ post_id, content }),
success: function(_data) {
window.location.reload();
},
error: errorToast
});
});
});