Merge pull request #511 from MITx/feature/dave/forum_fixes
Forum refactoring/fixes
This commit is contained in:
66
common/static/js/vendor/backbone-min.js
vendored
66
common/static/js/vendor/backbone-min.js
vendored
@@ -4,35 +4,37 @@
|
||||
// Backbone may be freely distributed under the MIT license.
|
||||
// For all details and documentation:
|
||||
// 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=
|
||||
{});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=
|
||||
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=
|
||||
{};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==
|
||||
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:
|
||||
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)};
|
||||
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,
|
||||
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();
|
||||
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._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);
|
||||
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])?
|
||||
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)?
|
||||
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},
|
||||
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?
|
||||
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,
|
||||
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=
|
||||
{};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,
|
||||
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,
|
||||
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,
|
||||
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]:
|
||||
""},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=
|
||||
!(!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.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()},
|
||||
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,
|
||||
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.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(",");
|
||||
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();
|
||||
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,
|
||||
!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",
|
||||
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,
|
||||
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);
|
||||
(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},
|
||||
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=
|
||||
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._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)},
|
||||
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?
|
||||
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",
|
||||
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=
|
||||
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")||
|
||||
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 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==
|
||||
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.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");
|
||||
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;
|
||||
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,
|
||||
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]!==
|
||||
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,
|
||||
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,
|
||||
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;
|
||||
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(" "),
|
||||
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,
|
||||
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)}});
|
||||
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.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.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&&
|
||||
!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",
|
||||
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),
|
||||
!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,
|
||||
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(){},
|
||||
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&&
|
||||
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=
|
||||
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)};
|
||||
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):
|
||||
# For now, just a static page
|
||||
context = {'course': course,
|
||||
'staff_access': True,}
|
||||
|
||||
return render_to_response('courseware/instructor_dashboard.html', context)
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -51,7 +51,8 @@ def ajax_content_response(request, course_id, content, template_name):
|
||||
'content': content,
|
||||
}
|
||||
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({
|
||||
'html': html,
|
||||
'content': content,
|
||||
|
||||
@@ -11,11 +11,11 @@ from courseware.courses import get_course_with_access
|
||||
from courseware.access import has_access
|
||||
|
||||
from urllib import urlencode
|
||||
from operator import methodcaller
|
||||
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 dateutil
|
||||
import django_comment_client.utils as utils
|
||||
import comment_client as cc
|
||||
|
||||
@@ -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])),
|
||||
}[discussion_type]()
|
||||
|
||||
annotated_content_infos = map(lambda x: utils.get_annotated_content_infos(course_id, x, request.user), threads)
|
||||
annotated_content_info = reduce(merge_dict, annotated_content_infos, {})
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
|
||||
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 = {
|
||||
'threads': threads,
|
||||
'discussion_id': discussion_id,
|
||||
'user_id': user_id,
|
||||
'user_info': json.dumps(cc.User.from_django_user(request.user).to_dict()),
|
||||
'course_id': course_id,
|
||||
'request': request,
|
||||
'performed_search': _should_perform_search(request),
|
||||
'pages_nearby_delta': PAGES_NEARBY_DELTA,
|
||||
'discussion_type': discussion_type,
|
||||
'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),
|
||||
'discussion_data': json.dumps({ discussion_id: threads }),
|
||||
}
|
||||
context = dict(context.items() + query_params.items())
|
||||
return render_to_string(template, context)
|
||||
@@ -121,7 +125,11 @@ def inline_discussion(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, \
|
||||
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=''):
|
||||
if not discussion_id:
|
||||
@@ -138,19 +146,21 @@ def forum_form_discussion(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)
|
||||
|
||||
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():
|
||||
return utils.HtmlResponse(content)
|
||||
return utils.JsonResponse({
|
||||
'html': content,
|
||||
'discussionData': threads,
|
||||
})
|
||||
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 = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
@@ -164,17 +174,19 @@ def forum_form_discussion(request, course_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()
|
||||
|
||||
annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread.to_dict(), user=request.user)
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
|
||||
annotated_content_info = utils.get_annotated_content_infos(course_id, thread=thread, user=request.user, user_info=user_info)
|
||||
|
||||
context = {
|
||||
'discussion_id': discussion_id,
|
||||
'thread': thread,
|
||||
'user_info': json.dumps(cc.User.from_django_user(request.user).to_dict()),
|
||||
'annotated_content_info': json.dumps(annotated_content_info),
|
||||
'course_id': course_id,
|
||||
'request': request,
|
||||
'discussion_data': json.dumps({ discussion_id: [thread] }),
|
||||
}
|
||||
return render_to_string('discussion/_single_thread.html', context)
|
||||
|
||||
@@ -182,13 +194,15 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
|
||||
if request.is_ajax():
|
||||
|
||||
user_info = cc.User.from_django_user(request.user).to_dict()
|
||||
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}
|
||||
html = render_to_string('discussion/_ajax_single_thread.html', context)
|
||||
|
||||
return utils.JsonResponse({
|
||||
'html': html,
|
||||
'content': thread.to_dict(),
|
||||
'annotated_content_info': annotated_content_info,
|
||||
})
|
||||
|
||||
@@ -200,7 +214,6 @@ def single_thread(request, course_id, discussion_id, thread_id):
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'init': '',
|
||||
'content': render_single_thread(request, discussion_id, course_id, thread_id),
|
||||
'accordion': render_accordion(request, course, discussion_id),
|
||||
'course': course,
|
||||
'course_id': course.id,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from utils import *
|
||||
from mustache_helpers import mustache_helpers
|
||||
from django.core.urlresolvers import reverse
|
||||
from functools import partial
|
||||
|
||||
from utils import *
|
||||
|
||||
import pystache_custom as pystache
|
||||
import urllib
|
||||
import os
|
||||
|
||||
def pluralize(singular_term, count):
|
||||
if int(count) >= 2:
|
||||
@@ -18,12 +22,32 @@ def show_if(text, condition):
|
||||
else:
|
||||
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={}):
|
||||
content_info = {
|
||||
'displayed_title': content.get('highlighted_title') or content.get('title', ''),
|
||||
'displayed_body': content.get('highlighted_body') or content.get('body', ''),
|
||||
'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 = {
|
||||
'content': merge_dict(content, content_info),
|
||||
content['type']: True,
|
||||
@@ -31,4 +55,4 @@ def render_content(content, additional_context={}):
|
||||
context = merge_dict(context, additional_context)
|
||||
partial_mustache_helpers = {k: partial(v, content) for k, v in mustache_helpers.items()}
|
||||
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
|
||||
def pluralize(content, text):
|
||||
num, word = text.split(' ')
|
||||
if int(num or '0') >= 2:
|
||||
return num + ' ' + word + 's'
|
||||
return word + 's'
|
||||
else:
|
||||
return num + ' ' + word
|
||||
return word
|
||||
|
||||
def url_for_user(content, 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):
|
||||
return ''.join(random.choice(chars) for x in range(length))
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.course_id = "MITx/6.002x/2012_Fall"
|
||||
|
||||
self.moderator_role = Role.objects.get_or_create(name="Moderator", course_id=self.course_id)[0]
|
||||
|
||||
@@ -160,23 +160,32 @@ class QueryCountDebugMiddleware(object):
|
||||
logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time))
|
||||
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 {
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
|
||||
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_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"),
|
||||
'voted': voted,
|
||||
'subscribed': content['id'] in user_info['subscribed_thread_ids'],
|
||||
'ability': {
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
|
||||
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_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 = {}
|
||||
def _annotate(content):
|
||||
infos[str(content['id'])] = get_annotated_content_info(course_id, content, user)
|
||||
def annotate(content):
|
||||
infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, user_info)
|
||||
for child in content.get('children', []):
|
||||
_annotate(child)
|
||||
_annotate(thread)
|
||||
annotate(child)
|
||||
annotate(thread)
|
||||
return infos
|
||||
|
||||
def render_mustache(template_name, dictionary, *args, **kwargs):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from utils import *
|
||||
|
||||
from thread import Thread
|
||||
import models
|
||||
import settings
|
||||
|
||||
@@ -10,7 +11,7 @@ class Comment(models.Model):
|
||||
'endorsed', 'parent_id', 'thread_id',
|
||||
'username', 'votes', 'user_id', 'closed',
|
||||
'created_at', 'updated_at', 'depth',
|
||||
'at_position_list', 'type',
|
||||
'at_position_list', 'type', 'commentable_id',
|
||||
]
|
||||
|
||||
updatable_fields = [
|
||||
@@ -23,6 +24,10 @@ class Comment(models.Model):
|
||||
base_url = "{prefix}/comments".format(prefix=settings.PREFIX)
|
||||
type = 'comment'
|
||||
|
||||
@property
|
||||
def thread(self):
|
||||
return Thread(id=self.thread_id, type='thread')
|
||||
|
||||
@classmethod
|
||||
def url_for_comments(cls, params={}):
|
||||
if params.get('thread_id'):
|
||||
|
||||
@@ -43,6 +43,9 @@ class Model(object):
|
||||
raise KeyError("Field {0} does not exist".format(key))
|
||||
self.attributes.__setitem__(key, value)
|
||||
|
||||
def items(self, *args, **kwargs):
|
||||
return self.attributes.items(*args, **kwargs)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.attributes.get(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class Thread(models.Model):
|
||||
'commentable_id', 'username', 'user_id',
|
||||
'created_at', 'updated_at', 'comments_count',
|
||||
'at_position_list', 'children', 'type',
|
||||
'highlighted_title', 'highlighted_body',
|
||||
]
|
||||
|
||||
updatable_fields = [
|
||||
|
||||
@@ -18,8 +18,10 @@ def extract(dic, keys):
|
||||
|
||||
def merge_dict(dic1, dic2):
|
||||
return dict(dic1.items() + dic2.items())
|
||||
|
||||
|
||||
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
|
||||
if method in ['post', 'put', 'patch']:
|
||||
response = requests.request(method, url, data=data_or_params)
|
||||
|
||||
@@ -1,411 +1,410 @@
|
||||
if not @Discussion?
|
||||
@Discussion = {}
|
||||
class @Content extends Backbone.Model
|
||||
|
||||
Discussion = @Discussion
|
||||
template: -> DiscussionUtil.getTemplate('_content')
|
||||
|
||||
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")
|
||||
actions:
|
||||
editable: '.admin-edit'
|
||||
can_reply: '.discussion-reply'
|
||||
can_endorse: '.admin-endorse'
|
||||
can_delete: '.admin-delete'
|
||||
can_openclose: '.admin-openclose'
|
||||
|
||||
urlMappers: {}
|
||||
|
||||
initializeFollowThread = (thread) ->
|
||||
$thread = $(thread)
|
||||
id = $thread.attr("_id")
|
||||
$thread.children(".discussion-content")
|
||||
.find(".follow-wrapper")
|
||||
.append(Discussion.subscriptionLink('thread', id))
|
||||
urlFor: (name) ->
|
||||
@urlMappers[name].apply(@)
|
||||
|
||||
@Discussion = $.extend @Discussion,
|
||||
can: (action) ->
|
||||
DiscussionUtil.getContentInfo @id, action
|
||||
|
||||
bindContentEvents: (content) ->
|
||||
updateInfo: (info) ->
|
||||
@set('ability', info.ability)
|
||||
@set('voted', info.voted)
|
||||
@set('subscribed', info.subscribed)
|
||||
|
||||
$content = $(content)
|
||||
$discussionContent = $content.children(".discussion-content")
|
||||
$local = Discussion.generateLocal($discussionContent)
|
||||
addComment: (comment, options) ->
|
||||
options ||= {}
|
||||
if not options.silent
|
||||
thread = @get('thread')
|
||||
comments_count = parseInt(thread.get('comments_count'))
|
||||
thread.set('comments_count', comments_count + 1)
|
||||
@get('children').push comment
|
||||
model = new Comment $.extend {}, comment, { thread: @get('thread') }
|
||||
@get('comments').add model
|
||||
model
|
||||
|
||||
id = $content.attr("_id")
|
||||
removeComment: (comment) ->
|
||||
thread = @get('thread')
|
||||
comments_count = parseInt(thread.get('comments_count'))
|
||||
thread.set('comments_count', comments_count - 1 - comment.getCommentsCount())
|
||||
|
||||
handleReply = (elem) ->
|
||||
$replyView = $local(".discussion-reply-new")
|
||||
if $replyView.length
|
||||
$replyView.show()
|
||||
resetComments: (children) ->
|
||||
@set 'children', []
|
||||
@set 'comments', new Comments()
|
||||
for comment in (children || [])
|
||||
@addComment comment, { silent: true }
|
||||
|
||||
initialize: ->
|
||||
DiscussionUtil.addContent @id, @
|
||||
@resetComments(@get('children'))
|
||||
|
||||
|
||||
class @ContentView extends Backbone.View
|
||||
|
||||
$: (selector) ->
|
||||
@$local.find(selector)
|
||||
|
||||
partial:
|
||||
endorsed: (endorsed) ->
|
||||
if endorsed
|
||||
@$el.addClass("endorsed")
|
||||
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()
|
||||
@$el.removeClass("endorsed")
|
||||
|
||||
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)
|
||||
closed: (closed) -> # we should just re-render the whole thread, or update according to new abilities
|
||||
if closed
|
||||
@$el.addClass("closed")
|
||||
@$(".admin-openclose").text "Re-open Thread"
|
||||
else
|
||||
return
|
||||
@$el.removeClass("closed")
|
||||
@$(".admin-openclose").text "Close Thread"
|
||||
|
||||
body = Discussion.getWmdContent $content, $local, "reply-body"
|
||||
voted: (voted) ->
|
||||
@$(".discussion-vote-up").removeClass("voted") if voted != "up"
|
||||
@$(".discussion-vote-down").removeClass("voted") if voted != "down"
|
||||
@$(".discussion-vote-#{voted}").addClass("voted") if voted in ["up", "down"]
|
||||
|
||||
anonymous = false || $local(".discussion-post-anonymously").is(":checked")
|
||||
autowatch = false || $local(".discussion-auto-watch").is(":checked")
|
||||
votes_point: (votes_point) ->
|
||||
@$(".discussion-votes-point").html(votes_point)
|
||||
|
||||
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
|
||||
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()
|
||||
comments_count: (comments_count) ->
|
||||
@$(".comments-count").html(comments_count)
|
||||
|
||||
Discussion.bindLocalEvents $local,
|
||||
subscribed: (subscribed) ->
|
||||
if subscribed
|
||||
@$(".discussion-follow-thread").addClass("discussion-unfollow-thread").html("Unfollow")
|
||||
else
|
||||
@$(".discussion-follow-thread").removeClass("discussion-unfollow-thread").html("Follow")
|
||||
|
||||
"click .thread-title": ->
|
||||
handleShowSingleThread(this)
|
||||
ability: (ability) ->
|
||||
for action, elemSelector of @model.actions
|
||||
if not ability[action]
|
||||
@$(elemSelector).parent().remove()
|
||||
|
||||
"click .discussion-show-comments": ->
|
||||
handleShowSingleThread(this)
|
||||
$discussionContent: ->
|
||||
@_discussionContent ||= @$el.children(".discussion-content")
|
||||
|
||||
"click .discussion-hide-comments": ->
|
||||
handleHideSingleThread(this)
|
||||
$showComments: ->
|
||||
@_showComments ||= @$(".discussion-show-comments")
|
||||
|
||||
"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(/\<\;highlight\>\;/g, "<span class='search-highlight'>")
|
||||
.replace(/\<\;\/highlight\>\;/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)
|
||||
updateShowComments: ->
|
||||
if @showed
|
||||
@$showComments().html @$showComments().html().replace "Show", "Hide"
|
||||
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
|
||||
@$showComments().html @$showComments().html().replace "Hide", "Show"
|
||||
|
||||
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"
|
||||
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
|
||||
$contentTitle.html DiscussionUtil.unescapeHighlightTag DiscussionUtil.stripLatexHighlight $contentTitle.html()
|
||||
|
||||
initBody: ->
|
||||
$contentBody = @$(".content-body")
|
||||
$contentBody.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight $contentBody.html()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, $contentBody.attr("id")]
|
||||
|
||||
initTimeago: ->
|
||||
@$("span.timeago").timeago()
|
||||
|
||||
initPermalink: ->
|
||||
@$(".discussion-permanent-link").attr "href", @model.permalink()
|
||||
|
||||
renderPartial: ->
|
||||
for attr, value of @model.changedAttributes()
|
||||
if @partial[attr]
|
||||
@partial[attr].apply(@, [value])
|
||||
|
||||
initBindings: ->
|
||||
@model.view = @
|
||||
@model.bind('change', @renderPartial, @)
|
||||
|
||||
initialize: ->
|
||||
@initBindings()
|
||||
@initLocal()
|
||||
@initTimeago()
|
||||
@initTitle()
|
||||
@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)
|
||||
|
||||
@@ -1,190 +1,167 @@
|
||||
if not @Discussion?
|
||||
@Discussion = {}
|
||||
class @Discussion extends Backbone.Collection
|
||||
model: Thread
|
||||
|
||||
Discussion = @Discussion
|
||||
initialize: ->
|
||||
DiscussionUtil.addDiscussion @id, @
|
||||
@bind "add", (item) =>
|
||||
item.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))
|
||||
find: (id) ->
|
||||
_.first @where(id: id)
|
||||
|
||||
@Discussion = $.extend @Discussion,
|
||||
addThread: (thread, options) ->
|
||||
options ||= {}
|
||||
model = new Thread thread
|
||||
@add model
|
||||
model
|
||||
|
||||
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)
|
||||
class @DiscussionView extends Backbone.View
|
||||
|
||||
#initializeFollowDiscussion(discussion) TODO move this somewhere else
|
||||
$: (selector) ->
|
||||
@$local.find(selector)
|
||||
|
||||
bindDiscussionEvents: (discussion) ->
|
||||
initLocal: ->
|
||||
@$local = @$el.children(".local")
|
||||
@$delegateElement = @$local
|
||||
|
||||
$discussion = $(discussion)
|
||||
$discussionNonContent = $discussion.children(".discussion-non-content")
|
||||
$local = Discussion.generateLocal($discussion.children(".discussion-local"))
|
||||
initialize: ->
|
||||
@initLocal()
|
||||
@model.id = @$el.attr("_id")
|
||||
@model.view = @
|
||||
@$el.children(".threads").children(".thread").each (index, elem) =>
|
||||
threadView = new ThreadView el: elem, model: @model.find $(elem).attr("_id")
|
||||
if @$el.hasClass("forum-discussion")
|
||||
$(".discussion-sidebar").find(".sidebar-new-post-button")
|
||||
.unbind('click').click $.proxy @newPost, @
|
||||
else if @$el.hasClass("inline-discussion")
|
||||
@newPost()
|
||||
|
||||
id = $discussion.attr("_id")
|
||||
reload: ($elem, url) ->
|
||||
if not url then return
|
||||
DiscussionUtil.get $elem, url, {}, (response, textStatus) =>
|
||||
$parent = @$el.parent()
|
||||
@$el.replaceWith(response.html)
|
||||
$discussion = $parent.find("section.discussion")
|
||||
@model.reset(response.discussionData, { silent: false })
|
||||
view = new DiscussionView el: $discussion[0], model: @model
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
|
||||
|
||||
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
|
||||
loadSimilarPost: (event) ->
|
||||
$title = @$(".new-post-title")
|
||||
$wrapper = @$(".new-post-similar-posts-wrapper")
|
||||
$similarPosts = @$(".new-post-similar-posts")
|
||||
prevText = $title.attr("prev-text")
|
||||
text = $title.val()
|
||||
if text == prevText
|
||||
if @$(".similar-post").length
|
||||
$wrapper.show()
|
||||
else if $.trim(text).length
|
||||
$elem = $(event.target)
|
||||
url = DiscussionUtil.urlFor 'search_similar_threads', @model.id
|
||||
data = { text: @$(".new-post-title").val() }
|
||||
DiscussionUtil.get $elem, url, data, (response, textStatus) =>
|
||||
$similarPosts.empty()
|
||||
if $.type(response) == "array" and response.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)
|
||||
for thread in response
|
||||
$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"
|
||||
newPost: ->
|
||||
if not @$(".wmd-panel").length
|
||||
view = { discussion_id: @model.id }
|
||||
@$el.children(".discussion-non-content").append Mustache.render DiscussionUtil.getTemplate("_new_post"), view
|
||||
$newPostBody = @$(".new-post-body")
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "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')
|
||||
$input = DiscussionUtil.getWmdInput @$el, $.proxy(@$, @), "new-post-body"
|
||||
$input.attr("placeholder", "post a new topic...")
|
||||
if @$el.hasClass("inline-discussion")
|
||||
$input.bind 'focus', (e) =>
|
||||
@$(".new-post-form").removeClass('collapsed')
|
||||
else if @$el.hasClass("forum-discussion")
|
||||
@$(".new-post-form").removeClass('collapsed')
|
||||
|
||||
$local(".new-post-tags").tagsInput Discussion.tagsInputOptions()
|
||||
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
|
||||
|
||||
$local(".new-post-title").blur ->
|
||||
handleSimilarPost(this)
|
||||
@$(".new-post-title").blur $.proxy(@loadSimilarPost, @)
|
||||
|
||||
$local(".hide-similar-posts").click ->
|
||||
$local(".new-post-similar-posts-wrapper").hide()
|
||||
@$(".hide-similar-posts").click =>
|
||||
@$(".new-post-similar-posts-wrapper").hide()
|
||||
|
||||
$local(".discussion-submit-post").click ->
|
||||
handleSubmitNewPost(this)
|
||||
$local(".discussion-cancel-post").click ->
|
||||
handleCancelNewPost(this)
|
||||
@$(".discussion-submit-post").click $.proxy(@submitNewPost, @)
|
||||
@$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @)
|
||||
|
||||
|
||||
$local(".new-post-form").show()
|
||||
@$(".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)
|
||||
submitNewPost: (event) ->
|
||||
title = @$(".new-post-title").val()
|
||||
body = DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), "new-post-body"
|
||||
tags = @$(".new-post-tags").val()
|
||||
anonymous = false || @$(".discussion-post-anonymously").is(":checked")
|
||||
autowatch = false || @$(".discussion-auto-watch").is(":checked")
|
||||
url = DiscussionUtil.urlFor('create_thread', @model.id)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
title: title
|
||||
body: body
|
||||
tags: tags
|
||||
anonymous: anonymous
|
||||
auto_subscribe: autowatch
|
||||
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
|
||||
$thread = $(response.html)
|
||||
@$el.children(".threads").prepend($thread)
|
||||
|
||||
handleAjaxSearch = (elem) ->
|
||||
$elem = $(elem)
|
||||
url = URI($elem.attr("action")).addSearch({text: $local(".search-input").val()})
|
||||
handleAjaxReloadDiscussion($elem, url)
|
||||
@$(".new-post-title").val("")
|
||||
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), "new-post-body", ""
|
||||
@$(".new-post-tags").val("")
|
||||
@$(".new-post-tags").importTags("")
|
||||
|
||||
handleAjaxSort = (elem) ->
|
||||
$elem = $(elem)
|
||||
url = $elem.attr("sort-url")
|
||||
handleAjaxReloadDiscussion($elem, url)
|
||||
thread = @model.addThread response.content
|
||||
threadView = new ThreadView el: $thread[0], model: thread
|
||||
thread.updateInfo response.annotated_content_info
|
||||
@cancelNewPost()
|
||||
|
||||
|
||||
handleAjaxPage = (elem) ->
|
||||
$elem = $(elem)
|
||||
url = $elem.attr("page-url")
|
||||
handleAjaxReloadDiscussion($elem, url)
|
||||
cancelNewPost: (event) ->
|
||||
if @$el.hasClass("inline-discussion")
|
||||
@$(".new-post-form").addClass("collapsed")
|
||||
else if @$el.hasClass("forum-discussion")
|
||||
@$(".new-post-form").hide()
|
||||
|
||||
if $discussion.hasClass("inline-discussion")
|
||||
initializeNewPost()
|
||||
search: (event) ->
|
||||
event.preventDefault()
|
||||
$elem = $(event.target)
|
||||
url = URI($elem.attr("action")).addSearch({text: @$(".search-input").val()})
|
||||
@reload($elem, url)
|
||||
|
||||
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()
|
||||
sort: ->
|
||||
$elem = $(event.target)
|
||||
url = $elem.attr("sort-url")
|
||||
@reload($elem, url)
|
||||
|
||||
Discussion.bindLocalEvents $local,
|
||||
page: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = $elem.attr("page-url")
|
||||
@reload($elem, url)
|
||||
|
||||
"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)
|
||||
events:
|
||||
"submit .search-wrapper>.discussion-search-form": "search"
|
||||
"click .discussion-search-link": "search"
|
||||
"click .discussion-sort-link": "sort"
|
||||
"click .discussion-page-link": "page"
|
||||
|
||||
@@ -1,42 +1,32 @@
|
||||
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
|
||||
class @DiscussionModuleView extends Backbone.View
|
||||
events:
|
||||
"click .discussion-show": "toggleDiscussion"
|
||||
toggleDiscussion: (event) ->
|
||||
if @showed
|
||||
@$("section.discussion").hide()
|
||||
$(event.target).html("Show Discussion")
|
||||
@showed = false
|
||||
else
|
||||
if @retrieved
|
||||
@$("section.discussion").show()
|
||||
$(event.target).html("Hide Discussion")
|
||||
@showed = true
|
||||
else
|
||||
$elem = $(event.target)
|
||||
discussion_id = $elem.attr("discussion_id")
|
||||
url = Discussion.urlFor 'retrieve_discussion', discussion_id
|
||||
url = DiscussionUtil.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)
|
||||
dataType: 'json'
|
||||
success: (response, textStatus) =>
|
||||
@$el.append(response.html)
|
||||
$discussion = @$el.find("section.discussion")
|
||||
$(event.target).html("Hide Discussion")
|
||||
discussion = new Discussion()
|
||||
discussion.reset(response.discussionData, {silent: false})
|
||||
view = new DiscussionView(el: $discussion[0], model: discussion)
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
|
||||
@retrieved = true
|
||||
@showed = true
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
$ ->
|
||||
|
||||
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()
|
||||
window.$$contents = {}
|
||||
window.$$discussions = {}
|
||||
|
||||
$(".discussion-module").each (index, elem) ->
|
||||
Discussion.initializeDiscussionModule(elem)
|
||||
view = new DiscussionModuleView(el: elem)
|
||||
|
||||
$("section.discussion").each (index, discussion) ->
|
||||
Discussion.initializeDiscussion(discussion)
|
||||
Discussion.bindDiscussionEvents(discussion)
|
||||
$("section.discussion").each (index, elem) ->
|
||||
discussionData = DiscussionUtil.getDiscussionData($(elem).attr("_id"))
|
||||
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)
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
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>
|
||||
"""
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
if not @Discussion?
|
||||
@Discussion = {}
|
||||
class @DiscussionUtil
|
||||
|
||||
Discussion = @Discussion
|
||||
@wmdEditors: {}
|
||||
|
||||
wmdEditors = {}
|
||||
@getTemplate: (id) ->
|
||||
$("script##{id}").html()
|
||||
|
||||
@Discussion = $.extend @Discussion,
|
||||
@getDiscussionData: (id) ->
|
||||
return $$discussion_data[id]
|
||||
|
||||
generateLocal: (elem) ->
|
||||
(selector) -> $(elem).find(selector)
|
||||
@addContent: (id, content) -> window.$$contents[id] = content
|
||||
|
||||
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")
|
||||
.attr("href", "javascript:void(0)")
|
||||
.addClass(cls).html(txt)
|
||||
.click -> handler(this)
|
||||
|
||||
urlFor: (name, param, param1, param2) ->
|
||||
@urlFor: (name, param, param1, param2) ->
|
||||
{
|
||||
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
|
||||
unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow"
|
||||
@@ -48,7 +58,7 @@ wmdEditors = {}
|
||||
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
|
||||
}[name]
|
||||
|
||||
safeAjax: (params) ->
|
||||
@safeAjax: (params) ->
|
||||
$elem = params.$elem
|
||||
if $elem.attr("disabled")
|
||||
return
|
||||
@@ -56,17 +66,31 @@ wmdEditors = {}
|
||||
$.ajax(params).always ->
|
||||
$elem.removeAttr("disabled")
|
||||
|
||||
handleAnchorAndReload: (response) ->
|
||||
#window.location = window.location.pathname + "#" + response['id']
|
||||
window.location.reload()
|
||||
@get: ($elem, url, data, success) ->
|
||||
@safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: "json"
|
||||
data: data
|
||||
success: success
|
||||
|
||||
bindLocalEvents: ($local, eventsHandler) ->
|
||||
@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
|
||||
[event, selector] = eventSelector.split(' ')
|
||||
$local(selector).unbind(event)[event] handler
|
||||
|
||||
tagsInputOptions: ->
|
||||
autocomplete_url: Discussion.urlFor('tags_autocomplete')
|
||||
@tagsInputOptions: ->
|
||||
autocomplete_url: @urlFor('tags_autocomplete')
|
||||
autocomplete:
|
||||
remoteDataType: 'json'
|
||||
interactive: true
|
||||
@@ -75,23 +99,7 @@ wmdEditors = {}
|
||||
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) ->
|
||||
@formErrorHandler: (errorsField) ->
|
||||
(xhr, textStatus, error) ->
|
||||
response = JSON.parse(xhr.responseText)
|
||||
if response.errors? and response.errors.length > 0
|
||||
@@ -99,13 +107,13 @@ wmdEditors = {}
|
||||
for error in response.errors
|
||||
errorsField.append($("<li>").addClass("new-post-form-error").html(error))
|
||||
|
||||
clearFormErrors: (errorsField) ->
|
||||
@clearFormErrors: (errorsField) ->
|
||||
errorsField.empty()
|
||||
|
||||
postMathJaxProcessor: (text) ->
|
||||
@postMathJaxProcessor: (text) ->
|
||||
RE_INLINEMATH = /^\$([^\$]*)\$/g
|
||||
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
|
||||
Discussion.processEachMathAndCode text, (s, type) ->
|
||||
@processEachMathAndCode text, (s, type) ->
|
||||
if type == 'display'
|
||||
s.replace RE_DISPLAYMATH, ($0, $1) ->
|
||||
"\\[" + $1 + "\\]"
|
||||
@@ -115,61 +123,43 @@ wmdEditors = {}
|
||||
else
|
||||
s
|
||||
|
||||
makeWmdEditor: ($content, $local, cls_identifier) ->
|
||||
@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
|
||||
imageUploadUrl = @urlFor('upload')
|
||||
_processor = (_this) ->
|
||||
(text) -> _this.postMathJaxProcessor(text)
|
||||
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
|
||||
@wmdEditors["#{cls_identifier}-#{id}"] = editor
|
||||
editor
|
||||
|
||||
getWmdEditor: ($content, $local, cls_identifier) ->
|
||||
@getWmdEditor: ($content, $local, cls_identifier) ->
|
||||
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")
|
||||
$local("#wmd-input-#{cls_identifier}-#{id}")
|
||||
|
||||
getWmdContent: ($content, $local, cls_identifier) ->
|
||||
Discussion.getWmdInput($content, $local, cls_identifier).val()
|
||||
@getWmdContent: ($content, $local, cls_identifier) ->
|
||||
@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()
|
||||
@setWmdContent: ($content, $local, cls_identifier, text) ->
|
||||
@getWmdInput($content, $local, cls_identifier).val(text)
|
||||
@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) ->
|
||||
@subscriptionLink: (type, id) ->
|
||||
followLink = ->
|
||||
Discussion.generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow)
|
||||
@generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow)
|
||||
|
||||
unfollowLink = ->
|
||||
Discussion.generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow)
|
||||
@generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow)
|
||||
|
||||
handleFollow = (elem) ->
|
||||
Discussion.safeAjax
|
||||
@safeAjax
|
||||
$elem: $(elem)
|
||||
url: Discussion.urlFor("follow_#{type}", id)
|
||||
url: @urlFor("follow_#{type}", id)
|
||||
type: "POST"
|
||||
success: (response, textStatus) ->
|
||||
if textStatus == "success"
|
||||
@@ -177,21 +167,21 @@ wmdEditors = {}
|
||||
dataType: 'json'
|
||||
|
||||
handleUnfollow = (elem) ->
|
||||
Discussion.safeAjax
|
||||
@safeAjax
|
||||
$elem: $(elem)
|
||||
url: Discussion.urlFor("unfollow_#{type}", id)
|
||||
url: @urlFor("unfollow_#{type}", id)
|
||||
type: "POST"
|
||||
success: (response, textStatus) ->
|
||||
if textStatus == "success"
|
||||
$(elem).replaceWith followLink()
|
||||
dataType: 'json'
|
||||
|
||||
if Discussion.isSubscribed(id, type)
|
||||
if @isSubscribed(id, type)
|
||||
unfollowLink()
|
||||
else
|
||||
followLink()
|
||||
|
||||
processEachMathAndCode: (text, processor) ->
|
||||
@processEachMathAndCode: (text, processor) ->
|
||||
|
||||
codeArchive = []
|
||||
|
||||
@@ -242,3 +232,18 @@ wmdEditors = {}
|
||||
text = $div.html()
|
||||
|
||||
text
|
||||
|
||||
@unescapeHighlightTag: (text) ->
|
||||
text.replace(/\<\;highlight\>\;/g, "<span class='search-highlight'>")
|
||||
.replace(/\<\;\/highlight\>\;/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
|
||||
|
||||
412
lms/static/coffee/src/old_discussion/content.coffee
Normal file
412
lms/static/coffee/src/old_discussion/content.coffee
Normal file
@@ -0,0 +1,412 @@
|
||||
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(/\<\;highlight\>\;/g, "<span class='search-highlight'>")
|
||||
.replace(/\<\;\/highlight\>\;/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"
|
||||
190
lms/static/coffee/src/old_discussion/discussion.coffee
Normal file
190
lms/static/coffee/src/old_discussion/discussion.coffee
Normal file
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
@@ -0,0 +1,42 @@
|
||||
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)
|
||||
23
lms/static/coffee/src/old_discussion/main.coffee
Normal file
23
lms/static/coffee/src/old_discussion/main.coffee
Normal file
@@ -0,0 +1,23 @@
|
||||
$ ->
|
||||
|
||||
#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"))
|
||||
73
lms/static/coffee/src/old_discussion/templates.coffee
Normal file
73
lms/static/coffee/src/old_discussion/templates.coffee
Normal file
@@ -0,0 +1,73 @@
|
||||
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>
|
||||
"""
|
||||
34
lms/static/coffee/src/old_discussion/user_profile.coffee
Normal file
34
lms/static/coffee/src/old_discussion/user_profile.coffee
Normal file
@@ -0,0 +1,34 @@
|
||||
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) ->
|
||||
244
lms/static/coffee/src/old_discussion/utils.coffee
Normal file
244
lms/static/coffee/src/old_discussion/utils.coffee
Normal file
@@ -0,0 +1,244 @@
|
||||
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;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.comment-count {
|
||||
.show-comments-wrapper {
|
||||
display: inline;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<%include file="../discussion/_js_head_dependencies.html" />
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
@@ -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/python/python.js')}"></script>
|
||||
|
||||
<%static:js group='courseware'/>
|
||||
<%static:js group='courseware'/>
|
||||
|
||||
<%include file="../discussion/_js_dependencies.html" />
|
||||
<%include file="/mathjax_include.html" />
|
||||
<%include file="../discussion/_js_body_dependencies.html" />
|
||||
|
||||
|
||||
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
</%def>
|
||||
|
||||
<%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)}
|
||||
% if content.get('children') is not None:
|
||||
${render_comments(content['children'])}
|
||||
% endif
|
||||
${render_comments(content.get('children', []))}
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<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">
|
||||
<%include file="_search_bar.html" />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<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">
|
||||
% for thread in threads:
|
||||
|
||||
4
lms/templates/discussion/_js_body_dependencies.html
Normal file
4
lms/templates/discussion/_js_body_dependencies.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<%! from django_comment_client.helpers import include_mustache_templates %>
|
||||
|
||||
<%include file="/mathjax_include.html" />
|
||||
${include_mustache_templates()}
|
||||
@@ -1,10 +1,13 @@
|
||||
<%! from django.template.defaultfilters import escapejs %>
|
||||
|
||||
<script type="text/javascript">
|
||||
var $$user_info = JSON.parse("${user_info | 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 = {};
|
||||
}
|
||||
$$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>
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
<%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/jquery.ajaxfileupload.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/Markdown.Converter.js')}"></script>
|
||||
@@ -27,5 +10,8 @@
|
||||
<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">
|
||||
|
||||
17
lms/templates/discussion/_js_head_dependencies.html
Normal file
17
lms/templates/discussion/_js_head_dependencies.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<%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 @@
|
||||
% endfor
|
||||
</%def>
|
||||
|
||||
<div class="discussion-${discussion_type}-paginator discussion-paginator">
|
||||
<div class="discussion-${discussion_type}-paginator discussion-paginator local">
|
||||
<div class="prev-page">
|
||||
% if page > 1:
|
||||
${link_to_page(page - 1, "< Previous page")}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
<section class="discussion" _id="${discussion_id}">
|
||||
<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>
|
||||
|
||||
<%include file="_js_data.html" />
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<a class="discussion-sort-link ${cls}" href="javascript:void(0)" sort-url="${url_for_sort(key, order)}">${title}</a>
|
||||
</%def>
|
||||
|
||||
<div class="discussion-sort discussion-local">
|
||||
<div class="discussion-sort local">
|
||||
<span class="discussion-label">Sort by:</span>
|
||||
${link_to_sort('activity', 'top')}
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<%include file="_js_head_dependencies.html" />
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%include file="_js_dependencies.html" />
|
||||
<%include file="_js_body_dependencies.html" />
|
||||
</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='discussion'" />
|
||||
<%include file="../courseware/course_navigation.html" args="active_page='discussion'" />
|
||||
|
||||
<section class="container">
|
||||
<div class="course-wrapper">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<div class="discussion-content">
|
||||
<div class="discussion-content local">
|
||||
<div class="discussion-content-wrapper">
|
||||
<div class="discussion-votes">
|
||||
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)">▲</a>
|
||||
<a class="discussion-vote discussion-vote-up" href="javascript:void(0)" value="up">▲</a>
|
||||
<div class="discussion-votes-point">{{content.votes.point}}</div>
|
||||
<a class="discussion-vote discussion-vote-down" href="javascript:void(0)">▼</a>
|
||||
<a class="discussion-vote discussion-vote-down" href="javascript:void(0)" value="down">▼</a>
|
||||
</div>
|
||||
<div class="discussion-right-wrapper">
|
||||
<ul class="admin-actions">
|
||||
@@ -15,12 +15,12 @@
|
||||
{{/thread}}
|
||||
</ul>
|
||||
{{#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>
|
||||
{{/thread}}
|
||||
<div class="discussion-content-view">
|
||||
<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>
|
||||
{{#thread}}
|
||||
<div class="thread-tags">
|
||||
@@ -37,23 +37,25 @@
|
||||
anonymous
|
||||
{{/content.anonymous}}
|
||||
{{^content.anonymous}}
|
||||
<a href="{{##url_for_user}}{{content.user_id}}{{/url_for_user}}">{{content.username}}</a>
|
||||
{{content.username}}
|
||||
{{/content.anonymous}}
|
||||
</div>
|
||||
<div class="comment-count">
|
||||
<div class="show-comments-wrapper">
|
||||
{{#thread}}
|
||||
{{#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}}
|
||||
<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}}
|
||||
{{/thread}}
|
||||
</div>
|
||||
<ul class="discussion-actions">
|
||||
<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>
|
||||
<li><a class="discussion-link discussion-permanent-link" href="javascript:void(0)">Permanent Link</a></li>
|
||||
{{#thread}}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
8
lms/templates/discussion/mustache/_edit_comment.mustache
Normal file
8
lms/templates/discussion/mustache/_edit_comment.mustache
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
10
lms/templates/discussion/mustache/_edit_thread.mustache
Normal file
10
lms/templates/discussion/mustache/_edit_thread.mustache
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
21
lms/templates/discussion/mustache/_new_post.mustache
Normal file
21
lms/templates/discussion/mustache/_new_post.mustache
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
15
lms/templates/discussion/mustache/_reply.mustache
Normal file
15
lms/templates/discussion/mustache/_reply.mustache
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user