diff --git a/lms/djangoapps/django_comment_client/base/views.py b/lms/djangoapps/django_comment_client/base/views.py index 68250a035e..2fee03c5bd 100644 --- a/lms/djangoapps/django_comment_client/base/views.py +++ b/lms/djangoapps/django_comment_client/base/views.py @@ -38,11 +38,10 @@ def permitted(fn): else: content = None return content - if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name): return fn(request, *args, **kwargs) else: - return JsonError("unauthorized") + return JsonError("unauthorized", status=401) return wrapper def ajax_content_response(request, course_id, content, template_name): @@ -214,7 +213,7 @@ def undo_vote_for_thread(request, course_id, thread_id): thread = cc.Thread.find(thread_id) user.unvote(thread) return JsonResponse(utils.safe_content(thread.to_dict())) - + @require_POST @login_required @@ -288,7 +287,7 @@ def update_moderator_status(request, course_id, user_id): course = get_course_with_access(request.user, course_id, 'load') discussion_user = cc.User(id=user_id, course_id=course_id) context = { - 'course': course, + 'course': course, 'course_id': course_id, 'user': request.user, 'django_user': user, @@ -327,7 +326,7 @@ def tags_autocomplete(request, course_id): @require_POST @login_required @csrf.csrf_exempt -def upload(request, course_id):#ajax upload file to a question or answer +def upload(request, course_id):#ajax upload file to a question or answer """view that handles file upload via Ajax """ @@ -337,7 +336,7 @@ def upload(request, course_id):#ajax upload file to a question or answer new_file_name = '' try: # TODO authorization - #may raise exceptions.PermissionDenied + #may raise exceptions.PermissionDenied #if request.user.is_anonymous(): # msg = _('Sorry, anonymous users cannot upload files') # raise exceptions.PermissionDenied(msg) @@ -357,7 +356,7 @@ def upload(request, course_id):#ajax upload file to a question or answer new_file_name = str( time.time() ).replace( - '.', + '.', str(random.randint(0,100000)) ) + file_extension @@ -386,7 +385,7 @@ def upload(request, course_id):#ajax upload file to a question or answer parsed_url = urlparse.urlparse(file_url) file_url = urlparse.urlunparse( urlparse.ParseResult( - parsed_url.scheme, + parsed_url.scheme, parsed_url.netloc, parsed_url.path, '', '', '' diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index af896f6f80..c21bfcbd6f 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -136,10 +136,16 @@ def inline_discussion(request, course_id, discussion_id): # html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, \ # query_params=query_params) 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), {}) return utils.JsonResponse({ # 'html': html, 'discussion_data': map(utils.safe_content, threads), 'user_info': user_info, + 'annotated_content_info': annotated_content_info }) def render_search_bar(request, course_id, discussion_id=None, text=''): diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index b0a570caad..1c99cdc72b 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -100,24 +100,24 @@ def initialize_discussion_info(course): unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key}) - category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)} + category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)} for category_path, entries in unexpanded_category_map.items(): node = category_map["subcategories"] path = [x.strip() for x in category_path.split("/")] for level in path[:-1]: if level not in node: - node[level] = {"subcategories": defaultdict(dict), + node[level] = {"subcategories": defaultdict(dict), "entries": defaultdict(dict), - "sort_key": level} + "sort_key": level} node = node[level]["subcategories"] level = path[-1] if level not in node: - node[level] = {"subcategories": defaultdict(dict), - "entries": defaultdict(dict), + node[level] = {"subcategories": defaultdict(dict), + "entries": defaultdict(dict), "sort_key": level} for entry in entries: - node[level]["entries"][entry["title"]] = {"id": entry["id"], + node[level]["entries"][entry["title"]] = {"id": entry["id"], "sort_key": entry["sort_key"]} for topic, entry in course.metadata.get('discussion_topics', {}).items(): @@ -134,7 +134,7 @@ def initialize_discussion_info(course): def get_courseware_context(content, course): id_map = get_discussion_id_map(course) - id = content['commentable_id'] + id = content['commentable_id'] content_info = None if id in id_map: location = id_map[id]["location"].url() @@ -149,21 +149,21 @@ class JsonResponse(HttpResponse): mimetype='application/json; charset=utf8') class JsonError(HttpResponse): - def __init__(self, error_messages=[]): + def __init__(self, error_messages=[], status=400): if isinstance(error_messages, str): error_messages = [error_messages] content = simplejson.dumps({'errors': error_messages}, indent=2, ensure_ascii=False) super(JsonError, self).__init__(content, - mimetype='application/json; charset=utf8', status=400) + mimetype='application/json; charset=utf8', status=status) class HtmlResponse(HttpResponse): def __init__(self, html=''): super(HtmlResponse, self).__init__(html, content_type='text/plain') -class ViewNameMiddleware(object): - def process_view(self, request, view_func, view_args, view_kwargs): +class ViewNameMiddleware(object): + def process_view(self, request, view_func, view_args, view_kwargs): request.view_name = view_func.__name__ class QueryCountDebugMiddleware(object): diff --git a/lms/lib/comment_client/thread.py b/lms/lib/comment_client/thread.py index 1f6d081f7f..e4ada77499 100644 --- a/lms/lib/comment_client/thread.py +++ b/lms/lib/comment_client/thread.py @@ -12,6 +12,7 @@ class Thread(models.Model): 'created_at', 'updated_at', 'comments_count', 'at_position_list', 'children', 'type', 'highlighted_title', 'highlighted_body', + 'endorsed' ] updatable_fields = [ diff --git a/lms/static/coffee/src/discussion/discussion.coffee b/lms/static/coffee/src/discussion/discussion.coffee index 002593855c..2944449f61 100644 --- a/lms/static/coffee/src/discussion/discussion.coffee +++ b/lms/static/coffee/src/discussion/discussion.coffee @@ -7,7 +7,6 @@ if Backbone? item.discussion = @ @comparator = @sortByDateRecentFirst @on "thread:remove", (thread) => - console.log "remove triggered" @remove(thread) find: (id) -> @@ -24,8 +23,8 @@ if Backbone? sortByDateRecentFirst: (thread) -> -(new Date(thread.get("created_at")).getTime()) - #return String.fromCharCode.apply(String, - # _.map(thread.get("created_at").split(""), + #return String.fromCharCode.apply(String, + # _.map(thread.get("created_at").split(""), # ((c) -> return 0xffff - c.charChodeAt())) #) @@ -134,7 +133,7 @@ if Backbone? @$(".discussion-submit-post").click $.proxy(@submitNewPost, @) @$(".discussion-cancel-post").click $.proxy(@cancelNewPost, @) - + @$el.children(".blank").hide() @$(".new-post-form").show() @@ -177,7 +176,7 @@ if Backbone? threadView = new ThreadView el: $thread[0], model: thread thread.updateInfo response.annotated_content_info @cancelNewPost() - + cancelNewPost: (event) -> if @$el.hasClass("inline-discussion") diff --git a/lms/static/coffee/src/discussion/discussion_module.coffee b/lms/static/coffee/src/discussion/discussion_module.coffee deleted file mode 100644 index 360a3a2535..0000000000 --- a/lms/static/coffee/src/discussion/discussion_module.coffee +++ /dev/null @@ -1,39 +0,0 @@ -if Backbone? - 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 = DiscussionUtil.urlFor 'retrieve_discussion', discussion_id - DiscussionUtil.safeAjax - $elem: $elem - $loading: $elem - url: url - type: "GET" - dataType: 'json' - success: (response, textStatus) => - #@$el.append(response.html) - window.user = new DiscussionUser(response.user_info) - $(event.target).html("Hide Discussion") - discussion = new Discussion() - discussion.reset(response.discussion_data, {silent: false}) - $discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data}) - $(".discussion-module").append($discussion) - discussion.each (thread) -> - element = $("article#thread_#{thread.id}") - dtv = new DiscussionThreadInlineView el: element, model: thread - dtv.render() - DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) - @retrieved = true - @showed = true diff --git a/lms/static/coffee/src/discussion/discussion_module_view.coffee b/lms/static/coffee/src/discussion/discussion_module_view.coffee new file mode 100644 index 0000000000..06ed320623 --- /dev/null +++ b/lms/static/coffee/src/discussion/discussion_module_view.coffee @@ -0,0 +1,64 @@ +if Backbone? + class @DiscussionModuleView extends Backbone.View + events: + "click .discussion-show": "toggleDiscussion" + "click .new-post-btn": "toggleNewPost" + "click .new-post-cancel": "hideNewPost" + initialize: -> + + toggleNewPost: (event) -> + if @newPostForm.is(':hidden') + @newPostForm.slideDown(300) + else + @newPostForm.slideUp(300) + hideNewPost: (event) -> + @newPostForm.slideUp(300) + + 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) + discussionId = $elem.data("discussion-id") + url = DiscussionUtil.urlFor 'retrieve_discussion', discussionId + DiscussionUtil.safeAjax + $elem: $elem + $loading: $elem + url: url + type: "GET" + dataType: 'json' + success: (response, textStatus, jqXHR) => @createDiscussion(event, response, textStatus, discussionId) + + createDiscussion: (event, response, textStatus, discussionId) => + window.user = new DiscussionUser(response.user_info) + Content.loadContentInfos(response.annotated_content_info) + $(event.target).html("Hide Discussion") + @discussion = new Discussion() + @discussion.reset(response.discussion_data, {silent: false}) + $discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data, 'discussionId': discussionId}) + $(".discussion-module").append($discussion) + @newPostForm = $('.new-post-article') + @threadviews = @discussion.map (thread) -> + new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread + _.each @threadviews, (dtv) -> dtv.render() + DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info) + @newPostView = new NewPostInlineView el: @$('.new-post-article'), collection: @discussion + @discussion.on "add", @addThread + @retrieved = true + @showed = true + + addThread: (thread, collection, options) => + # TODO: When doing pagination, this will need to repaginate + article = $("
") + @$('section.discussion > .threads').prepend(article) + threadView = new DiscussionThreadInlineView el: article, model: thread + threadView.render() + @threadviews.unshift threadView + diff --git a/lms/static/coffee/src/discussion/utils.coffee b/lms/static/coffee/src/discussion/utils.coffee index 392aa04538..7c388d6d20 100644 --- a/lms/static/coffee/src/discussion/utils.coffee +++ b/lms/static/coffee/src/discussion/utils.coffee @@ -3,7 +3,7 @@ $ -> window.$$contents = {} $.fn.extend loading: -> - @$_loading = $("") + @$_loading = $("
") $(this).after(@$_loading) loaded: -> @$_loading.remove() @@ -107,6 +107,9 @@ class @DiscussionUtil [event, selector] = eventSelector.split(' ') $local(selector).unbind(event)[event] handler + @processTag: (text) -> + text.toLowerCase() + @tagsInputOptions: -> autocomplete_url: @urlFor('tags_autocomplete') autocomplete: @@ -116,6 +119,7 @@ class @DiscussionUtil width: '100%' defaultText: "Tag your post: press enter after each tag" removeWithBackspace: true + preprocessTag: @processTag @formErrorHandler: (errorsField) -> (xhr, textStatus, error) -> @@ -123,7 +127,7 @@ class @DiscussionUtil if response.errors? and response.errors.length > 0 errorsField.empty() for error in response.errors - errorsField.append($("
  • ").addClass("new-post-form-error").html(error)) + errorsField.append($("
  • ").addClass("new-post-form-error").html(error)).show() @clearFormErrors: (errorsField) -> errorsField.empty() diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_inline_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_inline_view.coffee index 30f0ff8cfc..76037d7341 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_inline_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_inline_view.coffee @@ -21,10 +21,6 @@ class @DiscussionThreadInlineView extends DiscussionContentView @model.on "change", @updateModelDetails render: -> - #TODO: Debugging, remove when done - if not window.$disc - window.$disc = [] - window.$disc.push(@) if not @model.has('abbreviatedBody') @abbreviateBody() @$el.html(Mustache.render(@template(), $.extend(@model.toJSON(),{expanded: @expanded}) )) @@ -38,8 +34,6 @@ class @DiscussionThreadInlineView extends DiscussionContentView if @expanded @makeWmdEditor "reply-body" @renderResponses() -# @highlight @$(".post-body") -# @highlight @$("h1") @ renderDogear: -> @@ -58,12 +52,13 @@ class @DiscussionThreadInlineView extends DiscussionContentView convertMath: -> element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor(element.html()) + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] renderResponses: -> DiscussionUtil.safeAjax url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}" + $loading: @$el success: (data, textStatus, xhr) => @$el.find(".loading").remove() Content.loadContentInfos(data['annotated_content_info']) @@ -192,16 +187,14 @@ class @DiscussionThreadInlineView extends DiscussionContentView success: (response, textStatus) => @model.set('endorsed', not endorsed) - highlight: (el) -> - el.html(el.html().replace(/<mark>/g, "").replace(/<\/mark>/g, "")) - abbreviateBody: -> - abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140 # Because twitter + abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140 @model.set('abbreviatedBody', abbreviated) expandPost: (event) -> @expanded = true @$el.find('.post-body').html(@model.get('body')) + @convertMath() @$el.find('.expand-post').hide() @$el.find('.collapse-post').show() @$el.find('.post-extended-content').show() @@ -212,6 +205,7 @@ class @DiscussionThreadInlineView extends DiscussionContentView collapsePost: (event) -> @expanded = false @$el.find('.post-body').html(@model.get('abbreviatedBody')) + @convertMath() @$el.find('.collapse-post').hide() @$el.find('.post-extended-content').hide() @$el.find('.expand-post').show() diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee index 6b3a17b4c4..08f9fb754f 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_list_view.coffee @@ -15,6 +15,7 @@ class @DiscussionThreadListView extends Backbone.View @collection.on "add", @addAndSelectThread @sidebar_padding = 10 @sidebar_header_height = 87 + @boardName reloadDisplayedCollection: (thread) => thread_id = thread.get('id') @@ -41,8 +42,8 @@ class @DiscussionThreadListView extends Backbone.View windowHeight = $(window).height(); discussionBody = $(".discussion-article") - discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top; - discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight(); + discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top + discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight() sidebar = $(".sidebar") if scrollTop > discussionsBodyTop - @sidebar_padding @@ -62,10 +63,11 @@ class @DiscussionThreadListView extends Backbone.View amount = Math.max(topOffset - discussionBottomOffset, 0) sidebarHeight = sidebarHeight - @sidebar_padding - amount - sidebar.css 'height', Math.min(Math.max(sidebarHeight, 400), discussionBody.outerHeight()) + sidebarHeight = Math.min(Math.max(sidebarHeight, 400), discussionBody.outerHeight()) + sidebar.css 'height', sidebarHeight postListWrapper = @$('.post-list-wrapper') - postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px'); + postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px') # Because we want the behavior that when the body is clicked the menu is @@ -101,6 +103,8 @@ class @DiscussionThreadListView extends Backbone.View content = $(_.template($("#thread-list-item-template").html())(thread.toJSON())) if thread.get('subscribed') content.addClass("followed") + if thread.get('endorsed') + content.addClass("resolved") @highlight(content) @@ -137,28 +141,66 @@ class @DiscussionThreadListView extends Backbone.View @$(".browse").toggleClass('is-dropped') if @$(".browse").hasClass('is-dropped') @$(".browse-topic-drop-menu-wrapper").show() - $('body').bind 'click', @toggleTopicDrop - $('body').bind 'keydown', @setActiveItem + $(".browse-topic-drop-search-input").focus() + $("body").bind "click", @toggleTopicDrop + $("body").bind "keydown", @setActiveItem else @$(".browse-topic-drop-menu-wrapper").hide() - $('body').unbind 'click', @toggleTopicDrop - $('body').unbind 'keydown', @setActiveItem + $("body").unbind "click", @toggleTopicDrop + $("body").unbind "keydown", @setActiveItem setTopic: (event) -> item = $(event.target).closest('a') boardName = item.find(".board-name").html() _.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) -> boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName - @$(".current-board").html(boardName) + @$(".current-board").html(@fitName(boardName)) fontSize = 16 @$(".current-board").css('font-size', '16px') - while @$(".current-board").width() > (@$el.width() * .8) - 40 fontSize-- if fontSize < 11 break @$(".current-board").css('font-size', fontSize + 'px') + setSelectedTopic: (name) -> + @$(".current-board").html(@fitName(name)) + + getNameWidth: (name) -> + test = $("
    ") + test.css + "font-size": @$(".current-board").css('font-size') + opacity: 0 + position: 'absolute' + left: -1000 + top: -1000 + $("body").append(test) + test.html(name) + width = test.width() + test.remove() + return width + + fitName: (name) -> + width = @getNameWidth(name) + if width < @maxNameWidth + return name + path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/")) + while path.length > 1 + path.shift() + partialName = "... / " + path.join(" / ") + if @getNameWidth(partialName) < @maxNameWidth + return partialName + + rawName = path[0] + + name = "... / " + rawName + + while @getNameWidth(name) > @maxNameWidth + rawName = rawName[0...rawName.length-1] + name = "... / " + rawName + " ..." + + return name + filterTopic: (event) -> @setTopic(event) item = $(event.target).closest('li') diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_view.coffee index 84c99796ee..8f1054a310 100644 --- a/lms/static/coffee/src/discussion/views/discussion_thread_view.coffee +++ b/lms/static/coffee/src/discussion/views/discussion_thread_view.coffee @@ -49,7 +49,7 @@ class @DiscussionThreadView extends DiscussionContentView convertMath: -> element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor(element.html()) + element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] renderResponses: -> @@ -66,12 +66,17 @@ class @DiscussionThreadView extends DiscussionContentView response.set('thread', @model) view = new ThreadResponseView(model: response) view.on "comment:add", @addComment + view.on "comment:endorse", @endorseThread view.render() @$el.find(".responses").append(view.el) addComment: => @model.comment() + endorseThread: (endorsed) => + is_endorsed = @$el.find(".is-endorsed").length + @model.set 'endorsed', is_endorsed + toggleVote: (event) -> event.preventDefault() if window.user.voted(@model) diff --git a/lms/static/coffee/src/discussion/views/new_post_inline_vew.coffee b/lms/static/coffee/src/discussion/views/new_post_inline_vew.coffee new file mode 100644 index 0000000000..f3b04d14ce --- /dev/null +++ b/lms/static/coffee/src/discussion/views/new_post_inline_vew.coffee @@ -0,0 +1,55 @@ +class @NewPostInlineView extends Backbone.View + + initialize: () -> + + @topicId = @$(".topic").first().data("discussion-id") + + @maxNameWidth = 100 + + DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body" + @$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions() + + events: + "submit .new-post-form": "createPost" + + # Because we want the behavior that when the body is clicked the menu is + # closed, we need to ignore clicks in the search field and stop propagation. + # Without this, clicking the search field would also close the menu. + ignoreClick: (event) -> + event.stopPropagation() + + createPost: (event) -> + event.preventDefault() + title = @$(".new-post-title").val() + body = @$(".new-post-body").find(".wmd-input").val() + tags = @$(".new-post-tags").val() + + anonymous = false || @$("input.discussion-anonymous").is(":checked") + follow = false || @$("input.discussion-follow").is(":checked") + + url = DiscussionUtil.urlFor('create_thread', @topicId) + + DiscussionUtil.safeAjax + $elem: $(event.target) + $loading: $(event.target) if event + url: url + type: "POST" + dataType: 'json' + async: false # TODO when the rest of the stuff below is made to work properly.. + data: + title: title + body: body + tags: tags + anonymous: anonymous + auto_subscribe: follow + error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors")) + success: (response, textStatus) => + # TODO: Move this out of the callback, this makes it feel sluggish + thread = new Thread response['content'] + DiscussionUtil.clearFormErrors(@$(".new-post-form-errors")) + @$el.hide() + @$(".new-post-title").val("").attr("prev-text", "") + @$(".new-post-body textarea").val("").attr("prev-text", "") + @$(".new-post-tags").val("") + @$(".new-post-tags").importTags("") + @collection.add thread diff --git a/lms/static/coffee/src/discussion/views/new_post_view.coffee b/lms/static/coffee/src/discussion/views/new_post_view.coffee index e1a859b62a..927aa76536 100644 --- a/lms/static/coffee/src/discussion/views/new_post_view.coffee +++ b/lms/static/coffee/src/discussion/views/new_post_view.coffee @@ -38,9 +38,10 @@ class @NewPostView extends Backbone.View @menuOpen = true @dropdownButton.addClass('dropped') @topicMenu.show() + $(".form-topic-drop-search-input").focus() - $('body').bind 'keydown', @setActiveItem - $('body').bind 'click', @hideTopicDropdown + $("body").bind "keydown", @setActiveItem + $("body").bind "click", @hideTopicDropdown # Set here because 1) the window might get resized and things could # change and 2) can't set in initialize because the button is hidden @@ -52,8 +53,8 @@ class @NewPostView extends Backbone.View @dropdownButton.removeClass('dropped') @topicMenu.hide() - $('body').unbind 'keydown', @setActiveItem - $('body').unbind 'click', @hideTopicDropdown + $("body").unbind "keydown", @setActiveItem + $("body").unbind "click", @hideTopicDropdown setTopic: (event) -> $target = $(event.target) @@ -142,7 +143,7 @@ class @NewPostView extends Backbone.View DiscussionUtil.clearFormErrors(@$(".new-post-form-errors")) @$el.hide() @$(".new-post-title").val("").attr("prev-text", "") - @$(".new-post-body").val("").attr("prev-text", "") + @$(".new-post-body textarea").val("").attr("prev-text", "") @$(".new-post-tags").val("") @$(".new-post-tags").importTags("") @collection.add thread @@ -168,7 +169,7 @@ class @NewPostView extends Backbone.View itemTop = $(items[index]).parent().offset().top scrollTop = $(".topic_menu").scrollTop() - itemFromTop = $(".topic_menu").offset().top - itemTop + itemFromTop = $(".topic_menu").offset().top - itemTop scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop) scrollTarget = Math.max(scrollTop - itemFromTop - $(".topic_menu").height() + $(items[index]).height() + 20, scrollTarget) $(".topic_menu").scrollTop(scrollTarget) diff --git a/lms/static/coffee/src/discussion/views/response_comment_view.coffee b/lms/static/coffee/src/discussion/views/response_comment_view.coffee index 3131912ced..262617eb84 100644 --- a/lms/static/coffee/src/discussion/views/response_comment_view.coffee +++ b/lms/static/coffee/src/discussion/views/response_comment_view.coffee @@ -2,6 +2,7 @@ class @ResponseCommentView extends DiscussionContentView tagName: "li" template: _.template($("#response-comment-template").html()) initLocal: -> + # TODO .response-local is the parent of the comments so @$local is null, not sure what was intended here... @$local = @$el.find(".response-local") @$delegateElement = @$local @@ -14,8 +15,9 @@ class @ResponseCommentView extends DiscussionContentView @convertMath() @ convertMath: -> - body = @$(".response-body") + body = @$el.find(".response-body") body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html() + # This removes paragraphs so that comments are more compact body.children("p").each (index, elem) -> $(elem).replaceWith($(elem).html()) MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]] diff --git a/lms/static/coffee/src/discussion/views/thread_response_view.coffee b/lms/static/coffee/src/discussion/views/thread_response_view.coffee index fc7d9d56e4..86deb82e6d 100644 --- a/lms/static/coffee/src/discussion/views/thread_response_view.coffee +++ b/lms/static/coffee/src/discussion/views/thread_response_view.coffee @@ -109,6 +109,7 @@ class @ThreadResponseView extends DiscussionContentView endorsed = @model.get('endorsed') data = { endorsed: not endorsed } @model.set('endorsed', not endorsed) + @trigger "comment:endorse", not endorsed DiscussionUtil.safeAjax $elem: $elem url: url diff --git a/lms/static/images/staff-icons.png b/lms/static/images/staff-icons.png new file mode 100644 index 0000000000..7efb9a8cd1 Binary files /dev/null and b/lms/static/images/staff-icons.png differ diff --git a/lms/static/images/white-error-icon.png b/lms/static/images/white-error-icon.png new file mode 100644 index 0000000000..6204f44513 Binary files /dev/null and b/lms/static/images/white-error-icon.png differ diff --git a/lms/static/js/discussions-temp.js b/lms/static/js/discussions-temp.js index b0ce231eb5..dd6af6ef14 100644 --- a/lms/static/js/discussions-temp.js +++ b/lms/static/js/discussions-temp.js @@ -58,7 +58,7 @@ $(document).ready(function() { $('.new-post-btn').bind('click', newPost); $('.new-post-cancel').bind('click', closeNewPost); - $('[data-tooltip]').bind({ + $body.delegate('[data-tooltip]', { 'mouseover': showTooltip, 'mousemove': moveTooltip, 'mouseout': hideTooltip, @@ -66,14 +66,6 @@ $(document).ready(function() { }); $body.delegate('.browse-topic-drop-search-input, .form-topic-drop-search-input', 'keyup', filterDrop); - -// $(window).bind('resize', updateSidebar); -// $(window).bind('scroll', updateSidebar); -// $('.discussion-column').bind("input", function (e) { -// console.log("resized"); -// updateSidebar(); -// }) -// updateSidebar(); }); function filterDrop(e) { @@ -276,6 +268,7 @@ function setTopic(e) { function newPost(e) { $newPost.slideDown(300); + $('.new-post-title').focus(); } function closeNewPost(e) { diff --git a/lms/static/js/jquery.tagsinput.js b/lms/static/js/jquery.tagsinput.js index b05a87ca99..563567ac46 100644 --- a/lms/static/js/jquery.tagsinput.js +++ b/lms/static/js/jquery.tagsinput.js @@ -1,12 +1,12 @@ /* jQuery Tags Input Plugin 1.3.3 - + Copyright (c) 2011 XOXCO, Inc - + Documentation for this plugin lives here: http://xoxco.com/clickable/jquery-tags-input - + Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php @@ -24,9 +24,9 @@ val = '', input = $(this), testSubject = $('#'+$(this).data('tester_id')); - + if (val === (val = input.val())) {return;} - + // Enter new content into testSubject var escaped = val.replace(/&/g, '&').replace(/\s/g,' ').replace(//g, '>'); testSubject.html(escaped); @@ -36,7 +36,7 @@ currentWidth = input.width(), isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth) || (newWidth > minWidth && newWidth < maxWidth); - + // Animate width if (isValidWidthChange) { input.width(newWidth); @@ -72,19 +72,24 @@ input.data('tester_id', testerId); input.css('width', minWidth); }; - + $.fn.addTag = function(value,options) { options = jQuery.extend({focus:false,callback:true},options); - this.each(function() { + this.each(function() { var id = $(this).attr('id'); var tagslist = $(this).val().split(delimiter[id]); - if (tagslist[0] == '') { + if (tagslist[0] == '') { tagslist = new Array(); } value = jQuery.trim(value); - + + if (options.callback && tags_callbacks[id] && tags_callbacks[id]['preprocessTag']) { + var f = tags_callbacks[id]['preprocessTag']; + value = f.call(this, value); + } + if (options.unique) { var skipTag = $(this).tagExist(value); if(skipTag == true) { @@ -92,10 +97,10 @@ $('#'+id+'_tag').addClass('not_valid'); } } else { - var skipTag = false; + var skipTag = false; } - - if (value !='' && skipTag != true) { + + if (value !='' && skipTag != true) { $('').addClass('tag').append( $('').text(value).append('  '), $('', { @@ -108,16 +113,16 @@ ).insertBefore('#' + id + '_addTag'); tagslist.push(value); - + $('#'+id+'_tag').val(''); if (options.focus) { $('#'+id+'_tag').focus(); - } else { + } else { $('#'+id+'_tag').blur(); } - + $.fn.tagsInput.updateTagsField(this,tagslist); - + if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) { var f = tags_callbacks[id]['onAddTag']; f.call(this, value); @@ -127,29 +132,29 @@ var i = tagslist.length; var f = tags_callbacks[id]['onChange']; f.call(this, $(this), tagslist[i-1]); - } + } } - - }); - + + }); + return false; }; - - $.fn.removeTag = function(value) { + + $.fn.removeTag = function(value) { value = unescape(value); - this.each(function() { + this.each(function() { var id = $(this).attr('id'); - + var old = $(this).val().split(delimiter[id]); - + $('#'+id+'_tagsinput .tag').remove(); str = ''; - for (i=0; i< old.length; i++) { - if (old[i]!=value) { + for (i=0; i< old.length; i++) { + if (old[i]!=value) { str = str + delimiter[id] +old[i]; } } - + $.fn.tagsInput.importTags(this,str); if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) { @@ -157,24 +162,24 @@ f.call(this, value); } }); - + return false; }; - + $.fn.tagExist = function(val) { var id = $(this).attr('id'); var tagslist = $(this).val().split(delimiter[id]); return (jQuery.inArray(val, tagslist) >= 0); //true when tag exists, false when not }; - + // clear all existing tags and import new ones from a string $.fn.importTags = function(str) { id = $(this).attr('id'); $('#'+id+'_tagsinput .tag').remove(); $.fn.tagsInput.importTags(this,str); } - - $.fn.tagsInput = function(options) { + + $.fn.tagsInput = function(options) { var settings = jQuery.extend({ interactive:true, defaultText:'add a tag', @@ -192,15 +197,15 @@ inputPadding: 6*2 },options); - this.each(function() { - if (settings.hide) { - $(this).hide(); + this.each(function() { + if (settings.hide) { + $(this).hide(); } var id = $(this).attr('id'); if (!id || delimiter[$(this).attr('id')]) { id = $(this).attr('id', 'tags' + new Date().getTime()).attr('id'); } - + var data = jQuery.extend({ pid:id, real_input: '#'+id, @@ -208,57 +213,58 @@ input_wrapper: '#'+id+'_addTag', fake_input: '#'+id+'_tag' },settings); - + delimiter[id] = data.delimiter; - - if (settings.onAddTag || settings.onRemoveTag || settings.onChange) { + + if (settings.onAddTag || settings.onRemoveTag || settings.onChange || settings.preprocessTag) { tags_callbacks[id] = new Array(); tags_callbacks[id]['onAddTag'] = settings.onAddTag; tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag; tags_callbacks[id]['onChange'] = settings.onChange; + tags_callbacks[id]['preprocessTag'] = settings.preprocessTag; } - + var markup = '
    '; - + if (settings.interactive) { markup = markup + ''; } - + markup = markup + '
    '; - + $(markup).insertAfter(this); - + $(data.holder).css('width',settings.width); $(data.holder).css('min-height',settings.height); $(data.holder).css('height','100%'); - - if ($(data.real_input).val()!='') { + + if ($(data.real_input).val()!='') { $.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val()); - } - if (settings.interactive) { + } + if (settings.interactive) { $(data.fake_input).val($(data.fake_input).attr('data-default')); $(data.fake_input).css('color',settings.placeholderColor); $(data.fake_input).resetAutosize(settings); - + $(data.fake_input).doAutosize(settings); $(data.holder).bind('click',data,function(event) { $(event.data.fake_input).focus(); }); - + $(data.fake_input).bind('focus',data,function(event) { - if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) { + if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) { $(event.data.fake_input).val(''); } - $(event.data.fake_input).css('color','#000000'); + $(event.data.fake_input).css('color','#000000'); }); - + if (settings.autocomplete_url != undefined) { autocomplete_options = {source: settings.autocomplete_url}; - for (attrname in settings.autocomplete) { - autocomplete_options[attrname] = settings.autocomplete[attrname]; + for (attrname in settings.autocomplete) { + autocomplete_options[attrname] = settings.autocomplete[attrname]; } - + if (jQuery.Autocompleter !== undefined) { onSelectCallback = settings.autocomplete.onItemSelect; settings.autocomplete.onItemSelect = function() { @@ -278,18 +284,18 @@ $(data.fake_input).autocomplete(autocomplete_options); $(data.fake_input).bind('autocompleteselect',data,function(event,ui) { $(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)}); - + return false; }); } - - + + } else { // if a user tabs out of the field, create a new tag // this is only available if autocomplete is not used. - $(data.fake_input).bind('blur',data,function(event) { + $(data.fake_input).bind('blur',data,function(event) { var d = $(this).attr('data-default'); - if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) { + if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) { if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) ) $(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)}); } else { @@ -298,7 +304,7 @@ } return false; }); - + } // if user types a comma, create a new tag $(data.fake_input).bind('keypress',data,function(event) { @@ -326,7 +332,7 @@ } }); $(data.fake_input).blur(); - + //Removes the not_valid class when user changes the value of the fake input if(data.unique) { $(data.fake_input).keydown(function(event){ @@ -337,21 +343,21 @@ } } // if settings.interactive }); - + return this; - + }; - - $.fn.tagsInput.updateTagsField = function(obj,tagslist) { + + $.fn.tagsInput.updateTagsField = function(obj,tagslist) { var id = $(obj).attr('id'); $(obj).val(tagslist.join(delimiter[id])); }; - - $.fn.tagsInput.importTags = function(obj,val) { + + $.fn.tagsInput.importTags = function(obj,val) { $(obj).val(''); var id = $(obj).attr('id'); var tags = val.split(delimiter[id]); - for (i=0; i
    diff --git a/lms/templates/discussion/_inline_new_post.html b/lms/templates/discussion/_inline_new_post.html new file mode 100644 index 0000000000..7bc17d4467 --- /dev/null +++ b/lms/templates/discussion/_inline_new_post.html @@ -0,0 +1,29 @@ +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    + + Cancel +
    +
    +
    +
    diff --git a/lms/templates/discussion/_new_post.html b/lms/templates/discussion/_new_post.html index be09f61c79..e2ace9413c 100644 --- a/lms/templates/discussion/_new_post.html +++ b/lms/templates/discussion/_new_post.html @@ -22,9 +22,7 @@
    -
    -
    -
    +
    @@ -46,6 +44,7 @@
    +
      diff --git a/lms/templates/discussion/index.html b/lms/templates/discussion/index.html index 40284638f3..53cbe2d9ed 100644 --- a/lms/templates/discussion/index.html +++ b/lms/templates/discussion/index.html @@ -27,7 +27,7 @@
      -
      +

      ${course.title} discussions

      diff --git a/lms/templates/discussion/mustache/_inline_discussion.mustache b/lms/templates/discussion/mustache/_inline_discussion.mustache index f82b3810e5..8d55f9949b 100644 --- a/lms/templates/discussion/mustache/_inline_discussion.mustache +++ b/lms/templates/discussion/mustache/_inline_discussion.mustache @@ -1,6 +1,39 @@ -
      - {{#threads}} -
      +
      + New Post + +
      + +
      +
      +
      + +
      +
      + +
      +
      +
      + +
      +
      + +
      + + Cancel +
      + +
      + +
      +
      + +
      - {{/threads}} + +
      + {{#threads}} +
      +
      + {{/threads}} +
      diff --git a/lms/templates/discussion/mustache/_inline_thread.mustache b/lms/templates/discussion/mustache/_inline_thread.mustache index cd33f2130e..b45f3ff47b 100644 --- a/lms/templates/discussion/mustache/_inline_thread.mustache +++ b/lms/templates/discussion/mustache/_inline_thread.mustache @@ -1,21 +1,27 @@