Post-tags (#9)

Reviewed-on: #9
Co-authored-by: Armored Dragon <publicmail@armoreddragon.com>
Co-committed-by: Armored Dragon <publicmail@armoreddragon.com>
pull/2/head
Armored Dragon 2024-03-22 09:24:19 +00:00 committed by Armored Dragon
parent d24f87e23a
commit 78923279be
11 changed files with 108 additions and 33 deletions

View File

@ -74,7 +74,7 @@ async function registerUser(username, password, options) {
return { success: true, message: `Successfully created ${username}` }; return { success: true, message: `Successfully created ${username}` };
} }
// Posts // Posts
async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, page = 0, tags = [], search_title = false, search_content = false, search }) { async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, page = 0, search_title = false, search_content = false, search_tags = false, search }) {
// If we have an ID, we want a single post // If we have an ID, we want a single post
if (id) { if (id) {
// Get the post by the id // Get the post by the id
@ -110,14 +110,22 @@ async function getBlog({ id, visibility = "PUBLISHED", owner_id, limit = 10, pag
ownerid: owner_id, ownerid: owner_id,
}, },
], ],
AND: [],
AND: [
{
OR: [
]
}
],
}; };
// Build the "where_object" object // Build the "where_object" object
if (tags.length > 0) { if (search){
if (search_tags) where_object["AND"][0]["OR"].push({ tags: { hasSome: [search?.toLowerCase()] }});
if (search_title) where_object["AND"][0]["OR"].push({ title: { contains: search, mode: "insensitive" } });
if (search_content) where_object["AND"][0]["OR"].push({ content: { contains: search, mode: "insensitive" } });
} }
if (search_title) where_object["AND"].push({ title: { contains: search, mode: "insensitive" } });
if (search_content) where_object["AND"].push({ content: { contains: search, mode: "insensitive" } });
// Execute search // Execute search
const blog_posts = await prisma.blogPost.findMany({ const blog_posts = await prisma.blogPost.findMany({
@ -171,6 +179,7 @@ async function postBlog(blog_post, owner_id) {
content: blog_post.content, content: blog_post.content,
visibility: blog_post.visibility, visibility: blog_post.visibility,
publish_date: blog_post.publish_date, publish_date: blog_post.publish_date,
tags: blog_post.tags,
}; };
// Save to database // Save to database
@ -203,13 +212,15 @@ async function postBlog(blog_post, owner_id) {
} }
async function deleteBlog(blog_id, requester_id) { async function deleteBlog(blog_id, requester_id) {
const user = await getUser({ id: requester_id }); const user = await getUser({ id: requester_id });
const post = await getPost({ id: blog_id }); const post = await getBlog({ id: blog_id });
let can_delete = post.owner.id === user.data.id || user.data.role === "ADMIN"; if (!post.success) return { success: false, message: post.message || "Post does not exist" };
let can_delete = post.data.owner.id === user.data.id || user.data.role === "ADMIN";
if (can_delete) { if (can_delete) {
await prisma.blogPost.delete({ where: { id: post.id } }); await prisma.blogPost.delete({ where: { id: post.data.id } });
_deleteS3Directory(post.id, "blog"); _deleteS3Directory(post.data.id, "blog");
return { success: true }; return { success: true };
} }
@ -217,27 +228,35 @@ async function deleteBlog(blog_id, requester_id) {
} }
async function updateBlog(blog_post, requester_id) { async function updateBlog(blog_post, requester_id) {
const user = await getUser({ id: requester_id }); const user = await getUser({ id: requester_id });
const post = await getPost({ id: blog_post.id, raw: true }); const post = await getBlog({ id: blog_post.id, raw: true });
let publish_date = null;
delete blog_post.id; delete blog_post.id;
let can_update = post.owner.id === user.data.id || user.data.role === "ADMIN"; if (!post.success) return { success: false, message: post.message || "Post not found" };
let can_update = post.data.owner.id === user.data.id || user.data.role === "ADMIN";
if (!can_update) return { success: false, message: "User not permitted" }; if (!can_update) return { success: false, message: "User not permitted" };
const [year, month, day] = blog_post.date.split("-"); // FIXME: Unsure if this actually works
const [hour, minute] = blog_post.time.split(":"); // Check if we already have a formatted publish date
let publish_date = new Date(year, month - 1, day, hour, minute); if (typeof blog_post.publish_date !== "object") {
const [year, month, day] = blog_post.date.split("-");
const [hour, minute] = blog_post.time.split(":");
publish_date = new Date(year, month - 1, day, hour, minute);
}
let blog_post_formatted = { let blog_post_formatted = {
title: blog_post.title, title: blog_post.title,
description: blog_post.description, description: blog_post.description,
content: blog_post.content, content: blog_post.content,
visibility: blog_post.unlisted ? "UNLISTED" : "PUBLISHED", visibility: blog_post.unlisted ? "UNLISTED" : "PUBLISHED",
publish_date: publish_date, publish_date: publish_date || blog_post.publish_date,
tags: blog_post.tags,
}; };
await prisma.blogPost.update({ where: { id: post.id }, data: blog_post_formatted }); await prisma.blogPost.update({ where: { id: post.data.id }, data: blog_post_formatted });
let uploaded_images = []; let uploaded_images = [];
let uploaded_thumbnail = "DEFAULT"; let uploaded_thumbnail = "DEFAULT";
@ -253,24 +272,24 @@ async function updateBlog(blog_post, requester_id) {
} }
let data_to_update = { let data_to_update = {
images: [...post.raw_images, ...uploaded_images], images: [...post.data.raw_images, ...uploaded_images],
}; };
if (blog_post.thumbnail) { if (blog_post.thumbnail) {
const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64"); const image_data = Buffer.from(blog_post.thumbnail.data_blob.split(",")[1], "base64");
const name = await _uploadImage(post.id, "blog", true, image_data, blog_post.thumbnail.id); const name = await _uploadImage(post.data.id, "blog", true, image_data, blog_post.thumbnail.id);
uploaded_thumbnail = name; uploaded_thumbnail = name;
data_to_update.thumbnail = uploaded_thumbnail; data_to_update.thumbnail = uploaded_thumbnail;
} }
await prisma.blogPost.update({ where: { id: post.id }, data: data_to_update }); await prisma.blogPost.update({ where: { id: post.data.id }, data: data_to_update });
return { success: true }; return { success: true };
} }
async function deleteImage(image, requester_id) { async function deleteImage(image, requester_id) {
const user = await getUser({ id: requester_id }); const user = await getUser({ id: requester_id });
const post = await getPost({ id: image.parent, raw: true }); const post = await getBlog({ id: image.parent, raw: true });
// Check if post exists // Check if post exists
if (!post) return { success: false, message: "Post does not exist" }; if (!post) return { success: false, message: "Post does not exist" };
@ -391,9 +410,7 @@ function _format_blog_content(content, images) {
const video = /{video:([^}]+)}/g; const video = /{video:([^}]+)}/g;
content = content.replace(video, (match, inner_content) => { content = content.replace(video, (match, inner_content) => {
return `<div class='video-embed'><iframe src="${_getVideoEmbed( return `<div class='video-embed'><iframe src="${_getVideoEmbed(inner_content)}" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
inner_content
)}" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
}); });
content = content.replace(image_regex, (match, image_name) => { content = content.replace(image_regex, (match, image_name) => {

View File

@ -9,8 +9,7 @@ async function postRegister(req, res) {
// User registration disabled? // User registration disabled?
// We also check if the server was setup. If it was not set up, the server will proceed anyways. // We also check if the server was setup. If it was not set up, the server will proceed anyways.
if (!core.settings["ACCOUNT_REGISTRATION"] && core.settings["SETUP_COMPLETE"]) if (!core.settings["ACCOUNT_REGISTRATION"] && core.settings["SETUP_COMPLETE"]) return res.json({ success: false, message: "Account registrations are disabled" });
return res.json({ success: false, message: "Account registrations are disabled" });
// User data valid? // User data valid?
if (!form_validation.success) return res.json({ success: false, message: form_validation.message }); if (!form_validation.success) return res.json({ success: false, message: form_validation.message });
@ -70,8 +69,15 @@ async function deleteBlog(req, res) {
return res.json(await core.deleteBlog(req.body.id, req.session.user.id)); return res.json(await core.deleteBlog(req.body.id, req.session.user.id));
} }
async function patchBlog(req, res) { async function patchBlog(req, res) {
// FIXME: validate does not return post id
// Can user change post?
// User is admin, or user is author
// Validate blog info
const valid = await validate.postBlog(req.body);
// TODO: Permissions for updating blog // TODO: Permissions for updating blog
return res.json(await core.updateBlog(req.body, req.session.user.id)); return res.json(await core.updateBlog({ ...valid.data, id: req.body.id }, req.session.user.id));
} }
module.exports = { postRegister, postLogin, postSetting, deleteImage, postBlog, deleteBlog, patchBlog }; module.exports = { postRegister, postLogin, postSetting, deleteImage, postBlog, deleteBlog, patchBlog };

View File

@ -25,6 +25,19 @@ async function postBlog(blog_object) {
const [hour, minute] = blog_object.time.split(":"); const [hour, minute] = blog_object.time.split(":");
let publish_date = new Date(year, month - 1, day, hour, minute); let publish_date = new Date(year, month - 1, day, hour, minute);
// Go though our tags and ensure they are:
let valid_tag_array = [];
blog_object.tags.forEach((tag) => {
// Trimmed
tag = tag.trim();
// Lowercase
tag = tag.toLowerCase();
// Non-empty
if (tag.length !== 0) valid_tag_array.push(tag);
});
// Format our data to save // Format our data to save
let blog_post_formatted = { let blog_post_formatted = {
title: blog_object.title, title: blog_object.title,
@ -32,6 +45,7 @@ async function postBlog(blog_object) {
content: blog_object.content, content: blog_object.content,
visibility: blog_object.visibility, visibility: blog_object.visibility,
publish_date: publish_date, publish_date: publish_date,
tags: valid_tag_array,
images: blog_object.images, images: blog_object.images,
thumbnail: blog_object.thumbnail, thumbnail: blog_object.thumbnail,
}; };

View File

@ -27,7 +27,7 @@ async function author(req, res) {
res.render("author.ejs", { ...getDefaults(req), blog_post: profile.data }); res.render("author.ejs", { ...getDefaults(req), blog_post: profile.data });
} }
async function blogList(req, res) { async function blogList(req, res) {
const blog_list = await core.getBlog({ owner_id: req.session.user?.id, page: req.query.page || 0, search: req.query.search, search_title: true }); const blog_list = await core.getBlog({ owner_id: req.session.user?.id, page: req.query.page || 0, search: req.query.search, search_tags: true, search_title: true });
res.render("blogList.ejs", { res.render("blogList.ejs", {
...getDefaults(req), ...getDefaults(req),
blog_list: blog_list.data, blog_list: blog_list.data,

View File

@ -221,6 +221,7 @@ a.bad {
.container .setting-row .setting-toggleable input { .container .setting-row .setting-toggleable input {
padding: 0; padding: 0;
margin: auto 0 auto auto; margin: auto 0 auto auto;
height: 25px;
width: 100px; width: 100px;
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: center;

View File

@ -66,7 +66,7 @@
input { input {
padding: 0; padding: 0;
margin: auto 0 auto auto; margin: auto 0 auto auto;
// width: 100%; height: 25px;
width: 100px; width: 100px;
box-sizing: border-box; box-sizing: border-box;
text-align: center; text-align: center;

View File

@ -130,6 +130,20 @@
outline: 0; outline: 0;
} }
.e-tags {
min-height: 40px;
width: 100%;
background-color: #222;
display: flex;
flex-direction: row;
padding: 5px;
box-sizing: border-box;
margin-bottom: 1rem;
}
.e-tags input {
width: 100%;
}
.e-settings { .e-settings {
min-height: 40px; min-height: 40px;
width: 100%; width: 100%;

View File

@ -145,6 +145,21 @@ $background-body: #222;
} }
} }
.e-tags {
min-height: 40px;
width: 100%;
background-color: $background-body;
display: flex;
flex-direction: row;
padding: 5px;
box-sizing: border-box;
margin-bottom: 1rem;
input {
width: 100%;
}
}
.e-settings { .e-settings {
min-height: 40px; min-height: 40px;
width: 100%; width: 100%;

View File

@ -88,10 +88,18 @@ async function publishBlog(unlisted, edit) {
description: qs("#description").value, description: qs("#description").value,
content: qs("#content").value, content: qs("#content").value,
visibility: unlisted ? "UNLISTED" : "PUBLISHED", visibility: unlisted ? "UNLISTED" : "PUBLISHED",
tags: [],
date: qs("#date").value, date: qs("#date").value,
time: qs("#time").value, time: qs("#time").value,
}; };
// Get our tags, trim them, then shove them into an array
const tags_value = qs("#tags").value || "";
if (tags_value.length) {
let tags_array = qs("#tags").value.split(",");
tags_array.forEach((tag) => form_data.tags.push(tag.trim()));
}
// If we have a thumbnail, read the thumbnail image and store it // If we have a thumbnail, read the thumbnail image and store it
if (pending_thumbnail.data_blob) { if (pending_thumbnail.data_blob) {
form_data.thumbnail = { ...pending_thumbnail, data_blob: await _readFile(pending_thumbnail.data_blob) }; form_data.thumbnail = { ...pending_thumbnail, data_blob: await _readFile(pending_thumbnail.data_blob) };
@ -161,8 +169,7 @@ function customDragString() {
} }
function updateImages() { function updateImages() {
const image_div = (img_id, img_url) => const image_div = (img_id, img_url) => `<div class="image"><img data-image_id="${img_id}" src="${img_url}" /><div><a onclick="deleteImage('${img_id}')">X</a></div></div>`;
`<div class="image"><img data-image_id="${img_id}" src="${img_url}" /><div><a onclick="deleteImage('${img_id}')">X</a></div></div>`;
// Clear existing listings // Clear existing listings
qsa(".e-image-area .image").forEach((entry) => entry.remove()); qsa(".e-image-area .image").forEach((entry) => entry.remove());

View File

@ -67,7 +67,9 @@
</div> </div>
<textarea id="content" placeholder="Tell us about your subject..."><%= existing_blog.raw_content %></textarea> <textarea id="content" placeholder="Tell us about your subject..."><%= existing_blog.raw_content %></textarea>
</div> </div>
<div class="e-tags">
<input id="tags" type="text" placeholder="Enter a comma separated list of tags" value="<%= existing_blog.tags %>" />
</div>
<div class="e-settings"> <div class="e-settings">
<div class="publish-date"> <div class="publish-date">
<div>Publish On</div> <div>Publish On</div>

View File

@ -36,8 +36,7 @@ model BlogPost {
ownerid String? ownerid String?
// Tags // Tags
organization_tag String? tags String[]
keyword_tags String[]
// Dates // Dates
publish_date DateTime? publish_date DateTime?