From 4697d5421e1b16daab003321f027dc6cd0824e9c Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Tue, 9 Dec 2014 16:16:32 +0100 Subject: [PATCH 1/9] Added .idea folder (from RubyMine IDE) to .gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6566f59..d8b44f5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ node_modules tmp docs/api builds + +# ide +.idea From d5345bf14b09375c5f9ffc8b0da62e57a79b1cf3 Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Fri, 12 Dec 2014 17:23:43 +0100 Subject: [PATCH 2/9] First commit for Activity Stream. Basic C[R]UD events for entries, buckets and users. WIP. See comments in bounty #82. --- .../controllers/buckets_controller.coffee | 8 +++- client/source/helpers.coffee | 22 +++++++++ client/source/models/activities.coffee | 6 +++ client/source/models/activity.coffee | 7 +++ client/source/templates/buckets/dashboard.hbs | 26 +++++++++- client/source/views/buckets/dashboard.coffee | 5 ++ client/source/views/entries/browser.coffee | 1 + client/style/_views.styl | 13 +++++ server/models/activity.coffee | 47 ++++++++++++++----- server/models/bucket.coffee | 12 +++++ server/models/entry.coffee | 14 +++++- server/models/user.coffee | 9 ++++ server/routes/api/activities.coffee | 16 +++++++ server/routes/api/buckets.coffee | 9 +++- server/routes/api/entries.coffee | 18 +++++-- server/routes/api/index.coffee | 1 + server/routes/api/users.coffee | 14 ++++-- 17 files changed, 203 insertions(+), 25 deletions(-) create mode 100644 client/source/models/activities.coffee create mode 100644 client/source/models/activity.coffee create mode 100644 server/routes/api/activities.coffee diff --git a/client/source/controllers/buckets_controller.coffee b/client/source/controllers/buckets_controller.coffee index 1ff6e4a..2f2932e 100644 --- a/client/source/controllers/buckets_controller.coffee +++ b/client/source/controllers/buckets_controller.coffee @@ -7,6 +7,8 @@ DashboardView = require 'views/buckets/dashboard' EntriesBrowser = require 'views/entries/browser' EntryEditView = require 'views/entries/edit' +Activity = require 'models/activity' +Activities = require 'models/activities' Bucket = require 'models/bucket' Buckets = require 'models/buckets' Fields = require 'models/fields' @@ -20,7 +22,11 @@ mediator = require('chaplin').mediator module.exports = class BucketsController extends Controller dashboard: -> - @view = new DashboardView + @activities = new Activities + + @activities.fetch().done => + @view = new DashboardView + collection: @activities add: -> @adjustTitle 'New Bucket' diff --git a/client/source/helpers.coffee b/client/source/helpers.coffee index 978470a..acf91ec 100644 --- a/client/source/helpers.coffee +++ b/client/source/helpers.coffee @@ -88,3 +88,25 @@ Handlebars.registerHelper 'startsWith', (string1, string2, options) -> options.fn @ else options.inverse @ + +Handlebars.registerHelper 'activityResourceLink', (resource) -> + escapeExpression = (expression) -> + Handlebars.Utils.escapeExpression expression + id = escapeExpression resource.id + type = escapeExpression resource.type + name = escapeExpression resource.name + email = escapeExpression resource.email + bucketSlug = escapeExpression resource.bucket?.slug + + if resource.id + switch resource.type + when 'entry' then href = '/' + mediator.options.adminSegment + '/buckets/' + bucketSlug + '/' + id + when 'bucket' then href = '/' + mediator.options.adminSegment + '/buckets/' + bucketSlug + when 'user' then href = '/' + mediator.options.adminSegment + '/users/' + email + + link = '' + name + '' + else + link = name + + new Handlebars.SafeString link; + diff --git a/client/source/models/activities.coffee b/client/source/models/activities.coffee new file mode 100644 index 0000000..bf9545a --- /dev/null +++ b/client/source/models/activities.coffee @@ -0,0 +1,6 @@ +Collection = require 'lib/collection' +Activity = require 'models/activity' + +module.exports = class Activities extends Collection + url: '/api/activities' + model: Activity \ No newline at end of file diff --git a/client/source/models/activity.coffee b/client/source/models/activity.coffee new file mode 100644 index 0000000..5a81d47 --- /dev/null +++ b/client/source/models/activity.coffee @@ -0,0 +1,7 @@ +Model = require 'lib/model' + +module.exports = class Activity extends Model + urlRoot: '/api/activities' + + hello: -> + 'test' \ No newline at end of file diff --git a/client/source/templates/buckets/dashboard.hbs b/client/source/templates/buckets/dashboard.hbs index 1c5ebba..5c70b9a 100644 --- a/client/source/templates/buckets/dashboard.hbs +++ b/client/source/templates/buckets/dashboard.hbs @@ -1,11 +1,33 @@

Buckets

-
+
-

Buckets is an open-source CMS, built in Node.js, which is being actively developed by the community at Assembly.

+

Buckets is an open-source CMS, built in Node.js, which is being actively developed by the community at Assembly.

+ +

Recent Activity

+ +
+
+ {{#if activities}} + {{#each activities}} +
+ {{gravatar actor.email_hash}} {{actor.name}} + {{action}} {{kind}} + {{activityResourceLink resource}} + {{timeAgo publishDate}} +
+ {{/each}} + {{else}} +
+ There has been no activity yet. +
+ {{/if}} +
+
diff --git a/client/source/views/buckets/dashboard.coffee b/client/source/views/buckets/dashboard.coffee index 650b3ad..3b885dd 100644 --- a/client/source/views/buckets/dashboard.coffee +++ b/client/source/views/buckets/dashboard.coffee @@ -1,6 +1,11 @@ +_ = require 'underscore' PageView = require 'views/base/page' tpl = require 'templates/buckets/dashboard' module.exports = class DashboardView extends PageView template: tpl + + getTemplateData: -> + _.extend super, + activities: @collection.toJSON() \ No newline at end of file diff --git a/client/source/views/entries/browser.coffee b/client/source/views/entries/browser.coffee index 08fedf3..f7c3b3d 100644 --- a/client/source/views/entries/browser.coffee +++ b/client/source/views/entries/browser.coffee @@ -4,6 +4,7 @@ Chaplin = require 'chaplin' PageView = require 'views/base/page' EntriesList = require 'views/entries/list' +Activity = require 'models/activity' Entry = require 'models/entry' EntryEditView = require 'views/entries/edit' diff --git a/client/style/_views.styl b/client/style/_views.styl index eb2d447..5bfb520 100644 --- a/client/style/_views.styl +++ b/client/style/_views.styl @@ -95,6 +95,19 @@ .handle cursor move +// Activities + +.activities + .activity + padding 15px + + a.link-with-avatar:hover + text-decoration: none + + span + text-decoration: underline + + // Entries .entry-publish diff --git a/server/models/activity.coffee b/server/models/activity.coffee index 07c6b75..7b5b218 100644 --- a/server/models/activity.coffee +++ b/server/models/activity.coffee @@ -4,30 +4,55 @@ db = require '../lib/database' # Conforms, at least somewhat, to the activity stream spec outlined at # http://activitystrea.ms/specs/json/1.0 activitySchema = new mongoose.Schema - published: + publishDate: type: Date default: Date.now actor: + type: mongoose.Schema.Types.ObjectId + ref: 'User' + required: true + action: + type: String + enum: ['created', 'updated', 'deleted'] + required: true + resource: id: type: mongoose.Schema.Types.ObjectId - ref: 'User' + type: + type: String required: true - verb: + enum: ['entry', 'bucket', 'user'] name: type: String - enum: ['post', 'update'] required: true - object: - objectType: + # for users + email: type: String - required: true - enum: ['entry', 'bucket', 'user'] - id: - type: mongoose.Schema.Types.ObjectId - required: true + bucket: + id: + type: mongoose.Schema.Types.ObjectId + slug: + type: String + singular: + type: String , autoIndex: no activitySchema.set 'toJSON', virtuals: true +activitySchema.virtual 'kind' + .get -> + if @resource.type is 'entry' then @resource.bucket.singular.toLowerCase() else @resource.type + +activitySchema.statics.createForResource = (resource, action, actor, callback) -> + @model('Activity').create { resource, action, actor }, (err, activity) -> + if err + console.log 'Error creating Activity', activity, err + else + callback(action) if callback + +activitySchema.statics.unlinkActivities = (resource) -> + @model('Activity').update { 'resource.id': resource._id }, { $set: { 'resource.id': null }}, { multi: true }, (err) -> + console.log 'Error unlinking Activities', resource, err if err + module.exports = db.model 'Activity', activitySchema diff --git a/server/models/bucket.coffee b/server/models/bucket.coffee index b69f0c2..d65eabd 100644 --- a/server/models/bucket.coffee +++ b/server/models/bucket.coffee @@ -2,6 +2,7 @@ inflection = require 'inflection' mongoose = require 'mongoose' uniqueValidator = require 'mongoose-unique-validator' +Activity = require '../models/activity' Route = require '../models/route' db = require '../lib/database' {Sortable} = require '../lib/mongoose-plugins' @@ -114,4 +115,15 @@ bucketSchema.methods.getMembers = (callback) -> resourceId: @_id , callback +bucketSchema.methods.createActivity = (action, actor, callback) -> + Activity.createForResource + id: @_id + type: 'bucket' + name: @name + bucket: + id: @_id + slug: @slug + singular: @singular + , action, actor, callback + module.exports = db.model 'Bucket', bucketSchema diff --git a/server/models/entry.coffee b/server/models/entry.coffee index 514be95..c19fe80 100755 --- a/server/models/entry.coffee +++ b/server/models/entry.coffee @@ -6,6 +6,8 @@ getSlug = require 'speakingurl' db = require '../lib/database' +Activity = require '../models/activity' + # Add a parser to Chrono to understand "now" # A bit hacky because Chrono doesn't support ms yet chrono.parsers.NowParser = (text, ref, opt) -> @@ -97,7 +99,6 @@ entrySchema.path('publishDate').set (val='') -> parsed = chrono.parse(val)?[0]?.startDate parsed || Date.now() - entrySchema.path('description').validate (val) -> val?.length < 140 , 'Descriptions must be less than 140 characters.' @@ -107,6 +108,17 @@ entrySchema.path 'keywords' return unless _.isString val _.compact _.map val.split(','), (val) -> val.trim() +entrySchema.methods.createActivity = (action, actor, callback) -> + Activity.createForResource + id: @_id + type: 'entry' + name: @title + bucket: + id: @bucket._id + slug: @bucket.slug + singular: @bucket.singular + , action, actor, callback + entrySchema.statics.findByParams = (params, callback) -> settings = _.defaults params, diff --git a/server/models/user.coffee b/server/models/user.coffee index c22f632..bd2cca9 100755 --- a/server/models/user.coffee +++ b/server/models/user.coffee @@ -8,6 +8,7 @@ async = require 'async' fs = require 'fs-extra' _ = require 'underscore' db = require '../lib/database' +Activity = require '../models/activity' if process.env.DROPBOX_APP_KEY and process.env.DROPBOX_APP_SECRET dbox_app = dbox.app @@ -265,6 +266,14 @@ userSchema.methods.syncDropbox = (host='', reset, callback) -> callback e, written console.log "Saved new Dropbox cursor for User." +userSchema.methods.createActivity = (action, actor, callback) -> + Activity.createForResource + id: @_id + type: 'user' + name: @name + email: @email + , action, actor, callback + userSchema.virtual 'email_hash' .get -> crypto.createHash('md5').update(@email).digest('hex') if @email diff --git a/server/routes/api/activities.coffee b/server/routes/api/activities.coffee new file mode 100644 index 0000000..6128985 --- /dev/null +++ b/server/routes/api/activities.coffee @@ -0,0 +1,16 @@ +express = require 'express' + +Activity = require '../../models/activity' + +module.exports = app = express() + + +app.route('/activities') + .get (req, res) -> + return res.status(401).end() unless req.user + + Activity.find({}).sort('-publishDate').limit(20).populate('actor').exec (err, activities) -> + if err + res.send err, 400 + else if activities + res.send activities \ No newline at end of file diff --git a/server/routes/api/buckets.coffee b/server/routes/api/buckets.coffee index ff4adef..8349308 100644 --- a/server/routes/api/buckets.coffee +++ b/server/routes/api/buckets.coffee @@ -1,5 +1,6 @@ express = require 'express' +Activity = require '../../models/activity' Bucket = require '../../models/bucket' User = require '../../models/user' @@ -159,6 +160,7 @@ app.route('/buckets') if err res.status(400).send err else if bucket + bucket.createActivity 'created', req.user res.status(200).send bucket .get (req, res) -> @@ -197,14 +199,16 @@ app.route('/buckets/:bucketID') .delete (req, res) -> return res.status(401).end() unless req.user?.hasRole ['administrator'] - Bucket.findById req.params.bucketID, (err, bkt) -> + Bucket.findById req.params.bucketID, (err, bucket) -> if err res.send 400, err else - bkt.remove (err) -> + bucket.remove (err) -> if err res.status(400).send err else + bucket.createActivity 'deleted', req.user, -> + Activity.unlinkActivities bucket res.status(204).end() .put (req, res) -> @@ -215,6 +219,7 @@ app.route('/buckets/:bucketID') return res.status(400).send e: err if err bucket.set(req.body).save (err, bucket) -> return res.status(400).send err if err + bucket.createActivity 'updated', req.user res.status(200).send bucket ### diff --git a/server/routes/api/entries.coffee b/server/routes/api/entries.coffee index af8cd9c..4194c2e 100644 --- a/server/routes/api/entries.coffee +++ b/server/routes/api/entries.coffee @@ -1,5 +1,6 @@ express = require 'express' +Activity = require '../../models/activity' Bucket = require '../../models/bucket' Entry = require '../../models/entry' @@ -72,6 +73,7 @@ app.route '/entries' res.status(400).send err else entry.populate 'bucket author', -> + entry.createActivity 'created', req.user res.status(200).send entry .get (req, res) -> @@ -161,11 +163,17 @@ app.route('/entries/:entryID') return res.status(400).send err if err entry.populate 'bucket author', -> + entry.createActivity 'updated', req.user res.status(200).send entry .delete (req, res) -> - Entry.findById(req.params.entryID).remove (err) -> - if err - res.status(400).send e: err - else - res.status(204).end() + Entry.findById req.params.entryID, (error, entry) -> + entry.remove (err) -> + if err + res.status(400).send e: err + else + entry.populate 'bucket', -> + entry.createActivity 'deleted', req.user, -> + Activity.unlinkActivities entry + + res.status(204).end() diff --git a/server/routes/api/index.coffee b/server/routes/api/index.coffee index a3082e6..c6af5d7 100644 --- a/server/routes/api/index.coffee +++ b/server/routes/api/index.coffee @@ -2,6 +2,7 @@ express = require 'express' module.exports = app = express() +app.use require './activities' app.use require './buckets' app.use require './entries' app.use require './install' diff --git a/server/routes/api/users.coffee b/server/routes/api/users.coffee index 5acd94f..43017df 100644 --- a/server/routes/api/users.coffee +++ b/server/routes/api/users.coffee @@ -5,6 +5,7 @@ crypto = require 'crypto' mailer = require '../../lib/mailer' config = require '../../lib/config' +Activity = require '../../models/activity' User = require '../../models/user' module.exports = app = express() @@ -97,6 +98,7 @@ app.route('/users') newUser.save (err) -> return res.status(400).send err if err + newUser.createActivity 'created', req.user res.status(200).send newUser .get (req, res) -> @@ -161,9 +163,14 @@ app.route('/users/:userID') .delete (req, res) -> return res.status(401).end() unless req.user?.hasRole ['administrator'] - User.remove _id: req.params.userID, (err) -> - return res.status(400).end() if err - res.status(200).end() + User.findById req.params.userID, (err, user) -> + return res.status(400).end() if err or not user + + user.remove (err) -> + return res.status(400).end() if err + user.createActivity 'deleted', req.user, -> + Activity.unlinkActivities user + res.status(200).end() .put (req, res) -> return res.status(401).end() unless req.user?.hasRole ['administrator'] or req.user?._id is req.params.userID @@ -184,6 +191,7 @@ app.route('/users/:userID') user.set(req.body).save (err, user) -> return res.status(400).send err if err + user.createActivity 'updated', req.user res.status(200).send user ### From ca1102c3e8a903d5370363e99144dd93c99c4086 Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Mon, 15 Dec 2014 21:39:58 +0100 Subject: [PATCH 3/9] Added user docs for Activities. --- docs/user-docs/activities.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/user-docs/activities.md diff --git a/docs/user-docs/activities.md b/docs/user-docs/activities.md new file mode 100644 index 0000000..c5e41b5 --- /dev/null +++ b/docs/user-docs/activities.md @@ -0,0 +1,24 @@ +# Activities + +Activities allow you to keep track of the changes that have been done to your site. +The admin dashboard displays the 20 most recent activities. + +Find below a list of actions that currently create activities: + + +## For Entries + +When a user adds, updates or deletes an entry, the following activity will be created: +`[USER NAME] [added|udpated|deleted] [SINGULAR BUCKET NAME] [ENTRY TITLE]` + +## For Buckets + +When a user adds, updates or deletes a bucket, the following activity will be created: +`[USER NAME] [added|udpated|deleted] bucket [BUCKET NAME]` + +## For Users + +When a user adds, updates or deletes a user, the following activity will be created: +`[USER NAME] [added|udpated|deleted] user [USER NAME]` + + From 23bc381f4f696e7ef0441e26181a86122f68f90f Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Mon, 15 Dec 2014 23:32:39 +0100 Subject: [PATCH 4/9] Commented out all Activity server model tests to allow the other tests to run through while we figure out Activity design. --- test/server/models/activity.coffee | 98 +++++++++++++++--------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/test/server/models/activity.coffee b/test/server/models/activity.coffee index cd12dc8..f97df8c 100644 --- a/test/server/models/activity.coffee +++ b/test/server/models/activity.coffee @@ -1,49 +1,49 @@ -db = require '../../../server/lib/database' -mongoose = require 'mongoose' - -Activity = require '../../../server/models/activity' - -{expect} = require 'chai' -sinon = require 'sinon' - -describe 'Model#Activity', -> - - afterEach (done) -> - for _, c of db.connection.collections - c.remove(->) - done() - - describe 'Validation', -> - it 'requires an actor', (done) -> - Activity.create {verb: {name: 'post'}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> - expect(activity).to.be.undefined - expect(e).to.be.an 'Object' - expect(e.errors).to.have.property 'actor.id' - done() - - it 'requires a verb', (done) -> - Activity.create {actor: {id: new mongoose.Types.ObjectId()}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> - expect(activity).to.be.undefined - expect(e).to.be.an 'Object' - expect(e.errors).to.have.property 'verb.name' - done() - - it 'requires an object type', (done) -> - Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {id: new mongoose.Types.ObjectId()}}, (e, activity) -> - expect(activity).to.be.undefined - expect(e).to.be.an 'Object' - expect(e.errors).to.have.property 'object.objectType' - done() - - it 'requires an object id', (done) -> - Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {objectType: 'entry'}}, (e, activity) -> - expect(activity).to.be.undefined - expect(e).to.be.an 'Object' - expect(e.errors).to.have.property 'object.id' - done() - - describe 'Creation', -> - it 'automatically populates published date if one is not provided', (done) -> - Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> - expect(activity.published.toISOString()).to.exist - done() +#db = require '../../../server/lib/database' +#mongoose = require 'mongoose' +# +#Activity = require '../../../server/models/activity' +# +#{expect} = require 'chai' +#sinon = require 'sinon' +# +#describe 'Model#Activity', -> +# +# afterEach (done) -> +# for _, c of db.connection.collections +# c.remove(->) +# done() +# +# describe 'Validation', -> +# it 'requires an actor', (done) -> +# Activity.create {verb: {name: 'post'}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> +# expect(activity).to.be.undefined +# expect(e).to.be.an 'Object' +# expect(e.errors).to.have.property 'actor.id' +# done() +# +# it 'requires a verb', (done) -> +# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> +# expect(activity).to.be.undefined +# expect(e).to.be.an 'Object' +# expect(e.errors).to.have.property 'verb.name' +# done() +# +# it 'requires an object type', (done) -> +# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {id: new mongoose.Types.ObjectId()}}, (e, activity) -> +# expect(activity).to.be.undefined +# expect(e).to.be.an 'Object' +# expect(e.errors).to.have.property 'object.objectType' +# done() +# +# it 'requires an object id', (done) -> +# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {objectType: 'entry'}}, (e, activity) -> +# expect(activity).to.be.undefined +# expect(e).to.be.an 'Object' +# expect(e.errors).to.have.property 'object.id' +# done() +# +# describe 'Creation', -> +# it 'automatically populates published date if one is not provided', (done) -> +# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> +# expect(activity.published.toISOString()).to.exist +# done() From d9c02c64ccf892c787bbb1e0527962ef35c6f413 Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Mon, 15 Dec 2014 23:35:47 +0100 Subject: [PATCH 5/9] Increased timeout for mocha tests as tests timed out arbitrarily for me. Time to get a new machine? --- Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index de46ef8..7fc730b 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -90,7 +90,7 @@ module.exports = (grunt) -> shell: mocha: - command: 'NODE_ENV=test ./node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --recursive test/server -b' + command: 'NODE_ENV=test ./node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --recursive test/server -b --timeout 5000' # 5000ms timeout to prevent timeout on older/slow? machines cov: command: 'NODE_ENV=test ./node_modules/mocha/bin/mocha --compilers coffee:coffee-script/register --recursive test/server --require blanket --reporter html-cov > coverage.html' publish: From f5d93b5fc3545b2bcf06f138f598aca50ae7a388 Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Mon, 15 Dec 2014 23:43:50 +0100 Subject: [PATCH 6/9] Docs typo fix: p before d. --- docs/user-docs/activities.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user-docs/activities.md b/docs/user-docs/activities.md index c5e41b5..bbc75cf 100644 --- a/docs/user-docs/activities.md +++ b/docs/user-docs/activities.md @@ -9,16 +9,16 @@ Find below a list of actions that currently create activities: ## For Entries When a user adds, updates or deletes an entry, the following activity will be created: -`[USER NAME] [added|udpated|deleted] [SINGULAR BUCKET NAME] [ENTRY TITLE]` +`[USER NAME] [added|updated|deleted] [SINGULAR BUCKET NAME] [ENTRY TITLE]` ## For Buckets When a user adds, updates or deletes a bucket, the following activity will be created: -`[USER NAME] [added|udpated|deleted] bucket [BUCKET NAME]` +`[USER NAME] [added|updated|deleted] bucket [BUCKET NAME]` ## For Users When a user adds, updates or deletes a user, the following activity will be created: -`[USER NAME] [added|udpated|deleted] user [USER NAME]` +`[USER NAME] [added|updated|deleted] user [USER NAME]` From a72153b5f4cb4f2cc5f9b5498a69333a4a554c87 Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Tue, 16 Dec 2014 16:49:11 +0100 Subject: [PATCH 7/9] Activity stream: Moved building resource link from helper to template and model as discussed in https://github.com/asm-products/buckets/pull/79/files#r21846004. --- client/source/helpers.coffee | 22 ------------------- client/source/templates/buckets/dashboard.hbs | 8 +++++-- server/models/activity.coffee | 10 ++++++++- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/client/source/helpers.coffee b/client/source/helpers.coffee index acf91ec..978470a 100644 --- a/client/source/helpers.coffee +++ b/client/source/helpers.coffee @@ -88,25 +88,3 @@ Handlebars.registerHelper 'startsWith', (string1, string2, options) -> options.fn @ else options.inverse @ - -Handlebars.registerHelper 'activityResourceLink', (resource) -> - escapeExpression = (expression) -> - Handlebars.Utils.escapeExpression expression - id = escapeExpression resource.id - type = escapeExpression resource.type - name = escapeExpression resource.name - email = escapeExpression resource.email - bucketSlug = escapeExpression resource.bucket?.slug - - if resource.id - switch resource.type - when 'entry' then href = '/' + mediator.options.adminSegment + '/buckets/' + bucketSlug + '/' + id - when 'bucket' then href = '/' + mediator.options.adminSegment + '/buckets/' + bucketSlug - when 'user' then href = '/' + mediator.options.adminSegment + '/users/' + email - - link = '' + name + '' - else - link = name - - new Handlebars.SafeString link; - diff --git a/client/source/templates/buckets/dashboard.hbs b/client/source/templates/buckets/dashboard.hbs index 5c70b9a..b8c9ab7 100644 --- a/client/source/templates/buckets/dashboard.hbs +++ b/client/source/templates/buckets/dashboard.hbs @@ -18,8 +18,12 @@ {{#each activities}}
{{gravatar actor.email_hash}} {{actor.name}} - {{action}} {{kind}} - {{activityResourceLink resource}} + {{action}} {{resource.kind}} + {{#if resource.id}} + {{resource.name}} + {{else}} + {{resource.name}} + {{/if}} {{timeAgo publishDate}}
{{/each}} diff --git a/server/models/activity.coffee b/server/models/activity.coffee index 7b5b218..1869d36 100644 --- a/server/models/activity.coffee +++ b/server/models/activity.coffee @@ -40,10 +40,18 @@ activitySchema = new mongoose.Schema activitySchema.set 'toJSON', virtuals: true -activitySchema.virtual 'kind' +activitySchema.virtual 'resource.kind' .get -> if @resource.type is 'entry' then @resource.bucket.singular.toLowerCase() else @resource.type +activitySchema.virtual 'resource.path' + .get -> + switch @resource.type + when 'entry' then path = '/buckets/' + @resource.bucket.slug + '/' + @resource.id + when 'bucket' then path = '/buckets/' + @resource.bucket.slug + when 'user' then path = '/users/' + @resource.email + path + activitySchema.statics.createForResource = (resource, action, actor, callback) -> @model('Activity').create { resource, action, actor }, (err, activity) -> if err From 367f88dadda90a17a3fd57177f7cb53e158bbc7f Mon Sep 17 00:00:00 2001 From: Steffen Hiller Date: Tue, 16 Dec 2014 19:11:42 +0100 Subject: [PATCH 8/9] Changed activity model to use mongoose refs for its resource associations instead of using a home grown polymorphic'ish ref system as discussed in bounty #82. --- client/source/templates/buckets/dashboard.hbs | 2 +- server/models/activity.coffee | 51 +++++++++---------- server/models/bucket.coffee | 8 +-- server/models/entry.coffee | 9 ++-- server/models/user.coffee | 5 +- server/routes/api/activities.coffee | 17 +++++-- server/routes/api/buckets.coffee | 2 +- server/routes/api/entries.coffee | 2 +- server/routes/api/users.coffee | 2 +- 9 files changed, 48 insertions(+), 50 deletions(-) diff --git a/client/source/templates/buckets/dashboard.hbs b/client/source/templates/buckets/dashboard.hbs index b8c9ab7..8276b18 100644 --- a/client/source/templates/buckets/dashboard.hbs +++ b/client/source/templates/buckets/dashboard.hbs @@ -19,7 +19,7 @@
{{gravatar actor.email_hash}} {{actor.name}} {{action}} {{resource.kind}} - {{#if resource.id}} + {{#if resource.path}} {{resource.name}} {{else}} {{resource.name}} diff --git a/server/models/activity.coffee b/server/models/activity.coffee index 1869d36..d03ac50 100644 --- a/server/models/activity.coffee +++ b/server/models/activity.coffee @@ -16,41 +16,32 @@ activitySchema = new mongoose.Schema enum: ['created', 'updated', 'deleted'] required: true resource: - id: - type: mongoose.Schema.Types.ObjectId - type: + kind: type: String - required: true - enum: ['entry', 'bucket', 'user'] name: type: String required: true - # for users - email: - type: String + entry: + type: mongoose.Schema.Types.ObjectId + ref: 'Entry' bucket: - id: - type: mongoose.Schema.Types.ObjectId - slug: - type: String - singular: - type: String + type: mongoose.Schema.Types.ObjectId + ref: 'Bucket' + user: + type: mongoose.Schema.Types.ObjectId + ref: 'User' , autoIndex: no activitySchema.set 'toJSON', virtuals: true -activitySchema.virtual 'resource.kind' - .get -> - if @resource.type is 'entry' then @resource.bucket.singular.toLowerCase() else @resource.type - activitySchema.virtual 'resource.path' .get -> - switch @resource.type - when 'entry' then path = '/buckets/' + @resource.bucket.slug + '/' + @resource.id - when 'bucket' then path = '/buckets/' + @resource.bucket.slug - when 'user' then path = '/users/' + @resource.email - path + if @resource.entry or @resource.bucket or @resource.user + switch @resource.kind + when 'bucket' then '/buckets/' + @resource.bucket.slug + when 'user' then '/users/' + @resource.user.email + else '/buckets/' + @resource.bucket.slug + '/' + @resource.entry.id activitySchema.statics.createForResource = (resource, action, actor, callback) -> @model('Activity').create { resource, action, actor }, (err, activity) -> @@ -59,8 +50,16 @@ activitySchema.statics.createForResource = (resource, action, actor, callback) - else callback(action) if callback -activitySchema.statics.unlinkActivities = (resource) -> - @model('Activity').update { 'resource.id': resource._id }, { $set: { 'resource.id': null }}, { multi: true }, (err) -> - console.log 'Error unlinking Activities', resource, err if err +activitySchema.statics.unlinkActivities = (conditions) -> + @model('Activity').update conditions, + { + $set: + 'resource.entry': null + 'resource.bucket': null + 'resource.user': null + }, + { multi: true }, + (err) -> + console.log 'Error unlinking Activities', resource, err if err module.exports = db.model 'Activity', activitySchema diff --git a/server/models/bucket.coffee b/server/models/bucket.coffee index d65eabd..4f1d33a 100644 --- a/server/models/bucket.coffee +++ b/server/models/bucket.coffee @@ -117,13 +117,9 @@ bucketSchema.methods.getMembers = (callback) -> bucketSchema.methods.createActivity = (action, actor, callback) -> Activity.createForResource - id: @_id - type: 'bucket' + kind: 'bucket' name: @name - bucket: - id: @_id - slug: @slug - singular: @singular + bucket: @ , action, actor, callback module.exports = db.model 'Bucket', bucketSchema diff --git a/server/models/entry.coffee b/server/models/entry.coffee index c19fe80..9473d7e 100755 --- a/server/models/entry.coffee +++ b/server/models/entry.coffee @@ -110,13 +110,10 @@ entrySchema.path 'keywords' entrySchema.methods.createActivity = (action, actor, callback) -> Activity.createForResource - id: @_id - type: 'entry' + kind: @bucket.singular.toLowerCase() name: @title - bucket: - id: @bucket._id - slug: @bucket.slug - singular: @bucket.singular + entry: @ + bucket: @bucket , action, actor, callback entrySchema.statics.findByParams = (params, callback) -> diff --git a/server/models/user.coffee b/server/models/user.coffee index bd2cca9..143f09f 100755 --- a/server/models/user.coffee +++ b/server/models/user.coffee @@ -268,10 +268,9 @@ userSchema.methods.syncDropbox = (host='', reset, callback) -> userSchema.methods.createActivity = (action, actor, callback) -> Activity.createForResource - id: @_id - type: 'user' + kind: 'user' name: @name - email: @email + user: @ , action, actor, callback userSchema.virtual 'email_hash' diff --git a/server/routes/api/activities.coffee b/server/routes/api/activities.coffee index 6128985..88d66e1 100644 --- a/server/routes/api/activities.coffee +++ b/server/routes/api/activities.coffee @@ -9,8 +9,15 @@ app.route('/activities') .get (req, res) -> return res.status(401).end() unless req.user - Activity.find({}).sort('-publishDate').limit(20).populate('actor').exec (err, activities) -> - if err - res.send err, 400 - else if activities - res.send activities \ No newline at end of file + Activity + .find {} + .sort '-publishDate' + .limit 20 + .populate 'actor resource.user', 'name email' + .populate 'resource.entry', 'id' + .populate 'resource.bucket', 'slug' + .exec (err, activities) -> + if err + res.send err, 400 + else if activities + res.send activities \ No newline at end of file diff --git a/server/routes/api/buckets.coffee b/server/routes/api/buckets.coffee index 8349308..4ac5701 100644 --- a/server/routes/api/buckets.coffee +++ b/server/routes/api/buckets.coffee @@ -208,7 +208,7 @@ app.route('/buckets/:bucketID') res.status(400).send err else bucket.createActivity 'deleted', req.user, -> - Activity.unlinkActivities bucket + Activity.unlinkActivities { 'resource.bucket': bucket } res.status(204).end() .put (req, res) -> diff --git a/server/routes/api/entries.coffee b/server/routes/api/entries.coffee index 4194c2e..d64b52a 100644 --- a/server/routes/api/entries.coffee +++ b/server/routes/api/entries.coffee @@ -174,6 +174,6 @@ app.route('/entries/:entryID') else entry.populate 'bucket', -> entry.createActivity 'deleted', req.user, -> - Activity.unlinkActivities entry + Activity.unlinkActivities { 'resource.entry': entry } res.status(204).end() diff --git a/server/routes/api/users.coffee b/server/routes/api/users.coffee index 43017df..0f3771d 100644 --- a/server/routes/api/users.coffee +++ b/server/routes/api/users.coffee @@ -169,7 +169,7 @@ app.route('/users/:userID') user.remove (err) -> return res.status(400).end() if err user.createActivity 'deleted', req.user, -> - Activity.unlinkActivities user + Activity.unlinkActivities { 'resource.user': user } res.status(200).end() .put (req, res) -> From f6587d9666fde5a0b2ccdc65ca7ececaa1cabb5d Mon Sep 17 00:00:00 2001 From: David Kaneda Date: Wed, 31 Dec 2014 12:41:28 -0500 Subject: [PATCH 9/9] Continue Activities works * Re-implement tests * User logger in Activity model --- server/models/activity.coffee | 31 +++--- test/server/integration/builds.coffee | 2 - test/server/models/activity.coffee | 146 +++++++++++++++++--------- test/server/routes/api/users.coffee | 2 +- 4 files changed, 117 insertions(+), 64 deletions(-) diff --git a/server/models/activity.coffee b/server/models/activity.coffee index 1869d36..160ebef 100644 --- a/server/models/activity.coffee +++ b/server/models/activity.coffee @@ -1,5 +1,6 @@ mongoose = require 'mongoose' db = require '../lib/database' +logger = require '../lib/logger' # Conforms, at least somewhat, to the activity stream spec outlined at # http://activitystrea.ms/specs/json/1.0 @@ -18,6 +19,7 @@ activitySchema = new mongoose.Schema resource: id: type: mongoose.Schema.Types.ObjectId + required: true type: type: String required: true @@ -36,31 +38,36 @@ activitySchema = new mongoose.Schema singular: type: String , - autoIndex: no - -activitySchema.set 'toJSON', virtuals: true + toJSON: + virtuals: yes + transform: (doc, ret, options) -> + delete ret._id + delete ret.__v + ret activitySchema.virtual 'resource.kind' .get -> - if @resource.type is 'entry' then @resource.bucket.singular.toLowerCase() else @resource.type + if @resource.type is 'entry' + @resource.bucket.singular.toLowerCase() + else + @resource.type activitySchema.virtual 'resource.path' .get -> switch @resource.type - when 'entry' then path = '/buckets/' + @resource.bucket.slug + '/' + @resource.id - when 'bucket' then path = '/buckets/' + @resource.bucket.slug - when 'user' then path = '/users/' + @resource.email - path + when 'entry' then "/buckets/#{@resource.bucket.slug}/#{@resource.id}" + when 'bucket' then "/buckets/#{@resource.bucket.slug}" + when 'user' then "/users/#{@resource.email}" activitySchema.statics.createForResource = (resource, action, actor, callback) -> - @model('Activity').create { resource, action, actor }, (err, activity) -> + @create { resource, action, actor }, (err, activity) -> if err - console.log 'Error creating Activity', activity, err + logger.error 'Error creating Activity', activity, err else callback(action) if callback activitySchema.statics.unlinkActivities = (resource) -> - @model('Activity').update { 'resource.id': resource._id }, { $set: { 'resource.id': null }}, { multi: true }, (err) -> - console.log 'Error unlinking Activities', resource, err if err + @update { 'resource.id': resource._id }, { $set: { 'resource.id': null }}, { multi: true }, (err) -> + logger.error 'Error unlinking Activities', resource, err if err module.exports = db.model 'Activity', activitySchema diff --git a/test/server/integration/builds.coffee b/test/server/integration/builds.coffee index af42bd8..ec8e27d 100644 --- a/test/server/integration/builds.coffee +++ b/test/server/integration/builds.coffee @@ -11,8 +11,6 @@ hbs = require 'hbs' request = require 'supertest' describe 'Integration#Builds', -> - @timeout 5000 - beforeEach (done) -> buckets -> reset.builds -> diff --git a/test/server/models/activity.coffee b/test/server/models/activity.coffee index f97df8c..c077419 100644 --- a/test/server/models/activity.coffee +++ b/test/server/models/activity.coffee @@ -1,49 +1,97 @@ -#db = require '../../../server/lib/database' -#mongoose = require 'mongoose' -# -#Activity = require '../../../server/models/activity' -# -#{expect} = require 'chai' -#sinon = require 'sinon' -# -#describe 'Model#Activity', -> -# -# afterEach (done) -> -# for _, c of db.connection.collections -# c.remove(->) -# done() -# -# describe 'Validation', -> -# it 'requires an actor', (done) -> -# Activity.create {verb: {name: 'post'}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> -# expect(activity).to.be.undefined -# expect(e).to.be.an 'Object' -# expect(e.errors).to.have.property 'actor.id' -# done() -# -# it 'requires a verb', (done) -> -# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> -# expect(activity).to.be.undefined -# expect(e).to.be.an 'Object' -# expect(e.errors).to.have.property 'verb.name' -# done() -# -# it 'requires an object type', (done) -> -# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {id: new mongoose.Types.ObjectId()}}, (e, activity) -> -# expect(activity).to.be.undefined -# expect(e).to.be.an 'Object' -# expect(e.errors).to.have.property 'object.objectType' -# done() -# -# it 'requires an object id', (done) -> -# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {objectType: 'entry'}}, (e, activity) -> -# expect(activity).to.be.undefined -# expect(e).to.be.an 'Object' -# expect(e.errors).to.have.property 'object.id' -# done() -# -# describe 'Creation', -> -# it 'automatically populates published date if one is not provided', (done) -> -# Activity.create {actor: {id: new mongoose.Types.ObjectId()}, verb: {name: 'post'}, object: {objectType: 'entry', id: new mongoose.Types.ObjectId()}}, (e, activity) -> -# expect(activity.published.toISOString()).to.exist -# done() +db = require '../../../server/lib/database' +reset = require '../../reset' +mongoose = require 'mongoose' + +Activity = require '../../../server/models/activity' + +{expect} = require 'chai' +sinon = require 'sinon' + +describe 'Model#Activity', -> + afterEach reset.db + + describe 'Validation', -> + it 'requires an actor', (done) -> + Activity.create + verb: 'created' + resource: + type: 'entry' + id: new mongoose.Types.ObjectId() + + , (e, activity) -> + expect(activity).to.be.undefined + expect(e).to.be.an 'Object' + expect(e.errors).to.have.property 'actor' + done() + + it 'requires an action', (done) -> + Activity.create + actor: new mongoose.Types.ObjectId() + resource: + type: 'entry' + id: new mongoose.Types.ObjectId() + , (e, activity) -> + expect(activity).to.be.undefined + expect(e).to.be.an 'Object' + expect(e.errors).to.have.property 'action' + done() + + it 'requires a resource type', (done) -> + Activity.create + actor: new mongoose.Types.ObjectId() + verb: 'created' + resource: + id: new mongoose.Types.ObjectId() + , (e, activity) -> + expect(activity).to.be.undefined + expect(e).to.be.an 'Object' + expect(e.errors).to.have.property 'resource.type' + done() + + it 'requires a resource id', (done) -> + Activity.create + actor: new mongoose.Types.ObjectId() + action: + name: 'post', + resource: + type: 'entry' + , (e, activity) -> + expect(activity).to.be.undefined + expect(e).to.be.an 'Object' + expect(e.errors).to.have.property 'resource.id' + done() + + describe 'Creation', -> + it 'automatically populates published date if one is not provided', (done) -> + Activity.create + actor: new mongoose.Types.ObjectId() + action: 'created' + resource: + type: 'entry' + id: new mongoose.Types.ObjectId() + name: 'Test Activity' + , (e, activity) -> + expect(activity.publishDate.toISOString()).to.exist + done() + + it 'automatically creates a resource.path for an Entry', (done) -> + entryId = new mongoose.Types.ObjectId() + Activity.create + actor: new mongoose.Types.ObjectId() + action: 'created' + resource: + type: 'entry' + id: entryId + name: 'Test Activity' + bucket: + slug: 'test' + , (e, activity) -> + expect(activity.resource.path).to.equal "/buckets/test/#{entryId}" + done() + + it 'automatically creates a resource.path for a Bucket' + it 'automatically creates a resource.path for a User' + + describe 'Activity#unlinkActivities', -> + it 'unlinks activities' + describe 'Activity#createForResource', -> diff --git a/test/server/routes/api/users.coffee b/test/server/routes/api/users.coffee index cbb7a8d..d177cd4 100644 --- a/test/server/routes/api/users.coffee +++ b/test/server/routes/api/users.coffee @@ -117,7 +117,7 @@ describe 'REST#Users', -> .expect 400 .end done - it 'returns a 400 if password doesnot have a number', (done) -> + it 'returns a 400 if password doesn’t have a number', (done) -> auth.createAdmin (err, admin) -> admin .post "/#{apiSegment}/users"