+ % if state == 'unsubmitted': + unanswered + % elif state == 'correct': + correct + % elif state == 'incorrect': + incorrect + % elif state == 'incomplete': + incomplete + % endif +
-- % if state == 'unsubmitted': - unanswered - % elif state == 'correct': - correct - % elif state == 'incorrect': - incorrect - % elif state == 'incomplete': - incomplete - % endif -
+ - - -commit_id=%s
' % new_commit_id + track.views.server_track(request, 'reloaded %s now at %s (pid=%s)' % (reload_dir, + new_commit_id, + os.getpid()), {}, page='migrate') #---------------------------------------- @@ -94,6 +127,8 @@ def manage_modulestores(request,reload_dir=None): html += 'commit_id=%s
' % get_commit_id(course) + for field in dumpfields: data = getattr(course,field) html += 'Grade distribution: %s
" % gsv + + # generate grade histogram + ghist = [] + + axisopts = """{ + xaxes: [{ + axisLabel: 'Grade' + }], + yaxes: [{ + position: 'left', + axisLabel: 'Count' + }] + }""" + + if gsv.max > max_grade: + msg += "Something is wrong: max_grade=%s, but max(grades)=%s
" % (max_grade, gsv.max) + max_grade = gsv.max + + if max_grade > 1: + ghist = make_histogram(grades, np.linspace(0, max_grade, max_grade + 1)) + ghist_json = json.dumps(ghist.items()) + + plot = {'title': "Grade histogram for %s" % problem, + 'id': 'histogram', + 'info': '', + 'data': "var dhist = %s;\n" % ghist_json, + 'cmd': '[ {data: dhist, bars: { show: true, align: "center" }} ], %s' % axisopts, + } + plots.append(plot) + else: + msg += "Time differences between checks: %s
" % dtsv + bins = np.linspace(0, 1.5 * dtsv.sdv(), 30) + dbar = bins[1] - bins[0] + thist = make_histogram(dtset, bins) + thist_json = json.dumps(sorted(thist.items(), key=lambda(x): x[0])) + + axisopts = """{ xaxes: [{ axisLabel: 'Time (min)'}], yaxes: [{position: 'left',axisLabel: 'Count'}]}""" + + plot = {'title': "Histogram of time differences between checks", + 'id': 'thistogram', + 'info': '', + 'data': "var thist = %s;\n" % thist_json, + 'cmd': '[ {data: thist, bars: { show: true, align: "center", barWidth:%f }} ], %s' % (dbar, axisopts), + } + plots.append(plot) + + # one IRT plot curve for each grade received (TODO: this assumes integer grades) + for grade in range(1, int(max_grade) + 1): + yset = {} + gset = pmdset.filter(studentmodule__grade=grade) + ngset = gset.count() + if ngset == 0: + continue + ydat = [] + ylast = 0 + for x in xdat: + y = gset.filter(attempts=x).count() / ngset + ydat.append( y + ylast ) + ylast = y + ylast + yset['ydat'] = ydat + + if len(ydat) > 3: # try to fit to logistic function if enough data points + cfp = curve_fit(func_2pl, xdat, ydat, [1.0, max_attempts / 2.0]) + yset['fitparam'] = cfp + yset['fitpts'] = func_2pl(np.array(xdat), *cfp[0]) + yset['fiterr'] = [yd - yf for (yd, yf) in zip(ydat, yset['fitpts'])] + fitx = np.linspace(xdat[0], xdat[-1], 100) + yset['fitx'] = fitx + yset['fity'] = func_2pl(np.array(fitx), *cfp[0]) + + dataset['grade_%d' % grade] = yset + + axisopts = """{ + xaxes: [{ + axisLabel: 'Number of Attempts' + }], + yaxes: [{ + max:1.0, + position: 'left', + axisLabel: 'Probability of correctness' + }] + }""" + + # generate points for flot plot + for grade in range(1, int(max_grade) + 1): + jsdata = "" + jsplots = [] + gkey = 'grade_%d' % grade + if gkey in dataset: + yset = dataset[gkey] + jsdata += "var d%d = %s;\n" % (grade, json.dumps(zip(xdat, yset['ydat']))) + jsplots.append('{ data: d%d, lines: { show: false }, points: { show: true}, color: "red" }' % grade) + if 'fitpts' in yset: + jsdata += 'var fit = %s;\n' % (json.dumps(zip(yset['fitx'], yset['fity']))) + jsplots.append('{ data: fit, lines: { show: true }, color: "blue" }') + (a, b) = yset['fitparam'][0] + irtinfo = "(2PL: D=1.7, a=%6.3f, b=%6.3f)" % (a, b) + else: + irtinfo = "" + + plots.append({'title': 'IRT Plot for grade=%s %s' % (grade, irtinfo), + 'id': "irt%s" % grade, + 'info': '', + 'data': jsdata, + 'cmd': '[%s], %s' % (','.join(jsplots), axisopts), + }) + + #log.debug('plots = %s' % plots) + return msg, plots + +#----------------------------------------------------------------------------- + + +def make_psychometrics_data_update_handler(studentmodule): + """ + Construct and return a procedure which may be called to update + the PsychometricsData instance for the given StudentModule instance. + """ + sm = studentmodule + try: + pmd = PsychometricData.objects.using(db).get(studentmodule=sm) + except PsychometricData.DoesNotExist: + pmd = PsychometricData(studentmodule=sm) + + def psychometrics_data_update_handler(state): + """ + This function may be called each time a problem is successfully checked + (eg on save_problem_check events in capa_module). + + state = instance state (a nice, uniform way to interface - for more future psychometric feature extraction) + """ + try: + state = json.loads(sm.state) + done = state['done'] + except: + log.exception("Oops, failed to eval state for %s (state=%s)" % (sm, sm.state)) + return + + pmd.done = done + pmd.attempts = state['attempts'] + try: + checktimes = eval(pmd.checktimes) # update log of attempt timestamps + except: + checktimes = [] + checktimes.append(datetime.datetime.now()) + pmd.checktimes = checktimes + try: + pmd.save() + except: + log.exception("Error in updating psychometrics data for %s" % sm) + + return psychometrics_data_update_handler diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 136f311a5d..08150c9acd 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -70,6 +70,7 @@ SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"] AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"] +AWS_STORAGE_BUCKET_NAME = 'edxuploads' DATABASES = AUTH_TOKENS['DATABASES'] diff --git a/lms/envs/common.py b/lms/envs/common.py index 63301d420b..6c127b8049 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -71,6 +71,8 @@ MITX_FEATURES = { 'ENABLE_DISCUSSION' : False, 'ENABLE_DISCUSSION_SERVICE': True, + 'ENABLE_PSYCHOMETRICS': False, # real-time psychometrics (eg item response theory analysis in instructor dashboard) + 'ENABLE_SQL_TRACKING_LOGS': False, 'ENABLE_LMS_MIGRATION': False, 'ENABLE_MANUAL_GIT_RELOAD': False, @@ -441,12 +443,12 @@ courseware_only_js += [ main_vendor_js = [ 'js/vendor/jquery.min.js', 'js/vendor/jquery-ui.min.js', - 'js/vendor/swfobject/swfobject.js', 'js/vendor/jquery.cookie.js', 'js/vendor/jquery.qtip.min.js', + 'js/vendor/swfobject/swfobject.js', ] -discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/*.coffee')) +discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/**/*.coffee')) # Load javascript from all of the available xmodules, and # prep it for use in pipeline js @@ -619,6 +621,7 @@ INSTALLED_APPS = ( 'util', 'certificates', 'instructor', + 'psychometrics', #For the wiki 'wiki', # The new django-wiki from benjaoming diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 50befeb875..a9f1454193 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -20,6 +20,8 @@ MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = False # Enable to test subdomains- MITX_FEATURES['SUBDOMAIN_BRANDING'] = True MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True +MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) + WIKI_ENABLED = True diff --git a/lms/lib/comment_client/comment.py b/lms/lib/comment_client/comment.py index 52a0aef70f..6d0bafee02 100644 --- a/lms/lib/comment_client/comment.py +++ b/lms/lib/comment_client/comment.py @@ -7,15 +7,14 @@ import settings class Comment(models.Model): accessible_fields = [ - 'id', 'body', 'anonymous', 'course_id', - 'endorsed', 'parent_id', 'thread_id', - 'username', 'votes', 'user_id', 'closed', - 'created_at', 'updated_at', 'depth', - 'at_position_list', 'type', 'commentable_id', + 'id', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', + 'endorsed', 'parent_id', 'thread_id', 'username', 'votes', 'user_id', + 'closed', 'created_at', 'updated_at', 'depth', 'at_position_list', + 'type', 'commentable_id', ] updatable_fields = [ - 'body', 'anonymous', 'course_id', 'closed', + 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed', 'user_id', 'endorsed', ] diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 1f6d081f7f..bda032bbdf 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -6,16 +6,14 @@ import settings class Thread(models.Model): accessible_fields = [ - 'id', 'title', 'body', 'anonymous', - 'course_id', 'closed', 'tags', 'votes', - 'commentable_id', 'username', 'user_id', - 'created_at', 'updated_at', 'comments_count', - 'at_position_list', 'children', 'type', - 'highlighted_title', 'highlighted_body', + 'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', + 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', + 'created_at', 'updated_at', 'comments_count', 'at_position_list', + 'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed' ] updatable_fields = [ - 'title', 'body', 'anonymous', 'course_id', + 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'closed', 'tags', 'user_id', 'commentable_id', ] @@ -32,7 +30,7 @@ class Thread(models.Model): 'course_id': query_params['course_id'], 'recursive': False} params = merge_dict(default_params, strip_blank(strip_none(query_params))) - if query_params.get('text') or query_params.get('tags'): + if query_params.get('text') or query_params.get('tags') or query_params.get('commentable_ids'): url = cls.url(action='search') else: url = cls.url(action='get_all', params=extract(params, 'commentable_id')) @@ -40,7 +38,7 @@ class Thread(models.Model): del params['commentable_id'] response = perform_request('get', url, params, *args, **kwargs) return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1) - + @classmethod def url_for_threads(cls, params={}): if params.get('commentable_id'): diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index eea6fda407..9813e9a199 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -8,7 +8,8 @@ class User(models.Model): accessible_fields = ['username', 'email', 'follower_ids', 'upvoted_ids', 'downvoted_ids', 'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id', 'subscribed_thread_ids', 'subscribed_commentable_ids', - 'threads_count', 'comments_count', 'default_sort_key' + 'subscribed_course_ids', 'threads_count', 'comments_count', + 'default_sort_key' ] updatable_fields = ['username', 'external_id', 'email', 'default_sort_key'] diff --git a/lms/lib/comment_client/utils.py b/lms/lib/comment_client/utils.py index 417d4eda8c..f50797d5e0 100644 --- a/lms/lib/comment_client/utils.py +++ b/lms/lib/comment_client/utils.py @@ -28,9 +28,9 @@ def perform_request(method, url, data_or_params=None, *args, **kwargs): data_or_params['api_key'] = settings.API_KEY try: if method in ['post', 'put', 'patch']: - response = requests.request(method, url, data=data_or_params) + response = requests.request(method, url, data=data_or_params, timeout=5) else: - response = requests.request(method, url, params=data_or_params) + response = requests.request(method, url, params=data_or_params, timeout=5) except Exception as err: log.exception("Trying to call {method} on {url} with params {params}".format( method=method, url=url, params=data_or_params)) diff --git a/lms/static/coffee/src/customwmd.coffee b/lms/static/coffee/src/customwmd.coffee index 74be7ddbfe..838112059e 100644 --- a/lms/static/coffee/src/customwmd.coffee +++ b/lms/static/coffee/src/customwmd.coffee @@ -45,6 +45,7 @@ $ -> removeMath: (text) -> + text = text || "" @math = [] start = end = last = null braces = 0 @@ -111,7 +112,7 @@ $ -> (text) -> _this.replaceMath(text) if Markdown? - + Markdown.getMathCompatibleConverter = (postProcessor) -> postProcessor ||= ((text) -> text) converter = Markdown.getSanitizingConverter() @@ -123,11 +124,9 @@ $ -> Markdown.makeWmdEditor = (elem, appended_id, imageUploadUrl, postProcessor) -> $elem = $(elem) - if not $elem.length console.log "warning: elem for makeWmdEditor doesn't exist" return - if not $elem.find(".wmd-panel").length initialText = $elem.html() $elem.empty() @@ -162,7 +161,7 @@ $ -> alert(e) if startUploadHandler $('#file-upload').unbind('change').change(startUploadHandler) - + imageUploadHandler = (elem, input) -> ajaxFileUpload(imageUploadUrl, input, imageUploadHandler) diff --git a/lms/static/coffee/src/discussion/content.coffee b/lms/static/coffee/src/discussion/content.coffee index 9f95c201f4..d489782571 100644 --- a/lms/static/coffee/src/discussion/content.coffee +++ b/lms/static/coffee/src/discussion/content.coffee @@ -1,6 +1,9 @@ if Backbone? class @Content extends Backbone.Model + @contents: {} + @contentInfos: {} + template: -> DiscussionUtil.getTemplate('_content') actions: @@ -9,19 +12,20 @@ if Backbone? can_endorse: '.admin-endorse' can_delete: '.admin-delete' can_openclose: '.admin-openclose' - + urlMappers: {} urlFor: (name) -> @urlMappers[name].apply(@) can: (action) -> - DiscussionUtil.getContentInfo @id, action + (@get('ability') || {})[action] updateInfo: (info) -> - @set('ability', info.ability) - @set('voted', info.voted) - @set('subscribed', info.subscribed) + if info + @set('ability', info.ability) + @set('voted', info.voted) + @set('subscribed', info.subscribed) addComment: (comment, options) -> options ||= {} @@ -32,12 +36,14 @@ if Backbone? @get('children').push comment model = new Comment $.extend {}, comment, { thread: @get('thread') } @get('comments').add model + @trigger "comment:add" model removeComment: (comment) -> thread = @get('thread') comments_count = parseInt(thread.get('comments_count')) thread.set('comments_count', comments_count - 1 - comment.getCommentsCount()) + @trigger "comment:remove" resetComments: (children) -> @set 'children', [] @@ -46,364 +52,33 @@ if Backbone? @addComment comment, { silent: true } initialize: -> - DiscussionUtil.addContent @id, @ + Content.addContent @id, @ + if Content.getInfo(@id) + @updateInfo(Content.getInfo(@id)) + @set 'user_url', DiscussionUtil.urlFor('user_profile', @get('user_id')) @resetComments(@get('children')) - - class @ContentView extends Backbone.View + remove: -> - $: (selector) -> - @$local.find(selector) - - partial: - endorsed: (endorsed) -> - if endorsed - @$el.addClass("endorsed") - else - @$el.removeClass("endorsed") - - 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 - @$el.removeClass("closed") - @$(".admin-openclose").text "Close Thread" - - 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"] - - votes_point: (votes_point) -> - @$(".discussion-votes-point").html(votes_point) - - comments_count: (comments_count) -> - @$(".comments-count").html(comments_count) - - subscribed: (subscribed) -> - if subscribed - @$(".discussion-follow-thread").addClass("discussion-unfollow-thread").html("Unfollow") - else - @$(".discussion-follow-thread").removeClass("discussion-unfollow-thread").html("Follow") - - ability: (ability) -> - for action, elemSelector of @model.actions - if not ability[action] - @$(elemSelector).parent().hide() - else - @$(elemSelector).parent().show() - - $discussionContent: -> - @_discussionContent ||= @$el.children(".discussion-content") - - $showComments: -> - @_showComments ||= @$(".discussion-show-comments") - - updateShowComments: -> - if @showed - @$showComments().html @$showComments().html().replace "Show", "Hide" + if @get('type') == 'comment' + @get('thread').removeComment(@) + @get('thread').trigger "comment:remove", @ else - @$showComments().html @$showComments().html().replace "Hide", "Show" + @trigger "thread:remove", @ - retrieved: -> - @$showComments().hasClass("retrieved") - - hideSingleThread: (event) -> - @$el.children(".comments").hide() - @showed = false - @updateShowComments() + @addContent: (id, content) -> @contents[id] = content - showSingleThread: (event) -> - if @retrieved() - @$el.children(".comments").show() - @showed = true - @updateShowComments() - else - $elem = $.merge @$(".thread-title"), @$showComments() - url = @model.urlFor('retrieve') - DiscussionUtil.safeAjax - $elem: $elem - $loading: @$(".discussion-show-comments") - type: "GET" - url: url - success: (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 + @getContent: (id) -> @contents[id] - 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 + @getInfo: (id) -> + @contentInfos[id] - 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() + @loadContentInfos: (infos) -> + for id, info of infos + if @getContent(id) + @getContent(id).updateInfo(info) + $.extend @contentInfos, infos - 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) - $loading: $(event.target) if event - 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 - if autowatch - @model.get('thread').set('subscribed', true) - @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.safeAjax - $elem: $elem - url: url - type: "POST" - success: (response, textStatus) => - @model.set('voted', '') - @model.set('votes_point', response.votes.point) - - vote: (event, value) -> - url = @model.urlFor("#{value}vote") - $elem = @$(".discussion-vote") - DiscussionUtil.safeAjax - $elem: $elem - url: url - type: "POST" - success: (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.safeAjax - $elem: $elem - url: url - data: data - type: "POST" - success: (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.safeAjax - $elem: $elem - url: url - type: "POST" - success: (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.safeAjax - $elem: $elem - url: url - type: "POST" - data: data - success: (response, textStatus) => - @model.set('closed', not closed) - @model.set('ability', response.ability) - - 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 = @model.get('title') - view.body = @model.get('body') - view.tags = @model.get('tags') - else - view.body = @model.get('body') - @$discussionContent().append Mustache.render DiscussionUtil.getTemplate("_edit_#{@model.get('type')}"), view - DiscussionUtil.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) - $loading: $(event.target) if event - 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) - if @model.get('type') == 'thread' - @model = new Thread response.content - else - @model = new Comment $.extend {}, response.content, { thread: @model.get('thread') } - @reconstruct() - @model.updateInfo response.annotated_content_info, { forceUpdate: true } - - 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.safeAjax - $elem: $elem - url: url - type: "POST" - success: (response, textStatus) => - @$el.remove() - if @model.get('type') == 'comment' - @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").each (index, element) -> - elem = $(element) - elem.html("posted on #{$.timeago.parse(elem.html()).toLocaleString()}") - @$("span.timeago").timeago() - - 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() - - reconstruct: -> - @initBindings() - @initLocal() - @initTimeago() - @initTitle() - @initBody() - @delegateEvents() - class @Thread extends @Content urlMappers: 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id) @@ -421,7 +96,38 @@ if Backbone? @set('thread', @) super() - class @ThreadView extends @ContentView + comment: -> + @set("comments_count", parseInt(@get("comments_count")) + 1) + + follow: -> + @set('subscribed', true) + + unfollow: -> + @set('subscribed', false) + + vote: -> + @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1 + @trigger "change", @ + + unvote: -> + @get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1 + @trigger "change", @ + + display_body: -> + if @has("highlighted_body") + String(@get("highlighted_body")).replace(/