apostrophe-blog
Advanced tools
Comparing version 0.0.2 to 0.0.3
378
index.js
var async = require('async'); | ||
var _ = require('underscore'); | ||
var extend = require('extend'); | ||
var snippets = require('apostrophe-snippets'); | ||
var util = require('util'); | ||
var moment = require('moment'); | ||
module.exports = function(options, callback) { | ||
return new Blog(options, callback); | ||
}; | ||
// Creating an instance of the blog module is easy: | ||
// var blog = require('apostrophe-blog')(options, callback); | ||
// | ||
// If you want to access the constructor function for use in the | ||
// constructor of a module that extends this one, consider: | ||
// | ||
// var blog = require('apostrophe-blog'); | ||
// ... Inside the constructor for the new object ... | ||
// blog.Blog.call(this, options, null); | ||
// | ||
// In fact, this module does exactly that to extend the snippets module | ||
// (see below). Something similar happens on the browser side in | ||
// main.js. | ||
function Blog(options, callback) { | ||
var apos = options.apos; | ||
var pages = options.pages; | ||
var app = options.app; | ||
var aposBlog = self = this; | ||
module.exports = blog; | ||
// For visibility in other scopes | ||
aposBlog.options = options; | ||
function blog(options, callback) { | ||
return new blog.Blog(options, callback); | ||
} | ||
// Make sure that aposScripts and aposStylesheets summon our | ||
// browser-side UI assets for managing pages | ||
apos.scripts.push('/apos-blog/js/blog.js'); | ||
apos.stylesheets.push('/apos-blog/css/blog.css'); | ||
apos.templates.push(__dirname + '/views/newPost'); | ||
apos.templates.push(__dirname + '/views/editPost'); | ||
apos.templates.push(__dirname + '/views/editPosts'); | ||
apos.templates.push(__dirname + '/views/pageSettings'); | ||
pages.addGroup('blog', { | ||
settings: { | ||
sanitize: function(data, callback) { | ||
var ok = {}; | ||
ok.tags = apos.sanitizeTags(data.tags); | ||
return callback(null, ok); | ||
} | ||
} | ||
blog.Blog = function(options, callback) { | ||
var self = this; | ||
_.defaults(options, { | ||
instance: 'blogPost', | ||
name: options.name || 'blog', | ||
label: options.name || 'Blog', | ||
// Overridden separately so that one can have types that just | ||
// override the templates and don't mess with replacing | ||
// all of the javascript and CSS | ||
webAssetDir: __dirname + '/public', | ||
// The default would be aposBlogPostMenu, this is more natural | ||
menuName: 'aposBlogMenu' | ||
}); | ||
app.post('/apos-blog/new', function(req, res) { | ||
var post; | ||
var title; | ||
var content; | ||
var slug; | ||
var tags; | ||
// Find our templates before the snippet templates (a chain of overrides) | ||
options.dirs = (options.dirs || []).concat([ __dirname ]); | ||
title = req.body.title.trim(); | ||
// Validation is annoying, automatic cleanup is awesome | ||
if (!title.length) { | ||
title = 'New Post'; | ||
} | ||
slug = apos.slugify(title); | ||
// Call the base class constructor. Don't pass the callback, we want to invoke it | ||
// ourselves after constructing more stuff | ||
snippets.Snippets.call(this, options, null); | ||
content = JSON.parse(req.body.content); | ||
apos.sanitizeItems(content); | ||
// The snippet dispatcher is almost perfect for our needs, except that | ||
// we expect the publication date of the blog post to appear before the slug | ||
// of the blog post in the URL. So spot that situation, change req.remainder | ||
// to just the slug of the blog post, and invoke the original version of | ||
// "dispatch." | ||
tags = req.body.tags; | ||
// Grab the "superclass" version of the dispatch method so we can call it | ||
var superDispatch = self.dispatch; | ||
async.series([ permissions, insertPost ], sendPost); | ||
function permissions(callback) { | ||
return apos.permissions(req, 'edit-post', null, function(err) { | ||
// If there is no permissions error then we are cool | ||
// enough to create a post | ||
return callback(err); | ||
}); | ||
} | ||
function insertPost(callback) { | ||
post = { title: title, type: 'blogPost', tags: tags, areas: { body: { items: content } }, slug: slug, createdAt: new Date(), publishedAt: new Date() }; | ||
apos.putPage(slug, post, callback); | ||
} | ||
function sendPost(err) { | ||
if (err) { | ||
res.statusCode = 500; | ||
return res.send('error'); | ||
self.dispatch = function(req, callback) { | ||
if (req.remainder.length) { | ||
var matches = req.remainder.match(/^\/\d+\/\d+\/\d+\/(.*)$/); | ||
if (matches) { | ||
req.remainder = '/' + matches[1]; | ||
} | ||
return res.send(JSON.stringify(post)); | ||
} | ||
}); | ||
superDispatch.call(this, req, callback); | ||
}; | ||
app.post('/apos-blog/edit', function(req, res) { | ||
var post; | ||
var title; | ||
var content; | ||
var originalSlug; | ||
var slug; | ||
var tags; | ||
title = req.body.title.trim(); | ||
// Validation is annoying, automatic cleanup is awesome | ||
if (!title.length) { | ||
title = 'Updated Post'; | ||
} | ||
tags = req.body.tags; | ||
var originalSlug = req.body.originalSlug; | ||
slug = apos.slugify(req.body.slug); | ||
if (!slug.length) { | ||
slug = originalSlug; | ||
} | ||
content = JSON.parse(req.body.content); | ||
apos.sanitizeItems(content); | ||
async.series([ getPost, permissions, updatePost, redirect ], sendPost); | ||
function getPost(callback) { | ||
apos.getPage(originalSlug, function(err, page) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (!page) { | ||
return callback('No such blog post'); | ||
} | ||
if (page.type !== 'blogPost') { | ||
return callback('Not a blog post'); | ||
} | ||
post = page; | ||
return callback(null); | ||
}); | ||
} | ||
function permissions(callback) { | ||
return apos.permissions(req, 'edit-post', post, function(err) { | ||
// If there is no permissions error then we are cool | ||
// enough to create a post | ||
return callback(err); | ||
}); | ||
} | ||
function updatePost(callback) { | ||
post.title = title; | ||
post.slug = slug; | ||
post.tags = tags; | ||
post.areas = { body: { items: content } }; | ||
apos.putPage(originalSlug, post, callback); | ||
} | ||
function redirect(callback) { | ||
apos.updateRedirect(originalSlug, slug, callback); | ||
} | ||
function sendPost(err) { | ||
if (err) { | ||
res.statusCode = 500; | ||
return res.send('error'); | ||
} | ||
return res.send(JSON.stringify(post)); | ||
} | ||
}); | ||
app.post('/apos-blog/delete', function(req, res) { | ||
async.series([ getPost, permissions, deletePost], respond); | ||
var slug; | ||
var post; | ||
function getPost(callback) { | ||
slug = req.body.slug; | ||
return apos.getPage(slug, function(err, postArg) { | ||
post = postArg; | ||
if(!post) { | ||
return callback('Not Found'); | ||
} | ||
if (post.type !== 'blogPost') { | ||
return callback('Not a blog post'); | ||
} | ||
return callback(err); | ||
}); | ||
} | ||
function permissions(callback) { | ||
return apos.permissions(req, 'delete-post', post, function(err) { | ||
// If there is no permissions error then we are cool | ||
// enough to delete the post | ||
return callback(err); | ||
}); | ||
} | ||
function deletePost(callback) { | ||
apos.pages.remove({slug: post.slug}, callback); | ||
} | ||
function respond(err) { | ||
if (err) { | ||
return res.send(JSON.stringify({ | ||
status: err | ||
})); | ||
} | ||
return res.send(JSON.stringify({ | ||
status: 'ok' | ||
})); | ||
} | ||
}); | ||
app.get('/apos-blog/get-posts', function(req, res) { | ||
self.getPosts(req, req.query, function(err, posts) { | ||
return res.send(JSON.stringify(posts)); | ||
}); | ||
}); | ||
app.get('/apos-blog/get-post', function(req, res) { | ||
self.getPosts(req, req.query, function(err, posts) { | ||
if (posts && posts.length) { | ||
res.send(JSON.stringify(posts[0])); | ||
} else { | ||
res.send(JSON.stringify(null)); | ||
} | ||
}); | ||
}); | ||
// Serve our assets. This is the final route so it doesn't | ||
// beat out the rest | ||
app.get('/apos-blog/*', apos.static(__dirname + '/public')); | ||
apos.addLocal('aposEditBlog', function(options) { | ||
return apos.partial('editBlog.html', options, __dirname + '/views'); | ||
}); | ||
// Returns recent posts the current user is permitted to read, in | ||
// blog order (reverse chrono). If criteria.editable is true, only | ||
// posts this user can edit are returned. All other properties of | ||
// criteria are merged with the MongoDB criteria object used to | ||
// select the relevant posts. | ||
self.getPosts = function(req, criteriaArg, callback) { | ||
if (!callback) { | ||
callback = options; | ||
options = {}; | ||
} | ||
var criteria = extend({}, criteriaArg); | ||
var editable = criteria.editable; | ||
if (criteria.editable !== undefined) { | ||
delete criteria['editable']; | ||
} | ||
criteria.type = 'blogPost'; | ||
if (req.query.slug) { | ||
criteria.slug = req.query.slug; | ||
} | ||
apos.pages.find(criteria).sort({ publishedAt: -1 }).toArray(function(err, posts) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
async.filter(posts, function(post, callback) { | ||
apos.permissions(req, editable ? 'edit-post' : 'view-post', post, function(err) { | ||
return callback(!err); | ||
}); | ||
}, function(posts) { | ||
return callback(null, posts); | ||
}); | ||
}); | ||
self.getDefaultTitle = function() { | ||
return 'My Article'; | ||
}; | ||
// This is a loader function, for use with the `load` option of | ||
// the pages module's `serve` method. | ||
// Returns a "permalink" URL to the snippet, beginning with the | ||
// slug of the specified page. See findBestPage for a good way to | ||
// choose a page beneath which to link this snippet. | ||
// | ||
// If the page type does not begin with "blog," this loader does nothing. | ||
// | ||
// Otherwise, if the page type begins with "blog" and the page matches the URL | ||
// exactly, this function serves up the "main index" page of the blog (a list of | ||
// posts in blog order). | ||
// | ||
// If the page is an inexact match, this function looks at the remainder of the | ||
// URL to decide what to do. If the remainder is of the form | ||
// /YYYY/MM/DD/slug, then the blog post with that slug is served (a blog | ||
// post permalink page). | ||
// | ||
// TODO: make this easier to extend for use in other modules like the | ||
// events module. Pay attention to attributes of the page to decide which | ||
// blog posts are relevant (like categories for blog pages in A1.5). Move | ||
// the default versions of the blog macros and templates into this module, with ways | ||
// to override them. | ||
// It is commonplace to override this function. For instance, | ||
// blog posts add the publication date to the URL. | ||
self.loader = function(req, callback) { | ||
async.series([permissions, blog], callback); | ||
function permissions(callback) { | ||
// Load additional permissions so we know whether this person is a blogger etc. | ||
apos.permissions(req, 'blogger', null, function(err) { | ||
req.extras.blogger = !err; | ||
return callback(null); | ||
}); | ||
} | ||
function blog(callback) { | ||
if (!req.bestPage) { | ||
return callback(null); | ||
} | ||
// If the page type isn't part of the blog group, | ||
// then this is outside our purview | ||
var type = pages.getType(req.bestPage.type); | ||
if (type.group !== 'blog') { | ||
return callback(null); | ||
} | ||
// We consider a partial match to be good enough, depending on the | ||
// remainder of the URL | ||
req.page = req.bestPage; | ||
if (req.remainder.length) { | ||
// Is it a blog post permalink? | ||
var matches = req.remainder.match(/^\/\d+\/\d+\/\d+\/(.*)$/); | ||
if (matches) { | ||
// Yep, change templates and filter to retrieve just that one blog post | ||
req.query.slug = matches[1]; | ||
req.type = 'blogPost'; | ||
} else { | ||
req.type = 'notfound'; | ||
} | ||
} else { | ||
// No additional URL: index page | ||
} | ||
var criteria = {}; | ||
if (req.page.blog && req.page.blog.tags.length) { | ||
criteria.tags = { $in: req.page.blog.tags }; | ||
} | ||
self.getPosts(req, criteria, function(err, posts) { | ||
if (req.type === 'blogPost') { | ||
if (!posts.length) { | ||
req.type = 'notfound'; | ||
} else { | ||
req.extras.post = posts[0]; | ||
} | ||
} else { | ||
req.extras.posts = posts; | ||
} | ||
return callback(err); | ||
}); | ||
} | ||
self.permalink = function(snippet, page) { | ||
return page.slug + '/' + moment(snippet.publishedAt).format('YYYY/MM/DD') + '/' + snippet.slug; | ||
}; | ||
@@ -343,3 +87,3 @@ | ||
process.nextTick(function() { return callback(null); }); | ||
} | ||
}; | ||
{ | ||
"name": "apostrophe-blog", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Blogging for the Apostrophe content management system", | ||
@@ -25,4 +25,6 @@ "main": "index.js", | ||
"underscore": "~1.4.4", | ||
"extend": "~1.1.3" | ||
"extend": "~1.1.3", | ||
"apostrophe-snippets": "0.0.x", | ||
"moment": "~2.0.0" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
15
12072
5
98
1
+ Addedapostrophe-snippets@0.0.x
+ Addedmoment@~2.0.0
+ Addedabsolution@1.0.4(transitive)
+ Addedapostrophe-schemas@0.1.10(transitive)
+ Addedapostrophe-snippets@0.0.102(transitive)
+ Addedcore-util-is@1.0.3(transitive)
+ Addedcsv@0.2.9(transitive)
+ Addeddomelementtype@1.3.1(transitive)
+ Addeddomhandler@2.1.0(transitive)
+ Addeddomutils@1.1.6(transitive)
+ Addedextend@1.2.1(transitive)
+ Addedhtmlparser2@3.3.0(transitive)
+ Addedinherits@2.0.4(transitive)
+ Addedisarray@0.0.1(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedmime@1.2.11(transitive)
+ Addedmoment@2.0.02.4.0(transitive)
+ Addedreadable-stream@1.0.34(transitive)
+ Addedrss@0.2.1(transitive)
+ Addedstring_decoder@0.10.31(transitive)
+ Addedunderscore@1.5.2(transitive)
+ Addedxml@0.0.12(transitive)