From bd6f166bad143c36800336dbff0f89f5f82762ea Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Sat, 26 Nov 2022 13:11:47 -0600 Subject: [PATCH 1/6] progress on tags for articles --- .gitignore | 1 + app/controllers/user.js | 2 +- app/models/post.js | 1 + app/services/otp-auth.js | 2 +- app/services/post.js | 14 ++++++++++++++ 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 00c8845..3868498 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ ssl/*key data/minio node_modules dist +start-local-* \ No newline at end of file diff --git a/app/controllers/user.js b/app/controllers/user.js index 3accdc5..fb58272 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -218,7 +218,7 @@ class UserController extends SiteController { if (error) { return next(error); } - res.redirect(`/user/${res.locals.user._id}`); + res.redirect(`/user/${res.locals.user.username}`); }); } catch (error) { this.log.error('failed to create new user', { error }); diff --git a/app/models/post.js b/app/models/post.js index 736338f..e5e44a0 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -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] }, slug: { type: String, required: true, lowercase: true, unique: true }, summary: { type: String }, content: { type: String, select: false }, diff --git a/app/services/otp-auth.js b/app/services/otp-auth.js index 476c608..6223dd7 100644 --- a/app/services/otp-auth.js +++ b/app/services/otp-auth.js @@ -211,7 +211,7 @@ class OtpAuthService extends SiteService { async destroyOtpSession (req, serviceName) { delete req.session.otp[serviceName]; - await this.saveSession(req) + await this.saveSession(req); } async isUserProtected (user, serviceName) { diff --git a/app/services/post.js b/app/services/post.js index eee7062..32bcc84 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -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,6 +137,13 @@ class PostService extends SiteService { if (postDefinition.content) { updateOp.$set.content = postDefinition.content.trim(); } + if (postDefinition.tags) { + postDefinition.tags = postDefinition.tags.split(',').map((tag) => striptags(tag.trim())); + } else { + postDefinition.tags = [ ]; + } + updateOp.$set.tags = postDefinition.tags; + if (!postDefinition.status) { throw new SiteError(406, 'Must include post status'); } From be6d741cc40787bc0bceacabf3fad3d3db1e0e57 Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Sun, 11 Dec 2022 20:36:08 -0600 Subject: [PATCH 2/6] progress on tags --- app/controllers/post.js | 29 +++++++++++++++++++++++++++++ app/models/post.js | 2 +- app/services/post.js | 29 ++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/controllers/post.js b/app/controllers/post.js index 0fa81cc..c108800 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -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)); @@ -206,6 +208,33 @@ class PostController extends SiteController { 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; diff --git a/app/models/post.js b/app/models/post.js index e5e44a0..3ba931a 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -23,7 +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] }, + tags: { type: [String], lowercase: true }, slug: { type: String, required: true, lowercase: true, unique: true }, summary: { type: String }, content: { type: String, select: false }, diff --git a/app/services/post.js b/app/services/post.js index 32bcc84..3cdd13a 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -148,7 +148,10 @@ class PostService extends SiteService { 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'); } @@ -185,6 +188,30 @@ class PostService extends SiteService { } } + // pass the post._id and its tags to function + async updateTags (id, tags) { + if (tags) { + tags = tags.tags.split(',').map((tag) => striptags(tag.trim())); + } 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 updateImage (user, post, file) { const { image: imageService } = this.dtp.services; From 5eb67304dd612582800e53b155ac89e8e214e87d Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Mon, 12 Dec 2022 17:46:17 -0600 Subject: [PATCH 3/6] added tags and viewing posts with tag when searched --- app/controllers/post.js | 2 +- app/controllers/tag.js | 85 +++++++++++++++++++++++++++++++ app/services/post.js | 17 ++++--- app/views/post/editor.pug | 5 +- app/views/post/view.pug | 10 +++- app/views/tag/components/list.pug | 17 +++++++ app/views/tag/view.pug | 9 ++++ 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 app/controllers/tag.js create mode 100644 app/views/tag/components/list.pug create mode 100644 app/views/tag/view.pug diff --git a/app/controllers/post.js b/app/controllers/post.js index c108800..0261186 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -204,7 +204,7 @@ 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); } } diff --git a/app/controllers/tag.js b/app/controllers/tag.js new file mode 100644 index 0000000..59c9187 --- /dev/null +++ b/app/controllers/tag.js @@ -0,0 +1,85 @@ +// 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 { + tagSlug = tagSlug.replace("_", " "); + res.locals.posts = await postService.getByTags(tagSlug); + 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 = `${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; + }, +}; \ No newline at end of file diff --git a/app/services/post.js b/app/services/post.js index 3cdd13a..021d2be 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -137,12 +137,8 @@ class PostService extends SiteService { if (postDefinition.content) { updateOp.$set.content = postDefinition.content.trim(); } - if (postDefinition.tags) { - postDefinition.tags = postDefinition.tags.split(',').map((tag) => striptags(tag.trim())); - } else { - postDefinition.tags = [ ]; - } - updateOp.$set.tags = postDefinition.tags; + + await this.updateTags(post._id, postDefinition.tags); if (!postDefinition.status) { throw new SiteError(406, 'Must include post status'); @@ -191,7 +187,7 @@ class PostService extends SiteService { // pass the post._id and its tags to function async updateTags (id, tags) { if (tags) { - tags = tags.tags.split(',').map((tag) => striptags(tag.trim())); + tags = tags.split(',').map((tag) => striptags(tag.trim().toLowerCase())); } else { tags = [ ]; } @@ -212,6 +208,13 @@ class PostService extends SiteService { ); } + async getByTags (tag) { + const posts = await Post.find( { tags: tag } ) + .sort({ created: -1 }) + .populate(this.populatePost); + return posts; + } + async updateImage (user, post, file) { const { image: imageService } = this.dtp.services; diff --git a/app/views/post/editor.pug b/app/views/post/editor.pug index c37f5a5..56b97e3 100644 --- a/app/views/post/editor.pug +++ b/app/views/post/editor.pug @@ -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 diff --git a/app/views/post/view.pug b/app/views/post/view.pug index ae9f140..aab9db8 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -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 diff --git a/app/views/tag/components/list.pug b/app/views/tag/components/list.pug new file mode 100644 index 0000000..6bb667c --- /dev/null +++ b/app/views/tag/components/list.pug @@ -0,0 +1,17 @@ +mixin renderPostList (posts) + if Array.isArray(posts) && (posts.length > 0) + ul.uk-list.uk-list-divider + each post in posts + li + a(href=`/post/${post.slug}`).uk-display-block + div= 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}` + + else + div There are no posts with this tag. \ No newline at end of file diff --git a/app/views/tag/view.pug b/app/views/tag/view.pug new file mode 100644 index 0000000..57375d0 --- /dev/null +++ b/app/views/tag/view.pug @@ -0,0 +1,9 @@ +extends ../layouts/main-sidebar + +include components/list.pug + +block content + + + h2.uk-text-lead= `Posts tagged with ${tag}` + +renderPostList(posts) \ No newline at end of file From d0ef1ce1f81a80bcadbeb4179b27de99ffb0342b Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Tue, 13 Dec 2022 01:43:46 -0600 Subject: [PATCH 4/6] fixes for displaying tags --- app/views/tag/view.pug | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/views/tag/view.pug b/app/views/tag/view.pug index 57375d0..29166d6 100644 --- a/app/views/tag/view.pug +++ b/app/views/tag/view.pug @@ -5,5 +5,20 @@ include components/list.pug block content - h2.uk-text-lead= `Posts tagged with ${tag}` - +renderPostList(posts) \ No newline at end of file + if Array.isArray(posts) && (posts.length > 0) + h3= `Posts with the tag ${tag}.` + ul.uk-list.uk-list-divider + each post in posts + li + a(href=`/post/${post.slug}`).uk-display-block + div= 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}` + + else + h3= `There are no posts with the tag ${tag}.` \ No newline at end of file From 1f7f7ff6725f35e631f0d729f544c3e833d2e349 Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Tue, 20 Dec 2022 13:32:18 -0600 Subject: [PATCH 5/6] add better tag view --- app/views/tag/components/list.pug | 34 +++++++++++++++---------------- app/views/tag/view.pug | 12 +++++++++-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/views/tag/components/list.pug b/app/views/tag/components/list.pug index 6bb667c..5c953d6 100644 --- a/app/views/tag/components/list.pug +++ b/app/views/tag/components/list.pug @@ -1,17 +1,17 @@ -mixin renderPostList (posts) - if Array.isArray(posts) && (posts.length > 0) - ul.uk-list.uk-list-divider - each post in posts - li - a(href=`/post/${post.slug}`).uk-display-block - div= 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}` - - else - div There are no posts with this tag. \ No newline at end of file +mixin renderPostSummaryFull (post) + div(uk-grid).uk-grid-small + if post.image + .uk-width-auto + img(src= `/image/${post.image}`).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.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}` + div= post.summary \ No newline at end of file diff --git a/app/views/tag/view.pug b/app/views/tag/view.pug index 29166d6..d7dabff 100644 --- a/app/views/tag/view.pug +++ b/app/views/tag/view.pug @@ -1,6 +1,6 @@ extends ../layouts/main-sidebar -include components/list.pug +include components/list block content @@ -10,8 +10,14 @@ block content ul.uk-list.uk-list-divider each post in posts li + +renderPostSummaryFull(post) + //- 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= post.title + div.h2= post.title .uk-article-meta div(uk-grid).uk-grid-small.uk-text-small @@ -19,6 +25,8 @@ block content 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}.` \ No newline at end of file From 6cdc5250f770e5c82828497a29aa89c948a73b07 Mon Sep 17 00:00:00 2001 From: Andrew Woodlee Date: Wed, 21 Dec 2022 01:44:21 -0600 Subject: [PATCH 6/6] improved tag view added getting drafts when viewing tag as admin --- app/controllers/tag.js | 16 ++++++++++++++-- app/services/post.js | 9 +++++++-- app/views/tag/components/list.pug | 6 ++++-- app/views/tag/view.pug | 5 +++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/controllers/tag.js b/app/controllers/tag.js index 59c9187..485555b 100644 --- a/app/controllers/tag.js +++ b/app/controllers/tag.js @@ -41,10 +41,22 @@ class TagController extends SiteController { 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.posts = await postService.getByTags(tagSlug); + 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); @@ -53,7 +65,7 @@ class TagController extends SiteController { async getSearchView (req, res) { try { - res.locals.pageTitle = `${res.locals.tag} on ${this.dtp.config.site.name}`; + res.locals.pageTitle = `Tag ${res.locals.tag} on ${this.dtp.config.site.name}`; res.render('tag/view'); } catch (error) { diff --git a/app/services/post.js b/app/services/post.js index 021d2be..a4378fd 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -208,9 +208,14 @@ class PostService extends SiteService { ); } - async getByTags (tag) { - const posts = await Post.find( { tags: tag } ) + 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; } diff --git a/app/views/tag/components/list.pug b/app/views/tag/components/list.pug index 5c953d6..534d976 100644 --- a/app/views/tag/components/list.pug +++ b/app/views/tag/components/list.pug @@ -2,16 +2,18 @@ mixin renderPostSummaryFull (post) div(uk-grid).uk-grid-small if post.image .uk-width-auto - img(src= `/image/${post.image}`).uk-width-medium + 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.slug}`)= `${post.title}` + 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 \ No newline at end of file diff --git a/app/views/tag/view.pug b/app/views/tag/view.pug index d7dabff..9ace8d5 100644 --- a/app/views/tag/view.pug +++ b/app/views/tag/view.pug @@ -2,6 +2,8 @@ extends ../layouts/main-sidebar include components/list +include ../components/pagination-bar + block content @@ -11,6 +13,9 @@ block content 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