diff --git a/doc/discussion.md b/doc/discussion.md index 4f8ab9a01a..2446485497 100644 --- a/doc/discussion.md +++ b/doc/discussion.md @@ -9,7 +9,7 @@ If you haven't done so already: brew install mongodb Make sure that you have mongodb running. You can simply open a new terminal tab and type: - + mongod ## Installing elasticsearch @@ -72,9 +72,9 @@ For convenience, add the following environment variables to the terminal (assumi export DJANGO_SETTINGS_MODULE=lms.envs.dev export PYTHONPATH=. -Now initialzie roles and permissions: +Now initialzie roles and permissions, providing a course id eg.: - django-admin.py seed_permissions_roles + django-admin.py seed_permissions_roles "MITx/6.002x/2012_Fall" To assign yourself as a moderator, use the following command (assuming your username is "test", and the course id is "MITx/6.002x/2012_Fall"): diff --git a/lms/djangoapps/django_comment_client/forum/views.py b/lms/djangoapps/django_comment_client/forum/views.py index 76b00a62cd..f9e1baa8ed 100644 --- a/lms/djangoapps/django_comment_client/forum/views.py +++ b/lms/djangoapps/django_comment_client/forum/views.py @@ -25,7 +25,7 @@ import xml.sax.saxutils as saxutils THREADS_PER_PAGE = 200 INLINE_THREADS_PER_PAGE = 5 PAGES_NEARBY_DELTA = 2 - +escapedict = {'"': '"'} log = logging.getLogger("edx.discussions") def _general_discussion_id(course_id): @@ -83,7 +83,7 @@ def render_discussion(request, course_id, threads, *args, **kwargs): thread['courseware_title'] = courseware_context['courseware_title'] context = { - #'threads': map(utils.safe_content, threads), # TODO Delete, this is redundant with discussion_data + 'threads': map(utils.safe_content, threads), 'discussion_id': discussion_id, 'user_id': user_id, 'course_id': course_id, @@ -94,7 +94,8 @@ def render_discussion(request, course_id, threads, *args, **kwargs): 'base_url': base_url, '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 or user_id): map(utils.safe_content, threads) }) + #'discussion_data': json.dumps({ (discussion_id or user_id): map(utils.safe_content, threads) }) + # TODO: Delete the above, nothing uses this } context = dict(context.items() + query_params.items()) return render_to_string(template, context) @@ -161,17 +162,12 @@ def inline_discussion(request, course_id, discussion_id): # checking for errors on request. Check and fix as needed. raise Http404 - # TODO: Remove all of this stuff or switch back to server side rendering once templates are mustache again - #html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, \ - # query_params=query_params) - 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, @@ -193,8 +189,6 @@ def forum_form_discussion(request, course_id): except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: raise Http404 - #content = render_forum_discussion(request, course_id, threads, discussion_id=_general_discussion_id(course_id), query_params=query_params) - user_info = cc.User.from_django_user(request.user).to_dict() def infogetter(thread): @@ -208,8 +202,7 @@ def forum_form_discussion(request, course_id): thread['courseware_title'] = courseware_context['courseware_title'] if request.is_ajax(): return utils.JsonResponse({ - #'html': content, - 'discussion_data': threads, + 'discussion_data': threads, # TODO: Standardize on 'discussion_data' vs 'threads' 'annotated_content_info': annotated_content_info, }) else: @@ -223,11 +216,9 @@ def forum_form_discussion(request, course_id): # course_id, #) - escapedict = {'"': '"'} context = { 'csrf': csrf(request)['csrf_token'], 'course': course, - #'content': content, #'recent_active_threads': recent_active_threads, #'trending_tags': trending_tags, 'staff_access' : has_access(request.user, course, 'staff'), @@ -256,12 +247,12 @@ def single_thread(request, course_id, discussion_id, thread_id): 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} # TODO: Remove completely or switch back to server side rendering - html = render_to_string('discussion/_ajax_single_thread.html', context) + # html = render_to_string('discussion/_ajax_single_thread.html', context) content = utils.safe_content(thread.to_dict()) if courseware_context: content.update(courseware_context) return utils.JsonResponse({ - 'html': html, + #'html': html, 'content': content, 'annotated_content_info': annotated_content_info, }) @@ -293,7 +284,7 @@ def single_thread(request, course_id, discussion_id, thread_id): #) user_info = cc.User.from_django_user(request.user).to_dict() - escapedict = {'"': '"'} + def infogetter(thread): return utils.get_annotated_content_infos(course_id, thread, request.user, user_info) @@ -303,13 +294,13 @@ def single_thread(request, course_id, discussion_id, thread_id): context = { 'discussion_id': discussion_id, 'csrf': csrf(request)['csrf_token'], - 'init': '', + 'init': '', #TODO: What is this? 'user_info': saxutils.escape(json.dumps(user_info),escapedict), 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict), 'course': course, #'recent_active_threads': recent_active_threads, #'trending_tags': trending_tags, - 'course_id': course.id, + 'course_id': course.id, #TODO: Why pass both course and course.id to template? 'thread_id': thread_id, 'threads': saxutils.escape(json.dumps(threads), escapedict), 'category_map': category_map, @@ -323,11 +314,15 @@ def user_profile(request, course_id, user_id): course = get_course_with_access(request.user, course_id, 'load') try: profiled_user = cc.User(id=user_id, course_id=course_id) - + query_params = { + 'page': request.GET.get('page', 1), + 'per_page': INLINE_THREADS_PER_PAGE, + } + threads, page, num_pages = profiled_user.active_threads(query_params) query_params['page'] = page query_params['num_pages'] = num_pages - content = render_user_discussion(request, course_id, threads, user_id=user_id, query_params=query_params) +# content = render_user_discussion(request, course_id, threads, user_id=user_id, query_params=query_params) if request.is_ajax(): return utils.JsonResponse({ @@ -335,12 +330,21 @@ def user_profile(request, course_id, user_id): 'discussion_data': map(utils.safe_content, threads), }) else: + 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 = { 'course': course, 'user': request.user, 'django_user': User.objects.get(id=user_id), 'profiled_user': profiled_user.to_dict(), - 'content': content, + 'threads': saxutils.escape(json.dumps(threads), escapedict), + 'user_info': saxutils.escape(json.dumps(user_info),escapedict), + 'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info),escapedict), +# 'content': content, } return render_to_response('discussion/user_profile.html', context) diff --git a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py index 5987d5c677..f303abf930 100644 --- a/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py +++ b/lms/djangoapps/django_comment_client/management/commands/seed_permissions_roles.py @@ -7,8 +7,10 @@ class Command(BaseCommand): help = 'Seed default permisssions and roles' def handle(self, *args, **options): - if len(args) != 1: - raise CommandError("The number of arguments does not match. ") + if len(args) == 0: + raise CommandError("Please provide a course id") + if len(args) > 1: + raise CommandError("Too many arguments") course_id = args[0] administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0] moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0] diff --git a/lms/static/coffee/src/discussion/main.coffee b/lms/static/coffee/src/discussion/main.coffee index ed31a532c1..029eeda176 100644 --- a/lms/static/coffee/src/discussion/main.coffee +++ b/lms/static/coffee/src/discussion/main.coffee @@ -12,7 +12,16 @@ if Backbone? discussion = new Discussion(threads) new DiscussionRouter({discussion: discussion}) Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"}) - + DiscussionProfileApp = + start: (elem) -> + element = $(elem) + window.$$course_id = element.data("course-id") + threads = element.data("threads") + user_info = element.data("user-info") + window.user = new DiscussionUser(user_info) + new DiscussionUserProfileView(el: element, collection: threads) $ -> $("section.discussion").each (index, elem) -> DiscussionApp.start(elem) + $("section.discussion-user-threads").each (index, elem) -> + DiscussionProfileApp.start(elem) diff --git a/lms/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee b/lms/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee new file mode 100644 index 0000000000..410c573d08 --- /dev/null +++ b/lms/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee @@ -0,0 +1,146 @@ +if Backbone? + class @DiscussionThreadProfileView extends DiscussionContentView + expanded = false + events: + "click .discussion-vote": "toggleVote" + "click .action-follow": "toggleFollowing" + "click .expand-post": "expandPost" + "click .collapse-post": "collapsePost" + + initLocal: -> + @$local = @$el.children(".discussion-article").children(".local") + @$delegateElement = @$local + + initialize: -> + super() + @model.on "change", @updateModelDetails + + render: -> + @template = DiscussionUtil.getTemplate("_profile_thread") + + if not @model.has('abbreviatedBody') + @abbreviateBody() + params = $.extend(@model.toJSON(),{expanded: @expanded}) + if not @model.get('anonymous') + params = $.extend(params, user:{username: @model.username, user_url: @model.user_url}) + @$el.html(Mustache.render(@template, params)) + @initLocal() + @delegateEvents() + @renderDogear() + @renderVoted() + @renderAttrs() + @$("span.timeago").timeago() + @convertMath() + if @expanded + @renderResponses() + @ + + renderDogear: -> + if window.user.following(@model) + @$(".dogear").addClass("is-followed") + + renderVoted: => + if window.user.voted(@model) + @$("[data-role=discussion-vote]").addClass("is-cast") + else + @$("[data-role=discussion-vote]").removeClass("is-cast") + + updateModelDetails: => + @renderVoted() + @$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"]) + + convertMath: -> + element = @$(".post-body") + 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']) + comments = new Comments(data['content']['children']) + comments.each @renderResponse + @trigger "thread:responses:rendered" + + renderResponse: (response) => + response.set('thread', @model) + view = new ThreadResponseView(model: response) + view.on "comment:add", @addComment + view.render() + @$el.find(".responses").append(view.el) + + addComment: => + @model.comment() + + toggleVote: (event) -> + event.preventDefault() + if window.user.voted(@model) + @unvote() + else + @vote() + + toggleFollowing: (event) -> + $elem = $(event.target) + url = null + console.log "follow" + if not @model.get('subscribed') + @model.follow() + url = @model.urlFor("follow") + else + @model.unfollow() + url = @model.urlFor("unfollow") + DiscussionUtil.safeAjax + $elem: $elem + url: url + type: "POST" + + vote: -> + window.user.vote(@model) + url = @model.urlFor("upvote") + DiscussionUtil.safeAjax + $elem: @$(".discussion-vote") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set(response) + + unvote: -> + window.user.unvote(@model) + url = @model.urlFor("unvote") + DiscussionUtil.safeAjax + $elem: @$(".discussion-vote") + url: url + type: "POST" + success: (response, textStatus) => + if textStatus == 'success' + @model.set(response) + + edit: -> + + abbreviateBody: -> + abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140 + @model.set('abbreviatedBody', abbreviated) + + expandPost: (event) -> + @expanded = true + @$el.addClass('expanded') + @$el.find('.post-body').html(@model.get('body')) + @convertMath() + @$el.find('.expand-post').css('display', 'none') + @$el.find('.collapse-post').css('display', 'block') + @$el.find('.post-extended-content').show() + if @$el.find('.loading').length + @renderResponses() + + collapsePost: (event) -> + @expanded = false + @$el.removeClass('expanded') + @$el.find('.post-body').html(@model.get('abbreviatedBody')) + @convertMath() + @$el.find('.collapse-post').css('display', 'none') + @$el.find('.post-extended-content').hide() + @$el.find('.expand-post').css('display', 'block') diff --git a/lms/static/coffee/src/discussion/views/discussion_user_profile_view.coffee b/lms/static/coffee/src/discussion/views/discussion_user_profile_view.coffee new file mode 100644 index 0000000000..ed5645e5e5 --- /dev/null +++ b/lms/static/coffee/src/discussion/views/discussion_user_profile_view.coffee @@ -0,0 +1,26 @@ +if Backbone? + class @DiscussionUserProfileView extends Backbone.View +# events: +# "":"" + initialize: (options) -> + @renderThreads @$el, @collection + renderThreads: ($elem, threads) => + #Content.loadContentInfos(response.annotated_content_info) + console.log threads + @discussion = new Discussion() + @discussion.reset(threads, {silent: false}) + $discussion = $(Mustache.render $("script#_user_profile").html(), {'threads':threads}) + console.log $discussion + $elem.append($discussion) + @threadviews = @discussion.map (thread) -> + new DiscussionThreadProfileView el: @$("article#thread_#{thread.id}"), model: thread + console.log @threadviews + _.each @threadviews, (dtv) -> dtv.render() + + addThread: (thread, collection, options) => + # TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1? + article = $("
") + @$('section.discussion > .threads').prepend(article) + threadView = new DiscussionThreadInlineView el: article, model: thread + threadView.render() + @threadviews.unshift threadView diff --git a/lms/static/sass/_discussion.scss b/lms/static/sass/_discussion.scss index 8bc4a30d8f..35632af9dc 100644 --- a/lms/static/sass/_discussion.scss +++ b/lms/static/sass/_discussion.scss @@ -593,7 +593,7 @@ body.discussion { background-repeat: no-repeat; background-position: 0px 0px; width: 20px; - height: 20px; + height: 20px; } .wmd-spacer1 { @@ -1782,7 +1782,7 @@ body.discussion { display: none; } - + } } } @@ -2087,7 +2087,7 @@ body.discussion { background-repeat: no-repeat; background-position: 0px 0px; width: 20px; - height: 20px; + height: 20px; } .wmd-spacer1 { @@ -2142,7 +2142,7 @@ body.discussion { .wmd-button-row { // this is being hidden now because the inline styles to position the icons are not being written position: relative; - height: 12px; + height: 12px; } .wmd-button { @@ -2166,3 +2166,7 @@ body.discussion { left: 300px; } } + +.discussion-user-threads { + @extend .discussion-module +} diff --git a/lms/templates/discussion/mustache/_profile_thread.mustache b/lms/templates/discussion/mustache/_profile_thread.mustache new file mode 100644 index 0000000000..9c310875df --- /dev/null +++ b/lms/templates/discussion/mustache/_profile_thread.mustache @@ -0,0 +1,31 @@ +
+
+
+
+ + {{votes.up_count}} +

{{title}}

+

+ {{created_at}} by + {{#user}} + {{username}} + {{/user}} + {{^user}} + anonymous + {{/user}} + +

+
+
{{abbreviatedBody}}
+
+
    +
  1. +
+ +
+ View discussion + Hide discussion +
+ +
diff --git a/lms/templates/discussion/mustache/_user_profile.mustache b/lms/templates/discussion/mustache/_user_profile.mustache new file mode 100644 index 0000000000..be9f3791ca --- /dev/null +++ b/lms/templates/discussion/mustache/_user_profile.mustache @@ -0,0 +1,10 @@ +
+
+ {{#threads}} +
+
+ {{/threads}} + +
+
+
diff --git a/lms/templates/discussion/user_profile.html b/lms/templates/discussion/user_profile.html index 4c067db710..3b273f15fd 100644 --- a/lms/templates/discussion/user_profile.html +++ b/lms/templates/discussion/user_profile.html @@ -21,7 +21,7 @@
-
- ${content.decode('utf-8')} +
+

Active Threads

@@ -39,3 +39,4 @@ var $$profiled_user_id = "${django_user.id | escapejs}"; var $$course_id = "${course.id | escapejs}"; +<%include file="_underscore_templates.html" />