From 074ef0fbdb8dfefac8fcef29dc432e01ba6e17cd Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Wed, 2 Oct 2019 18:47:20 +0000 Subject: [PATCH] Added article by support --- api/article/model.mjs | 2 +- api/article/routes.mjs | 4 +++ api/article/security.mjs | 1 + api/authentication/helper.mjs | 2 +- api/authentication/routes.mjs | 2 +- api/jwt.mjs | 3 ++- api/serveindex.mjs | 8 ++++-- api/staff/model.mjs | 4 +-- app/admin/articles.js | 9 +++---- app/admin/articles.scss | 8 ++++++ app/admin/editarticle.js | 49 ++++++++++++++++++++++++++++++----- app/api/article.p.js | 2 +- app/article/article.js | 8 ++++++ app/article/article.scss | 38 ++++++++++++++++++++++++++- app/frontpage/frontpage.js | 2 +- app/widgets/common.scss | 1 + app/widgets/newsentry.js | 1 - app/widgets/newsitem.js | 1 + 18 files changed, 119 insertions(+), 26 deletions(-) diff --git a/api/article/model.mjs b/api/article/model.mjs index 78c60d0..02a736b 100644 --- a/api/article/model.mjs +++ b/api/article/model.mjs @@ -136,7 +136,7 @@ const Article = bookshelf.createModel({ .fetchPage({ pageSize: 10, page: page, - withRelated: ['files', 'media', 'banner', 'parent'], + withRelated: ['files', 'media', 'banner', 'parent', 'staff'], }) }, }) diff --git a/api/article/routes.mjs b/api/article/routes.mjs index 299a856..f0145a1 100644 --- a/api/article/routes.mjs +++ b/api/article/routes.mjs @@ -55,6 +55,10 @@ export default class ArticleRoutes { async createArticle(ctx) { await this.security.validUpdate(ctx) + if (!ctx.request.body.staff_id) { + ctx.request.body.staff_id = ctx.state.user.id + } + ctx.body = await this.Article.create(ctx.request.body) } diff --git a/api/article/security.mjs b/api/article/security.mjs index 45f1b53..10e33c8 100644 --- a/api/article/security.mjs +++ b/api/article/security.mjs @@ -8,6 +8,7 @@ const requiredFields = [ const validFields = [ 'name', 'path', + 'staff_id', 'description', 'parent_id', 'media_id', diff --git a/api/authentication/helper.mjs b/api/authentication/helper.mjs index 1425a2e..f770c21 100644 --- a/api/authentication/helper.mjs +++ b/api/authentication/helper.mjs @@ -28,6 +28,6 @@ export default class AuthHelper { throw err } - return this.jwt.createToken(staff.get('email'), staff.get('level')) + return this.jwt.createToken(staff.id, staff.get('email'), staff.get('level')) } } diff --git a/api/authentication/routes.mjs b/api/authentication/routes.mjs index c6acd30..5c02e0a 100644 --- a/api/authentication/routes.mjs +++ b/api/authentication/routes.mjs @@ -38,7 +38,7 @@ export default class AuthRoutes { level = staff.get('level') } - ctx.body = { token: this.jwt.createToken(output.email, level) } + ctx.body = { token: this.jwt.createToken(staff.id, output.email, level) } } /* diff --git a/api/jwt.mjs b/api/jwt.mjs index 8ad2661..e5a2180 100644 --- a/api/jwt.mjs +++ b/api/jwt.mjs @@ -43,8 +43,9 @@ export default class Jwt { return this.jwt.decode(token) } - createToken(email, level, opts) { + createToken(id, email, level, opts) { return this.sign({ + id: id, email: email, level: level, }, email, opts) diff --git a/api/serveindex.mjs b/api/serveindex.mjs index 8699141..38d3447 100644 --- a/api/serveindex.mjs +++ b/api/serveindex.mjs @@ -17,6 +17,10 @@ function mapArticle(trim = false, x, includeBanner = false, includeFiles = true) path: x.path, description: x.description, name: x.name, + staff: x.staff && ({ + id: x.staff.id, + fullname: x.staff.fullname, + }) || null, media: x.media && ({ link: !trim && x.media.link || null, large_url: x.media.large_url, @@ -95,7 +99,7 @@ export async function serveIndex(ctx, path) { { id: x.id, name: x.name, path: x.path } )) )) - featured = await Article.getFeatured(['files', 'media', 'banner']) + featured = await Article.getFeatured(['media', 'banner']) if (featured) { featured = mapArticle(true, featured.toJSON(), true, false) } @@ -120,7 +124,7 @@ export async function serveIndex(ctx, path) { if (id) { let found if (path.startsWith('/article/')) { - found = await Article.getSingle(id, ['media', 'parent', 'banner', 'files'], false, null, true) + found = await Article.getSingle(id, ['media', 'parent', 'banner', 'files', 'staff'], false, null, true) if (found) { found = mapArticle(false, found.toJSON()) } diff --git a/api/staff/model.mjs b/api/staff/model.mjs index 92f8044..0357111 100644 --- a/api/staff/model.mjs +++ b/api/staff/model.mjs @@ -26,9 +26,7 @@ const Staff = bookshelf.createModel({ ]), }, { // Hide password from any relations and include requests. - publicFields: bookshelf.safeColumns([ - 'fullname', - ]), + publicFields: ['id', 'fullname'], hash(password) { return new Promise((resolve, reject) => diff --git a/app/admin/articles.js b/app/admin/articles.js index af13a4c..4523426 100644 --- a/app/admin/articles.js +++ b/app/admin/articles.js @@ -29,7 +29,7 @@ const AdminArticles = { return pagination.fetchPage(Article.getAllArticlesPagination({ per_page: 10, page: this.lastpage, - includes: ['parent'], + includes: ['parent', 'staff'], })) .then(function(result) { vnode.state.articles = result.data @@ -70,13 +70,10 @@ const AdminArticles = { name: '-- Frontpage --', } } - let other = '' let className = '' if (new Date() < new Date(article.published_at)) { - other = '(hidden)' className = 'rowhidden' } else if (article.is_featured) { - other = '(featured)' className = 'rowfeatured' } return [ @@ -85,7 +82,7 @@ const AdminArticles = { m('td', m(m.route.Link, { href: parent.path }, parent.name)), m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)), m('td.right', article.published_at.replace('T', ' ').split('.')[0]), - m('td.right', other), + m('td.right', article.staff && article.staff.fullname || 'Admin'), m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')), ]), ] @@ -113,7 +110,7 @@ const AdminArticles = { m('th', 'Page'), m('th', 'Path'), m('th.right', 'Publish'), - m('th.right', 'Other'), + m('th.right', 'By'), m('th.right', 'Actions'), ]) ), diff --git a/app/admin/articles.scss b/app/admin/articles.scss index b970532..832e940 100644 --- a/app/admin/articles.scss +++ b/app/admin/articles.scss @@ -40,6 +40,14 @@ article.editarticle { height: 300px; } + label.slim { + font-size: 0.7em; + } + input.slim { + font-size: 0.8em; + padding: 2px 5px; + } + .loading-spinner { height: 300px; position: relative; diff --git a/app/admin/editarticle.js b/app/admin/editarticle.js index 68547b3..e2073be 100644 --- a/app/admin/editarticle.js +++ b/app/admin/editarticle.js @@ -1,5 +1,6 @@ const Authentication = require('../authentication') const FileUpload = require('../widgets/fileupload') +const Staff = require('../api/staff') const Froala = require('./froala') const Page = require('../api/page') const File = require('../api/file') @@ -30,6 +31,18 @@ const EditArticle = { oninit: function(vnode) { this.froala = null this.loadedFroala = Froala.loadedFroala + this.staffers = [] + + Staff.getAllStaff() + .then(function(result) { + vnode.state.staffers = result + }) + .catch(function(err) { + vnode.state.error = err.message + }) + .then(function() { + m.redraw() + }) if (!this.loadedFroala) { Froala.createFroalaScript() @@ -121,6 +134,10 @@ const EditArticle = { } }, + updateStaffer: function(e) { + this.article.staff_id = Number(e.currentTarget.value) + }, + mediaUploaded: function(type, media) { this.article[type] = media }, @@ -159,6 +176,7 @@ const EditArticle = { media_id: this.article.media && this.article.media.id, published_at: new Date(this.article.published_at), is_featured: this.article.is_featured, + staff_id: this.article.staff_id, }) } else { promise = Article.createArticle({ @@ -170,6 +188,7 @@ const EditArticle = { media_id: this.article.media && this.article.media.id, published_at: new Date(this.article.published_at), is_featured: this.article.is_featured, + staff_id: this.article.staff_id, }) } @@ -225,8 +244,20 @@ const EditArticle = { return out }, + getStaffers: function() { + if (!this.article.staff_id) { + this.article.staff_id = 1 + } + let out = [] + this.staffers.forEach(function(item) { + out.push({ id: item.id, name: item.fullname }) + }) + return out + }, + view: function(vnode) { const parents = this.getFlatTree() + const staffers = this.getStaffers() return ( this.loading ? m('div.loading-spinner') @@ -265,18 +296,18 @@ const EditArticle = { m('select', { onchange: this.updateParent.bind(this), }, parents.map(function(item) { return m('option', { value: item.id || -1, selected: item.id === vnode.state.article.parent_id }, item.name) })), - m('label', 'Path'), - m('input', { - type: 'text', - value: this.article.path, - oninput: this.updateValue.bind(this, 'path'), - }), m('label', 'Name'), m('input', { type: 'text', value: this.article.name, oninput: this.updateValue.bind(this, 'name'), }), + m('label.slim', 'Path'), + m('input.slim', { + type: 'text', + value: this.article.path, + oninput: this.updateValue.bind(this, 'path'), + }), m('label', 'Description'), ( this.loadedFroala ? @@ -289,12 +320,16 @@ const EditArticle = { }) : null ), - m('label', 'Publish at'), + m('label', 'Published at'), m('input', { type: 'datetime-local', value: this.article.published_at, oninput: this.updateValue.bind(this, 'published_at'), }), + m('label', 'Published by'), + m('select', { + onchange: this.updateStaffer.bind(this), + }, staffers.map(function(item) { return m('option', { value: item.id, selected: item.id === vnode.state.article.staff_id }, item.name) })), m('label', 'Make featured'), m('input', { type: 'checkbox', diff --git a/app/api/article.p.js b/app/api/article.p.js index 499476a..4d7b908 100644 --- a/app/api/article.p.js +++ b/app/api/article.p.js @@ -41,6 +41,6 @@ exports.getAllPageArticlesPagination = function(pageId, options) { exports.getArticle = function(id) { return common.sendRequest({ method: 'GET', - url: '/api/articles/public/' + id + '?includes=media,parent,banner,files', + url: '/api/articles/public/' + id + '?includes=media,parent,banner,files,staff', }) } diff --git a/app/article/article.js b/app/article/article.js index 45ea1eb..a37575e 100644 --- a/app/article/article.js +++ b/app/article/article.js @@ -75,6 +75,7 @@ const Article = { this.loading ? m('div.loading-spinner') : m('article.article', [ + this.article.parent ? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.article.parent.path }, this.article.parent.name)]) : null, m('header', m('h1', this.article.name)), m('.fr-view', [ this.article.media @@ -89,6 +90,13 @@ const Article = { return m(Fileinfo, { file: file }) }) : null), + m('div.entrymeta', [ + 'Posted ', + (this.article.parent ? 'in' : ''), + (this.article.parent ? m(m.route.Link, { href: '/page/' + this.article.parent.path }, this.article.parent.name) : null), + 'at ' + (this.article.published_at.replace('T', ' ').split('.')[0]).substr(0, 16), + ' by ' + (this.article.staff && this.article.staff.fullname || 'Admin'), + ]), ]), Authentication.currentUser ? m('div.admin-actions', [ diff --git a/app/article/article.scss b/app/article/article.scss index 504d8a4..8588f3f 100644 --- a/app/article/article.scss +++ b/app/article/article.scss @@ -26,7 +26,7 @@ article.article { .fr-view { margin: 0 20px; - padding: 20px 20px 60px; + padding: 20px 20px 10px; width: calc(100% - 40px); max-width: 800px; flex: 2 0 0; @@ -73,6 +73,34 @@ article.article { min-height: 50px; position: relative; } + + .goback { + width: calc(100% - 40px); + max-width: 800px; + align-self: center; + padding: 10px 5px 0; + margin-bottom: -10px; + + a { + font-weight: bold; + text-decoration: none; + color: $secondary-dark-bg; + } + } + + .entrymeta { + flex-grow: 2; + font-size: 11px; + color: $meta-fg; + font-weight: bold; + margin: 40px 0 0; + + a { + color: $secondary-dark-bg; + margin: 0 4px; + text-decoration: none; + } + } } .darkmodeon { @@ -88,5 +116,13 @@ article.article { .fr-view { background: $dark_newsitem-bg; } + + .opencomments { + color: $dark_secondary-dark-bg; + } + + .goback a { + color: $dark_secondary-dark-bg; + } } } diff --git a/app/frontpage/frontpage.js b/app/frontpage/frontpage.js index 0081bf4..ad4b042 100644 --- a/app/frontpage/frontpage.js +++ b/app/frontpage/frontpage.js @@ -57,7 +57,7 @@ const Frontpage = { return Pagination.fetchPage(Article.getAllArticlesPagination({ per_page: 10, page: this.lastpage, - includes: ['parent', 'files', 'media', 'banner'], + includes: ['parent', 'files', 'media', 'banner', 'staff'], })) .then(function(result) { vnode.state.articles = result.data diff --git a/app/widgets/common.scss b/app/widgets/common.scss index da2bd74..a363e5e 100644 --- a/app/widgets/common.scss +++ b/app/widgets/common.scss @@ -58,6 +58,7 @@ newsentry { fileinfo { padding: 0 5px; margin-bottom: 5px; + display: block; &.slim { padding: 0; diff --git a/app/widgets/newsentry.js b/app/widgets/newsentry.js index e2d79b6..6b3c569 100644 --- a/app/widgets/newsentry.js +++ b/app/widgets/newsentry.js @@ -32,7 +32,6 @@ const Newsentry = { : vnode.attrs.description ? m('span.entrydescription', Newsentry.strip(vnode.attrs.description)) : null), - m('span.entrymeta', 'Posted ' + vnode.attrs.published_at.replace('T', ' ').split('.')[0]), ]), ]) }, diff --git a/app/widgets/newsitem.js b/app/widgets/newsitem.js index 40781f1..42bf9d4 100644 --- a/app/widgets/newsitem.js +++ b/app/widgets/newsitem.js @@ -30,6 +30,7 @@ const Newsitem = { (vnode.attrs.parent ? 'in' : ''), (vnode.attrs.parent ? m(m.route.Link, { href: '/page/' + vnode.attrs.parent.path }, vnode.attrs.parent.name) : null), 'at ' + (vnode.attrs.published_at.replace('T', ' ').split('.')[0]).substr(0, 16), + ' by ' + (vnode.attrs.staff && vnode.attrs.staff.fullname || 'Admin'), ]), ]), ]),