Merge remote-tracking branch 'origin/tagsForPosts' into moreStuff

master
Andrew Woodlee 2 years ago
commit a2a868bf42

@ -55,6 +55,8 @@ class PostController extends SiteController {
router.post('/:postId/image', requireAuthorPrivileges, upload.single('imageFile'), this.postUpdateImage.bind(this));
router.post('/:postId', requireAuthorPrivileges, this.postUpdatePost.bind(this));
router.post('/:postId/tags', requireAuthorPrivileges, this.postUpdatePostTags.bind(this));
router.post('/', requireAuthorPrivileges, this.postCreatePost.bind(this));
router.get('/:postId/edit', requireAuthorPrivileges, this.getEditor.bind(this));
@ -202,10 +204,37 @@ class PostController extends SiteController {
await postService.update(req.user, res.locals.post, req.body);
res.redirect(`/post/${res.locals.post.slug}`);
} catch (error) {
this.log.error('failed to update post', { newletterId: res.locals.post._id, error });
this.log.error('failed to update post', { postId: res.locals.post._id, error });
return next(error);
}
}
async postUpdatePostTags (req, res) {
const { post: postService } = this.dtp.services;
try {
if(!req.user.flags.isAdmin)
{
if (!req.user._id.equals(res.locals.post.author._id)) {
throw new SiteError(403, 'Only authors or admins can update tags.');
}
}
await postService.updateTags(req.user, res.locals.post, req.body);
const displayList = this.createDisplayList();
displayList.showNotification(
'Profile photo updated successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update post tags', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postCreatePost (req, res, next) {
const { post: postService } = this.dtp.services;

@ -0,0 +1,97 @@
// post.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
// const multer = require('multer');
const { SiteController, SiteError } = require('../../lib/site-lib');
class TagController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const { dtp } = this;
// const {
// post: postService,
// limiter: limiterService,
// session: sessionService,
// } = dtp.services;
// const authRequired = sessionService.authCheckMiddleware({ requiredLogin: true });
const router = express.Router();
dtp.app.use('/tag', router);
router.use(async (req, res, next) => {
res.locals.currentView = 'home';
return next();
});
router.param('tagSlug', this.populateTagSlug.bind(this));
router.get('/:tagSlug', this.getSearchView.bind(this));
}
async populateTagSlug (req, res, next, tagSlug) {
const { post: postService } = this.dtp.services;
try {
var allPosts = false;
var statusArray = ['published'];
if (req.user) {
if (req.user.flags.isAdmin) {
statusArray.push('draft');
allPosts = true;
}
}
res.locals.allPosts = allPosts;
res.locals.tagSlug = tagSlug;
tagSlug = tagSlug.replace("_", " ");
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getByTags(tagSlug, res.locals.pagination, statusArray);
res.locals.tag = tagSlug;
return next();
} catch (error) {
this.log.error('failed to populate tagSlug', { tagSlug, error });
return next(error);
}
}
async getSearchView (req, res) {
try {
res.locals.pageTitle = `Tag ${res.locals.tag} on ${this.dtp.config.site.name}`;
res.render('tag/view');
} catch (error) {
this.log.error('failed to service post view', { postId: res.locals.post._id, error });
throw SiteError("Error getting tag view:", error );
}
}
async getIndex (req, res, next) {
const { post: postService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination);
res.render('tag/index');
} catch (error) {
return next(error);
}
}
}
module.exports = {
slug: 'tag',
name: 'tag',
create: async (dtp) => {
let controller = new TagController(dtp);
return controller;
},
};

@ -23,6 +23,7 @@ const PostSchema = new Schema({
author: { type: Schema.ObjectId, required: true, index: 1, refPath: 'authorType' },
image: { type: Schema.ObjectId, ref: 'Image' },
title: { type: String, required: true },
tags: { type: [String], lowercase: true },
slug: { type: String, required: true, lowercase: true, unique: true },
summary: { type: String },
content: { type: String, select: false },

@ -74,6 +74,12 @@ class PostService extends SiteService {
}
}
if (postDefinition.tags) {
postDefinition.tags = postDefinition.tags.split(',').map((tag) => striptags(tag.trim()));
} else {
postDefinition.tags = [ ];
}
const post = new Post();
post.created = NOW;
post.authorType = author.type;
@ -82,6 +88,7 @@ class PostService extends SiteService {
post.slug = this.createPostSlug(post._id, post.title);
post.summary = striptags(postDefinition.summary.trim());
post.content = postDefinition.content.trim();
post.tags = postDefinition.tags;
post.status = postDefinition.status || 'draft';
post.flags = {
enableComments: postDefinition.enableComments === 'on',
@ -130,11 +137,17 @@ class PostService extends SiteService {
if (postDefinition.content) {
updateOp.$set.content = postDefinition.content.trim();
}
await this.updateTags(post._id, postDefinition.tags);
if (!postDefinition.status) {
throw new SiteError(406, 'Must include post status');
}
if (post.status !== 'published' && postDefinition.status === 'published') {
// const postWillBeUnpublished = post.status === 'published' && postDefinition.status !== 'published';
const postWillBePublished = post.status !== 'published' && postDefinition.status === 'published';
if (postWillBePublished) {
if (!user.permissions.canPublishPosts) {
throw new SiteError(403, 'You are not permitted to publish posts');
}
@ -171,6 +184,42 @@ class PostService extends SiteService {
}
}
// pass the post._id and its tags to function
async updateTags (id, tags) {
if (tags) {
tags = tags.split(',').map((tag) => striptags(tag.trim().toLowerCase()));
} else {
tags = [ ];
}
const NOW = new Date();
const updateOp = {
$setOnInsert: {
created: NOW,
},
$set: {
updated: NOW,
},
};
updateOp.$set.tags = tags;
await Post.findOneAndUpdate(
{ _id: id },
updateOp,
);
}
async getByTags (tag, pagination, status = ['published']) {
if (!Array.isArray(status)) {
status = [status];
}
const posts = await Post.find( { status: { $in: status }, tags: tag } )
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populatePost);
return posts;
}
async updateImage (user, post, file) {
const { image: imageService } = this.dtp.services;

@ -33,12 +33,15 @@ block content
}
input(id="slug", name="slug", type="text", placeholder= "Enter post URL slug", value= post ? postSlug : undefined).uk-input
.uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post ? post.slug : 'your-slug-here'}
.uk-margin
label(for="tags").uk-form-label Post tags
input(id="tags", name="tags", placeholder= "Enter a comma-separated list of tags", value= (post.tags || [ ]).join(', ')).uk-input
.uk-margin
label(for="summary").uk-form-label Post summary
textarea(id="summary", name="summary", rows="4", placeholder= "Enter post summary (text only, no HTML)").uk-textarea= post ? post.summary : undefined
div(uk-grid)
.uk-width-auto
button(type="submit").uk-button.uk-button-primary= post ? 'Update post' : 'Create post'
button(type="submit").uk-button.uk-button-primary= 'Update post'
.uk-margin
label(for="status").uk-form-label Status
select(id="status", name="status").uk-select

@ -24,7 +24,15 @@ block content
div(uk-grid)
.uk-width-auto
+renderGabShareButton(`https://${site.domainKey}/post/${post.slug}`, `${post.title} - ${post.summary}`)
+renderSectionTitle('Post tags')
if Array.isArray(post.tags) && (post.tags.length > 0)
div(uk-grid).uk-grid-small
each tag in post.tags
-
var tagSlug;
tagSlug = tag.replace(" ", "_")
a(href=`/tag/${tagSlug}`).uk-display-block.uk-link-reset.uk-margin-small= tag
.uk-margin
.uk-article-meta
div(uk-grid).uk-grid-small.uk-flex-top

@ -0,0 +1,19 @@
mixin renderPostSummaryFull (post)
div(uk-grid).uk-grid-small
if post.image
.uk-width-auto
img(src= `/image/${post.image._id}`).uk-width-medium
else
.uk-width-auto
img(src="/img/default-poster.jpg").uk-width-medium
.uk-width-expand
.uk-text-large.uk-text-bold(style="line-height: 1em;")
a(href=`/post/${post.slug}`)= `${post.title}`
.uk-text-small.uk-text-muted
div
div= moment(post.created).fromNow()
span by
a(href=`/user/${post.author.username}`)=` ${post.author.username}`
if user && allPosts
div= `Status: ${post.status}`
div= post.summary

@ -0,0 +1,37 @@
extends ../layouts/main-sidebar
include components/list
include ../components/pagination-bar
block content
if Array.isArray(posts) && (posts.length > 0)
h3= `Posts with the tag ${tag}.`
ul.uk-list.uk-list-divider
each post in posts
li
+renderPostSummaryFull(post)
.uk-card-footer
+renderPaginationBar(`/tag/${tagSlug}`, posts.length )
//- li
if post.image
img(src= `/image/${post.image._id}`, href=`/post/${post.slug}`, style="max-height: 350px; object-fit: cover; vertical-align:middle;margin:0px 20px;").responsive
else
img(src="/img/default-poster.jpg", href=`/post/${post.slug}`, style="max-height: 350px; object-fit: cover; vertical-align:middle;margin:0px 20px;").responsive
a(href=`/post/${post.slug}`).uk-display-block
div.h2= post.title
.uk-article-meta
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-expand
a(href=`/post/${post.slug}`)= moment(post.created).fromNow()
span by
a(href=`/user/${post.author.username}`)=` ${post.author.username}`
.uk-width-expand
div= post.summary
else
h3= `There are no posts with the tag ${tag}.`
Loading…
Cancel
Save