i definitely did this today
This commit is contained in:
parent
3760b49359
commit
74f5fd0921
18 changed files with 907 additions and 42 deletions
145
assets/less/mio/classes/comment.less
Normal file
145
assets/less/mio/classes/comment.less
Normal file
|
@ -0,0 +1,145 @@
|
|||
.comment {
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
//display: none;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
font-size: .9em;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
.comment__action--hide {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&--link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&--label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--post {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&--button {
|
||||
cursor: pointer;
|
||||
font: 12px/20px @mio-font-regular;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&--hide {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
vertical-align: text-top;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__replies {
|
||||
&--indent {
|
||||
&-1, &-2, &-3, &-4, &-5 {
|
||||
margin-left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
flex: 0 0 auto;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-right: 2px;
|
||||
|
||||
&--input {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 50px;
|
||||
font: 12px/20px @mio-font-regular;
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&__user {
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
|
||||
&--link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__date,
|
||||
&__pin {
|
||||
color: #666;
|
||||
font-size: .9em;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: #666;
|
||||
display: inline-flex;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__pin {
|
||||
&:before {
|
||||
content: "-";
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
33
assets/less/mio/classes/comments.less
Normal file
33
assets/less/mio/classes/comments.less
Normal file
|
@ -0,0 +1,33 @@
|
|||
.comments {
|
||||
margin: 1px;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
&__input,
|
||||
&__javascript {
|
||||
border-bottom: 1px solid #9475b2;
|
||||
padding-bottom: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&__none,
|
||||
&__javascript,
|
||||
&__notice {
|
||||
padding: 10px;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__notice__link {
|
||||
color: #22c;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: #c22;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,8 +8,8 @@
|
|||
background-color: #23172a;
|
||||
}
|
||||
|
||||
&--hidden {
|
||||
.container__content {
|
||||
&--hidden { // __title should always be the first element of a container
|
||||
:not(:first-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
&__content { // only use this for text going forward, just throw your child container in directly after __title
|
||||
margin: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,3 +92,7 @@ body {
|
|||
// Member listing
|
||||
@import "classes/members/user";
|
||||
@import "classes/members/users";
|
||||
|
||||
// Comments
|
||||
@import "classes/comment"; // entries
|
||||
@import "classes/comments"; // listing
|
||||
|
|
|
@ -24,6 +24,7 @@ function migrate_up(PDO $conn): void
|
|||
`comment_reply_to` INT(10) UNSIGNED NULL DEFAULT NULL,
|
||||
`comment_text` TEXT NOT NULL,
|
||||
`comment_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`comment_pinned` TIMESTAMP NULL DEFAULT NULL,
|
||||
`comment_edited` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`comment_deleted` TIMESTAMP NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`comment_id`),
|
||||
|
@ -32,6 +33,7 @@ function migrate_up(PDO $conn): void
|
|||
INDEX `comments_posts_reply_id` (`comment_reply_to`),
|
||||
INDEX `comments_posts_dates` (
|
||||
`comment_created`,
|
||||
`comment_pinned`,
|
||||
`comment_edited`,
|
||||
`comment_deleted`
|
||||
),
|
||||
|
@ -47,10 +49,32 @@ function migrate_up(PDO $conn): void
|
|||
ON DELETE SET NULL
|
||||
);
|
||||
');
|
||||
|
||||
$conn->exec("
|
||||
CREATE TABLE `msz_comments_votes` (
|
||||
`comment_id` INT(10) UNSIGNED NOT NULL,
|
||||
`user_id` INT(10) UNSIGNED NOT NULL,
|
||||
`comment_vote` ENUM('Like','Dislike') NULL,
|
||||
UNIQUE INDEX `comments_vote_unique` (`comment_id`, `user_id`),
|
||||
INDEX `comments_vote_user_foreign` (`user_id`),
|
||||
INDEX `comments_vote_index` (`comment_vote`),
|
||||
CONSTRAINT `comment_vote_id`
|
||||
FOREIGN KEY (`comment_id`)
|
||||
REFERENCES `msz_comments_posts` (`comment_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `comment_vote_user`
|
||||
FOREIGN KEY (`user_id`)
|
||||
REFERENCES `msz_users` (`user_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
function migrate_down(PDO $conn): void
|
||||
{
|
||||
$conn->exec('DROP TABLE `msz_comments_votes`');
|
||||
$conn->exec('DROP TABLE `msz_comments_posts`');
|
||||
$conn->exec('DROP TABLE `msz_comments_categories`');
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ if (PHP_SAPI === 'cli') {
|
|||
|
||||
case 'migrate':
|
||||
$migrationTargets = [
|
||||
'mysql-main' => __DIR__ . '/database',
|
||||
'mysql-main' => __DIR__ . '/database',
|
||||
];
|
||||
$doRollback = !empty($argv[2]) && $argv[2] === 'rollback';
|
||||
$targetDb = isset($argv[$doRollback ? 3 : 2]) ? $argv[$doRollback ? 3 : 2] : null;
|
||||
|
@ -232,7 +232,7 @@ MIG;
|
|||
$getUserDisplayInfo = Database::prepare('
|
||||
SELECT
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `colour`
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_users` as u
|
||||
LEFT JOIN `msz_roles` as r
|
||||
ON u.`display_role` = r.`role_id`
|
||||
|
|
|
@ -13,9 +13,12 @@ $changelogDate = $_GET['d'] ?? '';
|
|||
$changelogUser = (int)($_GET['u'] ?? 0);
|
||||
$changelogTags = $_GET['t'] ?? '';
|
||||
|
||||
$commentPerms = comments_get_perms($app->getUserId());
|
||||
|
||||
$tpl->vars([
|
||||
'changelog_offset' => $changelogOffset,
|
||||
'changelog_take' => $changelogRange,
|
||||
'comments_perms' => $commentPerms,
|
||||
]);
|
||||
|
||||
if ($changelogChange > 0) {
|
||||
|
@ -55,7 +58,14 @@ if ($changelogChange > 0) {
|
|||
$tpl->var('tags', $tags);
|
||||
}
|
||||
|
||||
echo $tpl->render('changelog.change', compact('change'));
|
||||
echo $tpl->render('changelog.change', [
|
||||
'change' => $change,
|
||||
'comments_category' => $commentsCategory = comments_category_info(
|
||||
"changelog-date-{$change['change_date']}",
|
||||
true
|
||||
),
|
||||
'comments' => comments_category_get($commentsCategory['category_id']),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -77,6 +87,13 @@ if (!$changes) {
|
|||
http_response_code(404);
|
||||
}
|
||||
|
||||
if (!empty($changelogDate)) {
|
||||
$tpl->vars([
|
||||
'comments_category' => $commentsCategory = comments_category_info("changelog-date-{$changelogDate}", true),
|
||||
'comments' => comments_category_get($commentsCategory['category_id']),
|
||||
]);
|
||||
}
|
||||
|
||||
echo $tpl->render('changelog.index', [
|
||||
'changes' => $changes,
|
||||
'changelog_count' => $changesCount,
|
||||
|
|
181
public/comments.php
Normal file
181
public/comments.php
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
use Misuzu\Database;
|
||||
|
||||
require_once __DIR__ . '/../misuzu.php';
|
||||
|
||||
// if false, display informational pages instead of outputting json.
|
||||
$isXHR = !empty($_SERVER['HTTP_MISUZU_XHR_REQUEST']);
|
||||
|
||||
if ($isXHR || $_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
if ($app->getUserId() < 1) {
|
||||
echo render_info_or_json($isXHR, 'You must be logged in to manage comments.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$redirect = !$isXHR && !empty($_SERVER['HTTP_REFERER'])
|
||||
&& is_local_url($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
|
||||
$commentPerms = comments_get_perms($app->getUserId());
|
||||
$commentId = (int)($_REQUEST['comment_id'] ?? 0);
|
||||
|
||||
if (isset($_POST['vote']) && array_key_exists((int)$_POST['vote'], MSZ_COMMENTS_VOTE_TYPES)) {
|
||||
echo comments_vote_add(
|
||||
$commentId,
|
||||
$app->getUserId(),
|
||||
MSZ_COMMENTS_VOTE_TYPES[(int)$_POST['vote']]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($_SERVER['REQUEST_METHOD']) {
|
||||
case 'GET':
|
||||
if ($commentId < 1) {
|
||||
echo render_info_or_json(true, 'Missing data.', 400);
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($_GET['fetch'] ?? '') {
|
||||
case 'replies':
|
||||
echo json_encode(comments_post_replies($commentId));
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(comments_post_get($commentId));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
if (!$commentPerms['can_comment']) {
|
||||
echo render_info_or_json($isXHR, "You're not allowed to post comments.", 403);
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($_POST['comment']) || !is_array($_POST['comment'])) {
|
||||
echo render_info_or_json($isXHR, 'Missing data.', 400);
|
||||
break;
|
||||
}
|
||||
|
||||
$categoryId = (int)($_POST['comment']['category'] ?? 0);
|
||||
$category = comments_category_info($categoryId);
|
||||
|
||||
if (!$category) {
|
||||
echo render_info_or_json($isXHR, 'This comment category doesn\'t exist.', 404);
|
||||
}
|
||||
|
||||
if (!is_null($category['category_locked']) || !$commentPerms['can_lock']) {
|
||||
echo render_info_or_json($isXHR, 'This comment category has been locked.', 403);
|
||||
}
|
||||
|
||||
$commentText = $_POST['comment']['text'] ?? '';
|
||||
$commentLock = !empty($_POST['comment']['lock']) && $commentPerms['can_lock'];
|
||||
$commentPin = !empty($_POST['comment']['pin']) && $commentPerms['can_pin'];
|
||||
$commentReply = (int)($_POST['comment']['reply'] ?? 0);
|
||||
|
||||
if ($commentLock) {
|
||||
comments_category_lock($categoryId, is_null($category['category_locked']));
|
||||
}
|
||||
|
||||
if (strlen($commentText) > 0) {
|
||||
$commentText = preg_replace("/[\r\n]{2,}/", "\n", $commentText);
|
||||
} else {
|
||||
if ($commentPerms['can_lock']) {
|
||||
echo render_info_or_json($isXHR, 'The action has been processed.');
|
||||
} else {
|
||||
echo render_info_or_json($isXHR, 'Your comment is too short.', 400);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (strlen($commentText) > 5000) {
|
||||
echo render_info_or_json($isXHR, 'Your comment is too long.', 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($commentReply > 0 && !comments_post_exists($commentReply)) {
|
||||
echo render_info_or_json($isXHR, 'The comment you tried to reply to does not exist.', 404);
|
||||
break;
|
||||
}
|
||||
|
||||
$commentId = comments_post_create(
|
||||
$app->getUserId(),
|
||||
$categoryId,
|
||||
$commentText,
|
||||
$commentPin,
|
||||
$commentReply
|
||||
);
|
||||
|
||||
if ($commentId < 1) {
|
||||
echo render_info_or_json($isXHR, 'Something went horribly wrong.', 500);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($redirect) {
|
||||
header('Location: ' . $redirect . '#comment-' . $commentId);
|
||||
break;
|
||||
}
|
||||
|
||||
echo json_encode(comments_post_get($commentId));
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
method_patch:
|
||||
if ($commentId < 1) {
|
||||
echo render_info_or_json($isXHR, 'Missing data.', 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$commentPerms['can_edit']) {
|
||||
echo render_info_or_json($isXHR, "You're not allowed to edit comments.", 403);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$commentPerms['can_edit_any']
|
||||
&& !comments_post_check_ownership($commentId, $app->getUserId())) {
|
||||
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($redirect) {
|
||||
header('Location: ' . $redirect . '#comment-' . $commentId);
|
||||
break;
|
||||
}
|
||||
|
||||
var_dump($_POST);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
method_delete:
|
||||
if ($commentId < 1) {
|
||||
echo render_info_or_json($isXHR, 'Missing data.', 400);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$commentPerms['can_delete']) {
|
||||
echo render_info_or_json($isXHR, "You're not allowed to delete comments.", 403);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$commentPerms['can_delete_any']
|
||||
&& !comments_post_check_ownership($commentId, $app->getUserId())) {
|
||||
echo render_info_or_json($isXHR, "You're not allowed to delete comments made by others.", 403);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!comments_post_delete($commentId)) {
|
||||
echo render_info_or_json($isXHR, 'Failed to delete comment.', 500);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($redirect) {
|
||||
header('Location: ' . $redirect);
|
||||
break;
|
||||
}
|
||||
|
||||
echo render_info_or_json($isXHR, 'Comment deleted.');
|
||||
break;
|
||||
|
||||
default:
|
||||
echo render_info_or_json($isXHR, 'Invalid request method.', 405);
|
||||
}
|
|
@ -19,6 +19,8 @@ class TemplateEngine
|
|||
*/
|
||||
private const FILE_EXTENSION = '.twig';
|
||||
|
||||
public const TWIG_DEFAULT = Twig_Loader_Filesystem::MAIN_NAMESPACE;
|
||||
|
||||
/**
|
||||
* Instance of the Twig Environment.
|
||||
* @var Twig_Environment
|
||||
|
|
235
src/comments.php
235
src/comments.php
|
@ -8,17 +8,65 @@ define('MSZ_COMMENTS_PERM_DELETE_OWN', 1 << 3);
|
|||
define('MSZ_COMMENTS_PERM_DELETE_ANY', 1 << 4);
|
||||
define('MSZ_COMMENTS_PERM_PIN', 1 << 5);
|
||||
define('MSZ_COMMENTS_PERM_LOCK', 1 << 6);
|
||||
define('MSZ_COMMENTS_PERM_VOTE', 1 << 7);
|
||||
|
||||
function comments_category_create(string $name): int
|
||||
define('MSZ_COMMENTS_VOTE_INDIFFERENT', null);
|
||||
define('MSZ_COMMENTS_VOTE_LIKE', 'Like');
|
||||
define('MSZ_COMMENTS_VOTE_DISLIKE', 'Dislike');
|
||||
define('MSZ_COMMENTS_VOTE_TYPES', [
|
||||
0 => MSZ_COMMENTS_VOTE_INDIFFERENT,
|
||||
1 => MSZ_COMMENTS_VOTE_LIKE,
|
||||
-1 => MSZ_COMMENTS_VOTE_DISLIKE,
|
||||
]);
|
||||
|
||||
// usually this is not how you're suppose to handle permission checking,
|
||||
// but in the context of comments this is fine since the same shit is used
|
||||
// for every comment section.
|
||||
function comments_get_perms(int $userId): array
|
||||
{
|
||||
$perms = perms_get_user(MSZ_PERMS_COMMENTS, $userId);
|
||||
return [
|
||||
'can_comment' => perms_check($perms, MSZ_COMMENTS_PERM_CREATE),
|
||||
'can_edit' => perms_check($perms, MSZ_COMMENTS_PERM_EDIT_OWN | MSZ_COMMENTS_PERM_EDIT_ANY),
|
||||
'can_edit_any' => perms_check($perms, MSZ_COMMENTS_PERM_EDIT_ANY),
|
||||
'can_delete' => perms_check($perms, MSZ_COMMENTS_PERM_DELETE_OWN | MSZ_COMMENTS_PERM_DELETE_ANY),
|
||||
'can_delete_any' => perms_check($perms, MSZ_COMMENTS_PERM_DELETE_ANY),
|
||||
'can_pin' => perms_check($perms, MSZ_COMMENTS_PERM_PIN),
|
||||
'can_lock' => perms_check($perms, MSZ_COMMENTS_PERM_LOCK),
|
||||
'can_vote' => perms_check($perms, MSZ_COMMENTS_PERM_VOTE),
|
||||
];
|
||||
}
|
||||
|
||||
function comments_vote_add(int $comment, int $user, ?string $vote): bool
|
||||
{
|
||||
if (!in_array($vote, MSZ_COMMENTS_VOTE_TYPES, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$setVote = Database::prepare('
|
||||
REPLACE INTO `msz_comments_votes`
|
||||
(`comment_id`, `user_id`, `comment_vote`)
|
||||
VALUES
|
||||
(:comment, :user, :vote)
|
||||
');
|
||||
$setVote->bindValue('comment', $comment);
|
||||
$setVote->bindValue('user', $user);
|
||||
$setVote->bindValue('vote', $vote);
|
||||
return $setVote->execute();
|
||||
}
|
||||
|
||||
function comments_category_create(string $name): array
|
||||
{
|
||||
$create = Database::prepare('
|
||||
INSERT INTO `msz_comments_categories`
|
||||
(`category_name`)
|
||||
VALUES
|
||||
(:name)
|
||||
(LOWER(:name))
|
||||
');
|
||||
$create->bindValue('name', $name);
|
||||
return $create->execute() ? Database::lastInsertId() : 0;
|
||||
return $create->execute()
|
||||
? comments_category_info((int)Database::lastInsertId(), false)
|
||||
: [];
|
||||
}
|
||||
|
||||
function comments_category_lock(int $category, bool $lock): void
|
||||
|
@ -33,22 +81,125 @@ function comments_category_lock(int $category, bool $lock): void
|
|||
$lock->execute();
|
||||
}
|
||||
|
||||
function comments_category_exists(string $name): bool
|
||||
define('MSZ_COMMENTS_CATEGORY_INFO_QUERY', '
|
||||
SELECT
|
||||
`category_id`, `category_locked`
|
||||
FROM `msz_comments_categories`
|
||||
WHERE `%s` = %s
|
||||
');
|
||||
define('MSZ_COMMENTS_CATEGORY_INFO_ID', sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_INFO_QUERY,
|
||||
'category_id',
|
||||
':category'
|
||||
));
|
||||
define('MSZ_COMMENTS_CATEGORY_INFO_NAME', sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_INFO_QUERY,
|
||||
'category_name',
|
||||
'LOWER(:category)'
|
||||
));
|
||||
|
||||
function comments_category_info($category, bool $createIfNone = false): array
|
||||
{
|
||||
$exists = Database::prepare('
|
||||
SELECT COUNT(`category_name`) > 0
|
||||
FROM `msz_comments_categories`
|
||||
WHERE `category_name` = :name
|
||||
');
|
||||
$exists->bindValue('name', $name);
|
||||
return $exists->execute() ? (bool)$exists->fetchColumn() : false;
|
||||
if (is_int($category)) {
|
||||
$getCategory = Database::prepare(MSZ_COMMENTS_CATEGORY_INFO_ID);
|
||||
$createIfNone = false;
|
||||
} elseif (is_string($category)) {
|
||||
$getCategory = Database::prepare(MSZ_COMMENTS_CATEGORY_INFO_NAME);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
$getCategory->bindValue('category', $category);
|
||||
$categoryInfo = $getCategory->execute() ? $getCategory->fetch(PDO::FETCH_ASSOC) : false;
|
||||
return $categoryInfo
|
||||
? $categoryInfo
|
||||
: (
|
||||
$createIfNone
|
||||
? comments_category_create($category)
|
||||
: []
|
||||
);
|
||||
}
|
||||
|
||||
function comments_category_get(int $category): array
|
||||
define('MSZ_COMMENTS_CATEGORY_QUERY', '
|
||||
SELECT
|
||||
p.`comment_id`, p.`comment_text`, p.`comment_reply_to`,
|
||||
p.`comment_created`, p.`comment_pinned`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_comments_posts` as p
|
||||
LEFT JOIN `msz_users` as u
|
||||
ON u.`user_id` = p.`user_id`
|
||||
LEFT JOIN `msz_roles` as r
|
||||
ON r.`role_id` = u.`display_role`
|
||||
WHERE p.`category_id` = :category
|
||||
%s
|
||||
ORDER BY p.`comment_pinned` DESC, p.`comment_id` DESC
|
||||
');
|
||||
define('MSZ_COMMENTS_CATEGORY_QUERY_ROOT', sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_QUERY,
|
||||
'AND p.`comment_reply_to` IS NULL'
|
||||
));
|
||||
define('MSZ_COMMENTS_CATEGORY_QUERY_REPLIES', sprintf(
|
||||
MSZ_COMMENTS_CATEGORY_QUERY,
|
||||
'AND p.`comment_reply_to` = :parent'
|
||||
));
|
||||
|
||||
// heavily recursive
|
||||
function comments_category_get(int $category, ?int $parent = null): array
|
||||
{
|
||||
$posts = Database::prepare('
|
||||
if ($parent !== null) {
|
||||
$getComments = Database::prepare(MSZ_COMMENTS_CATEGORY_QUERY_REPLIES);
|
||||
$getComments->bindValue('parent', $parent);
|
||||
} else {
|
||||
$getComments = Database::prepare(MSZ_COMMENTS_CATEGORY_QUERY_ROOT);
|
||||
}
|
||||
|
||||
$getComments->bindValue('category', $category);
|
||||
$comments = $getComments->execute() ? $getComments->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
|
||||
$commentsCount = count($comments);
|
||||
for ($i = 0; $i < $commentsCount; $i++) {
|
||||
$comments[$i]['comment_replies'] = comments_category_get($category, $comments[$i]['comment_id']);
|
||||
}
|
||||
|
||||
return $comments;
|
||||
}
|
||||
|
||||
function comments_post_create(int $user, int $category, string $text, bool $pinned = false, ?int $reply = null): int
|
||||
{
|
||||
$create = Database::prepare('
|
||||
INSERT INTO `msz_comments_posts`
|
||||
(`user_id`, `category_id`, `comment_text`, `comment_pinned`, `comment_reply_to`)
|
||||
VALUES
|
||||
(:user, :category, :text, IF(:pin, NOW(), NULL), :reply)
|
||||
');
|
||||
$create->bindValue('user', $user);
|
||||
$create->bindValue('category', $category);
|
||||
$create->bindValue('text', $text);
|
||||
$create->bindValue('pin', $pinned ? 1 : 0);
|
||||
$create->bindValue('reply', $reply < 1 ? null : $reply);
|
||||
return $create->execute() ? Database::lastInsertId() : 0;
|
||||
}
|
||||
|
||||
function comments_post_delete(int $commentId, bool $delete = true): bool
|
||||
{
|
||||
$deleteComment = Database::prepare('
|
||||
UPDATE `msz_comments_posts`
|
||||
SET `comment_deleted` = IF(:del, NOW(), NULL)
|
||||
WHERE `comment_id` = :id
|
||||
');
|
||||
$deleteComment->bindValue('id', $commentId);
|
||||
$deleteComment->bindValue('del', $delete ? 1 : 0);
|
||||
return $deleteComment->execute();
|
||||
}
|
||||
|
||||
function comments_post_get(int $commentId): array
|
||||
{
|
||||
$fetch = Database::prepare('
|
||||
SELECT
|
||||
p.`comment_id`, p.`comment_text`,
|
||||
p.`comment_id`, p.`category_id`, p.`comment_text`,
|
||||
p.`comment_created`, p.`comment_edited`, p.`comment_deleted`,
|
||||
p.`comment_reply_to`, p.`comment_pinned`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_comments_posts` as p
|
||||
|
@ -56,22 +207,52 @@ function comments_category_get(int $category): array
|
|||
ON u.`user_id` = p.`user_id`
|
||||
LEFT JOIN `msz_roles` as r
|
||||
ON r.`role_id` = u.`display_role`
|
||||
WHERE c.`category_id` = :category
|
||||
WHERE `comment_id` = :id
|
||||
');
|
||||
$posts->bindValue('category', $category);
|
||||
return $posts->execute() ? $posts->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
$fetch->bindValue('id', $commentId);
|
||||
return $fetch->execute() ? $fetch->fetch(PDO::FETCH_ASSOC) : [];
|
||||
}
|
||||
|
||||
function comments_post_create(int $user, int $category, string $text): int
|
||||
function comments_post_exists(int $commentId): bool
|
||||
{
|
||||
$create = Database::prepare('
|
||||
INSERT INTO `msz_comments_posts`
|
||||
(`user_id`, `category_id`, `comment_text`)
|
||||
VALUES
|
||||
(:user, :category, :text)
|
||||
$fetch = Database::prepare('
|
||||
SELECT COUNT(`comment_id`) > 0
|
||||
FROM `msz_comments_posts`
|
||||
WHERE `comment_id` = :id
|
||||
');
|
||||
$create->bindValue('user', $user);
|
||||
$create->bindValue('category', $category);
|
||||
$create->bindValue('text', $text);
|
||||
return $create->execute() ? Database::lastInsertId() : 0;
|
||||
$fetch->bindValue('id', $commentId);
|
||||
return $fetch->execute() ? (bool)$fetch->fetchColumn() : false;
|
||||
}
|
||||
|
||||
function comments_post_replies(int $commentId): array
|
||||
{
|
||||
$getComments = Database::prepare('
|
||||
SELECT
|
||||
p.`comment_id`, p.`category_id`, p.`comment_text`,
|
||||
p.`comment_created`, p.`comment_edited`, p.`comment_deleted`,
|
||||
p.`comment_reply_to`, p.`comment_pinned`,
|
||||
u.`user_id`, u.`username`,
|
||||
COALESCE(u.`user_colour`, r.`role_colour`) as `user_colour`
|
||||
FROM `msz_comments_posts` as p
|
||||
LEFT JOIN `msz_users` as u
|
||||
ON u.`user_id` = p.`user_id`
|
||||
LEFT JOIN `msz_roles` as r
|
||||
ON r.`role_id` = u.`display_role`
|
||||
WHERE `comment_reply_to` = :id
|
||||
');
|
||||
$getComments->bindValue('id', $commentId);
|
||||
return $getComments->execute() ? $getComments->fetchAll(PDO::FETCH_ASSOC) : [];
|
||||
}
|
||||
|
||||
function comments_post_check_ownership(int $commentId, int $userId): bool
|
||||
{
|
||||
$checkUser = Database::prepare('
|
||||
SELECT COUNT(`comment_id`) > 0
|
||||
FROM `msz_comments_posts`
|
||||
WHERE `comment_id` = :comment
|
||||
AND `user_id` = :user
|
||||
');
|
||||
$checkUser->bindValue('comment', $commentId);
|
||||
$checkUser->bindValue('user', $userId);
|
||||
return $checkUser->execute() ? (bool)$checkUser->fetchColumn() : false;
|
||||
}
|
||||
|
|
|
@ -440,6 +440,16 @@ function manage_perms_list(array $rawPerms): array
|
|||
$rawPerms['comments_perms_deny']
|
||||
),
|
||||
],
|
||||
[
|
||||
'section' => 'vote',
|
||||
'title' => 'Can like or dislike comments.',
|
||||
'perm' => MSZ_COMMENTS_PERM_VOTE,
|
||||
'value' => manage_perms_value(
|
||||
MSZ_COMMENTS_PERM_VOTE,
|
||||
$rawPerms['comments_perms_allow'],
|
||||
$rawPerms['comments_perms_deny']
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
|
54
utility.php
54
utility.php
|
@ -225,6 +225,22 @@ function parse_bbcode(string $text): string
|
|||
return \Misuzu\Parsers\BBCode\BBCodeParser::instance()->parseText($text);
|
||||
}
|
||||
|
||||
function is_local_url(string $url): bool
|
||||
{
|
||||
$length = strlen($url);
|
||||
|
||||
if ($length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($url[0] === '/' && ($length > 1 ? $url[1] !== '/' : true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$prefix = 'http' . (empty($_SERVER['HTTPS']) ? '' : 's') . '://' . $_SERVER['HTTP_HOST'] . '/';
|
||||
return starts_with($url, $prefix);
|
||||
}
|
||||
|
||||
function parse_text(string $text, string $parser): string
|
||||
{
|
||||
switch (strtolower($parser)) {
|
||||
|
@ -259,15 +275,47 @@ function parse_line(string $line, string $parser): string
|
|||
|
||||
function render_error(int $code, string $template = 'errors.%d'): string
|
||||
{
|
||||
http_response_code($code);
|
||||
return render_info(null, $code, $template);
|
||||
}
|
||||
|
||||
function render_info(?string $message, int $httpCode, string $template = 'errors.%d'): string
|
||||
{
|
||||
http_response_code($httpCode);
|
||||
|
||||
try {
|
||||
return \Misuzu\Application::getInstance()->getTemplating()->render(sprintf($template, $code));
|
||||
$tpl = \Misuzu\Application::getInstance()->getTemplating();
|
||||
|
||||
$tpl->var('http_code', $httpCode);
|
||||
|
||||
if (strlen($message)) {
|
||||
$tpl->var('message', $message);
|
||||
}
|
||||
|
||||
$template = sprintf($template, $httpCode);
|
||||
|
||||
if (!$tpl->exists($template, \Misuzu\TemplateEngine::TWIG_DEFAULT)) {
|
||||
$template = 'errors.master';
|
||||
}
|
||||
|
||||
return $tpl->render(sprintf($template, $httpCode));
|
||||
} catch (Exception $ex) {
|
||||
return '';
|
||||
echo $ex->getMessage();
|
||||
return $message ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
function render_info_or_json(bool $json, string $message, int $httpCode = 200, string $template = 'errors.%d'): string
|
||||
{
|
||||
$error = $httpCode >= 400;
|
||||
http_response_code($httpCode);
|
||||
|
||||
if ($json) {
|
||||
return json_encode([($error ? 'error' : 'message') => $message]);
|
||||
}
|
||||
|
||||
return render_info($message, $httpCode, $template);
|
||||
}
|
||||
|
||||
function html_link(string $url, ?string $content = null, $attributes = []): string
|
||||
{
|
||||
$content = $content ?? $url;
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<div class="header__user">
|
||||
<div class="header__menu">
|
||||
<input type="checkbox" id="menu-user-state" class="header__menu__state">
|
||||
<label for="menu-user-state" class="header__menu__toggle header__menu__toggle--profile" style="background-image:url('/profile.php?u={{ current_user.user_id }}&m=avatar');{{ current_user.colour|html_colour }}">{{ current_user.username }}</label>
|
||||
<label for="menu-user-state" class="header__menu__toggle header__menu__toggle--profile" style="background-image:url('/profile.php?u={{ current_user.user_id }}&m=avatar');{{ current_user.user_colour|html_colour }}">{{ current_user.username }}</label>
|
||||
<div class="header__menu__options header__menu__options--user">
|
||||
<div class="header__menu__section">
|
||||
<a class="header__menu__link" href="/profile.php?u={{ current_user.user_id }}">Profile</a>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% block content %}
|
||||
<div class="container listing user-listing">
|
||||
{% for user in manage_users %}
|
||||
<a href="?v=view&u={{ user.user_id }}" class="listing__entry user-listing__entry"{% if not user.colour|colour_get_inherit %} style="{{ user.colour|html_colour({'border-color':'%s'}) }}"{% endif %}>
|
||||
<a href="?v=view&u={{ user.user_id }}" class="listing__entry user-listing__entry"{% if not user.user_colour|colour_get_inherit %} style="{{ user.user_colour|html_colour({'border-color':'%s'}) }}"{% endif %}>
|
||||
<div class="listing__entry__content user-listing__entry__content">
|
||||
<div class="user-listing__info">
|
||||
<div class="user-listing__username">
|
||||
|
|
193
views/mio/_layout/comments.twig
Normal file
193
views/mio/_layout/comments.twig
Normal file
|
@ -0,0 +1,193 @@
|
|||
{% macro comments_input(category, user, perms, reply_to) %}
|
||||
{% set reply_mode = reply_to is not null %}
|
||||
|
||||
<form class="comment comment--input{% if reply_mode %} comment--hidden{% endif %}"
|
||||
method="post" action="/comments.php"
|
||||
id="comment-{{ reply_mode ? 'reply-' ~ reply_to.comment_id : 'create-' ~ category.category_id }}">
|
||||
<input type="hidden" name="comment[category]" value="{{ category.category_id }}">
|
||||
{% if reply_mode %}
|
||||
<input type="hidden" name="comment[reply]" value="{{ reply_to.comment_id }}">
|
||||
{% endif %}
|
||||
<div class="comment__container">
|
||||
<div class="avatar comment__avatar"
|
||||
style="background-image:url('/profile.php?m=avatar&u={{ user.user_id }}')">
|
||||
</div>
|
||||
<div class="comment__content">
|
||||
<div class="comment__info">
|
||||
<div class="comment__user"
|
||||
style="{{ user.user_colour|html_colour }}">{{ user.username }}</div>
|
||||
</div>
|
||||
<textarea
|
||||
class="comment__text input__textarea comment__text--input"
|
||||
name="comment[text]" placeholder="Share your extensive insight..."></textarea>
|
||||
<div class="comment__actions">
|
||||
{% if not reply_mode %}
|
||||
{% if perms.can_pin %}
|
||||
<label class="comment__action comment__action--label">
|
||||
<input type="checkbox" class="comment__action__checkbox" name="comment[pin]">
|
||||
Pin this comment
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if perms.can_lock %}
|
||||
<label class="comment__action comment__action--label">
|
||||
<input type="checkbox" class="comment__action__checkbox" name="comment[lock]">
|
||||
Toggle locked status
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button class="comment__action comment__action--button comment__action--post">{{ reply_mode ? 'Reply' : 'Post' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro comments_section(comments, category, user, perms) %}
|
||||
<div class="comments">
|
||||
<div class="comments__input">
|
||||
{% if user|default(null) is null %}
|
||||
<div class="comments__notice">
|
||||
Please <a href="/auth.php?m=login" class="comments__notice__link">login</a> to comment.
|
||||
</div>
|
||||
{% elseif category|default(null) is null or perms|default(null) is null %}
|
||||
<div class="comments__notice">
|
||||
Posting new comments here is disabled.
|
||||
</div>
|
||||
{% elseif not perms.can_comment %}
|
||||
<div class="comments__notice">
|
||||
You are not allowed to post comments.
|
||||
</div>
|
||||
{% else %}
|
||||
{% from _self import comments_input %}
|
||||
{{ comments_input(category, user, perms) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<div class="comments__javascript">
|
||||
The comments need Javascript to be enabled to function properly, without it functionality is limited.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
{% if comments|length > 0 %}
|
||||
<div class="comments__listing">
|
||||
{% from _self import comments_entry %}
|
||||
{% for comment in comments %}
|
||||
{{ comments_entry(comment, 1, category, user, perms) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="comments__none">
|
||||
There are no comments yet.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.can_comment %}
|
||||
<script>
|
||||
var commentPostLock = false;
|
||||
|
||||
function commentPost(form)
|
||||
{
|
||||
console.log(form);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if perms.can_vote %}
|
||||
<script>
|
||||
var commentVoteLock = false;
|
||||
|
||||
// this is kinda temporary
|
||||
// needs to be able to change the state of both the dislike and like buttons
|
||||
function commentVote(elem, id, vote)
|
||||
{
|
||||
if (id < 1 || commentVoteLock)
|
||||
return;
|
||||
commentVoteLock = true;
|
||||
|
||||
var originalText = elem.textContent;
|
||||
elem.textContent = '...';
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('comment_id', id);
|
||||
formData.append('vote', vote);
|
||||
formData.append('csrf', '{{ csrf_token() }}');
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
console.log(this.readyState + ' ' + this.status + ': ' + this.responseText);
|
||||
if (this.readyState === 4) {
|
||||
elem.textContent = originalText;
|
||||
commentVoteLock = false;
|
||||
}
|
||||
};
|
||||
xhr.open('POST', '/comments.php');
|
||||
xhr.send(formData);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro comments_entry(comment, indent, category, user, perms) %}
|
||||
<div class="comment" id="comment-{{ comment.comment_id }}">
|
||||
<div class="comment__container">
|
||||
<a class="avatar comment__avatar"
|
||||
href="/profile.php?u={{ comment.user_id }}"
|
||||
style="background-image:url('/profile.php?m=avatar&u={{ comment.user_id }}')">
|
||||
</a>
|
||||
<div class="comment__content">
|
||||
<div class="comment__info">
|
||||
<a class="comment__user comment__user--link"
|
||||
href="/profile.php?u={{ comment.user_id }}"
|
||||
style="{{ comment.user_colour|html_colour }}">{{ comment.username }}</a>
|
||||
<a class="comment__link" href="#comment-{{ comment.comment_id }}">
|
||||
<time class="comment__date"
|
||||
title="{{ comment.comment_created|date('r') }}"
|
||||
datetime="{{ comment.comment_created|date('c') }}">
|
||||
{{ comment.comment_created|time_diff }}
|
||||
</time>
|
||||
</a>
|
||||
{% if comment.comment_pinned is not null %}
|
||||
<span class="comment__pin">{% spaceless %}
|
||||
Pinned
|
||||
{% if comment.comment_pinned != comment.comment_created %}
|
||||
<time title="{{ comment.comment_pinned|date('r') }}"
|
||||
datetime="{{ comment.comment_pinned|date('c') }}">
|
||||
{{ comment.comment_pinned|time_diff }}
|
||||
</time>
|
||||
{% endif %}
|
||||
{% endspaceless %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="comment__text">
|
||||
{{ comment.comment_text|nl2br }}
|
||||
</div>
|
||||
<div class="comment__actions">
|
||||
{% if perms.can_vote %}
|
||||
<a class="comment__action comment__action--link" href="javascript:void(0)" onclick="commentVote(this, {{ comment.comment_id }}, 1)">Like</a>
|
||||
<a class="comment__action comment__action--link" href="javascript:void(0)" onclick="commentVote(this, {{ comment.comment_id }}, -1)">Dislike</a>
|
||||
{% endif %}
|
||||
{% if perms.can_comment %}
|
||||
<a class="comment__action comment__action--link" href="#">Reply</a>
|
||||
{% endif %}
|
||||
{% if user is not null %}
|
||||
<a class="comment__action comment__action--link comment__action--hide" href="#">Report</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comment__replies comment__replies--indent-{{ indent }}" id="comment-{{ comment.comment_id }}-replies">
|
||||
{% from _self import comments_entry, comments_input %}
|
||||
{% if user|default(null) is not null and category|default(null) is not null and perms|default(null) is not null and perms.can_comment %}
|
||||
{{ comments_input(category, user, perms, comment) }}
|
||||
{% endif %}
|
||||
{% if comment.comment_replies is defined and comment.comment_replies|length > 0 %}
|
||||
{% for reply in comment.comment_replies %}
|
||||
{{ comments_entry(reply, indent + 1, category, user, perms) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
|
@ -1,5 +1,6 @@
|
|||
{% extends '@mio/changelog/master.twig' %}
|
||||
{% from '@mio/macros.twig' import navigation %}
|
||||
{% from '@mio/_layout/comments.twig' import comments_section %}
|
||||
|
||||
{% set is_valid = change|length > 0 %}
|
||||
{% set title = 'Changelog » ' ~ (is_valid ? 'Change #' ~ change.change_id : 'Unknown Change') %}
|
||||
|
@ -96,5 +97,14 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
{% if comments is defined %}
|
||||
<div class="container">
|
||||
<div class="container__title">
|
||||
Comments for {{ change.change_date }}
|
||||
</div>
|
||||
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ navigation(mio_navigation) }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% extends '@mio/changelog/master.twig' %}
|
||||
{% from '@mio/macros.twig' import navigation, pagination %}
|
||||
{% from '@mio/changelog/macros.twig' import changelog_listing %}
|
||||
{% from '@mio/_layout/comments.twig' import comments_section %}
|
||||
|
||||
{% set is_valid = changes|length > 0 %}
|
||||
{% set is_date = changelog_date|length > 0 %}
|
||||
|
@ -37,5 +38,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if comments is defined %}
|
||||
<div class="container">
|
||||
<div class="container__title">
|
||||
Comments
|
||||
</div>
|
||||
{{ comments_section(comments, comments_category, current_user|default(null), comments_perms) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ navigation(mio_navigation) }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,11 +3,18 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="container__title">Error {{ error_code }}{{ error_text is defined ? ' - ' ~ error_text : '' }}</div>
|
||||
<div class="container__title">
|
||||
{{ title|default(error_code|default(http_code) >= 400 ? 'Error' : 'Information') }} {{ error_code|default(http_code >= 400 ? http_code : '') }}{{ error_text is defined ? ' - ' ~ error_text : '' }}
|
||||
</div>
|
||||
|
||||
<div class="container__content">
|
||||
{% block error_message %}
|
||||
<p>Try again later, perhaps.</p>
|
||||
{% endblock %}
|
||||
{% if message is defined %}
|
||||
<p>{{ message }}</p>
|
||||
{% else %}
|
||||
{% block error_message %}
|
||||
<p>Try again later, perhaps.</p>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue