Skip to content
This repository has been archived by the owner on Jun 5, 2019. It is now read-only.

Activity Stream #79

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ node_modules
tmp
docs/api
builds

# ide
.idea
2 changes: 1 addition & 1 deletion Gruntfile.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion client/source/controllers/buckets_controller.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions client/source/models/activities.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Collection = require 'lib/collection'
Activity = require 'models/activity'

module.exports = class Activities extends Collection
url: '/api/activities'
model: Activity
7 changes: 7 additions & 0 deletions client/source/models/activity.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Model = require 'lib/model'

module.exports = class Activity extends Model
urlRoot: '/api/activities'

hello: ->
'test'
30 changes: 28 additions & 2 deletions client/source/templates/buckets/dashboard.hbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
<div class="row">
<h1 class="page-title color-yellow">Buckets</h1>

<div class="col-md-4 col-sm-6">
<div class="col-md-12 col-sm-6">
<div class="panel">
<div class="panel-body">
<p class="lead">Buckets is an open-source CMS, built in Node.js, which is being actively developed by <a href="https://assembly.com/buckets" target="_blank">the community at Assembly</a>.</p>
<p class="lead">Buckets is an open-source CMS, built in Node.js, which is being actively developed by <a
href="https://assembly.com/buckets" target="_blank">the community at Assembly</a>.</p>
</div>
</div>
</div>

<h1 class="page-title">Recent Activity</h1>

<div class="col-md-12 col-sm-4">
<div class="activities">
{{#if activities}}
{{#each activities}}
<div class="activity">
<a class="link-with-avatar" href="/{{adminSegment}}/users/{{actor.email}}">{{gravatar actor.email_hash}} <span class="actor-name">{{actor.name}}</span></a>
{{action}} {{resource.kind}}
{{#if resource.path}}
<a href="/{{adminSegment}}{{resource.path}}">{{resource.name}}</a>
{{else}}
{{resource.name}}
{{/if}}
{{timeAgo publishDate}}
</div>
{{/each}}
{{else}}
<div class="activity">
There has been no activity yet.
</div>
{{/if}}
</div>
</div>
</div>
5 changes: 5 additions & 0 deletions client/source/views/buckets/dashboard.coffee
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions client/source/views/entries/browser.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
13 changes: 13 additions & 0 deletions client/style/_views.styl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions docs/user-docs/activities.md
Original file line number Diff line number Diff line change
@@ -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|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|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|updated|deleted] user [USER NAME]`


68 changes: 52 additions & 16 deletions server/models/activity.coffee
Original file line number Diff line number Diff line change
@@ -1,33 +1,69 @@
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
activitySchema = new mongoose.Schema
published:
publishDate:
type: Date
default: Date.now
actor:
id:
type: mongoose.Schema.Types.ObjectId
ref: 'User'
required: true
verb:
name:
type: mongoose.Schema.Types.ObjectId
ref: 'User'
required: true
action:
type: String
enum: ['created', 'updated', 'deleted']
required: true
resource:
kind:
type: String
enum: ['post', 'update']
required: true
object:
objectType:
name:
type: String
required: true
enum: ['entry', 'bucket', 'user']
id:
entry:
type: mongoose.Schema.Types.ObjectId
required: true
ref: 'Entry'
bucket:
type: mongoose.Schema.Types.ObjectId
ref: 'Bucket'
user:
type: mongoose.Schema.Types.ObjectId
ref: 'User'
,
autoIndex: no
toJSON:
virtuals: yes
transform: (doc, ret, options) ->
delete ret._id
delete ret.__v
ret

activitySchema.virtual 'resource.path'
.get ->
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) ->
@create { resource, action, actor }, (err, activity) ->
if err
logger.error 'Error creating Activity', activity, err
else
callback(action) if callback

activitySchema.set 'toJSON', virtuals: true
activitySchema.statics.unlinkActivities = (conditions) ->
@update conditions,
{
$set:
'resource.entry': null
'resource.bucket': null
'resource.user': null
},
{ multi: true },
(err) ->
logger.error 'Error unlinking Activities', resource, err if err

module.exports = db.model 'Activity', activitySchema
8 changes: 8 additions & 0 deletions server/models/bucket.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -114,4 +115,11 @@ bucketSchema.methods.getMembers = (callback) ->
resourceId: @_id
, callback

bucketSchema.methods.createActivity = (action, actor, callback) ->
Activity.createForResource
kind: 'bucket'
name: @name
bucket: @
, action, actor, callback

module.exports = db.model 'Bucket', bucketSchema
11 changes: 10 additions & 1 deletion server/models/entry.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down Expand Up @@ -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.'
Expand All @@ -107,6 +108,14 @@ entrySchema.path 'keywords'
return unless _.isString val
_.compact _.map val.split(','), (val) -> val.trim()

entrySchema.methods.createActivity = (action, actor, callback) ->
Activity.createForResource
kind: @bucket.singular.toLowerCase()
name: @title
entry: @
bucket: @bucket
, action, actor, callback

entrySchema.statics.findByParams = (params, callback) ->

settings = _.defaults params,
Expand Down
8 changes: 8 additions & 0 deletions server/models/user.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -265,6 +266,13 @@ 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
kind: 'user'
name: @name
user: @
, action, actor, callback

userSchema.virtual 'email_hash'
.get ->
crypto.createHash('md5').update(@email).digest('hex') if @email
Expand Down
23 changes: 23 additions & 0 deletions server/routes/api/activities.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 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
9 changes: 7 additions & 2 deletions server/routes/api/buckets.coffee
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
express = require 'express'

Activity = require '../../models/activity'
Bucket = require '../../models/bucket'
User = require '../../models/user'

Expand Down Expand Up @@ -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) ->
Expand Down Expand Up @@ -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 { 'resource.bucket': bucket }
res.status(204).end()

.put (req, res) ->
Expand All @@ -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

###
Expand Down
Loading