Finalised official release 2.0.0

master
Jonatan Nilsson 2019-10-02 00:16:11 +00:00
parent ecbf015cb6
commit f760ec6d8b
14 changed files with 203 additions and 37 deletions

View File

@ -47,15 +47,55 @@ const Article = bookshelf.createModel({
})
},
}, {
getSingle(id, withRelated = [], require = true, ctx = null) {
getAll(ctx, where = {}, withRelated = [], orderBy = 'id', limitToday = false) {
return this.query(qb => {
qb.where({ id: Number(id) || 0 })
.orWhere({ path: id })
this.baseQueryAll(ctx, qb, where, orderBy)
if (limitToday) {
qb.where('published_at', '<=', (new Date()).toISOString())
}
})
.fetchPage({
pageSize: ctx.state.pagination.perPage,
page: ctx.state.pagination.page,
withRelated,
ctx: ctx,
})
.then(result => {
ctx.state.pagination.total = result.pagination.rowCount
return result
})
},
getSingle(id, withRelated = [], require = true, ctx = null, limitToday = false) {
return this.query(qb => {
qb.where(subq => {
subq.where({ id: Number(id) || 0 })
.orWhere({ path: id })
})
if (limitToday && (!ctx || !ctx.state.user || ctx.state.user.level < 10)) {
qb.where('published_at', '<=', (new Date()).toISOString())
}
})
.fetch({ require, withRelated, ctx })
},
getAllFromPage(ctx, pageId, withRelated = [], orderBy = 'id') {
async getFeatured(withRelated = [], ctx = null) {
let data = await this.query(qb => {
qb.where({ is_featured: true })
.where('published_at', '<=', (new Date()).toISOString())
})
.fetch({ require: false, withRelated, ctx })
if (!data) {
data = await this.query(qb => {
qb.where('published_at', '<=', (new Date()).toISOString())
.whereNotNull('banner_id')
})
.fetch({ require: false, withRelated, ctx })
}
return data
},
getAllFromPage(ctx, pageId, withRelated = [], orderBy = 'id', limitToday = false) {
return this.query(qb => {
this.baseQueryAll(ctx, qb, {}, orderBy)
qb.leftOuterJoin('pages', 'articles.parent_id', 'pages.id')
@ -63,6 +103,9 @@ const Article = bookshelf.createModel({
subq.where('pages.id', pageId)
.orWhere('pages.parent_id', pageId)
})
if (limitToday) {
qb.where('published_at', '<=', (new Date()).toISOString())
}
qb.select('articles.*')
})
.fetchPage({
@ -77,9 +120,18 @@ const Article = bookshelf.createModel({
})
},
setAllUnfeatured() {
return bookshelf.knex('articles')
.where({ is_featured: true })
.update({
is_featured: false,
})
},
getFrontpageArticles(page = 1) {
return this.query(qb => {
qb.orderBy('updated_at', 'DESC')
qb.orderBy('published_at', 'DESC')
.where('published_at', '<=', (new Date()).toISOString())
})
.fetchPage({
pageSize: 10,

View File

@ -13,14 +13,14 @@ export default class ArticleRoutes {
async getAllArticles(ctx) {
await this.security.ensureIncludes(ctx)
ctx.body = await this.Article.getAll(ctx, { }, ctx.state.filter.includes, ctx.query.sort || '-id')
ctx.body = await this.Article.getAll(ctx, { }, ctx.state.filter.includes, ctx.query.sort || '-published_at')
}
/** GET: /api/pages/:pageId/articles */
async getAllPageArticles(ctx) {
await this.security.ensureIncludes(ctx)
ctx.body = await this.Article.getAllFromPage(ctx, ctx.params.pageId, ctx.state.filter.includes, ctx.query.sort || '-id')
ctx.body = await this.Article.getAllFromPage(ctx, ctx.params.pageId, ctx.state.filter.includes, ctx.query.sort || '-published_at')
}
/** GET: /api/articles/:id */
@ -30,6 +30,27 @@ export default class ArticleRoutes {
ctx.body = await this.Article.getSingle(ctx.params.id, ctx.state.filter.includes, true, ctx)
}
/** GET: /api/articles/public */
async getPublicAllArticles(ctx) {
await this.security.ensureIncludes(ctx)
ctx.body = await this.Article.getAll(ctx, { }, ctx.state.filter.includes, ctx.query.sort || '-published_at', true)
}
/** GET: /api/pages/:pageId/articles/public */
async getPublicAllPageArticles(ctx) {
await this.security.ensureIncludes(ctx)
ctx.body = await this.Article.getAllFromPage(ctx, ctx.params.pageId, ctx.state.filter.includes, ctx.query.sort || '-published_at', true)
}
/** GET: /api/articles/public/:id */
async getPublicSingleArticle(ctx) {
await this.security.ensureIncludes(ctx)
ctx.body = await this.Article.getSingle(ctx.params.id, ctx.state.filter.includes, true, ctx, true)
}
/** POST: /api/articles */
async createArticle(ctx) {
await this.security.validUpdate(ctx)
@ -41,6 +62,10 @@ export default class ArticleRoutes {
async updateArticle(ctx) {
await this.security.validUpdate(ctx)
if (ctx.request.body.is_featured) {
await Article.setAllUnfeatured()
}
let page = await this.Article.getSingle(ctx.params.id)
page.set(ctx.request.body)

View File

@ -12,6 +12,8 @@ const validFields = [
'parent_id',
'media_id',
'banner_id',
'published_at',
'is_featured',
]
export async function ensureIncludes(ctx) {
@ -34,4 +36,8 @@ export async function validUpdate(ctx) {
if (out.length > 0) {
ctx.throw(422, `Body had following invalid properties: ${out.join(', ')}`)
}
if (ctx.request.body.published_at) {
ctx.request.body.published_at = new Date(ctx.request.body.published_at)
}
}

View File

@ -37,9 +37,12 @@ router.put('/api/pages/:id', restrict(access.Manager), page.updatePage.bind(page
router.del('/api/pages/:id', restrict(access.Manager), page.removePage.bind(page))
const article = new ArticleRoutes()
router.get('/api/articles', article.getAllArticles.bind(article))
router.get('/api/pages/:pageId/articles', article.getAllPageArticles.bind(article))
router.get('/api/articles/:id', article.getSingleArticle.bind(article))
router.get('/api/articles', restrict(access.Manager), article.getAllArticles.bind(article))
router.get('/api/articles/public', article.getPublicAllArticles.bind(article))
router.get('/api/articles/public/:id', article.getPublicSingleArticle.bind(article))
router.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article))
router.get('/api/pages/:pageId/articles', restrict(access.Manager), article.getAllPageArticles.bind(article))
router.get('/api/articles/:id', restrict(access.Manager), article.getSingleArticle.bind(article))
router.post('/api/articles', restrict(access.Manager), article.createArticle.bind(article))
router.put('/api/articles/:id', restrict(access.Manager), article.updateArticle.bind(article))
router.del('/api/articles/:id', restrict(access.Manager), article.removeArticle.bind(article))

View File

@ -3,6 +3,7 @@ import defaults from './defaults.mjs'
import access from './access/index.mjs'
import { restrict } from './access/middleware.mjs'
import { serveIndex } from './serveindex.mjs'
import config from './config.mjs'
const restrictAdmin = restrict(access.Manager)
@ -36,8 +37,13 @@ export function serve(docRoot, pathname, options = {}) {
if (filepath.indexOf('admin') >= 0
&& (filepath.indexOf('js') >= 0
|| filepath.indexOf('css') >= 0)) {
await restrictAdmin(ctx)
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
if (filepath.indexOf('.map') === -1) {
await restrictAdmin(ctx)
ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate')
} else if (config.get('NODE_ENV') !== 'development') {
ctx.status = 404
return
}
}
return send(ctx, filepath, opts).catch((er) => {

View File

@ -77,6 +77,7 @@ export async function serveIndex(ctx, path) {
let tree = null
let data = null
let links = null
let featured = null
let url = frontend + ctx.request.url
let image = frontend + '/assets/img/heart.jpg'
let title = 'NFP Moe - Anime/Manga translation group'
@ -88,6 +89,11 @@ export async function serveIndex(ctx, path) {
{ id: x.id, name: x.name, path: x.path }
))
))
featured = await Article.getFeatured(['files', 'media', 'banner'])
if (featured) {
featured = mapArticle(featured.toJSON())
}
if (path === '/') {
data = await Article.getFrontpageArticles(Number(ctx.query.page || '1'))
@ -108,26 +114,30 @@ export async function serveIndex(ctx, path) {
if (id) {
let found
if (path.startsWith('/article/')) {
found = await Article.getSingle(id, ['media', 'parent', 'banner', 'files'])
found = mapArticle(found.toJSON())
found = await Article.getSingle(id, ['media', 'parent', 'banner', 'files'], false, null, true)
if (found) {
found = mapArticle(found.toJSON())
}
data = found
} else {
found = await Page.getSingle(id, ['media', 'banner', 'children'])
found = mapPage(found.toJSON())
data = found
}
if (found.media) {
image = found.media.large_url
} else if (found.banner) {
image = found.banner.large_url
}
if (found.description) {
description = striptags(found.description)
}
if (found.parent) {
title = found.name + ' - ' + found.parent.name + ' - NFP Moe'
} else {
title = found.name + ' - NFP Moe'
if (found) {
if (found.media) {
image = found.media.large_url
} else if (found.banner) {
image = found.banner.large_url
}
if (found.description) {
description = striptags(found.description)
}
if (found.parent) {
title = found.name + ' - ' + found.parent.name + ' - NFP Moe'
} else {
title = found.name + ' - NFP Moe'
}
}
}
}
@ -141,6 +151,7 @@ export async function serveIndex(ctx, path) {
tree: JSON.stringify(tree),
data: JSON.stringify(data),
links: JSON.stringify(links),
featured: JSON.stringify(featured),
url: url,
image: image,
title: title,

View File

@ -70,12 +70,22 @@ 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 [
m('tr', [
m('tr', { class: className }, [
m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)),
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.updated_at.replace('T', ' ').split('.')[0]),
m('td.right', article.published_at.replace('T', ' ').split('.')[0]),
m('td.right', other),
m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')),
]),
]
@ -102,7 +112,8 @@ const AdminArticles = {
m('th', 'Title'),
m('th', 'Page'),
m('th', 'Path'),
m('th.right', 'Updated'),
m('th.right', 'Publish'),
m('th.right', 'Other'),
m('th.right', 'Actions'),
])
),

View File

@ -46,6 +46,16 @@ article.editarticle {
}
}
table {
tr.rowhidden td {
background: #e6e6e6;
}
tr.rowfeatured td {
background: hsl(120, 60%, 85%);
}
}
h5 {
margin-bottom: 20px;
}

View File

@ -64,6 +64,8 @@ const EditArticle = {
media: null,
banner: null,
files: [],
is_featured: false,
published_at: new Date().toISOString(),
}
this.editedPath = false
this.loadedFroala = Froala.loadedFroala
@ -73,6 +75,7 @@ const EditArticle = {
.then(function(result) {
vnode.state.editedPath = true
vnode.state.article = result
EditArticle.parsePublishedAt(vnode, null)
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe'
})
.catch(function(err) {
@ -86,6 +89,7 @@ const EditArticle = {
m.redraw()
})
} else {
EditArticle.parsePublishedAt(vnode, new Date())
document.title = 'Create Article - Admin NFP Moe'
if (vnode.state.froala) {
vnode.state.froala.html.set(this.article.description)
@ -93,8 +97,16 @@ const EditArticle = {
}
},
parsePublishedAt: function(vnode, date) {
vnode.state.article.published_at = ((date && date.toISOString() || vnode.state.article.published_at).split('.')[0]).substr(0, 16)
},
updateValue: function(name, e) {
this.article[name] = e.currentTarget.value
if (name === 'is_featured') {
this.article[name] = e.currentTarget.checked
} else {
this.article[name] = e.currentTarget.value
}
if (name === 'path') {
this.editedPath = true
} else if (name === 'name' && !this.editedPath) {
@ -145,6 +157,8 @@ const EditArticle = {
description: this.article.description,
banner_id: this.article.banner && this.article.banner.id,
media_id: this.article.media && this.article.media.id,
published_at: new Date(this.article.published_at),
is_featured: this.article.is_featured,
})
} else {
promise = Article.createArticle({
@ -154,6 +168,8 @@ const EditArticle = {
description: this.article.description,
banner_id: this.article.banner && this.article.banner.id,
media_id: this.article.media && this.article.media.id,
published_at: new Date(this.article.published_at),
is_featured: this.article.is_featured,
})
}
@ -163,6 +179,7 @@ const EditArticle = {
res.banner = vnode.state.article.banner
res.files = vnode.state.article.files
vnode.state.article = res
EditArticle.parsePublishedAt(vnode, null)
} else {
m.route.set('/admin/articles/' + res.id)
}
@ -248,6 +265,12 @@ 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',
@ -266,11 +289,17 @@ const EditArticle = {
})
: null
),
m('label', 'Path'),
m('label', 'Publish at'),
m('input', {
type: 'text',
value: this.article.path,
oninput: this.updateValue.bind(this, 'path'),
type: 'datetime-local',
value: this.article.published_at,
oninput: this.updateValue.bind(this, 'published_at'),
}),
m('label', 'Make featured'),
m('input', {
type: 'checkbox',
checked: this.article.is_featured,
oninput: this.updateValue.bind(this, 'is_featured'),
}),
m('div.loading-spinner', { hidden: this.loadedFroala }),
m('input', {

View File

@ -16,7 +16,7 @@ exports.getAllArticlesPagination = function(options) {
extra += '&includes=' + options.includes.join(',')
}
return '/api/articles?' + extra
return '/api/articles/public?' + extra
}
exports.getAllPageArticlesPagination = function(pageId, options) {
@ -35,12 +35,12 @@ exports.getAllPageArticlesPagination = function(pageId, options) {
extra += '&includes=' + options.includes.join(',')
}
return '/api/pages/' + pageId + '/articles?' + extra
return '/api/pages/' + pageId + '/articles/public?' + extra
}
exports.getArticle = function(id) {
return common.sendRequest({
method: 'GET',
url: '/api/articles/' + id + '?includes=media,parent,banner,files',
url: '/api/articles/public/' + id + '?includes=media,parent,banner,files',
})
}

View File

@ -133,6 +133,7 @@ form {
input[type=text],
input[type=password],
input[type=datetime-local],
select,
textarea {
width: 100%;

View File

@ -13,6 +13,12 @@ const Frontpage = {
this.featured = null
this.links = null
if (window.__nfpfeatured) {
this.featured = window.__nfpfeatured
}
console.log(this.featured)
if (window.__nfpdata
&& window.__nfplinks) {
this.links = window.__nfplinks

View File

@ -76,6 +76,11 @@ exports.up = function up(knex, Promise) {
table.boolean('is_deleted')
.notNullable()
.default(false)
table.timestamp('published_at')
.defaultTo(knex.fn.now())
table.boolean('is_featured')
.notNullable()
.default(false)
table.timestamps()
}),
knex.schema.createTable('files', function(table) {

View File

@ -27,6 +27,7 @@
<script type="text/javascript">
if (localStorage.getItem('darkmode')) {document.body.className = 'darkmodeon';}
window.__nfptree = {{=it.tree}};
window.__nfpfeatured = {{=it.featured}};
window.__nfpdata = {{=it.data}};
window.__nfplinks = {{=it.links}};
</script>