Commit 99614ea3 by David Ormsbee

Merge pull request #511 from MITx/feature/dave/forum_fixes

Forum refactoring/fixes
parents 3fa52057 baa6eadf
...@@ -4,35 +4,37 @@ ...@@ -4,35 +4,37 @@
// Backbone may be freely distributed under the MIT license. // Backbone may be freely distributed under the MIT license.
// For all details and documentation: // For all details and documentation:
// http://backbonejs.org // http://backbonejs.org
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= (function(){var k=this,y=k.Backbone,z=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:k.Backbone={};g.VERSION="0.9.2";var f=k._;!f&&"undefined"!==typeof require&&(f=require("underscore"));g.$=k.jQuery||k.Zepto||k.ender;g.noConflict=function(){k.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,h=g.Events={on:function(a,b,c){var d,e;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks={});e=a.shift();)e=d[e]||(d[e]=[]),e.push(b,c);return this},
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= off:function(a,b,c){var d,e,m;if(!(e=this._callbacks))return this;if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(!(m=e[d])||!b&&!c)delete e[d];else for(d=m.length-2;0<=d;d-=2)b&&m[d]!==b||c&&m[d+1]!==c||m.splice(d,2);return this},trigger:function(a){var b,c,d,e,f,g,j;if(!(c=this._callbacks))return this;j=[];a=a.split(p);e=1;for(f=arguments.length;e<f;e++)j[e-1]=arguments[e];for(;b=a.shift();){if(g=c.all)g=g.slice();if(d=c[b])d=d.slice();if(d){e=0;for(f=
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= d.length;e<f;e+=2)d[e].apply(d[e+1]||this,j)}if(g){b=[b].concat(j);e=0;for(f=g.length;e<f;e+=2)g[e].apply(g[e+1]||this,b)}}return this}};h.bind=h.on;h.unbind=h.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.collection&&(this.collection=b.collection);b&&b.parse&&(a=this.parse(a));if(c=l(this,"defaults"))a=f.extend({},c,a);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent={};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent=
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== {};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,h,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},sync:function(){return g.sync.apply(this,arguments)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==b?"":""+b)},
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},g=this.attributes,i=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(g[e],a)||c.unset&&f.has(g,e))delete i[e],(c.silent?this._silent:b)[e]=!0;c.unset?
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; delete g[e]:g[e]=a;!f.isEqual(j[e],a)||f.has(g,e)!==f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){b=f.extend({},b,{unset:!0});return this.set(a,null,b)},clear:function(a){a=f.extend({},a,{unset:!0});return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d,a);b.trigger("sync",
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, b,d,a)};a.error=g.wrapError(a.error,b,a);return this.sync("read",this,a)},save:function(a,b,c){var d,e,m;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c)||!d&&!this.isValid())return!1;var i=this,j=c.success;c.success=function(a,b,e){m=true;b=i.parse(a,e);c.wait&&(b=f.extend(d||{},b));if(!i.set(b,c))return false;j&&j(i,a,c);i.trigger("sync",i,a,c)};c.error=
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); g.wrapError(c.error,i,c);b=this.sync(this.isNew()?"create":"update",this,c);!m&&c.wait&&(this.clear(a),this.set(e,a));return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};a.success=function(e){(a.wait||b.isNew())&&d();c&&c(b,e,a);b.isNew()||b.trigger("sync",b,e,a)};if(this.isNew())return a.success(),!1;a.error=g.wrapError(a.error,b,a);var e=this.sync("delete",this,a);a.wait||d();return e},url:function(){var a=l(this,"urlRoot")||
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= l(this.collection,"url")||s();return this.isNew()?a:a+("/"===a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| {};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return null==a?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return null==
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); a||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate||!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var q=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);void 0!==b.comparator&&
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])? (this.comparator=b.comparator);this._reset();this.initialize.apply(this,arguments);a&&(b.parse&&(a=this.parse(a)),this.reset(a,{silent:!0,parse:b.parse}))};f.extend(q.prototype,h,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},sync:function(){return g.sync.apply(this,arguments)},add:function(a,b){var c,d,e,g,i,j={},k={},h=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)? g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?h.push(c):j[g]=k[i]=e}for(c=h.length;c--;)h[c]=a.splice(h[c],1)[0];c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;z.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));if(b.merge){c=0;for(d=h.length;c<d;c++)(e=this._byId[h[c].id])&&e.set(h[c],b)}this.comparator&&null==b.at&&this.sort({silent:!0});if(b.silent)return this;c=0;
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a}, for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length? b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},shift:function(a){var b=this.at(0);this.remove(b,a);return b},slice:function(a,b){return this.models.slice(a,b)},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d, b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1===this.comparator.length?this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId= f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d,a);b.trigger("sync",b,d,a)};a.error=g.wrapError(a.error,b,a);return this.sync("read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(a,b,f){f.wait&&c.add(a,
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this, f);d&&d(a,b,f)};a.save(null,b);return a},parse:function(a){return a},clone:function(){return new this.constructor(this.models)},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){if(a instanceof o)return a.collection||(a.collection=this),a;b||(b={});b.collection=this;var c=new this.model(a,b);return!c._validate(c.attributes,b)?!1:c},_removeReference:function(a){this===a.collection&&delete a.collection;
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g, a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"===a||"remove"===a)&&c!==this||("destroy"===a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],null!=b.id&&(this._byId[b.id]=b)),this.trigger.apply(this,arguments))}});f.each("forEach each map collect reduce foldl inject reduceRight foldr find detect filter select reject every all some any include contains invoke max min sortBy sortedIndex toArray size first head take initial rest tail last without indexOf shuffle lastIndexOf isEmpty groupBy".split(" "),
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b, function(a){q.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var t=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},A=/:\w+/g,B=/\*\w+/g,C=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(t.prototype,h,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new n);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]: d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(C,"\\$&").replace(A,"([^/]+)").replace(B,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState= var n=g.History=function(a){this.handlers=[];f.bindAll(this,"checkUrl");this.location=a&&a.location||k.location;this.history=a&&a.history||k.history},r=/^[#\/]/,D=/msie [\w.]+/,u=/\/$/;n.started=!1;f.extend(n.prototype,h,{interval:50,getHash:function(a){return(a=(a||this).location.href.match(/#(.*)$/))?a[1]:""},getFragment:function(a,b){if(null==a)if(this._hasPushState||!this._wantsHashChange||b){var a=this.location.pathname,c=this.options.root.replace(u,"");a.indexOf(c)||(a=a.substr(c.length))}else a=
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl, this.getHash();return decodeURIComponent(a.replace(r,""))},start:function(a){if(n.started)throw Error("Backbone.history has already been started");n.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!this.history||!this.history.pushState);var a=this.getFragment(),b=document.documentMode,b=D.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b);u.test(this.options.root)||
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()}, (this.options.root+="/");b&&this._wantsHashChange&&(this.iframe=g.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a));this._hasPushState?g.$(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?g.$(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,this.interval));this.fragment=a;a=this.location;b=a.pathname.replace(/[^/]$/,"$&/")===this.options.root&&
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers, !a.search;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),this.location.replace(this.options.root+this.location.search+"#"+this.fragment),!0;this._wantsPushState&&(this._hasPushState&&b&&a.hash)&&(this.fragment=this.getHash().replace(r,""),this.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},stop:function(){g.$(window).unbind("popstate",
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace|| this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);n.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a===this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a===this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(","); !0})},navigate:function(a,b){if(!n.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(r,"");if(this.fragment!==c){this.fragment=c;var d=(0!==c.indexOf(this.options.root)?this.options.root:"")+c;if(this._hasPushState)this.history[b.replace?"replaceState":"pushState"]({},document.title,d);else if(this._wantsHashChange)this._updateHash(this.location,c,b.replace),this.iframe&&c!==this.getFragment(this.getHash(this.iframe))&&(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents(); c,b.replace));else return this.location.assign(d);b.trigger&&this.loadUrl(a)}},_updateHash:function(a,b,c){c?a.replace(a.href.replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},E=/^(\S+)\s*(.*)$/,w="model collection el id attributes className tagName".split(" ");f.extend(v.prototype,h,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el, render:function(){return this},dispose:function(){this.undelegateEvents();this.model&&this.model.off(null,null,this);this.collection&&this.collection.off(null,null,this);return this},remove:function(){this.dispose();this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&g.$(a).attr(b);null!=c&&g.$(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof g.$?a:g.$(a);this.el=this.$el[0];this.$delegateElement=this.$el;!1!==b&&
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json", this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=l(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(E),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$delegateElement.bind(e,c):this.$delegateElement.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a, f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,!1);else{var a=f.extend({},l(this,"attributes"));this.id&&(a.id=l(this,"id"));this.className&&(a["class"]=l(this,"className"));this.setElement(this.make(l(this,"tagName"),a),!1)}}});o.extend=q.extend=t.extend=v.extend=function(a,b){var c=this,d;d=a&&a.hasOwnProperty("constructor")?a.constructor:function(){c.apply(this,arguments)};
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this); f.extend(d,c);x.prototype=c.prototype;d.prototype=new x;a&&f.extend(d.prototype,a);b&&f.extend(d,b);d.prototype.constructor=d;d.__super__=c.prototype;return d};var F={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=F[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=l(b,"url")||s());if(!c.data&&b&&("create"===a||"update"===a))e.contentType="application/json",e.data=JSON.stringify(b);g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=
e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return g.ajax(f.extend(e,c))};g.ajax=function(){return g.$.ajax.apply(g.$,arguments)};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},l=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?
a[b]():a[b]},s=function(){throw Error('A "url" property or function must be specified');}}).call(this);
...@@ -395,6 +395,7 @@ def instructor_dashboard(request, course_id): ...@@ -395,6 +395,7 @@ def instructor_dashboard(request, course_id):
# For now, just a static page # For now, just a static page
context = {'course': course, context = {'course': course,
'staff_access': True,} 'staff_access': True,}
return render_to_response('courseware/instructor_dashboard.html', context) return render_to_response('courseware/instructor_dashboard.html', context)
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -51,7 +51,8 @@ def ajax_content_response(request, course_id, content, template_name): ...@@ -51,7 +51,8 @@ def ajax_content_response(request, course_id, content, template_name):
'content': content, 'content': content,
} }
html = render_to_string(template_name, context) html = render_to_string(template_name, context)
annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user) user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
return JsonResponse({ return JsonResponse({
'html': html, 'html': html,
'content': content, 'content': content,
......
...@@ -11,11 +11,11 @@ from courseware.courses import get_course_with_access ...@@ -11,11 +11,11 @@ from courseware.courses import get_course_with_access
from courseware.access import has_access from courseware.access import has_access
from urllib import urlencode from urllib import urlencode
from operator import methodcaller
from django_comment_client.permissions import check_permissions_by_view from django_comment_client.permissions import check_permissions_by_view
from django_comment_client.utils import merge_dict, extract, strip_none from django_comment_client.utils import merge_dict, extract, strip_none, strip_blank
import json import json
import dateutil
import django_comment_client.utils as utils import django_comment_client.utils as utils
import comment_client as cc import comment_client as cc
...@@ -64,22 +64,26 @@ def render_discussion(request, course_id, threads, *args, **kwargs): ...@@ -64,22 +64,26 @@ def render_discussion(request, course_id, threads, *args, **kwargs):
'user': (lambda: reverse('django_comment_client.forum.views.user_profile', args=[course_id, user_id])), 'user': (lambda: reverse('django_comment_client.forum.views.user_profile', args=[course_id, user_id])),
}[discussion_type]() }[discussion_type]()
annotated_content_infos = map(lambda x: utils.get_annotated_content_infos(course_id, x, request.user), threads) user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = reduce(merge_dict, annotated_content_infos, {})
def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
annotated_content_info = reduce(merge_dict, map(infogetter, threads), {})
context = { context = {
'threads': threads, 'threads': threads,
'discussion_id': discussion_id, 'discussion_id': discussion_id,
'user_id': user_id, 'user_id': user_id,
'user_info': json.dumps(cc.User.from_django_user(request.user).to_dict()),
'course_id': course_id, 'course_id': course_id,
'request': request, 'request': request,
'performed_search': _should_perform_search(request), 'performed_search': _should_perform_search(request),
'pages_nearby_delta': PAGES_NEARBY_DELTA, 'pages_nearby_delta': PAGES_NEARBY_DELTA,
'discussion_type': discussion_type, 'discussion_type': discussion_type,
'base_url': base_url, 'base_url': base_url,
'query_params': strip_none(extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text'])), 'query_params': strip_blank(strip_none(extract(query_params, ['page', 'sort_key', 'sort_order', 'tags', 'text']))),
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': json.dumps(annotated_content_info),
'discussion_data': json.dumps({ discussion_id: threads }),
} }
context = dict(context.items() + query_params.items()) context = dict(context.items() + query_params.items())
return render_to_string(template, context) return render_to_string(template, context)
...@@ -121,7 +125,11 @@ def inline_discussion(request, course_id, discussion_id): ...@@ -121,7 +125,11 @@ def inline_discussion(request, course_id, discussion_id):
threads, query_params = get_threads(request, course_id, discussion_id) threads, query_params = get_threads(request, course_id, discussion_id)
html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, \ html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, \
query_params=query_params) query_params=query_params)
return utils.HtmlResponse(html)
return utils.JsonResponse({
'html': html,
'discussionData': threads,
})
def render_search_bar(request, course_id, discussion_id=None, text=''): def render_search_bar(request, course_id, discussion_id=None, text=''):
if not discussion_id: if not discussion_id:
...@@ -138,19 +146,21 @@ def forum_form_discussion(request, course_id): ...@@ -138,19 +146,21 @@ def forum_form_discussion(request, course_id):
threads, query_params = get_threads(request, course_id) threads, query_params = get_threads(request, course_id)
content = render_forum_discussion(request, course_id, threads, discussion_id=_general_discussion_id(course_id), query_params=query_params) content = render_forum_discussion(request, course_id, threads, discussion_id=_general_discussion_id(course_id), query_params=query_params)
recent_active_threads = cc.search_recent_active_threads(
course_id,
recursive=False,
query_params={'follower_id': request.user.id},
)
trending_tags = cc.search_trending_tags(
course_id,
)
if request.is_ajax(): if request.is_ajax():
return utils.HtmlResponse(content) return utils.JsonResponse({
'html': content,
'discussionData': threads,
})
else: else:
recent_active_threads = cc.search_recent_active_threads(
course_id,
recursive=False,
query_params={'follower_id': request.user.id},
)
trending_tags = cc.search_trending_tags(
course_id,
)
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'course': course, 'course': course,
...@@ -164,17 +174,19 @@ def forum_form_discussion(request, course_id): ...@@ -164,17 +174,19 @@ def forum_form_discussion(request, course_id):
def render_single_thread(request, discussion_id, course_id, thread_id): def render_single_thread(request, discussion_id, course_id, thread_id):
thread = cc.Thread.find(thread_id).retrieve(recursive=True) thread = cc.Thread.find(thread_id).retrieve(recursive=True).to_dict()
user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread.to_dict(), user=request.user) annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread, user=request.user, user_info=user_info)
context = { context = {
'discussion_id': discussion_id, 'discussion_id': discussion_id,
'thread': thread, 'thread': thread,
'user_info': json.dumps(cc.User.from_django_user(request.user).to_dict()),
'annotated_content_info': json.dumps(annotated_content_info), 'annotated_content_info': json.dumps(annotated_content_info),
'course_id': course_id, 'course_id': course_id,
'request': request, 'request': request,
'discussion_data': json.dumps({ discussion_id: [thread] }),
} }
return render_to_string('discussion/_single_thread.html', context) return render_to_string('discussion/_single_thread.html', context)
...@@ -182,13 +194,15 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -182,13 +194,15 @@ def single_thread(request, course_id, discussion_id, thread_id):
if request.is_ajax(): if request.is_ajax():
user_info = cc.User.from_django_user(request.user).to_dict()
thread = cc.Thread.find(thread_id).retrieve(recursive=True) thread = cc.Thread.find(thread_id).retrieve(recursive=True)
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user) annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
context = {'thread': thread.to_dict(), 'course_id': course_id} context = {'thread': thread.to_dict(), 'course_id': course_id}
html = render_to_string('discussion/_ajax_single_thread.html', context) html = render_to_string('discussion/_ajax_single_thread.html', context)
return utils.JsonResponse({ return utils.JsonResponse({
'html': html, 'html': html,
'content': thread.to_dict(),
'annotated_content_info': annotated_content_info, 'annotated_content_info': annotated_content_info,
}) })
...@@ -200,7 +214,6 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -200,7 +214,6 @@ def single_thread(request, course_id, discussion_id, thread_id):
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'init': '', 'init': '',
'content': render_single_thread(request, discussion_id, course_id, thread_id), 'content': render_single_thread(request, discussion_id, course_id, thread_id),
'accordion': render_accordion(request, course, discussion_id),
'course': course, 'course': course,
'course_id': course.id, 'course_id': course.id,
} }
......
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from utils import *
from mustache_helpers import mustache_helpers from mustache_helpers import mustache_helpers
from django.core.urlresolvers import reverse
from functools import partial from functools import partial
from utils import *
import pystache_custom as pystache import pystache_custom as pystache
import urllib import urllib
import os
def pluralize(singular_term, count): def pluralize(singular_term, count):
if int(count) >= 2: if int(count) >= 2:
...@@ -18,12 +22,32 @@ def show_if(text, condition): ...@@ -18,12 +22,32 @@ def show_if(text, condition):
else: else:
return '' return ''
# TODO there should be a better way to handle this
def include_mustache_templates():
mustache_dir = settings.PROJECT_ROOT / 'templates' / 'discussion' / 'mustache'
valid_file_name = lambda file_name: file_name.endswith('.mustache')
read_file = lambda file_name: (file_name, open(mustache_dir / file_name, "r").read())
strip_file_name = lambda x: (x[0].rpartition('.')[0], x[1])
wrap_in_tag = lambda x: "<script type='text/template' id='{0}'>{1}</script>".format(x[0], x[1])
file_contents = map(read_file, filter(valid_file_name, os.listdir(mustache_dir)))
return '\n'.join(map(wrap_in_tag, map(strip_file_name, file_contents)))
def render_content(content, additional_context={}): def render_content(content, additional_context={}):
content_info = { content_info = {
'displayed_title': content.get('highlighted_title') or content.get('title', ''), 'displayed_title': content.get('highlighted_title') or content.get('title', ''),
'displayed_body': content.get('highlighted_body') or content.get('body', ''), 'displayed_body': content.get('highlighted_body') or content.get('body', ''),
'raw_tags': ','.join(content.get('tags', [])), 'raw_tags': ','.join(content.get('tags', [])),
} }
print content_info
if content['type'] == 'thread':
content_info['permalink'] = reverse('django_comment_client.forum.views.single_thread',
args=[content['course_id'], content['commentable_id'], content['id']])
else:
content_info['permalink'] = reverse('django_comment_client.forum.views.single_thread',
args=[content['course_id'], content['commentable_id'], content['thread_id']]) + '#' + content['id']
context = { context = {
'content': merge_dict(content, content_info), 'content': merge_dict(content, content_info),
content['type']: True, content['type']: True,
...@@ -31,4 +55,4 @@ def render_content(content, additional_context={}): ...@@ -31,4 +55,4 @@ def render_content(content, additional_context={}):
context = merge_dict(context, additional_context) context = merge_dict(context, additional_context)
partial_mustache_helpers = {k: partial(v, content) for k, v in mustache_helpers.items()} partial_mustache_helpers = {k: partial(v, content) for k, v in mustache_helpers.items()}
context = merge_dict(context, partial_mustache_helpers) context = merge_dict(context, partial_mustache_helpers)
return render_mustache('discussion/_content.mustache', context) return render_mustache('discussion/mustache/_content.mustache', context)
...@@ -6,9 +6,9 @@ import inspect ...@@ -6,9 +6,9 @@ import inspect
def pluralize(content, text): def pluralize(content, text):
num, word = text.split(' ') num, word = text.split(' ')
if int(num or '0') >= 2: if int(num or '0') >= 2:
return num + ' ' + word + 's' return word + 's'
else: else:
return num + ' ' + word return word
def url_for_user(content, user_id): def url_for_user(content, user_id):
return urlresolvers.reverse('django_comment_client.forum.views.user_profile', args=[content['course_id'], user_id]) return urlresolvers.reverse('django_comment_client.forum.views.user_profile', args=[content['course_id'], user_id])
......
...@@ -18,6 +18,7 @@ class PermissionsTestCase(TestCase): ...@@ -18,6 +18,7 @@ class PermissionsTestCase(TestCase):
return ''.join(random.choice(chars) for x in range(length)) return ''.join(random.choice(chars) for x in range(length))
def setUp(self): def setUp(self):
self.course_id = "MITx/6.002x/2012_Fall" self.course_id = "MITx/6.002x/2012_Fall"
self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0] self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0]
......
...@@ -160,23 +160,32 @@ class QueryCountDebugMiddleware(object): ...@@ -160,23 +160,32 @@ class QueryCountDebugMiddleware(object):
logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time)) logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time))
return response return response
def get_annotated_content_info(course_id, content, user): def get_annotated_content_info(course_id, content, user, user_info):
voted = ''
if content['id'] in user_info['upvoted_ids']:
voted = 'up'
elif content['id'] in user_info['downvoted_ids']:
voted = 'down'
return { return {
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"), 'voted': voted,
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), 'subscribed': content['id'] in user_info['subscribed_thread_ids'],
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False, 'ability': {
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"), 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False, 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"),
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), 'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False,
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"),
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False,
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"),
},
} }
def get_annotated_content_infos(course_id, thread, user): def get_annotated_content_infos(course_id, thread, user, user_info):
infos = {} infos = {}
def _annotate(content): def annotate(content):
infos[str(content['id'])] = get_annotated_content_info(course_id, content, user) infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, user_info)
for child in content.get('children', []): for child in content.get('children', []):
_annotate(child) annotate(child)
_annotate(thread) annotate(thread)
return infos return infos
def render_mustache(template_name, dictionary, *args, **kwargs): def render_mustache(template_name, dictionary, *args, **kwargs):
......
from utils import * from utils import *
from thread import Thread
import models import models
import settings import settings
...@@ -10,7 +11,7 @@ class Comment(models.Model): ...@@ -10,7 +11,7 @@ class Comment(models.Model):
'endorsed', 'parent_id', 'thread_id', 'endorsed', 'parent_id', 'thread_id',
'username', 'votes', 'user_id', 'closed', 'username', 'votes', 'user_id', 'closed',
'created_at', 'updated_at', 'depth', 'created_at', 'updated_at', 'depth',
'at_position_list', 'type', 'at_position_list', 'type', 'commentable_id',
] ]
updatable_fields = [ updatable_fields = [
...@@ -23,6 +24,10 @@ class Comment(models.Model): ...@@ -23,6 +24,10 @@ class Comment(models.Model):
base_url = "{prefix}/comments".format(prefix=settings.PREFIX) base_url = "{prefix}/comments".format(prefix=settings.PREFIX)
type = 'comment' type = 'comment'
@property
def thread(self):
return Thread(id=self.thread_id, type='thread')
@classmethod @classmethod
def url_for_comments(cls, params={}): def url_for_comments(cls, params={}):
if params.get('thread_id'): if params.get('thread_id'):
......
...@@ -43,6 +43,9 @@ class Model(object): ...@@ -43,6 +43,9 @@ class Model(object):
raise KeyError("Field {0} does not exist".format(key)) raise KeyError("Field {0} does not exist".format(key))
self.attributes.__setitem__(key, value) self.attributes.__setitem__(key, value)
def items(self, *args, **kwargs):
return self.attributes.items(*args, **kwargs)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
return self.attributes.get(*args, **kwargs) return self.attributes.get(*args, **kwargs)
......
...@@ -11,6 +11,7 @@ class Thread(models.Model): ...@@ -11,6 +11,7 @@ class Thread(models.Model):
'commentable_id', 'username', 'user_id', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'created_at', 'updated_at', 'comments_count',
'at_position_list', 'children', 'type', 'at_position_list', 'children', 'type',
'highlighted_title', 'highlighted_body',
] ]
updatable_fields = [ updatable_fields = [
......
...@@ -18,8 +18,10 @@ def extract(dic, keys): ...@@ -18,8 +18,10 @@ def extract(dic, keys):
def merge_dict(dic1, dic2): def merge_dict(dic1, dic2):
return dict(dic1.items() + dic2.items()) return dict(dic1.items() + dic2.items())
def perform_request(method, url, data_or_params=None, *args, **kwargs): def perform_request(method, url, data_or_params=None, *args, **kwargs):
if data_or_params is None:
data_or_params = {}
data_or_params['api_key'] = settings.API_KEY data_or_params['api_key'] = settings.API_KEY
if method in ['post', 'put', 'patch']: if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params) response = requests.request(method, url, data=data_or_params)
......
if not @Discussion? class @Content extends Backbone.Model
@Discussion = {}
template: -> DiscussionUtil.getTemplate('_content')
Discussion = @Discussion
actions:
initializeVote = (content) -> editable: '.admin-edit'
$content = $(content) can_reply: '.discussion-reply'
$local = Discussion.generateLocal($content.children(".discussion-content")) can_endorse: '.admin-endorse'
id = $content.attr("_id") can_delete: '.admin-delete'
if Discussion.isUpvoted id can_openclose: '.admin-openclose'
$local(".discussion-vote-up").addClass("voted")
else if Discussion.isDownvoted id urlMappers: {}
$local(".discussion-vote-down").addClass("voted")
urlFor: (name) ->
initializeFollowThread = (thread) -> @urlMappers[name].apply(@)
$thread = $(thread)
id = $thread.attr("_id") can: (action) ->
$thread.children(".discussion-content") DiscussionUtil.getContentInfo @id, action
.find(".follow-wrapper")
.append(Discussion.subscriptionLink('thread', id)) updateInfo: (info) ->
@set('ability', info.ability)
@Discussion = $.extend @Discussion, @set('voted', info.voted)
@set('subscribed', info.subscribed)
bindContentEvents: (content) ->
addComment: (comment, options) ->
$content = $(content) options ||= {}
$discussionContent = $content.children(".discussion-content") if not options.silent
$local = Discussion.generateLocal($discussionContent) thread = @get('thread')
comments_count = parseInt(thread.get('comments_count'))
id = $content.attr("_id") thread.set('comments_count', comments_count + 1)
@get('children').push comment
handleReply = (elem) -> model = new Comment $.extend {}, comment, { thread: @get('thread') }
$replyView = $local(".discussion-reply-new") @get('comments').add model
if $replyView.length model
$replyView.show()
else removeComment: (comment) ->
thread_id = $discussionContent.parents(".thread").attr("_id") thread = @get('thread')
view = comments_count = parseInt(thread.get('comments_count'))
id: id thread.set('comments_count', comments_count - 1 - comment.getCommentsCount())
showWatchCheckbox: not Discussion.isSubscribed(thread_id, "thread")
$discussionContent.append Mustache.render Discussion.replyTemplate, view resetComments: (children) ->
Discussion.makeWmdEditor $content, $local, "reply-body" @set 'children', []
$local(".discussion-submit-post").click -> handleSubmitReply(this) @set 'comments', new Comments()
$local(".discussion-cancel-post").click -> handleCancelReply(this) for comment in (children || [])
$local(".discussion-reply").hide() @addComment comment, { silent: true }
$local(".discussion-edit").hide()
initialize: ->
handleCancelReply = (elem) -> DiscussionUtil.addContent @id, @
$replyView = $local(".discussion-reply-new") @resetComments(@get('children'))
if $replyView.length
$replyView.hide()
$local(".discussion-reply").show() class @ContentView extends Backbone.View
$local(".discussion-edit").show()
$: (selector) ->
handleSubmitReply = (elem) -> @$local.find(selector)
if $content.hasClass("thread")
url = Discussion.urlFor('create_comment', id) partial:
else if $content.hasClass("comment") endorsed: (endorsed) ->
url = Discussion.urlFor('create_sub_comment', id) if endorsed
else @$el.addClass("endorsed")
return
body = Discussion.getWmdContent $content, $local, "reply-body"
anonymous = false || $local(".discussion-post-anonymously").is(":checked")
autowatch = false || $local(".discussion-auto-watch").is(":checked")
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data:
body: body
anonymous: anonymous
autowatch: autowatch
error: Discussion.formErrorHandler($local(".discussion-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".discussion-errors"))
$comment = $(response.html)
$content.children(".comments").prepend($comment)
Discussion.setWmdContent $content, $local, "reply-body", ""
Discussion.setContentInfo response.content['id'], 'can_reply', true
Discussion.setContentInfo response.content['id'], 'editable', true
Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
Discussion.initializeContent($comment)
Discussion.bindContentEvents($comment)
$local(".discussion-reply-new").hide()
$local(".discussion-reply").show()
$local(".discussion-edit").show()
$discussionContent.attr("status", "normal")
handleVote = (elem, value) ->
contentType = if $content.hasClass("thread") then "thread" else "comment"
url = Discussion.urlFor("#{value}vote_#{contentType}", id)
Discussion.safeAjax
$elem: $local(".discussion-vote")
url: url
type: "POST"
dataType: "json"
success: (response, textStatus) ->
if textStatus == "success"
$local(".discussion-vote").removeClass("voted")
$local(".discussion-vote-#{value}").addClass("voted")
$local(".discussion-votes-point").html response.votes.point
handleUnvote = (elem, value) ->
contentType = if $content.hasClass("thread") then "thread" else "comment"
url = Discussion.urlFor("undo_vote_for_#{contentType}", id)
Discussion.safeAjax
$elem: $local(".discussion-vote")
url: url
type: "POST"
dataType: "json"
success: (response, textStatus) ->
if textStatus == "success"
$local(".discussion-vote").removeClass("voted")
$local(".discussion-votes-point").html response.votes.point
handleCancelEdit = (elem) ->
$local(".discussion-content-edit").hide()
$local(".discussion-content-wrapper").show()
handleEditThread = (elem) ->
$local(".discussion-content-wrapper").hide()
$editView = $local(".discussion-content-edit")
if $editView.length
$editView.show()
else
view = {
id: id
title: $local(".thread-raw-title").html()
body: $local(".thread-raw-body").html()
tags: $local(".thread-raw-tags").html()
}
$discussionContent.append Mustache.render Discussion.editThreadTemplate, view
Discussion.makeWmdEditor $content, $local, "thread-body-edit"
$local(".thread-tags-edit").tagsInput Discussion.tagsInputOptions()
$local(".discussion-submit-update").unbind("click").click -> handleSubmitEditThread(this)
$local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this)
handleSubmitEditThread = (elem) ->
url = Discussion.urlFor('update_thread', id)
title = $local(".thread-title-edit").val()
body = Discussion.getWmdContent $content, $local, "thread-body-edit"
tags = $local(".thread-tags-edit").val()
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data: {title: title, body: body, tags: tags},
error: Discussion.formErrorHandler($local(".discussion-update-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".discussion-update-errors"))
$discussionContent.replaceWith(response.html)
Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
Discussion.initializeContent($content)
Discussion.bindContentEvents($content)
handleEditComment = (elem) ->
$local(".discussion-content-wrapper").hide()
$editView = $local(".discussion-content-edit")
if $editView.length
$editView.show()
else
view = { id: id, body: $local(".comment-raw-body").html() }
$discussionContent.append Mustache.render Discussion.editCommentTemplate, view
Discussion.makeWmdEditor $content, $local, "comment-body-edit"
$local(".discussion-submit-update").unbind("click").click -> handleSubmitEditComment(this)
$local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this)
handleSubmitEditComment= (elem) ->
url = Discussion.urlFor('update_comment', id)
body = Discussion.getWmdContent $content, $local, "comment-body-edit"
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {body: body}
error: Discussion.formErrorHandler($local(".discussion-update-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".discussion-update-errors"))
$discussionContent.replaceWith(response.html)
Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
Discussion.initializeContent($content)
Discussion.bindContentEvents($content)
handleEndorse = (elem, endorsed) ->
url = Discussion.urlFor('endorse_comment', id)
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {endorsed: endorsed}
success: (response, textStatus) ->
if textStatus == "success"
if endorsed
$(content).addClass("endorsed")
else
$(content).removeClass("endorsed")
$(elem).unbind('click').click ->
handleEndorse(elem, !endorsed)
handleOpenClose = (elem, text) ->
url = Discussion.urlFor('openclose_thread', id)
closed = undefined
if text.match(/Close/)
closed = true
else if text.match(/[Oo]pen/)
closed = false
else else
console.log "Unexpected text " + text + "for open/close thread." @$el.removeClass("endorsed")
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {closed: closed}
success: (response, textStatus) =>
if textStatus == "success"
if closed
$(content).addClass("closed")
$(elem).text "Re-open Thread"
else
$(content).removeClass("closed")
$(elem).text "Close Thread"
error: (response, textStatus, e) ->
console.log e
handleDelete = (elem) ->
if $content.hasClass("thread")
url = Discussion.urlFor('delete_thread', id)
c = confirm "Are you sure to delete thread \"" + $content.find("a.thread-title").text() + "\"?"
else
url = Discussion.urlFor('delete_comment', id)
c = confirm "Are you sure to delete this comment? "
if c != true
return
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {}
success: (response, textStatus) =>
if textStatus == "success"
$(content).remove()
error: (response, textStatus, e) ->
console.log e
handleHideSingleThread = (elem) ->
$threadTitle = $local(".thread-title")
$hideComments = $local(".discussion-hide-comments")
$hideComments.removeClass("discussion-hide-comments")
.addClass("discussion-show-comments")
$content.children(".comments").hide()
$threadTitle.unbind('click').click handleShowSingleThread
$hideComments.unbind('click').click handleShowSingleThread
prevHtml = $hideComments.html()
$hideComments.html prevHtml.replace "Hide", "Show"
handleShowSingleThread = ->
$threadTitle = $local(".thread-title")
$showComments = $local(".discussion-show-comments")
if not $showComments.hasClass("first-time") and (not $showComments.length or not $threadTitle.length)
return
rebindHideEvents = ->
$threadTitle.unbind('click').click handleHideSingleThread
$showComments.unbind('click').click handleHideSingleThread
$showComments.removeClass("discussion-show-comments")
.addClass("discussion-hide-comments")
prevHtml = $showComments.html()
$showComments.html prevHtml.replace "Show", "Hide"
if not $showComments.hasClass("first-time") and $content.children(".comments").length
$content.children(".comments").show()
rebindHideEvents()
else
discussion_id = $threadTitle.parents(".discussion").attr("_id")
url = Discussion.urlFor('retrieve_single_thread', discussion_id, id)
Discussion.safeAjax
$elem: $.merge($threadTitle, $showComments)
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus) ->
Discussion.bulkExtendContentInfo response['annotated_content_info']
$content.append(response['html'])
$content.find(".comment").each (index, comment) ->
Discussion.initializeContent(comment)
Discussion.bindContentEvents(comment)
$showComments.removeClass("first-time")
rebindHideEvents()
Discussion.bindLocalEvents $local,
"click .thread-title": ->
handleShowSingleThread(this)
"click .discussion-show-comments": ->
handleShowSingleThread(this)
"click .discussion-hide-comments": ->
handleHideSingleThread(this)
"click .discussion-reply-thread": ->
handleShowSingleThread($local(".thread-title"))
handleReply(this)
"click .discussion-reply-comment": ->
handleReply(this)
"click .discussion-cancel-reply": ->
handleCancelReply(this)
"click .discussion-vote-up": ->
$elem = $(this)
if $elem.hasClass("voted")
handleUnvote($elem)
else
handleVote($elem, "up")
"click .discussion-vote-down": ->
$elem = $(this)
if $elem.hasClass("voted")
handleUnvote($elem)
else
handleVote($elem, "down")
"click .admin-endorse": ->
handleEndorse(this, not $content.hasClass("endorsed"))
"click .admin-openclose": -> closed: (closed) -> # we should just re-render the whole thread, or update according to new abilities
handleOpenClose(this, $(this).text()) if closed
@$el.addClass("closed")
"click .admin-edit": -> @$(".admin-openclose").text "Re-open Thread"
if $content.hasClass("thread") else
handleEditThread(this) @$el.removeClass("closed")
else @$(".admin-openclose").text "Close Thread"
handleEditComment(this)
"click .admin-delete": ->
handleDelete(this)
initializeContent: (content) ->
unescapeHighlightTag = (text) ->
text.replace(/\&lt\;highlight\&gt\;/g, "<span class='search-highlight'>")
.replace(/\&lt\;\/highlight\&gt\;/g, "</span>")
stripHighlight = (text, type) -> voted: (voted) ->
text.replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "") @$(".discussion-vote-up").removeClass("voted") if voted != "up"
.replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, "") @$(".discussion-vote-down").removeClass("voted") if voted != "down"
@$(".discussion-vote-#{voted}").addClass("voted") if voted in ["up", "down"]
votes_point: (votes_point) ->
@$(".discussion-votes-point").html(votes_point)
stripLatexHighlight = (text) -> comments_count: (comments_count) ->
Discussion.processEachMathAndCode text, stripHighlight @$(".comments-count").html(comments_count)
subscribed: (subscribed) ->
if subscribed
@$(".discussion-follow-thread").addClass("discussion-unfollow-thread").html("Unfollow")
else
@$(".discussion-follow-thread").removeClass("discussion-unfollow-thread").html("Follow")
markdownWithHighlight = (text) -> ability: (ability) ->
converter = Markdown.getMathCompatibleConverter() for action, elemSelector of @model.actions
unescapeHighlightTag stripLatexHighlight converter.makeHtml text if not ability[action]
@$(elemSelector).parent().remove()
$content = $(content) $discussionContent: ->
initializeVote $content @_discussionContent ||= @$el.children(".discussion-content")
if $content.hasClass("thread")
initializeFollowThread $content
$local = Discussion.generateLocal($content.children(".discussion-content"))
$local("span.timeago").timeago() $showComments: ->
@_showComments ||= @$(".discussion-show-comments")
$contentTitle = $local(".thread-title") updateShowComments: ->
if @showed
@$showComments().html @$showComments().html().replace "Show", "Hide"
else
@$showComments().html @$showComments().html().replace "Hide", "Show"
retrieved: ->
@$showComments().hasClass("retrieved")
hideSingleThread: (event) ->
@$el.children(".comments").hide()
@showed = false
@updateShowComments()
showSingleThread: (event) ->
if @retrieved()
@$el.children(".comments").show()
@showed = true
@updateShowComments()
else
$elem = $.merge @$(".thread-title"), @$showComments()
url = @model.urlFor('retrieve')
DiscussionUtil.get $elem, url, {}, (response, textStatus) =>
@showed = true
@updateShowComments()
@$showComments().addClass("retrieved")
@$el.children(".comments").replaceWith response.html
@model.resetComments response.content.children
@initCommentViews()
DiscussionUtil.bulkUpdateContentInfo response.annotated_content_info
toggleSingleThread: (event) ->
if @showed
@hideSingleThread(event)
else
@showSingleThread(event)
initCommentViews: ->
@$el.children(".comments").children(".comment").each (index, elem) =>
model = @model.get('comments').find $(elem).attr("_id")
if not model.view
commentView = new CommentView el: elem, model: model
reply: ->
if @model.get('type') == 'thread'
@showSingleThread()
$replyView = @$(".discussion-reply-new")
if $replyView.length
$replyView.show()
else
view = {}
view.id = @model.id
view.showWatchCheckbox = not @model.get('thread').get('subscribed')
html = Mustache.render DiscussionUtil.getTemplate('_reply'), view
@$discussionContent().append html
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "reply-body"
@$(".discussion-submit-post").click $.proxy(@submitReply, @)
@$(".discussion-cancel-post").click $.proxy(@cancelReply, @)
@$(".discussion-reply").hide()
@$(".discussion-edit").hide()
submitReply: (event) ->
url = @model.urlFor('reply')
body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "reply-body"
anonymous = false || @$(".discussion-post-anonymously").is(":checked")
autowatch = false || @$(".discussion-auto-watch").is(":checked")
DiscussionUtil.safeAjax
$elem: $(event.target)
url: url
type: "POST"
dataType: 'json'
data:
body: body
anonymous: anonymous
auto_subscribe: autowatch
error: DiscussionUtil.formErrorHandler @$(".discussion-errors")
success: (response, textStatus) =>
DiscussionUtil.clearFormErrors @$(".discussion-errors")
$comment = $(response.html)
@$el.children(".comments").prepend $comment
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), "reply-body", ""
comment = @model.addComment response.content
commentView = new CommentView el: $comment[0], model: comment
comment.updateInfo response.annotated_content_info
@cancelReply()
cancelReply: ->
$replyView = @$(".discussion-reply-new")
if $replyView.length
$replyView.hide()
@$(".discussion-reply").show()
@$(".discussion-edit").show()
unvote: (event) ->
url = @model.urlFor('unvote')
$elem = @$(".discussion-vote")
DiscussionUtil.post $elem, url, {}, (response, textStatus) =>
@model.set('voted', '')
@model.set('votes_point', response.votes.point)
vote: (event, value) ->
url = @model.urlFor("#{value}vote")
$elem = @$(".discussion-vote")
DiscussionUtil.post $elem, url, {}, (response, textStatus) =>
@model.set('voted', value)
@model.set('votes_point', response.votes.point)
toggleVote: (event) ->
$elem = $(event.target)
value = $elem.attr("value")
if @model.get("voted") == value
@unvote(event)
else
@vote(event, value)
toggleEndorse: (event) ->
$elem = $(event.target)
url = @model.urlFor('endorse')
endorsed = @model.get('endorsed')
data = { endorsed: not endorsed }
DiscussionUtil.post $elem, url, data, (response, textStatus) =>
@model.set('endorsed', not endorsed)
toggleFollow: (event) ->
$elem = $(event.target)
subscribed = @model.get('subscribed')
if subscribed
url = @model.urlFor('unfollow')
else
url = @model.urlFor('follow')
DiscussionUtil.post $elem, url, {}, (response, textStatus) =>
@model.set('subscribed', not subscribed)
toggleClosed: (event) ->
$elem = $(event.target)
url = @model.urlFor('close')
closed = @model.get('closed')
data = { closed: not closed }
DiscussionUtil.post $elem, url, data, (response, textStatus) =>
@model.set('closed', not closed)
edit: (event) ->
@$(".discussion-content-wrapper").hide()
$editView = @$(".discussion-content-edit")
if $editView.length
$editView.show()
else
view = {}
view.id = @model.id
if @model.get('type') == 'thread'
view.title = @$(".thread-raw-title").html()
view.body = @$(".thread-raw-body").html()
view.tags = @$(".thread-raw-tags").html()
else
view.body = @$(".comment-raw-body").html()
@$discussionContent().append Mustache.render DiscussionUtil.getTemplate("_edit_#{@model.get('type')}"), view
Discussion.makeWmdEditor @$el, $.proxy(@$, @), "#{@model.get('type')}-body-edit"
@$(".thread-tags-edit").tagsInput DiscussionUtil.tagsInputOptions()
@$(".discussion-submit-update").unbind("click").click $.proxy(@submitEdit, @)
@$(".discussion-cancel-update").unbind("click").click $.proxy(@cancelEdit, @)
submitEdit: (event) ->
url = @model.urlFor('update')
data = {}
if @model.get('type') == 'thread'
data.title = @$(".thread-title-edit").val()
data.body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "thread-body-edit"
data.tags = @$(".thread-tags-edit").val()
else
data.body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "comment-body-edit"
DiscussionUtil.safeAjax
$elem: $(event.target)
url: url
type: "POST"
dataType: 'json'
data: data
error: DiscussionUtil.formErrorHandler @$(".discussion-update-errors")
success: (response, textStatus) =>
DiscussionUtil.clearFormErrors @$(".discussion-update-errors")
@$discussionContent().replaceWith(response.html)
@model.set response.content
@model.updateInfo response.annotated_content_info
cancelEdit: (event) ->
@$(".discussion-content-edit").hide()
@$(".discussion-content-wrapper").show()
delete: (event) ->
url = @model.urlFor('delete')
if @model.get('type') == 'thread'
c = confirm "Are you sure to delete thread \"#{@model.get('title')}\"?"
else
c = confirm "Are you sure to delete this comment? "
if not c
return
$elem = $(event.target)
DiscussionUtil.post $elem, url, {}, (response, textStatus) =>
@$el.remove()
@model.get('thread').removeComment(@model)
events:
"click .discussion-follow-thread": "toggleFollow"
"click .thread-title": "toggleSingleThread"
"click .discussion-show-comments": "toggleSingleThread"
"click .discussion-reply-thread": "reply"
"click .discussion-reply-comment": "reply"
"click .discussion-cancel-reply": "cancelReply"
"click .discussion-vote-up": "toggleVote"
"click .discussion-vote-down": "toggleVote"
"click .admin-endorse": "toggleEndorse"
"click .admin-openclose": "toggleClosed"
"click .admin-edit": "edit"
"click .admin-delete": "delete"
initLocal: ->
@$local = @$el.children(".local")
@$delegateElement = @$local
initTitle: ->
$contentTitle = @$(".thread-title")
if $contentTitle.length if $contentTitle.length
$contentTitle.html unescapeHighlightTag stripLatexHighlight $contentTitle.html() $contentTitle.html DiscussionUtil.unescapeHighlightTag DiscussionUtil.stripLatexHighlight $contentTitle.html()
$contentBody = $local(".content-body")
$contentBody.html Discussion.postMathJaxProcessor markdownWithHighlight $contentBody.html()
initBody: ->
$contentBody = @$(".content-body")
$contentBody.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight $contentBody.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, $contentBody.attr("id")] MathJax.Hub.Queue ["Typeset", MathJax.Hub, $contentBody.attr("id")]
id = $content.attr("_id")
if $content.hasClass("thread") initTimeago: ->
discussion_id = $content.attr("_discussion_id") @$("span.timeago").timeago()
permalink = Discussion.urlFor("permanent_link_thread", discussion_id, id)
else initPermalink: ->
thread_id = $content.parents(".thread").attr("_id") @$(".discussion-permanent-link").attr "href", @model.permalink()
discussion_id = $content.parents(".thread").attr("_discussion_id")
permalink = Discussion.urlFor("permanent_link_comment", discussion_id, thread_id, id) renderPartial: ->
$local(".discussion-permanent-link").attr "href", permalink for attr, value of @model.changedAttributes()
if @partial[attr]
if not Discussion.getContentInfo id, 'editable' @partial[attr].apply(@, [value])
$local(".admin-edit").remove()
if not Discussion.getContentInfo id, 'can_reply' initBindings: ->
$local(".discussion-reply").remove() @model.view = @
if not Discussion.getContentInfo id, 'can_endorse' @model.bind('change', @renderPartial, @)
$local(".admin-endorse").remove()
if not Discussion.getContentInfo id, 'can_delete' initialize: ->
$local(".admin-delete").remove() @initBindings()
if not Discussion.getContentInfo id, 'can_openclose' @initLocal()
$local(".admin-openclose").remove() @initTimeago()
#if not Discussion.getContentInfo id, 'can_vote' @initTitle()
# $local(".discussion-vote").css "visibility", "hidden" @initBody()
@initCommentViews()
class @Thread extends @Content
urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
initialize: ->
@set('thread', @)
super()
permalink: ->
discussion_id = @get('commentable_id')
return Discussion.urlFor("permanent_link_thread", discussion_id, @id)
class @ThreadView extends @ContentView
class @Comment extends @Content
urlMappers:
'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id)
'unvote': -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote': -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote': -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
'update': -> DiscussionUtil.urlFor('update_comment', @id)
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
permalink: ->
thread_id = @get('thread').id
discussion_id = @get('thread').get('commentable_id')
return Discussion.urlFor("permanent_link_comment", discussion_id, thread_id, @id)
getCommentsCount: ->
count = 0
@get('comments').each (comment) ->
count += comment.getCommentsCount() + 1
count
class @CommentView extends @ContentView
class @Comments extends Backbone.Collection
model: Comment
initialize: ->
@bind "add", (item) =>
item.collection = @
find: (id) ->
_.first @where(id: id)
if not @Discussion? class @Discussion extends Backbone.Collection
@Discussion = {} model: Thread
Discussion = @Discussion initialize: ->
DiscussionUtil.addDiscussion @id, @
initializeFollowDiscussion = (discussion) -> @bind "add", (item) =>
$discussion = $(discussion) item.discussion = @
id = $following.attr("_id")
$local = Discussion.generateLocal() find: (id) ->
$discussion.children(".discussion-non-content") _.first @where(id: id)
.find(".discussion-title-wrapper")
.append(Discussion.subscriptionLink('discussion', id)) addThread: (thread, options) ->
options ||= {}
@Discussion = $.extend @Discussion, model = new Thread thread
@add model
initializeDiscussion: (discussion) -> model
$discussion = $(discussion)
$discussion.find(".thread").each (index, thread) -> class @DiscussionView extends Backbone.View
Discussion.initializeContent(thread)
Discussion.bindContentEvents(thread) $: (selector) ->
$discussion.find(".comment").each (index, comment) -> @$local.find(selector)
Discussion.initializeContent(comment)
Discussion.bindContentEvents(comment) initLocal: ->
@$local = @$el.children(".local")
#initializeFollowDiscussion(discussion) TODO move this somewhere else @$delegateElement = @$local
bindDiscussionEvents: (discussion) -> initialize: ->
@initLocal()
$discussion = $(discussion) @model.id = @$el.attr("_id")
$discussionNonContent = $discussion.children(".discussion-non-content") @model.view = @
$local = Discussion.generateLocal($discussion.children(".discussion-local")) @$el.children(".threads").children(".thread").each (index, elem) =>
threadView = new ThreadView el: elem, model: @model.find $(elem).attr("_id")
id = $discussion.attr("_id") if @$el.hasClass("forum-discussion")
$(".discussion-sidebar").find(".sidebar-new-post-button")
handleSubmitNewPost = (elem) -> .unbind('click').click $.proxy @newPost, @
title = $local(".new-post-title").val() else if @$el.hasClass("inline-discussion")
body = Discussion.getWmdContent $discussion, $local, "new-post-body" @newPost()
tags = $local(".new-post-tags").val()
url = Discussion.urlFor('create_thread', id) reload: ($elem, url) ->
Discussion.safeAjax if not url then return
$elem: $(elem) DiscussionUtil.get $elem, url, {}, (response, textStatus) =>
url: url $parent = @$el.parent()
type: "POST" @$el.replaceWith(response.html)
dataType: 'json' $discussion = $parent.find("section.discussion")
data: @model.reset(response.discussionData, { silent: false })
title: title view = new DiscussionView el: $discussion[0], model: @model
body: body DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
tags: tags
error: Discussion.formErrorHandler($local(".new-post-form-errors")) loadSimilarPost: (event) ->
success: (response, textStatus) -> $title = @$(".new-post-title")
Discussion.clearFormErrors($local(".new-post-form-errors")) $wrapper = @$(".new-post-similar-posts-wrapper")
$thread = $(response.html) $similarPosts = @$(".new-post-similar-posts")
$discussion.children(".threads").prepend($thread) prevText = $title.attr("prev-text")
$local(".new-post-title").val("") text = $title.val()
Discussion.setWmdContent $discussion, $local, "new-post-body", "" if text == prevText
$local(".new-post-tags").val("") if @$(".similar-post").length
if $discussion.hasClass("inline-discussion") $wrapper.show()
$local(".new-post-form").addClass("collapsed") else if $.trim(text).length
else if $discussion.hasClass("forum-discussion") $elem = $(event.target)
$local(".new-post-form").hide() url = DiscussionUtil.urlFor 'search_similar_threads', @model.id
data = { text: @$(".new-post-title").val() }
handleCancelNewPost = (elem) -> DiscussionUtil.get $elem, url, data, (response, textStatus) =>
if $discussion.hasClass("inline-discussion") $similarPosts.empty()
$local(".new-post-form").addClass("collapsed") if $.type(response) == "array" and response.length
else if $discussion.hasClass("forum-discussion")
$local(".new-post-form").hide()
handleSimilarPost = (elem) ->
$title = $local(".new-post-title")
$wrapper = $local(".new-post-similar-posts-wrapper")
$similarPosts = $local(".new-post-similar-posts")
prevText = $title.attr("prev-text")
text = $title.val()
if text == prevText
if $local(".similar-post").length
$wrapper.show() $wrapper.show()
else if $.trim(text).length for thread in response
Discussion.safeAjax $similarPost = $("<a>").addClass("similar-post")
$elem: $(elem) .html(thread["title"])
url: Discussion.urlFor 'search_similar_threads', id .attr("href", "javascript:void(0)") #TODO
type: "GET" .appendTo($similarPosts)
dateType: 'json' else
data: $wrapper.hide()
text: $local(".new-post-title").val() else
success: (response, textStatus) -> $wrapper.hide()
$similarPosts.empty() $title.attr("prev-text", text)
console.log response
if $.type(response) == "array" and response.length
$wrapper.show() newPost: ->
for thread in response if not @$(".wmd-panel").length
#singleThreadUrl = Discussion.urlFor 'retrieve_single_thread view = { discussion_id: @model.id }
$similarPost = $("<a>").addClass("similar-post") @$el.children(".discussion-non-content").append Mustache.render DiscussionUtil.getTemplate("_new_post"), view
.html(thread["title"]) $newPostBody = @$(".new-post-body")
.attr("href", "javascript:void(0)") #TODO DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
.appendTo($similarPosts)
else $input = DiscussionUtil.getWmdInput @$el, $.proxy(@$, @), "new-post-body"
$wrapper.hide() $input.attr("placeholder", "post a new topic...")
else if @$el.hasClass("inline-discussion")
$wrapper.hide() $input.bind 'focus', (e) =>
$title.attr("prev-text", text) @$(".new-post-form").removeClass('collapsed')
else if @$el.hasClass("forum-discussion")
initializeNewPost = -> @$(".new-post-form").removeClass('collapsed')
view = { discussion_id: id }
$discussionNonContent = $discussion.children(".discussion-non-content") @$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
if not $local(".wmd-panel").length @$(".new-post-title").blur $.proxy(@loadSimilarPost, @)
$discussionNonContent.append Mustache.render Discussion.newPostTemplate, view
$newPostBody = $local(".new-post-body") @$(".hide-similar-posts").click =>
Discussion.makeWmdEditor $discussion, $local, "new-post-body" @$(".new-post-similar-posts-wrapper").hide()
$input = Discussion.getWmdInput($discussion, $local, "new-post-body") @$(".discussion-submit-post").click $.proxy(@submitNewPost, @)
$input.attr("placeholder", "post a new topic...") @$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @)
if $discussion.hasClass("inline-discussion")
$input.bind 'focus', (e) ->
$local(".new-post-form").removeClass('collapsed') @$(".new-post-form").show()
else if $discussion.hasClass("forum-discussion")
$local(".new-post-form").removeClass('collapsed') submitNewPost: (event) ->
title = @$(".new-post-title").val()
$local(".new-post-tags").tagsInput Discussion.tagsInputOptions() body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "new-post-body"
tags = @$(".new-post-tags").val()
$local(".new-post-title").blur -> anonymous = false || @$(".discussion-post-anonymously").is(":checked")
handleSimilarPost(this) autowatch = false || @$(".discussion-auto-watch").is(":checked")
url = DiscussionUtil.urlFor('create_thread', @model.id)
$local(".hide-similar-posts").click -> DiscussionUtil.safeAjax
$local(".new-post-similar-posts-wrapper").hide() $elem: $(event.target)
url: url
$local(".discussion-submit-post").click -> type: "POST"
handleSubmitNewPost(this) dataType: 'json'
$local(".discussion-cancel-post").click -> data:
handleCancelNewPost(this) title: title
body: body
$local(".new-post-form").show() tags: tags
anonymous: anonymous
handleAjaxReloadDiscussion = (elem, url) -> auto_subscribe: autowatch
if not url then return error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
$elem = $(elem) success: (response, textStatus) =>
$discussion = $elem.parents("section.discussion") DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
Discussion.safeAjax $thread = $(response.html)
$elem: $elem @$el.children(".threads").prepend($thread)
url: url
type: "GET" @$(".new-post-title").val("")
dataType: 'html' DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), "new-post-body", ""
success: (data, textStatus) -> @$(".new-post-tags").val("")
$data = $(data) @$(".new-post-tags").importTags("")
$parent = $discussion.parent()
$discussion.replaceWith($data) thread = @model.addThread response.content
$discussion = $parent.children(".discussion") threadView = new ThreadView el: $thread[0], model: thread
Discussion.initializeDiscussion($discussion) thread.updateInfo response.annotated_content_info
Discussion.bindDiscussionEvents($discussion) @cancelNewPost()
handleAjaxSearch = (elem) ->
$elem = $(elem) cancelNewPost: (event) ->
url = URI($elem.attr("action")).addSearch({text: $local(".search-input").val()}) if @$el.hasClass("inline-discussion")
handleAjaxReloadDiscussion($elem, url) @$(".new-post-form").addClass("collapsed")
else if @$el.hasClass("forum-discussion")
handleAjaxSort = (elem) -> @$(".new-post-form").hide()
$elem = $(elem)
url = $elem.attr("sort-url") search: (event) ->
handleAjaxReloadDiscussion($elem, url) event.preventDefault()
$elem = $(event.target)
handleAjaxPage = (elem) -> url = URI($elem.attr("action")).addSearch({text: @$(".search-input").val()})
$elem = $(elem) @reload($elem, url)
url = $elem.attr("page-url")
handleAjaxReloadDiscussion($elem, url) sort: ->
$elem = $(event.target)
if $discussion.hasClass("inline-discussion") url = $elem.attr("sort-url")
initializeNewPost() @reload($elem, url)
if $discussion.hasClass("forum-discussion") page: (event) ->
$discussionSidebar = $(".discussion-sidebar") $elem = $(event.target)
if $discussionSidebar.length url = $elem.attr("page-url")
$sidebarLocal = Discussion.generateLocal($discussionSidebar) @reload($elem, url)
Discussion.bindLocalEvents $sidebarLocal,
"click .sidebar-new-post-button": (event) -> events:
initializeNewPost() "submit .search-wrapper>.discussion-search-form": "search"
"click .discussion-search-link": "search"
Discussion.bindLocalEvents $local, "click .discussion-sort-link": "sort"
"click .discussion-page-link": "page"
"submit .search-wrapper>.discussion-search-form": (event) ->
event.preventDefault()
handleAjaxSearch(this)
"click .discussion-search-link": ->
handleAjaxSearch($local(".search-wrapper>.discussion-search-form"))
"click .discussion-sort-link": ->
handleAjaxSort(this)
$discussion.children(".discussion-paginator").find(".discussion-page-link").unbind('click').click ->
handleAjaxPage(this)
if not @Discussion? class @DiscussionModuleView extends Backbone.View
@Discussion = {} events:
"click .discussion-show": "toggleDiscussion"
Discussion = @Discussion toggleDiscussion: (event) ->
if @showed
@Discussion = $.extend @Discussion, @$("section.discussion").hide()
initializeDiscussionModule: (elem) -> $(event.target).html("Show Discussion")
$discussionModule = $(elem) @showed = false
$local = Discussion.generateLocal($discussionModule) else
handleShowDiscussion = (elem) -> if @retrieved
$elem = $(elem) @$("section.discussion").show()
if not $local("section.discussion").length $(event.target).html("Hide Discussion")
@showed = true
else
$elem = $(event.target)
discussion_id = $elem.attr("discussion_id") discussion_id = $elem.attr("discussion_id")
url = Discussion.urlFor 'retrieve_discussion', discussion_id url = DiscussionUtil.urlFor 'retrieve_discussion', discussion_id
Discussion.safeAjax Discussion.safeAjax
$elem: $elem $elem: $elem
url: url url: url
type: "GET" type: "GET"
success: (data, textStatus, xhr) -> dataType: 'json'
$discussionModule.append(data) success: (response, textStatus) =>
discussion = $local("section.discussion") @$el.append(response.html)
Discussion.initializeDiscussion(discussion) $discussion = @$el.find("section.discussion")
Discussion.bindDiscussionEvents(discussion) $(event.target).html("Hide Discussion")
$elem.html("Hide Discussion") discussion = new Discussion()
$elem.unbind('click').click -> discussion.reset(response.discussionData, {silent: false})
handleHideDiscussion(this) view = new DiscussionView(el: $discussion[0], model: discussion)
dataType: 'html' DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
else @retrieved = true
$local("section.discussion").show() @showed = true
$elem.html("Hide Discussion")
$elem.unbind('click').click ->
handleHideDiscussion(this)
handleHideDiscussion = (elem) ->
$local("section.discussion").hide()
$elem = $(elem)
$elem.html("Show Discussion")
$elem.unbind('click').click ->
handleShowDiscussion(this)
$local(".discussion-show").click ->
handleShowDiscussion(this)
$ -> $ ->
toggle = -> window.$$contents = {}
$('.course-wrapper').toggleClass('closed') window.$$discussions = {}
Discussion = window.Discussion
if $('#accordion').length
active = $('#accordion ul:has(li.active)').index('#accordion ul')
$('#accordion').bind('accordionchange', @log).accordion
active: if active >= 0 then active else 1
header: 'h3'
autoHeight: false
$('#open_close_accordion a').click toggle
$('#accordion').show()
$(".discussion-module").each (index, elem) -> $(".discussion-module").each (index, elem) ->
Discussion.initializeDiscussionModule(elem) view = new DiscussionModuleView(el: elem)
$("section.discussion").each (index, discussion) -> $("section.discussion").each (index, elem) ->
Discussion.initializeDiscussion(discussion) discussionData = DiscussionUtil.getDiscussionData($(elem).attr("_id"))
Discussion.bindDiscussionEvents(discussion) discussion = new Discussion()
discussion.reset(discussionData, {silent: false})
view = new DiscussionView(el: elem, model: discussion)
Discussion.initializeUserProfile($(".discussion-sidebar>.user-profile")) DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
@Discussion = $.extend @Discussion,
newPostTemplate: """
<form class="new-post-form collapsed" id="new-post-form" style="display: block; ">
<ul class="new-post-form-errors discussion-errors"></ul>
<input type="text" class="new-post-title title-input" placeholder="Title" />
<div class="new-post-similar-posts-wrapper" style="display: none">
Similar Posts:
<a class="hide-similar-posts" href="javascript:void(0)">Hide</a>
<div class="new-post-similar-posts"></div>
</div>
<div class="new-post-body reply-body"></div>
<input class="new-post-tags" placeholder="Tags" />
<div class="post-options">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<label for="discussion-post-anonymously-${discussion_id}">post anonymously</label>
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-${discussion_id}" checked="">
<label for="discussion-auto-watch-${discussion_id}">follow this thread</label>
</div>
<div class="new-post-control post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
"""
replyTemplate: """
<form class="discussion-reply-new">
<ul class="discussion-errors"></ul>
<div class="reply-body"></div>
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-{{id}}" />
<label for="discussion-post-anonymously-{{id}}">post anonymously</label>
{{#showWatchCheckbox}}
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-{{id}}" checked />
<label for="discussion-auto-watch-{{id}}">follow this thread</label>
{{/showWatchCheckbox}}
<br />
<div class = "reply-post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
"""
editThreadTemplate: """
<form class="discussion-content-edit discussion-thread-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<input type="text" class="thread-title-edit title-input" placeholder="Title" value="{{title}}"/>
<div class="thread-body-edit body-input">{{body}}</div>
<input class="thread-tags-edit" placeholder="Tags" value="{{tags}}" />
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
"""
editCommentTemplate: """
<form class="discussion-content-edit discussion-comment-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<div class="comment-body-edit body-input">{{body}}</div>
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
"""
if not @Discussion? class @DiscussionUtil
@Discussion = {}
Discussion = @Discussion @wmdEditors: {}
wmdEditors = {} @getTemplate: (id) ->
$("script##{id}").html()
@Discussion = $.extend @Discussion, @getDiscussionData: (id) ->
return $$discussion_data[id]
generateLocal: (elem) -> @addContent: (id, content) -> window.$$contents[id] = content
(selector) -> $(elem).find(selector)
generateDiscussionLink: (cls, txt, handler) -> @getContent: (id) -> window.$$contents[id]
@addDiscussion: (id, discussion) -> window.$$discussions[id] = discussion
@getDiscussion: (id) -> window.$$discussions[id]
@bulkUpdateContentInfo: (infos) ->
for id, info of infos
@getContent(id).updateInfo(info)
@generateDiscussionLink: (cls, txt, handler) ->
$("<a>").addClass("discussion-link") $("<a>").addClass("discussion-link")
.attr("href", "javascript:void(0)") .attr("href", "javascript:void(0)")
.addClass(cls).html(txt) .addClass(cls).html(txt)
.click -> handler(this) .click -> handler(this)
urlFor: (name, param, param1, param2) -> @urlFor: (name, param, param1, param2) ->
{ {
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow" follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow" unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow"
...@@ -48,7 +58,7 @@ wmdEditors = {} ...@@ -48,7 +58,7 @@ wmdEditors = {}
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}" permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
}[name] }[name]
safeAjax: (params) -> @safeAjax: (params) ->
$elem = params.$elem $elem = params.$elem
if $elem.attr("disabled") if $elem.attr("disabled")
return return
...@@ -56,17 +66,31 @@ wmdEditors = {} ...@@ -56,17 +66,31 @@ wmdEditors = {}
$.ajax(params).always -> $.ajax(params).always ->
$elem.removeAttr("disabled") $elem.removeAttr("disabled")
handleAnchorAndReload: (response) -> @get: ($elem, url, data, success) ->
#window.location = window.location.pathname + "#" + response['id'] @safeAjax
window.location.reload() $elem: $elem
url: url
bindLocalEvents: ($local, eventsHandler) -> type: "GET"
dataType: "json"
data: data
success: success
@post: ($elem, url, data, success) ->
@safeAjax
$elem: $elem
url: url
type: "POST"
dataType: "json"
data: data
success: success
@bindLocalEvents: ($local, eventsHandler) ->
for eventSelector, handler of eventsHandler for eventSelector, handler of eventsHandler
[event, selector] = eventSelector.split(' ') [event, selector] = eventSelector.split(' ')
$local(selector).unbind(event)[event] handler $local(selector).unbind(event)[event] handler
tagsInputOptions: -> @tagsInputOptions: ->
autocomplete_url: Discussion.urlFor('tags_autocomplete') autocomplete_url: @urlFor('tags_autocomplete')
autocomplete: autocomplete:
remoteDataType: 'json' remoteDataType: 'json'
interactive: true interactive: true
...@@ -75,23 +99,7 @@ wmdEditors = {} ...@@ -75,23 +99,7 @@ wmdEditors = {}
defaultText: "Tag your post: press enter after each tag" defaultText: "Tag your post: press enter after each tag"
removeWithBackspace: true removeWithBackspace: true
isSubscribed: (id, type) -> @formErrorHandler: (errorsField) ->
$$user_info? and (
if type == "thread"
id in $$user_info.subscribed_thread_ids
else if type == "commentable" or type == "discussion"
id in $$user_info.subscribed_commentable_ids
else
id in $$user_info.subscribed_user_ids
)
isUpvoted: (id) ->
$$user_info? and (id in $$user_info.upvoted_ids)
isDownvoted: (id) ->
$$user_info? and (id in $$user_info.downvoted_ids)
formErrorHandler: (errorsField) ->
(xhr, textStatus, error) -> (xhr, textStatus, error) ->
response = JSON.parse(xhr.responseText) response = JSON.parse(xhr.responseText)
if response.errors? and response.errors.length > 0 if response.errors? and response.errors.length > 0
...@@ -99,13 +107,13 @@ wmdEditors = {} ...@@ -99,13 +107,13 @@ wmdEditors = {}
for error in response.errors for error in response.errors
errorsField.append($("<li>").addClass("new-post-form-error").html(error)) errorsField.append($("<li>").addClass("new-post-form-error").html(error))
clearFormErrors: (errorsField) -> @clearFormErrors: (errorsField) ->
errorsField.empty() errorsField.empty()
postMathJaxProcessor: (text) -> @postMathJaxProcessor: (text) ->
RE_INLINEMATH = /^\$([^\$]*)\$/g RE_INLINEMATH = /^\$([^\$]*)\$/g
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
Discussion.processEachMathAndCode text, (s, type) -> @processEachMathAndCode text, (s, type) ->
if type == 'display' if type == 'display'
s.replace RE_DISPLAYMATH, ($0, $1) -> s.replace RE_DISPLAYMATH, ($0, $1) ->
"\\[" + $1 + "\\]" "\\[" + $1 + "\\]"
...@@ -115,61 +123,43 @@ wmdEditors = {} ...@@ -115,61 +123,43 @@ wmdEditors = {}
else else
s s
makeWmdEditor: ($content, $local, cls_identifier) -> @makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}") elem = $local(".#{cls_identifier}")
id = $content.attr("_id") id = $content.attr("_id")
appended_id = "-#{cls_identifier}-#{id}" appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = Discussion.urlFor('upload') imageUploadUrl = @urlFor('upload')
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, Discussion.postMathJaxProcessor _processor = (_this) ->
wmdEditors["#{cls_identifier}-#{id}"] = editor (text) -> _this.postMathJaxProcessor(text)
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
@wmdEditors["#{cls_identifier}-#{id}"] = editor
editor editor
getWmdEditor: ($content, $local, cls_identifier) -> @getWmdEditor: ($content, $local, cls_identifier) ->
id = $content.attr("_id") id = $content.attr("_id")
wmdEditors["#{cls_identifier}-#{id}"] @wmdEditors["#{cls_identifier}-#{id}"]
getWmdInput: ($content, $local, cls_identifier) -> @getWmdInput: ($content, $local, cls_identifier) ->
id = $content.attr("_id") id = $content.attr("_id")
$local("#wmd-input-#{cls_identifier}-#{id}") $local("#wmd-input-#{cls_identifier}-#{id}")
getWmdContent: ($content, $local, cls_identifier) -> @getWmdContent: ($content, $local, cls_identifier) ->
Discussion.getWmdInput($content, $local, cls_identifier).val() @getWmdInput($content, $local, cls_identifier).val()
setWmdContent: ($content, $local, cls_identifier, text) -> @setWmdContent: ($content, $local, cls_identifier, text) ->
Discussion.getWmdInput($content, $local, cls_identifier).val(text) @getWmdInput($content, $local, cls_identifier).val(text)
Discussion.getWmdEditor($content, $local, cls_identifier).refreshPreview() @getWmdEditor($content, $local, cls_identifier).refreshPreview()
getContentInfo: (id, attr) -> @subscriptionLink: (type, id) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
(window.$$annotated_content_info[id] || {})[attr]
setContentInfo: (id, attr, value) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
window.$$annotated_content_info[id] ||= {}
window.$$annotated_content_info[id][attr] = value
extendContentInfo: (id, newInfo) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
window.$$annotated_content_info[id] = newInfo
bulkExtendContentInfo: (newInfos) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
window.$$annotated_content_info = $.extend window.$$annotated_content_info, newInfos
subscriptionLink: (type, id) ->
followLink = -> followLink = ->
Discussion.generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow) @generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow)
unfollowLink = -> unfollowLink = ->
Discussion.generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow) @generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow)
handleFollow = (elem) -> handleFollow = (elem) ->
Discussion.safeAjax @safeAjax
$elem: $(elem) $elem: $(elem)
url: Discussion.urlFor("follow_#{type}", id) url: @urlFor("follow_#{type}", id)
type: "POST" type: "POST"
success: (response, textStatus) -> success: (response, textStatus) ->
if textStatus == "success" if textStatus == "success"
...@@ -177,21 +167,21 @@ wmdEditors = {} ...@@ -177,21 +167,21 @@ wmdEditors = {}
dataType: 'json' dataType: 'json'
handleUnfollow = (elem) -> handleUnfollow = (elem) ->
Discussion.safeAjax @safeAjax
$elem: $(elem) $elem: $(elem)
url: Discussion.urlFor("unfollow_#{type}", id) url: @urlFor("unfollow_#{type}", id)
type: "POST" type: "POST"
success: (response, textStatus) -> success: (response, textStatus) ->
if textStatus == "success" if textStatus == "success"
$(elem).replaceWith followLink() $(elem).replaceWith followLink()
dataType: 'json' dataType: 'json'
if Discussion.isSubscribed(id, type) if @isSubscribed(id, type)
unfollowLink() unfollowLink()
else else
followLink() followLink()
processEachMathAndCode: (text, processor) -> @processEachMathAndCode: (text, processor) ->
codeArchive = [] codeArchive = []
...@@ -242,3 +232,18 @@ wmdEditors = {} ...@@ -242,3 +232,18 @@ wmdEditors = {}
text = $div.html() text = $div.html()
text text
@unescapeHighlightTag: (text) ->
text.replace(/\&lt\;highlight\&gt\;/g, "<span class='search-highlight'>")
.replace(/\&lt\;\/highlight\&gt\;/g, "</span>")
@stripHighlight: (text) ->
text.replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "")
.replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, "")
@stripLatexHighlight: (text) ->
@processEachMathAndCode text, @stripHighlight
@markdownWithHighlight: (text) ->
converter = Markdown.getMathCompatibleConverter()
@unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
initializeVote = (content) ->
$content = $(content)
$local = Discussion.generateLocal($content.children(".discussion-content"))
id = $content.attr("_id")
if Discussion.isUpvoted id
$local(".discussion-vote-up").addClass("voted")
else if Discussion.isDownvoted id
$local(".discussion-vote-down").addClass("voted")
initializeFollowThread = (thread) ->
$thread = $(thread)
id = $thread.attr("_id")
$thread.children(".discussion-content")
.find(".follow-wrapper")
.append(Discussion.subscriptionLink('thread', id))
@Discussion = $.extend @Discussion,
bindContentEvents: (content) ->
$content = $(content)
$discussionContent = $content.children(".discussion-content")
$local = Discussion.generateLocal($discussionContent)
id = $content.attr("_id")
handleReply = (elem) ->
$replyView = $local(".discussion-reply-new")
if $replyView.length
$replyView.show()
else
thread_id = $discussionContent.parents(".thread").attr("_id")
view =
id: id
showWatchCheckbox: not Discussion.isSubscribed(thread_id, "thread")
$discussionContent.append Mustache.render Discussion.replyTemplate, view
Discussion.makeWmdEditor $content, $local, "reply-body"
$local(".discussion-submit-post").click -> handleSubmitReply(this)
$local(".discussion-cancel-post").click -> handleCancelReply(this)
$local(".discussion-reply").hide()
$local(".discussion-edit").hide()
handleCancelReply = (elem) ->
$replyView = $local(".discussion-reply-new")
if $replyView.length
$replyView.hide()
$local(".discussion-reply").show()
$local(".discussion-edit").show()
handleSubmitReply = (elem) ->
if $content.hasClass("thread")
url = Discussion.urlFor('create_comment', id)
else if $content.hasClass("comment")
url = Discussion.urlFor('create_sub_comment', id)
else
return
body = Discussion.getWmdContent $content, $local, "reply-body"
anonymous = false || $local(".discussion-post-anonymously").is(":checked")
autowatch = false || $local(".discussion-auto-watch").is(":checked")
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data:
body: body
anonymous: anonymous
autowatch: autowatch
error: Discussion.formErrorHandler($local(".discussion-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".discussion-errors"))
$comment = $(response.html)
$content.children(".comments").prepend($comment)
Discussion.setWmdContent $content, $local, "reply-body", ""
Discussion.setContentInfo response.content['id'], 'can_reply', true
Discussion.setContentInfo response.content['id'], 'editable', true
Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
Discussion.initializeContent($comment)
Discussion.bindContentEvents($comment)
@cancelReply()
$local(".discussion-reply-new").hide()
$local(".discussion-reply").show()
$local(".discussion-edit").show()
$discussionContent.attr("status", "normal")
handleVote = (elem, value) ->
contentType = if $content.hasClass("thread") then "thread" else "comment"
url = Discussion.urlFor("#{value}vote_#{contentType}", id)
Discussion.safeAjax
$elem: $local(".discussion-vote")
url: url
type: "POST"
dataType: "json"
success: (response, textStatus) ->
if textStatus == "success"
$local(".discussion-vote").removeClass("voted")
$local(".discussion-vote-#{value}").addClass("voted")
$local(".discussion-votes-point").html response.votes.point
handleUnvote = (elem, value) ->
contentType = if $content.hasClass("thread") then "thread" else "comment"
url = Discussion.urlFor("undo_vote_for_#{contentType}", id)
Discussion.safeAjax
$elem: $local(".discussion-vote")
url: url
type: "POST"
dataType: "json"
success: (response, textStatus) ->
if textStatus == "success"
$local(".discussion-vote").removeClass("voted")
$local(".discussion-votes-point").html response.votes.point
handleCancelEdit = (elem) ->
$local(".discussion-content-edit").hide()
$local(".discussion-content-wrapper").show()
handleEditThread = (elem) ->
$local(".discussion-content-wrapper").hide()
$editView = $local(".discussion-content-edit")
if $editView.length
$editView.show()
else
view = {
id: id
title: $local(".thread-raw-title").html()
body: $local(".thread-raw-body").html()
tags: $local(".thread-raw-tags").html()
}
$discussionContent.append Mustache.render Discussion.editThreadTemplate, view
Discussion.makeWmdEditor $content, $local, "thread-body-edit"
$local(".thread-tags-edit").tagsInput Discussion.tagsInputOptions()
$local(".discussion-submit-update").unbind("click").click -> handleSubmitEditThread(this)
$local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this)
handleSubmitEditThread = (elem) ->
url = Discussion.urlFor('update_thread', id)
title = $local(".thread-title-edit").val()
body = Discussion.getWmdContent $content, $local, "thread-body-edit"
tags = $local(".thread-tags-edit").val()
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data: {title: title, body: body, tags: tags},
error: Discussion.formErrorHandler($local(".discussion-update-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".discussion-update-errors"))
$discussionContent.replaceWith(response.html)
Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
Discussion.initializeContent($content)
Discussion.bindContentEvents($content)
handleEditComment = (elem) ->
$local(".discussion-content-wrapper").hide()
$editView = $local(".discussion-content-edit")
if $editView.length
$editView.show()
else
view = { id: id, body: $local(".comment-raw-body").html() }
$discussionContent.append Mustache.render Discussion.editCommentTemplate, view
Discussion.makeWmdEditor $content, $local, "comment-body-edit"
$local(".discussion-submit-update").unbind("click").click -> handleSubmitEditComment(this)
$local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this)
handleSubmitEditComment= (elem) ->
url = Discussion.urlFor('update_comment', id)
body = Discussion.getWmdContent $content, $local, "comment-body-edit"
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {body: body}
error: Discussion.formErrorHandler($local(".discussion-update-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".discussion-update-errors"))
$discussionContent.replaceWith(response.html)
Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
Discussion.initializeContent($content)
Discussion.bindContentEvents($content)
handleEndorse = (elem, endorsed) ->
url = Discussion.urlFor('endorse_comment', id)
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {endorsed: endorsed}
success: (response, textStatus) ->
if textStatus == "success"
if endorsed
$(content).addClass("endorsed")
else
$(content).removeClass("endorsed")
$(elem).unbind('click').click ->
handleEndorse(elem, !endorsed)
handleOpenClose = (elem, text) ->
url = Discussion.urlFor('openclose_thread', id)
closed = undefined
if text.match(/Close/)
closed = true
else if text.match(/[Oo]pen/)
closed = false
else
console.log "Unexpected text " + text + "for open/close thread."
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {closed: closed}
success: (response, textStatus) =>
if textStatus == "success"
if closed
$(content).addClass("closed")
$(elem).text "Re-open Thread"
else
$(content).removeClass("closed")
$(elem).text "Close Thread"
error: (response, textStatus, e) ->
console.log e
handleDelete = (elem) ->
if $content.hasClass("thread")
url = Discussion.urlFor('delete_thread', id)
c = confirm "Are you sure to delete thread \"" + $content.find("a.thread-title").text() + "\"?"
else
url = Discussion.urlFor('delete_comment', id)
c = confirm "Are you sure to delete this comment? "
if c != true
return
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: "json"
data: {}
success: (response, textStatus) =>
if textStatus == "success"
$(content).remove()
error: (response, textStatus, e) ->
console.log e
handleHideSingleThread = (elem) ->
$threadTitle = $local(".thread-title")
$hideComments = $local(".discussion-hide-comments")
$hideComments.removeClass("discussion-hide-comments")
.addClass("discussion-show-comments")
$content.children(".comments").hide()
$threadTitle.unbind('click').click handleShowSingleThread
$hideComments.unbind('click').click handleShowSingleThread
prevHtml = $hideComments.html()
$hideComments.html prevHtml.replace "Hide", "Show"
handleShowSingleThread = ->
$threadTitle = $local(".thread-title")
$showComments = $local(".discussion-show-comments")
if not $showComments.hasClass("first-time") and (not $showComments.length or not $threadTitle.length)
return
rebindHideEvents = ->
$threadTitle.unbind('click').click handleHideSingleThread
$showComments.unbind('click').click handleHideSingleThread
$showComments.removeClass("discussion-show-comments")
.addClass("discussion-hide-comments")
prevHtml = $showComments.html()
$showComments.html prevHtml.replace "Show", "Hide"
if not $showComments.hasClass("first-time") and $content.children(".comments").length
$content.children(".comments").show()
rebindHideEvents()
else
discussion_id = $threadTitle.parents(".discussion").attr("_id")
url = Discussion.urlFor('retrieve_single_thread', discussion_id, id)
Discussion.safeAjax
$elem: $.merge($threadTitle, $showComments)
url: url
type: "GET"
dataType: 'json'
success: (response, textStatus) ->
Discussion.bulkExtendContentInfo response['annotated_content_info']
$content.append(response['html'])
$content.find(".comment").each (index, comment) ->
Discussion.initializeContent(comment)
Discussion.bindContentEvents(comment)
$showComments.removeClass("first-time")
rebindHideEvents()
Discussion.bindLocalEvents $local,
"click .thread-title": ->
handleShowSingleThread(this)
"click .discussion-show-comments": ->
handleShowSingleThread(this)
"click .discussion-hide-comments": ->
handleHideSingleThread(this)
"click .discussion-reply-thread": ->
handleShowSingleThread($local(".thread-title"))
handleReply(this)
"click .discussion-reply-comment": ->
handleReply(this)
"click .discussion-cancel-reply": ->
handleCancelReply(this)
"click .discussion-vote-up": ->
$elem = $(this)
if $elem.hasClass("voted")
handleUnvote($elem)
else
handleVote($elem, "up")
"click .discussion-vote-down": ->
$elem = $(this)
if $elem.hasClass("voted")
handleUnvote($elem)
else
handleVote($elem, "down")
"click .admin-endorse": ->
handleEndorse(this, not $content.hasClass("endorsed"))
"click .admin-openclose": ->
handleOpenClose(this, $(this).text())
"click .admin-edit": ->
if $content.hasClass("thread")
handleEditThread(this)
else
handleEditComment(this)
"click .admin-delete": ->
handleDelete(this)
initializeContent: (content) ->
unescapeHighlightTag = (text) ->
text.replace(/\&lt\;highlight\&gt\;/g, "<span class='search-highlight'>")
.replace(/\&lt\;\/highlight\&gt\;/g, "</span>")
stripHighlight = (text, type) ->
text.replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "")
.replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, "")
stripLatexHighlight = (text) ->
Discussion.processEachMathAndCode text, stripHighlight
markdownWithHighlight = (text) ->
converter = Markdown.getMathCompatibleConverter()
unescapeHighlightTag stripLatexHighlight converter.makeHtml text
$content = $(content)
initializeVote $content
if $content.hasClass("thread")
initializeFollowThread $content
$local = Discussion.generateLocal($content.children(".discussion-content"))
$local("span.timeago").timeago()
$contentTitle = $local(".thread-title")
if $contentTitle.length
$contentTitle.html unescapeHighlightTag stripLatexHighlight $contentTitle.html()
$contentBody = $local(".content-body")
$contentBody.html Discussion.postMathJaxProcessor markdownWithHighlight $contentBody.html()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, $contentBody.attr("id")]
id = $content.attr("_id")
if $content.hasClass("thread")
discussion_id = $content.attr("_discussion_id")
permalink = Discussion.urlFor("permanent_link_thread", discussion_id, id)
else
thread_id = $content.parents(".thread").attr("_id")
discussion_id = $content.parents(".thread").attr("_discussion_id")
permalink = Discussion.urlFor("permanent_link_comment", discussion_id, thread_id, id)
$local(".discussion-permanent-link").attr "href", permalink
if not Discussion.getContentInfo id, 'editable'
$local(".admin-edit").remove()
if not Discussion.getContentInfo id, 'can_reply'
$local(".discussion-reply").remove()
if not Discussion.getContentInfo id, 'can_endorse'
$local(".admin-endorse").remove()
if not Discussion.getContentInfo id, 'can_delete'
$local(".admin-delete").remove()
if not Discussion.getContentInfo id, 'can_openclose'
$local(".admin-openclose").remove()
#if not Discussion.getContentInfo id, 'can_vote'
# $local(".discussion-vote").css "visibility", "hidden"
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
initializeFollowDiscussion = (discussion) ->
$discussion = $(discussion)
id = $following.attr("_id")
$local = Discussion.generateLocal()
$discussion.children(".discussion-non-content")
.find(".discussion-title-wrapper")
.append(Discussion.subscriptionLink('discussion', id))
@Discussion = $.extend @Discussion,
initializeDiscussion: (discussion) ->
$discussion = $(discussion)
$discussion.find(".thread").each (index, thread) ->
Discussion.initializeContent(thread)
Discussion.bindContentEvents(thread)
$discussion.find(".comment").each (index, comment) ->
Discussion.initializeContent(comment)
Discussion.bindContentEvents(comment)
#initializeFollowDiscussion(discussion) TODO move this somewhere else
bindDiscussionEvents: (discussion) ->
$discussion = $(discussion)
$discussionNonContent = $discussion.children(".discussion-non-content")
$local = Discussion.generateLocal($discussion.children(".discussion-local"))
id = $discussion.attr("_id")
handleSubmitNewPost = (elem) ->
title = $local(".new-post-title").val()
body = Discussion.getWmdContent $discussion, $local, "new-post-body"
tags = $local(".new-post-tags").val()
url = Discussion.urlFor('create_thread', id)
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data:
title: title
body: body
tags: tags
error: Discussion.formErrorHandler($local(".new-post-form-errors"))
success: (response, textStatus) ->
Discussion.clearFormErrors($local(".new-post-form-errors"))
$thread = $(response.html)
$discussion.children(".threads").prepend($thread)
$local(".new-post-title").val("")
Discussion.setWmdContent $discussion, $local, "new-post-body", ""
$local(".new-post-tags").val("")
if $discussion.hasClass("inline-discussion")
$local(".new-post-form").addClass("collapsed")
else if $discussion.hasClass("forum-discussion")
$local(".new-post-form").hide()
handleCancelNewPost = (elem) ->
if $discussion.hasClass("inline-discussion")
$local(".new-post-form").addClass("collapsed")
else if $discussion.hasClass("forum-discussion")
$local(".new-post-form").hide()
handleSimilarPost = (elem) ->
$title = $local(".new-post-title")
$wrapper = $local(".new-post-similar-posts-wrapper")
$similarPosts = $local(".new-post-similar-posts")
prevText = $title.attr("prev-text")
text = $title.val()
if text == prevText
if $local(".similar-post").length
$wrapper.show()
else if $.trim(text).length
Discussion.safeAjax
$elem: $(elem)
url: Discussion.urlFor 'search_similar_threads', id
type: "GET"
dateType: 'json'
data:
text: $local(".new-post-title").val()
success: (response, textStatus) ->
$similarPosts.empty()
console.log response
if $.type(response) == "array" and response.length
$wrapper.show()
for thread in response
#singleThreadUrl = Discussion.urlFor 'retrieve_single_thread
$similarPost = $("<a>").addClass("similar-post")
.html(thread["title"])
.attr("href", "javascript:void(0)") #TODO
.appendTo($similarPosts)
else
$wrapper.hide()
else
$wrapper.hide()
$title.attr("prev-text", text)
initializeNewPost = ->
view = { discussion_id: id }
$discussionNonContent = $discussion.children(".discussion-non-content")
if not $local(".wmd-panel").length
$discussionNonContent.append Mustache.render Discussion.newPostTemplate, view
$newPostBody = $local(".new-post-body")
Discussion.makeWmdEditor $discussion, $local, "new-post-body"
$input = Discussion.getWmdInput($discussion, $local, "new-post-body")
$input.attr("placeholder", "post a new topic...")
if $discussion.hasClass("inline-discussion")
$input.bind 'focus', (e) ->
$local(".new-post-form").removeClass('collapsed')
else if $discussion.hasClass("forum-discussion")
$local(".new-post-form").removeClass('collapsed')
$local(".new-post-tags").tagsInput Discussion.tagsInputOptions()
$local(".new-post-title").blur ->
handleSimilarPost(this)
$local(".hide-similar-posts").click ->
$local(".new-post-similar-posts-wrapper").hide()
$local(".discussion-submit-post").click ->
handleSubmitNewPost(this)
$local(".discussion-cancel-post").click ->
handleCancelNewPost(this)
$local(".new-post-form").show()
handleAjaxReloadDiscussion = (elem, url) ->
if not url then return
$elem = $(elem)
$discussion = $elem.parents("section.discussion")
Discussion.safeAjax
$elem: $elem
url: url
type: "GET"
dataType: 'html'
success: (data, textStatus) ->
$data = $(data)
$parent = $discussion.parent()
$discussion.replaceWith($data)
$discussion = $parent.children(".discussion")
Discussion.initializeDiscussion($discussion)
Discussion.bindDiscussionEvents($discussion)
handleAjaxSearch = (elem) ->
$elem = $(elem)
url = URI($elem.attr("action")).addSearch({text: $local(".search-input").val()})
handleAjaxReloadDiscussion($elem, url)
handleAjaxSort = (elem) ->
$elem = $(elem)
url = $elem.attr("sort-url")
handleAjaxReloadDiscussion($elem, url)
handleAjaxPage = (elem) ->
$elem = $(elem)
url = $elem.attr("page-url")
handleAjaxReloadDiscussion($elem, url)
if $discussion.hasClass("inline-discussion")
initializeNewPost()
if $discussion.hasClass("forum-discussion")
$discussionSidebar = $(".discussion-sidebar")
if $discussionSidebar.length
$sidebarLocal = Discussion.generateLocal($discussionSidebar)
Discussion.bindLocalEvents $sidebarLocal,
"click .sidebar-new-post-button": (event) ->
initializeNewPost()
Discussion.bindLocalEvents $local,
"submit .search-wrapper>.discussion-search-form": (event) ->
event.preventDefault()
handleAjaxSearch(this)
"click .discussion-search-link": ->
handleAjaxSearch($local(".search-wrapper>.discussion-search-form"))
"click .discussion-sort-link": ->
handleAjaxSort(this)
$discussion.children(".discussion-paginator").find(".discussion-page-link").unbind('click').click ->
handleAjaxPage(this)
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
@Discussion = $.extend @Discussion,
initializeDiscussionModule: (elem) ->
$discussionModule = $(elem)
$local = Discussion.generateLocal($discussionModule)
handleShowDiscussion = (elem) ->
$elem = $(elem)
if not $local("section.discussion").length
discussion_id = $elem.attr("discussion_id")
url = Discussion.urlFor 'retrieve_discussion', discussion_id
Discussion.safeAjax
$elem: $elem
url: url
type: "GET"
success: (data, textStatus, xhr) ->
$discussionModule.append(data)
discussion = $local("section.discussion")
Discussion.initializeDiscussion(discussion)
Discussion.bindDiscussionEvents(discussion)
$elem.html("Hide Discussion")
$elem.unbind('click').click ->
handleHideDiscussion(this)
dataType: 'html'
else
$local("section.discussion").show()
$elem.html("Hide Discussion")
$elem.unbind('click').click ->
handleHideDiscussion(this)
handleHideDiscussion = (elem) ->
$local("section.discussion").hide()
$elem = $(elem)
$elem.html("Show Discussion")
$elem.unbind('click').click ->
handleShowDiscussion(this)
$local(".discussion-show").click ->
handleShowDiscussion(this)
$ ->
#toggle = ->
# $('.course-wrapper').toggleClass('closed')
#Discussion = window.Discussion
#if $('#accordion').length
# active = $('#accordion ul:has(li.active)').index('#accordion ul')
# $('#accordion').bind('accordionchange', @log).accordion
# active: if active >= 0 then active else 1
# header: 'h3'
# autoHeight: false
# $('#open_close_accordion a').click toggle
# $('#accordion').show()
#$(".discussion-module").each (index, elem) ->
# Discussion.initializeDiscussionModule(elem)
#$("section.discussion").each (index, discussion) ->
# Discussion.initializeDiscussion(discussion)
# Discussion.bindDiscussionEvents(discussion)
#Discussion.initializeUserProfile($(".discussion-sidebar>.user-profile"))
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
@Discussion = $.extend @Discussion,
newPostTemplate: """
<form class="new-post-form collapsed" id="new-post-form" style="display: block; ">
<ul class="new-post-form-errors discussion-errors"></ul>
<input type="text" class="new-post-title title-input" placeholder="Title" />
<div class="new-post-similar-posts-wrapper" style="display: none">
Similar Posts:
<a class="hide-similar-posts" href="javascript:void(0)">Hide</a>
<div class="new-post-similar-posts"></div>
</div>
<div class="new-post-body reply-body"></div>
<input class="new-post-tags" placeholder="Tags" />
<div class="post-options">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<label for="discussion-post-anonymously-${discussion_id}">post anonymously</label>
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-${discussion_id}" checked="">
<label for="discussion-auto-watch-${discussion_id}">follow this thread</label>
</div>
<div class="new-post-control post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
"""
replyTemplate: """
<form class="discussion-reply-new">
<ul class="discussion-errors"></ul>
<div class="reply-body"></div>
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-{{id}}" />
<label for="discussion-post-anonymously-{{id}}">post anonymously</label>
{{#showWatchCheckbox}}
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-{{id}}" checked />
<label for="discussion-auto-watch-{{id}}">follow this thread</label>
{{/showWatchCheckbox}}
<br />
<div class = "reply-post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
"""
editThreadTemplate: """
<form class="discussion-content-edit discussion-thread-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<input type="text" class="thread-title-edit title-input" placeholder="Title" value="{{title}}"/>
<div class="thread-body-edit body-input">{{body}}</div>
<input class="thread-tags-edit" placeholder="Tags" value="{{tags}}" />
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
"""
editCommentTemplate: """
<form class="discussion-content-edit discussion-comment-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<div class="comment-body-edit body-input">{{body}}</div>
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
"""
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
@Discussion = $.extend @Discussion,
initializeUserProfile: ($userProfile) ->
$local = Discussion.generateLocal $userProfile
handleUpdateModeratorStatus = (elem, isModerator) ->
confirmValue = confirm("Are you sure?")
if not confirmValue then return
url = Discussion.urlFor('update_moderator_status', $$profiled_user_id)
Discussion.safeAjax
$elem: $(elem)
url: url
type: "POST"
dataType: 'json'
data:
is_moderator: isModerator
error: (response, textStatus, e) ->
console.log e
success: (response, textStatus) ->
parent = $userProfile.parent()
$userProfile.replaceWith(response.html)
Discussion.initializeUserProfile parent.children(".user-profile")
Discussion.bindLocalEvents $local,
"click .sidebar-revoke-moderator-button": (event) ->
handleUpdateModeratorStatus(this, false)
"click .sidebar-promote-moderator-button": (event) ->
handleUpdateModeratorStatus(this, true)
initializeUserActiveDiscussion: ($discussion) ->
if not @Discussion?
@Discussion = {}
Discussion = @Discussion
wmdEditors = {}
@Discussion = $.extend @Discussion,
generateLocal: (elem) ->
(selector) -> $(elem).find(selector)
generateDiscussionLink: (cls, txt, handler) ->
$("<a>").addClass("discussion-link")
.attr("href", "javascript:void(0)")
.addClass(cls).html(txt)
.click -> handler(this)
urlFor: (name, param, param1, param2) ->
{
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow"
create_thread : "/courses/#{$$course_id}/discussion/#{param}/threads/create"
search_similar_threads : "/courses/#{$$course_id}/discussion/#{param}/threads/search_similar"
update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update"
create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply"
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update"
endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse"
create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply"
delete_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/delete"
upvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/upvote"
downvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/downvote"
undo_vote_for_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unvote"
upload : "/courses/#{$$course_id}/discussion/upload"
search : "/courses/#{$$course_id}/discussion/forum/search"
tags_autocomplete : "/courses/#{$$course_id}/discussion/threads/tags/autocomplete"
retrieve_discussion : "/courses/#{$$course_id}/discussion/forum/#{param}/inline"
retrieve_single_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
update_moderator_status : "/courses/#{$$course_id}/discussion/users/#{param}/update_moderator_status"
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
}[name]
safeAjax: (params) ->
$elem = params.$elem
if $elem.attr("disabled")
return
$elem.attr("disabled", "disabled")
$.ajax(params).always ->
$elem.removeAttr("disabled")
handleAnchorAndReload: (response) ->
#window.location = window.location.pathname + "#" + response['id']
window.location.reload()
bindLocalEvents: ($local, eventsHandler) ->
for eventSelector, handler of eventsHandler
[event, selector] = eventSelector.split(' ')
$local(selector).unbind(event)[event] handler
tagsInputOptions: ->
autocomplete_url: Discussion.urlFor('tags_autocomplete')
autocomplete:
remoteDataType: 'json'
interactive: true
height: '30px'
width: '100%'
defaultText: "Tag your post: press enter after each tag"
removeWithBackspace: true
isSubscribed: (id, type) ->
$$user_info? and (
if type == "thread"
id in $$user_info.subscribed_thread_ids
else if type == "commentable" or type == "discussion"
id in $$user_info.subscribed_commentable_ids
else
id in $$user_info.subscribed_user_ids
)
isUpvoted: (id) ->
$$user_info? and (id in $$user_info.upvoted_ids)
isDownvoted: (id) ->
$$user_info? and (id in $$user_info.downvoted_ids)
formErrorHandler: (errorsField) ->
(xhr, textStatus, error) ->
response = JSON.parse(xhr.responseText)
if response.errors? and response.errors.length > 0
errorsField.empty()
for error in response.errors
errorsField.append($("<li>").addClass("new-post-form-error").html(error))
clearFormErrors: (errorsField) ->
errorsField.empty()
postMathJaxProcessor: (text) ->
RE_INLINEMATH = /^\$([^\$]*)\$/g
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
Discussion.processEachMathAndCode text, (s, type) ->
if type == 'display'
s.replace RE_DISPLAYMATH, ($0, $1) ->
"\\[" + $1 + "\\]"
else if type == 'inline'
s.replace RE_INLINEMATH, ($0, $1) ->
"\\(" + $1 + "\\)"
else
s
makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = $content.attr("_id")
appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = Discussion.urlFor('upload')
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, Discussion.postMathJaxProcessor
wmdEditors["#{cls_identifier}-#{id}"] = editor
editor
getWmdEditor: ($content, $local, cls_identifier) ->
id = $content.attr("_id")
wmdEditors["#{cls_identifier}-#{id}"]
getWmdInput: ($content, $local, cls_identifier) ->
id = $content.attr("_id")
$local("#wmd-input-#{cls_identifier}-#{id}")
getWmdContent: ($content, $local, cls_identifier) ->
Discussion.getWmdInput($content, $local, cls_identifier).val()
setWmdContent: ($content, $local, cls_identifier, text) ->
Discussion.getWmdInput($content, $local, cls_identifier).val(text)
Discussion.getWmdEditor($content, $local, cls_identifier).refreshPreview()
getContentInfo: (id, attr) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
(window.$$annotated_content_info[id] || {})[attr]
setContentInfo: (id, attr, value) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
window.$$annotated_content_info[id] ||= {}
window.$$annotated_content_info[id][attr] = value
extendContentInfo: (id, newInfo) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
window.$$annotated_content_info[id] = newInfo
bulkExtendContentInfo: (newInfos) ->
if not window.$$annotated_content_info?
window.$$annotated_content_info = {}
window.$$annotated_content_info = $.extend window.$$annotated_content_info, newInfos
subscriptionLink: (type, id) ->
followLink = ->
Discussion.generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow)
unfollowLink = ->
Discussion.generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow)
handleFollow = (elem) ->
Discussion.safeAjax
$elem: $(elem)
url: Discussion.urlFor("follow_#{type}", id)
type: "POST"
success: (response, textStatus) ->
if textStatus == "success"
$(elem).replaceWith unfollowLink()
dataType: 'json'
handleUnfollow = (elem) ->
Discussion.safeAjax
$elem: $(elem)
url: Discussion.urlFor("unfollow_#{type}", id)
type: "POST"
success: (response, textStatus) ->
if textStatus == "success"
$(elem).replaceWith followLink()
dataType: 'json'
if Discussion.isSubscribed(id, type)
unfollowLink()
else
followLink()
processEachMathAndCode: (text, processor) ->
codeArchive = []
RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m
RE_INLINEMATH = /^([^\$]*?)\$([^\$]+?)\$(.*)$/m
ESCAPED_DOLLAR = '@@ESCAPED_D@@'
ESCAPED_BACKSLASH = '@@ESCAPED_B@@'
processedText = ""
$div = $("<div>").html(text)
$div.find("code").each (index, code) ->
codeArchive.push $(code).html()
$(code).html(codeArchive.length - 1)
text = $div.html()
text = text.replace /\\\$/g, ESCAPED_DOLLAR
while true
if RE_INLINEMATH.test(text)
text = text.replace RE_INLINEMATH, ($0, $1, $2, $3) ->
processedText += $1 + processor("$" + $2 + "$", 'inline')
$3
else if RE_DISPLAYMATH.test(text)
text = text.replace RE_DISPLAYMATH, ($0, $1, $2, $3) ->
processedText += $1 + processor("$$" + $2 + "$$", 'display')
$3
else
processedText += text
break
text = processedText
text = text.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$')
text = text.replace /\\\\\\\\/g, ESCAPED_BACKSLASH
text = text.replace /\\begin\{([a-z]*\*?)\}([\s\S]*?)\\end\{\1\}/img, ($0, $1, $2) ->
processor("\\begin{#{$1}}" + $2 + "\\end{#{$1}}")
text = text.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\')
$div = $("<div>").html(text)
cnt = 0
$div.find("code").each (index, code) ->
$(code).html(processor(codeArchive[cnt], 'code'))
cnt += 1
text = $div.html()
text
...@@ -350,7 +350,7 @@ $tag-text-color: #5b614f; ...@@ -350,7 +350,7 @@ $tag-text-color: #5b614f;
margin-right: 1em; margin-right: 1em;
} }
.comment-count { .show-comments-wrapper {
display: inline; display: inline;
margin-right: 20px; margin-right: 20px;
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='course'/>
<%include file="../discussion/_js_head_dependencies.html" />
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
...@@ -20,10 +21,10 @@ ...@@ -20,10 +21,10 @@
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/xml/xml.js')}"></script> ## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/xml/xml.js')}"></script>
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/python/python.js')}"></script> ## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/python/python.js')}"></script>
<%static:js group='courseware'/> <%static:js group='courseware'/>
<%include file="../discussion/_js_dependencies.html" /> <%include file="../discussion/_js_body_dependencies.html" />
<%include file="/mathjax_include.html" />
<!-- TODO: http://docs.jquery.com/Plugins/Validation --> <!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -5,11 +5,9 @@ ...@@ -5,11 +5,9 @@
</%def> </%def>
<%def name="render_content_with_comments(content)"> <%def name="render_content_with_comments(content)">
<div class="${content['type']}" _id="${content['id']}" _discussion_id="${content.get('commentable_id')}" _author_id="${helpers.show_if(content['user_id'], content.get('anonymous'))}"> <div class="${content['type']}${helpers.show_if(' endorsed', content.get('endorsed'))}" _id="${content['id']}" _discussion_id="${content.get('commentable_id', '')}" _author_id="${helpers.show_if(content['user_id'], not content.get('anonymous'))}">
${render_content(content)} ${render_content(content)}
% if content.get('children') is not None: ${render_comments(content.get('children', []))}
${render_comments(content['children'])}
% endif
</div> </div>
</%def> </%def>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<section class="discussion forum-discussion" _id="${discussion_id}"> <section class="discussion forum-discussion" _id="${discussion_id}">
<div class="discussion-non-content discussion-local"> <div class="discussion-non-content local">
<div class="search-wrapper"> <div class="search-wrapper">
<%include file="_search_bar.html" /> <%include file="_search_bar.html" />
</div> </div>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<section class="discussion inline-discussion" _id="${discussion_id}"> <section class="discussion inline-discussion" _id="${discussion_id}">
<div class="discussion-non-content discussion-local"></div> <div class="discussion-non-content local"></div>
<div class="threads"> <div class="threads">
% for thread in threads: % for thread in threads:
......
<%! from django_comment_client.helpers import include_mustache_templates %>
<%include file="/mathjax_include.html" />
${include_mustache_templates()}
<%! from django.template.defaultfilters import escapejs %> <%! from django.template.defaultfilters import escapejs %>
<script type="text/javascript"> <script type="text/javascript">
var $$user_info = JSON.parse("${user_info | escapejs}");
var $$course_id = "${course_id | escapejs}"; var $$course_id = "${course_id | escapejs}";
if (typeof $$annotated_content_info === undefined || $$annotated_content_info === null) { if (typeof $$annotated_content_info === undefined) {
var $$annotated_content_info = {}; var $$annotated_content_info = {};
} }
$$annotated_content_info = $.extend($$annotated_content_info, JSON.parse("${annotated_content_info | escapejs}")); $$annotated_content_info = $.extend($$annotated_content_info, JSON.parse("${annotated_content_info | escapejs}"));
if (typeof $$discussion_data === undefined) {
var $$discussion_data = {};
}
$$discussion_data = $.extend($$discussion_data, JSON.parse("${discussion_data | escapejs}"));
</script> </script>
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
inlineMath: [
["\\(","\\)"],
],
displayMath: [
["\\[","\\]"],
]
}
});
</script>
## This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
## It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of MathJax extension libraries
<script type="text/javascript" src="/static/js/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
<!---<script type="text/javascript" src="http://cdn.mathjax.org/mathjax/latest/MathJax.js"> </script>-->
<script type="text/javascript" src="${static.url('js/split.js')}"></script> <script type="text/javascript" src="${static.url('js/split.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.ajaxfileupload.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.ajaxfileupload.js')}"></script>
<script type="text/javascript" src="${static.url('js/Markdown.Converter.js')}"></script> <script type="text/javascript" src="${static.url('js/Markdown.Converter.js')}"></script>
...@@ -27,5 +10,8 @@ ...@@ -27,5 +10,8 @@
<script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script> <script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script>
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script> <script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/URI.min.js')}"></script> <script type="text/javascript" src="${static.url('js/URI.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<link href="${static.url('css/vendor/jquery.tagsinput.css')}" rel="stylesheet" type="text/css"> <link href="${static.url('css/vendor/jquery.tagsinput.css')}" rel="stylesheet" type="text/css">
<link href="${static.url('css/vendor/jquery.autocomplete.css')}" rel="stylesheet" type="text/css"> <link href="${static.url('css/vendor/jquery.autocomplete.css')}" rel="stylesheet" type="text/css">
<%namespace name='static' file='../static_content.html'/>
<script type="text/javascript" src="${static.url('js/split.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.ajaxfileupload.js')}"></script>
<script type="text/javascript" src="${static.url('js/Markdown.Converter.js')}"></script>
<script type="text/javascript" src="${static.url('js/Markdown.Sanitizer.js')}"></script>
<script type="text/javascript" src="${static.url('js/Markdown.Editor.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.autocomplete.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.timeago.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.tagsinput.js')}"></script>
<script type="text/javascript" src="${static.url('js/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/URI.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<link href="${static.url('css/vendor/jquery.tagsinput.css')}" rel="stylesheet" type="text/css">
<link href="${static.url('css/vendor/jquery.autocomplete.css')}" rel="stylesheet" type="text/css">
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
% endfor % endfor
</%def> </%def>
<div class="discussion-${discussion_type}-paginator discussion-paginator"> <div class="discussion-${discussion_type}-paginator discussion-paginator local">
<div class="prev-page"> <div class="prev-page">
% if page > 1: % if page > 1:
${link_to_page(page - 1, "&lt; Previous page")} ${link_to_page(page - 1, "&lt; Previous page")}
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
<section class="discussion" _id="${discussion_id}"> <section class="discussion" _id="${discussion_id}">
<a class="discussion-title" href="javascript:void(0)">Discussion</a> <a class="discussion-title" href="javascript:void(0)">Discussion</a>
${renderer.render_content_with_comments(thread)} <div class="threads">
${renderer.render_content_with_comments(thread)}
</div>
</section> </section>
<%include file="_js_data.html" /> <%include file="_js_data.html" />
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<a class="discussion-sort-link ${cls}" href="javascript:void(0)" sort-url="${url_for_sort(key, order)}">${title}</a> <a class="discussion-sort-link ${cls}" href="javascript:void(0)" sort-url="${url_for_sort(key, order)}">${title}</a>
</%def> </%def>
<div class="discussion-sort discussion-local"> <div class="discussion-sort local">
<span class="discussion-label">Sort by:</span> <span class="discussion-label">Sort by:</span>
${link_to_sort('activity', 'top')} ${link_to_sort('activity', 'top')}
......
...@@ -5,13 +5,14 @@ ...@@ -5,13 +5,14 @@
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='course'/>
<%include file="_js_head_dependencies.html" />
</%block> </%block>
<%block name="js_extra"> <%block name="js_extra">
<%include file="_js_dependencies.html" /> <%include file="_js_body_dependencies.html" />
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='discussion'" /> <%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container"> <section class="container">
<div class="course-wrapper"> <div class="course-wrapper">
......
<div class="discussion-content"> <div class="discussion-content local">
<div class="discussion-content-wrapper"> <div class="discussion-content-wrapper">
<div class="discussion-votes"> <div class="discussion-votes">
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)">&#9650;</a> <a class="discussion-vote discussion-vote-up" href="javascript:void(0)" value="up">&#9650;</a>
<div class="discussion-votes-point">{{content.votes.point}}</div> <div class="discussion-votes-point">{{content.votes.point}}</div>
<a class="discussion-vote discussion-vote-down" href="javascript:void(0)">&#9660;</a> <a class="discussion-vote discussion-vote-down" href="javascript:void(0)" value="down">&#9660;</a>
</div> </div>
<div class="discussion-right-wrapper"> <div class="discussion-right-wrapper">
<ul class="admin-actions"> <ul class="admin-actions">
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
{{/thread}} {{/thread}}
</ul> </ul>
{{#thread}} {{#thread}}
<a class="thread-title" name="{{content.id}}" href="javascript:void(0)">{{{content.displayed_title}}}</a> <a class="thread-title" name="{{content.id}}" href="javascript:void(0)">{{content.displayed_title}}</a>
<div class="thread-raw-title" style="display: none">{{{content.title}}}</div> <div class="thread-raw-title" style="display: none">{{{content.title}}}</div>
{{/thread}} {{/thread}}
<div class="discussion-content-view"> <div class="discussion-content-view">
<a name="{{content.id}}" style="width: 0; height: 0; padding: 0; border: none;"></a> <a name="{{content.id}}" style="width: 0; height: 0; padding: 0; border: none;"></a>
<div class="content-body {{content.type}}-body" id="content-body-{{content.id}}">{{{content.displayed_body}}}</div> <div class="content-body {{content.type}}-body" id="content-body-{{content.id}}">{{content.displayed_body}}</div>
<div class="content-raw-body {{content.type}}-raw-body" style="display: none">{{{content.body}}}</div> <div class="content-raw-body {{content.type}}-raw-body" style="display: none">{{{content.body}}}</div>
{{#thread}} {{#thread}}
<div class="thread-tags"> <div class="thread-tags">
...@@ -37,23 +37,25 @@ ...@@ -37,23 +37,25 @@
anonymous anonymous
{{/content.anonymous}} {{/content.anonymous}}
{{^content.anonymous}} {{^content.anonymous}}
<a href="{{##url_for_user}}{{content.user_id}}{{/url_for_user}}">{{content.username}}</a> {{content.username}}
{{/content.anonymous}} {{/content.anonymous}}
</div> </div>
<div class="comment-count"> <div class="show-comments-wrapper">
{{#thread}} {{#thread}}
{{#partial_comments}} {{#partial_comments}}
<a href="javascript:void(0)" class="discussion-show-comments first-time">Show all comments ({{content.comments_count}} total)</a> <a href="javascript:void(0)" class="discussion-show-comments first-time">Show all comments (<span class="comments-count">{{content.comments_count}}</span> total)</a>
{{/partial_comments}} {{/partial_comments}}
{{^partial_comments}} {{^partial_comments}}
<a href="javascript:void(0)" class="discussion-show-comments">Show {{##pluralize}}{{content.comments_count}} comment{{/pluralize}}</a> <a href="javascript:void(0)" class="discussion-show-comments">Show <span class="comments-count">{{content.comments_count}}</span> {{##pluralize}}{{content.comments_count}} comment{{/pluralize}}</a>
{{/partial_comments}} {{/partial_comments}}
{{/thread}} {{/thread}}
</div> </div>
<ul class="discussion-actions"> <ul class="discussion-actions">
<li><a class="discussion-link discussion-reply discussion-reply-{{content.type}}" href="javascript:void(0)">Reply</a></li> <li><a class="discussion-link discussion-reply discussion-reply-{{content.type}}" href="javascript:void(0)">Reply</a></li>
<li><div class="follow-wrapper"></div></li> {{#thread}}
<li><a class="discussion-link discussion-permanent-link" href="javascript:void(0)">Permanent Link</a></li> <li><div class="follow-wrapper"><a class="discussion-link discussion-follow-thread" href="javascript:void(0)">Follow</a></div></li>
{{/thread}}
<li><a class="discussion-link discussion-permanent-link" href="{{content.permalink}}">Permanent Link</a></li>
</ul> </ul>
</div> </div>
</div> </div>
......
<form class="discussion-content-edit discussion-comment-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<div class="comment-body-edit body-input">{{body}}</div>
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
<form class="discussion-content-edit discussion-thread-edit" _id="{{id}}">
<ul class="discussion-errors discussion-update-errors"></ul>
<input type="text" class="thread-title-edit title-input" placeholder="Title" value="{{title}}"/>
<div class="thread-body-edit body-input">{{body}}</div>
<input class="thread-tags-edit" placeholder="Tags" value="{{tags}}" />
<div class = "edit-post-control">
<a class="discussion-cancel-update" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-update control-button" href="javascript:void(0)">Update</a>
</div>
</form>
<form class="new-post-form collapsed" id="new-post-form" style="display: block; ">
<ul class="new-post-form-errors discussion-errors"></ul>
<input type="text" class="new-post-title title-input" placeholder="Title" />
<div class="new-post-similar-posts-wrapper" style="display: none">
Similar Posts:
<a class="hide-similar-posts" href="javascript:void(0)">Hide</a>
<div class="new-post-similar-posts"></div>
</div>
<div class="new-post-body reply-body"></div>
<input class="new-post-tags" placeholder="Tags" />
<div class="post-options">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<label for="discussion-post-anonymously-${discussion_id}">post anonymously</label>
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-${discussion_id}" checked="">
<label for="discussion-auto-watch-${discussion_id}">follow this thread</label>
</div>
<div class="new-post-control post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
<form class="discussion-reply-new">
<ul class="discussion-errors"></ul>
<div class="reply-body"></div>
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-{{id}}" />
<label for="discussion-post-anonymously-{{id}}">post anonymously</label>
{{#showWatchCheckbox}}
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-{{id}}" checked />
<label for="discussion-auto-watch-{{id}}">follow this thread</label>
{{/showWatchCheckbox}}
<br />
<div class="reply-post-control">
<a class="discussion-cancel-post" href="javascript:void(0)">Cancel</a>
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment