Finalised official release 2.0.0
This commit is contained in:
parent
ecbf015cb6
commit
f760ec6d8b
14 changed files with 203 additions and 37 deletions
|
@ -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 })
|
||||
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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)) {
|
||||
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) => {
|
||||
|
|
|
@ -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,14 +114,17 @@ export async function serveIndex(ctx, path) {
|
|||
if (id) {
|
||||
let found
|
||||
if (path.startsWith('/article/')) {
|
||||
found = await Article.getSingle(id, ['media', 'parent', 'banner', 'files'])
|
||||
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) {
|
||||
if (found.media) {
|
||||
image = found.media.large_url
|
||||
} else if (found.banner) {
|
||||
|
@ -131,6 +140,7 @@ export async function serveIndex(ctx, path) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.log.error(e)
|
||||
data = null
|
||||
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
])
|
||||
),
|
||||
|
|
|
@ -46,6 +46,16 @@ article.editarticle {
|
|||
}
|
||||
}
|
||||
|
||||
table {
|
||||
tr.rowhidden td {
|
||||
background: #e6e6e6;
|
||||
}
|
||||
|
||||
tr.rowfeatured td {
|
||||
background: hsl(120, 60%, 85%);
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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', {
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ form {
|
|||
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=datetime-local],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue