views.py 36.3 KB
Newer Older
1 2 3 4
"""
Views handling read (GET) requests for the Discussion tab and inline discussions.
"""

5
import logging
cahrens committed
6
from functools import wraps
7
from sets import Set
Arjun Singh committed
8

9
from django.conf import settings
10
from django.contrib.auth.decorators import login_required
11
from django.contrib.auth.models import User
12
from django.contrib.staticfiles.storage import staticfiles_storage
13
from django.template.context_processors import csrf
cahrens committed
14
from django.core.urlresolvers import reverse
15
from django.http import Http404, HttpResponseServerError
16
from django.shortcuts import render_to_response
17 18
from django.template.loader import render_to_string
from django.utils.translation import get_language_bidi
19
from django.views.decorators.csrf import ensure_csrf_cookie
cahrens committed
20
from django.views.decorators.http import require_GET, require_http_methods
21 22 23 24 25
from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from web_fragments.fragment import Fragment

import django_comment_client.utils as utils
26
from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context
27 28 29 30
import lms.lib.comment_client as cc
from courseware.access import has_access
from courseware.courses import get_course_with_access
from courseware.views.views import CourseTabView
31
from django_comment_client.base.views import track_thread_viewed_event
32
from django_comment_client.constants import TYPE_ENTRY
cahrens committed
33
from django_comment_client.permissions import get_team, has_permission
34
from django_comment_client.utils import (
cahrens committed
35
    add_courseware_context,
36
    available_division_schemes,
cahrens committed
37
    course_discussion_division_enabled,
38
    extract,
39
    get_group_id_for_comments_service,
cahrens committed
40
    get_group_id_for_user,
41
    get_group_names_by_id,
42
    is_commentable_divided,
cahrens committed
43 44
    merge_dict,
    strip_none
45
)
cahrens committed
46 47
from django_comment_common.utils import ThreadContext, get_course_discussion_settings, set_course_discussion_settings
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
48
from openedx.core.djangoapps.monitoring_utils import function_trace
cahrens committed
49 50 51
from student.models import CourseEnrollment
from util.json_request import JsonResponse, expect_json
from xmodule.modulestore.django import modulestore
52

53 54
from . import USE_BOOTSTRAP_FLAG

cahrens committed
55
log = logging.getLogger("edx.discussions")
56

57

58
THREADS_PER_PAGE = 20
59
INLINE_THREADS_PER_PAGE = 20
Rocky Duan committed
60
PAGES_NEARBY_DELTA = 2
61

62 63
BOOTSTRAP_DISCUSSION_CSS_PATH = 'css/discussion/lms-discussion-bootstrap.css'

Calen Pennington committed
64

65
def make_course_settings(course, user):
66 67 68 69
    """
    Generate a JSON-serializable model for course settings, which will be used to initialize a
    DiscussionCourseSettings object on the client.
    """
70 71
    course_discussion_settings = get_course_discussion_settings(course.id)
    group_names_by_id = get_group_names_by_id(course_discussion_settings)
72
    return {
73
        'is_discussion_division_enabled': course_discussion_division_enabled(course_discussion_settings),
74 75
        'allow_anonymous': course.allow_anonymous,
        'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
76 77 78
        'groups': [
            {"id": str(group_id), "name": group_name} for group_id, group_name in group_names_by_id.iteritems()
        ],
79
        'category_map': utils.get_discussion_category_map(course, user)
80 81
    }

82

83
def get_threads(request, course, user_info, discussion_id=None, per_page=THREADS_PER_PAGE):
84
    """
85
    This may raise an appropriate subclass of cc.utils.CommentClientError
86
    if something goes wrong, or ValueError if the group_id is invalid.
87 88

    Arguments:
89 90 91 92 93
        request (WSGIRequest): The user request.
        course (CourseDescriptorWithMixins): The course object.
        user_info (dict): The comment client User object as a dict.
        discussion_id (unicode): Optional discussion id/commentable id for context.
        per_page (int): Optional number of threads per page.
94 95

    Returns:
96 97
        (tuple of list, dict): A tuple of the list of threads and a dict of the
            query parameters used for the search.
98

99
    """
Rocky Duan committed
100 101
    default_query_params = {
        'page': 1,
102
        'per_page': per_page,
103
        'sort_key': 'activity',
Rocky Duan committed
104
        'text': '',
105
        'course_id': unicode(course.id),
Rocky Duan committed
106
        'user_id': request.user.id,
107
        'context': ThreadContext.COURSE,
108
        'group_id': get_group_id_for_comments_service(request, course.id, discussion_id),  # may raise ValueError
Rocky Duan committed
109
    }
Your Name committed
110

111 112 113 114
    # If provided with a discussion id, filter by discussion id in the
    # comments_service.
    if discussion_id is not None:
        default_query_params['commentable_id'] = discussion_id
115 116 117
        # Use the discussion id/commentable id to determine the context we are going to pass through to the backend.
        if get_team(discussion_id) is not None:
            default_query_params['context'] = ThreadContext.STANDALONE
118

119 120
    if not request.GET.get('sort_key'):
        # If the user did not select a sort key, use their last used sort key
121 122 123
        default_query_params['sort_key'] = user_info.get('default_sort_key') or default_query_params['sort_key']

    elif request.GET.get('sort_key') != user_info.get('default_sort_key'):
124
        # If the user clicked a sort key, update their default sort key
125 126 127
        cc_user = cc.User.from_django_user(request.user)
        cc_user.default_sort_key = request.GET.get('sort_key')
        cc_user.save()
128

Your Name committed
129 130 131
    #there are 2 dimensions to consider when executing a search with respect to group id
    #is user a moderator
    #did the user request a group
132

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
    query_params = merge_dict(
        default_query_params,
        strip_none(
            extract(
                request.GET,
                [
                    'page',
                    'sort_key',
                    'text',
                    'commentable_ids',
                    'flagged',
                    'unread',
                    'unanswered',
                ]
            )
        )
    )
Rocky Duan committed
150

151 152
    paginated_results = cc.Thread.search(query_params)
    threads = paginated_results.collection
153

154 155 156 157 158 159 160 161 162
    # If not provided with a discussion id, filter threads by commentable ids
    # which are accessible to the current user.
    if discussion_id is None:
        discussion_category_ids = set(utils.get_discussion_categories_ids(course, request.user))
        threads = [
            thread for thread in threads
            if thread.get('commentable_id') in discussion_category_ids
        ]

163
    for thread in threads:
David Baumgold committed
164 165
        # patch for backward compatibility to comments service
        if 'pinned' not in thread:
166
            thread['pinned'] = False
Your Name committed
167

168 169 170
    query_params['page'] = paginated_results.page
    query_params['num_pages'] = paginated_results.num_pages
    query_params['corrected_text'] = paginated_results.corrected_text
Rocky Duan committed
171 172 173

    return threads, query_params

Calen Pennington committed
174

175 176 177 178 179 180 181 182
def use_bulk_ops(view_func):
    """
    Wraps internal request handling inside a modulestore bulk op, significantly
    reducing redundant database calls.  Also converts the course_id parsed from
    the request uri to a CourseKey before passing to the view.
    """
    @wraps(view_func)
    def wrapped_view(request, course_id, *args, **kwargs):  # pylint: disable=missing-docstring
183
        course_key = CourseKey.from_string(course_id)
184 185 186 187 188
        with modulestore().bulk_operations(course_key):
            return view_func(request, course_key, *args, **kwargs)
    return wrapped_view


189
@login_required
190 191
@use_bulk_ops
def inline_discussion(request, course_key, discussion_id):
192 193 194
    """
    Renders JSON for DiscussionModules
    """
195

196
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
197 198
    cc_user = cc.User.from_django_user(request.user)
    user_info = cc_user.to_dict()
199

200
    try:
201
        threads, query_params = get_threads(request, course, user_info, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
202
    except ValueError:
203
        return HttpResponseServerError("Invalid group_id")
204

205
    with function_trace("get_metadata_for_threads"):
206
        annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
207

208
    is_staff = has_permission(request.user, 'openclose_thread', course.id)
209
    threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
210
    with function_trace("add_courseware_context"):
211
        add_courseware_context(threads, course, request.user)
212
    course_discussion_settings = get_course_discussion_settings(course.id)
213

Rocky Duan committed
214
    return utils.JsonResponse({
215
        'is_commentable_divided': is_commentable_divided(course_key, discussion_id),
216
        'discussion_data': threads,
217
        'user_info': user_info,
218
        'user_group_id': get_group_id_for_user(request.user, course_discussion_settings),
219 220
        'annotated_content_info': annotated_content_info,
        'page': query_params['page'],
221
        'num_pages': query_params['num_pages'],
222
        'roles': utils.get_role_ids(course_key),
223
        'course_settings': make_course_settings(course, request.user)
Rocky Duan committed
224
    })
225

226

227
@login_required
228 229
@use_bulk_ops
def forum_form_discussion(request, course_key):
230
    """
231
    Renders the main Discussion page, potentially filtered by a search query
232
    """
233
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
234 235 236
    if request.is_ajax():
        user = cc.User.from_django_user(request.user)
        user_info = user.to_dict()
237

238 239 240 241 242 243 244 245
        try:
            unsafethreads, query_params = get_threads(request, course, user_info)  # This might process a search query
            is_staff = has_permission(request.user, 'openclose_thread', course.id)
            threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
        except cc.utils.CommentClientMaintenanceError:
            return HttpResponseServerError('Forum is in maintenance mode', status=status.HTTP_503_SERVICE_UNAVAILABLE)
        except ValueError:
            return HttpResponseServerError("Invalid group_id")
246

247
        with function_trace("get_metadata_for_threads"):
248
            annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
249

250
        with function_trace("add_courseware_context"):
251
            add_courseware_context(threads, course, request.user)
252

253
        return utils.JsonResponse({
Calen Pennington committed
254
            'discussion_data': threads,   # TODO: Standardize on 'discussion_data' vs 'threads'
255
            'annotated_content_info': annotated_content_info,
256 257
            'num_pages': query_params['num_pages'],
            'page': query_params['page'],
258
            'corrected_text': query_params['corrected_text'],
259
        })
260
    else:
261 262 263
        course_id = unicode(course.id)
        tab_view = CourseTabView()
        return tab_view.get(request, course_id, 'discussion')
264

265

266
@require_GET
267
@login_required
268 269 270
@use_bulk_ops
def single_thread(request, course_key, discussion_id, thread_id):
    """
271 272 273 274 275
    Renders a response to display a single discussion thread.  This could either be a page refresh
    after navigating to a single thread, a direct link to a single thread, or an AJAX call from the
    discussions UI loading the responses/comments for a single thread.

    Depending on the HTTP headers, we'll adjust our response accordingly.
276
    """
277
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
Rocky Duan committed
278

279 280 281 282
    if request.is_ajax():
        cc_user = cc.User.from_django_user(request.user)
        user_info = cc_user.to_dict()
        is_staff = has_permission(request.user, 'openclose_thread', course.id)
283 284 285 286 287 288 289
        thread = _load_thread_for_viewing(
            request,
            course,
            discussion_id=discussion_id,
            thread_id=thread_id,
            raise_event=True,
        )
290

291
        with function_trace("get_annotated_content_infos"):
292 293 294 295 296 297
            annotated_content_info = utils.get_annotated_content_infos(
                course_key,
                thread,
                request.user,
                user_info=user_info
            )
298

299
        content = utils.prepare_content(thread.to_dict(), course_key, is_staff)
300
        with function_trace("add_courseware_context"):
301
            add_courseware_context([content], course, request.user)
302

303
        return utils.JsonResponse({
304
            'content': content,
Rocky Duan committed
305 306 307
            'annotated_content_info': annotated_content_info,
        })
    else:
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
        course_id = unicode(course.id)
        tab_view = CourseTabView()
        return tab_view.get(request, course_id, 'discussion', discussion_id=discussion_id, thread_id=thread_id)


def _find_thread(request, course, discussion_id, thread_id):
    """
    Finds the discussion thread with the specified ID.

    Args:
        request: The Django request.
        course_id: The ID of the owning course.
        discussion_id: The ID of the owning discussion.
        thread_id: The ID of the thread.

    Returns:
        The thread in question if the user can see it, else None.
    """
    try:
        thread = cc.Thread.find(thread_id).retrieve(
            with_responses=request.is_ajax(),
            recursive=request.is_ajax(),
            user_id=request.user.id,
            response_skip=request.GET.get("resp_skip"),
            response_limit=request.GET.get("resp_limit")
        )
    except cc.utils.CommentClientRequestError:
        return None

    # Verify that the student has access to this thread if belongs to a course discussion module
    thread_context = getattr(thread, "context", "course")
    if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id):
        return None

342
    # verify that the thread belongs to the requesting student's group
343
    is_moderator = has_permission(request.user, "see_all_cohorts", course.id)
344 345 346
    course_discussion_settings = get_course_discussion_settings(course.id)
    if is_commentable_divided(course.id, discussion_id, course_discussion_settings) and not is_moderator:
        user_group_id = get_group_id_for_user(request.user, course_discussion_settings)
347 348 349 350 351 352
        if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id:
            return None

    return thread


353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
def _load_thread_for_viewing(request, course, discussion_id, thread_id, raise_event):
    """
    Loads the discussion thread with the specified ID and fires an
    edx.forum.thread.viewed event.

    Args:
        request: The Django request.
        course_id: The ID of the owning course.
        discussion_id: The ID of the owning discussion.
        thread_id: The ID of the thread.
        raise_event: Whether an edx.forum.thread.viewed tracking event should
                     be raised

    Returns:
        The thread in question if the user can see it.

    Raises:
        Http404 if the thread does not exist or the user cannot
        see it.
    """
    thread = _find_thread(request, course, discussion_id=discussion_id, thread_id=thread_id)
    if not thread:
        raise Http404
    if raise_event:
        track_thread_viewed_event(request, course, thread)
    return thread


381 382 383 384 385 386 387 388 389
def _create_base_discussion_view_context(request, course_key):
    """
    Returns the default template context for rendering any discussion view.
    """
    user = request.user
    cc_user = cc.User.from_django_user(user)
    user_info = cc_user.to_dict()
    course = get_course_with_access(user, 'load', course_key, check_if_enrolled=True)
    course_settings = make_course_settings(course, user)
390
    uses_bootstrap = USE_BOOTSTRAP_FLAG.is_enabled()
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    return {
        'csrf': csrf(request)['csrf_token'],
        'course': course,
        'user': user,
        'user_info': user_info,
        'staff_access': bool(has_access(user, 'staff', course)),
        'roles': utils.get_role_ids(course_key),
        'can_create_comment': has_permission(user, "create_comment", course.id),
        'can_create_subcomment': has_permission(user, "create_sub_comment", course.id),
        'can_create_thread': has_permission(user, "create_thread", course.id),
        'flag_moderator': bool(
            has_permission(user, 'openclose_thread', course.id) or
            has_access(user, 'staff', course)
        ),
        'course_settings': course_settings,
        'disable_courseware_js': True,
407 408
        'uses_bootstrap': uses_bootstrap,
        'uses_pattern_library': not uses_bootstrap,
409 410 411
    }


412 413 414 415 416 417
def _get_discussion_default_topic_id(course):
    for topic, entry in course.discussion_topics.items():
        if entry.get('default') is True:
            return entry['id']


418
def _create_discussion_board_context(request, base_context, thread=None):
419 420 421
    """
    Returns the template context for rendering the discussion board.
    """
422
    context = base_context.copy()
423
    course = context['course']
424 425 426
    course_key = course.id
    thread_id = thread.id if thread else None
    discussion_id = thread.commentable_id if thread else None
427 428 429 430
    course_settings = context['course_settings']
    user = context['user']
    cc_user = cc.User.from_django_user(user)
    user_info = context['user_info']
431
    if thread:
432

433 434 435
        # Since we're in page render mode, and the discussions UI will request the thread list itself,
        # we need only return the thread information for this one.
        threads = [thread.to_dict()]
Rocky Duan committed
436

437
        for thread in threads:
David Baumgold committed
438 439
            # patch for backward compatibility with comments service
            if "pinned" not in thread:
440
                thread["pinned"] = False
441 442 443 444 445 446 447 448
        thread_pages = 1
        root_url = reverse('forum_form_discussion', args=[unicode(course.id)])
    else:
        threads, query_params = get_threads(request, course, user_info)   # This might process a search query
        thread_pages = query_params['num_pages']
        root_url = request.path
    is_staff = has_permission(user, 'openclose_thread', course.id)
    threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
449

450
    with function_trace("get_metadata_for_threads"):
451
        annotated_content_info = utils.get_metadata_for_threads(course_key, threads, user, user_info)
452

453
    with function_trace("add_courseware_context"):
454
        add_courseware_context(threads, course, user)
455

456
    with function_trace("get_cohort_info"):
457 458
        course_discussion_settings = get_course_discussion_settings(course_key)
        user_group_id = get_group_id_for_user(user, course_discussion_settings)
459 460 461 462 463 464 465 466 467

    context.update({
        'root_url': root_url,
        'discussion_id': discussion_id,
        'thread_id': thread_id,
        'threads': threads,
        'thread_pages': thread_pages,
        'annotated_content_info': annotated_content_info,
        'is_moderator': has_permission(user, "see_all_cohorts", course_key),
468
        'groups': course_settings["groups"],  # still needed to render _thread_list_template
469
        'user_group_id': user_group_id,  # read from container in NewPostView
470 471 472
        'sort_preference': cc_user.default_sort_key,
        'category_map': course_settings["category_map"],
        'course_settings': course_settings,
473
        'is_commentable_divided': is_commentable_divided(course_key, discussion_id, course_discussion_settings),
474 475
        # If the default topic id is None the front-end code will look for a topic that contains "General"
        'discussion_default_topic_id': _get_discussion_default_topic_id(course),
476
    })
477 478 479 480 481 482
    context.update(
        get_experiment_user_metadata_context(
            course,
            user,
        )
    )
483
    return context
484

485

486
@require_GET
487
@login_required
488 489 490 491 492 493
@use_bulk_ops
def user_profile(request, course_key, user_id):
    """
    Renders a response to display the user profile page (shown after clicking
    on a post author's username).
    """
494
    user = cc.User.from_django_user(request.user)
495
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
496

497
    try:
498 499 500 501 502
        # If user is not enrolled in the course, do not proceed.
        django_user = User.objects.get(id=user_id)
        if not CourseEnrollment.is_enrolled(django_user, course.id):
            raise Http404

503 504
        query_params = {
            'page': request.GET.get('page', 1),
Calen Pennington committed
505
            'per_page': THREADS_PER_PAGE,   # more than threads_per_page to show more activities
506
        }
507

508
        try:
509
            group_id = get_group_id_for_comments_service(request, course_key)
510
        except ValueError:
511
            return HttpResponseServerError("Invalid group_id")
512 513
        if group_id is not None:
            query_params['group_id'] = group_id
514 515 516
            profiled_user = cc.User(id=user_id, course_id=course_key, group_id=group_id)
        else:
            profiled_user = cc.User(id=user_id, course_id=course_key)
517

518
        threads, page, num_pages = profiled_user.active_threads(query_params)
519 520
        query_params['page'] = page
        query_params['num_pages'] = num_pages
521

522
        with function_trace("get_metadata_for_threads"):
523
            user_info = cc.User.from_django_user(request.user).to_dict()
524
            annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
525

526
        is_staff = has_permission(request.user, 'openclose_thread', course.id)
527
        threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
528
        with function_trace("add_courseware_context"):
529
            add_courseware_context(threads, course, request.user)
530 531
        if request.is_ajax():
            return utils.JsonResponse({
532
                'discussion_data': threads,
533 534
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
535
                'annotated_content_info': annotated_content_info,
536 537
            })
        else:
538 539 540 541
            user_roles = django_user.roles.filter(
                course_id=course.id
            ).order_by("name").values_list("name", flat=True).distinct()

542
            with function_trace("get_cohort_info"):
543 544
                course_discussion_settings = get_course_discussion_settings(course_key)
                user_group_id = get_group_id_for_user(request.user, course_discussion_settings)
545

546 547
            context = _create_base_discussion_view_context(request, course_key)
            context.update({
548
                'django_user': django_user,
549
                'django_user_roles': user_roles,
550
                'profiled_user': profiled_user.to_dict(),
551
                'threads': threads,
552
                'user_group_id': user_group_id,
553
                'annotated_content_info': annotated_content_info,
554 555
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
556
                'sort_preference': user.default_sort_key,
557
                'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}),
558
            })
559

560
            return render_to_response('discussion/discussion_profile_page.html', context)
561
    except User.DoesNotExist:
562
        raise Http404
563 564


565
@login_required
566 567 568 569 570
@use_bulk_ops
def followed_threads(request, course_key, user_id):
    """
    Ajax-only endpoint retrieving the threads followed by a specific user.
    """
571
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
572
    try:
573
        profiled_user = cc.User(id=user_id, course_id=course_key)
574

575 576
        default_query_params = {
            'page': 1,
Calen Pennington committed
577
            'per_page': THREADS_PER_PAGE,   # more than threads_per_page to show more activities
578
            'sort_key': 'date',
579
        }
580

581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
        query_params = merge_dict(
            default_query_params,
            strip_none(
                extract(
                    request.GET,
                    [
                        'page',
                        'sort_key',
                        'flagged',
                        'unread',
                        'unanswered',
                    ]
                )
            )
        )

597
        try:
598
            group_id = get_group_id_for_comments_service(request, course_key)
599
        except ValueError:
600
            return HttpResponseServerError("Invalid group_id")
601 602 603
        if group_id is not None:
            query_params['group_id'] = group_id

604 605 606 607 608
        paginated_results = profiled_user.subscribed_threads(query_params)
        print "\n \n \n paginated results \n \n \n "
        print paginated_results
        query_params['page'] = paginated_results.page
        query_params['num_pages'] = paginated_results.num_pages
609 610
        user_info = cc.User.from_django_user(request.user).to_dict()

611
        with function_trace("get_metadata_for_threads"):
612 613 614 615 616
            annotated_content_info = utils.get_metadata_for_threads(
                course_key,
                paginated_results.collection,
                request.user, user_info
            )
617
        if request.is_ajax():
618
            is_staff = has_permission(request.user, 'openclose_thread', course.id)
619 620
            return utils.JsonResponse({
                'annotated_content_info': annotated_content_info,
621 622 623
                'discussion_data': [
                    utils.prepare_content(thread, course_key, is_staff) for thread in paginated_results.collection
                ],
624 625
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
626
            })
627
        #TODO remove non-AJAX support, it does not appear to be used and does not appear to work.
628 629 630 631 632 633
        else:
            context = {
                'course': course,
                'user': request.user,
                'django_user': User.objects.get(id=user_id),
                'profiled_user': profiled_user.to_dict(),
634 635 636
                'threads': paginated_results.collection,
                'user_info': user_info,
                'annotated_content_info': annotated_content_info,
637
                #                'content': content,
638
            }
639 640

            return render_to_response('discussion/user_profile.html', context)
641
    except User.DoesNotExist:
642
        raise Http404
643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663


class DiscussionBoardFragmentView(EdxFragmentView):
    """
    Component implementation of the discussion board.
    """
    def render_to_fragment(self, request, course_id=None, discussion_id=None, thread_id=None, **kwargs):
        """
        Render the discussion board to a fragment.

        Args:
            request: The Django request.
            course_id: The id of the course in question.
            discussion_id: An optional discussion ID to be focused upon.
            thread_id: An optional ID of the thread to be shown.

        Returns:
            Fragment: The fragment representing the discussion board
        """
        course_key = CourseKey.from_string(course_id)
        try:
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680
            base_context = _create_base_discussion_view_context(request, course_key)
            # Note:
            #   After the thread is rendered in this fragment, an AJAX
            #   request is made and the thread is completely loaded again
            #   (yes, this is something to fix). Because of this, we pass in
            #   raise_event=False to _load_thread_for_viewing avoid duplicate
            #   tracking events.
            thread = (
                _load_thread_for_viewing(
                    request,
                    base_context['course'],
                    discussion_id=discussion_id,
                    thread_id=thread_id,
                    raise_event=False,
                )
                if thread_id
                else None
681
            )
682
            context = _create_discussion_board_context(request, base_context, thread=thread)
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
            html = render_to_string('discussion/discussion_board_fragment.html', context)
            inline_js = render_to_string('discussion/discussion_board_js.template', context)

            fragment = Fragment(html)
            self.add_fragment_resource_urls(fragment)
            fragment.add_javascript(inline_js)
            if not settings.REQUIRE_DEBUG:
                fragment.add_javascript_url(staticfiles_storage.url('discussion/js/discussion_board_factory.js'))
            return fragment
        except cc.utils.CommentClientMaintenanceError:
            log.warning('Forum is in maintenance mode')
            html = render_to_response('discussion/maintenance_fragment.html', {
                'disable_courseware_js': True,
                'uses_pattern_library': True,
            })
            return Fragment(html)

    def vendor_js_dependencies(self):
        """
        Returns list of vendor JS files that this view depends on.

        The helper function that it uses to obtain the list of vendor JS files
        works in conjunction with the Django pipeline to ensure that in development mode
        the files are loaded individually, but in production just the single bundle is loaded.
        """
        dependencies = Set()
        dependencies.update(self.get_js_dependencies('discussion_vendor'))
        return list(dependencies)

    def js_dependencies(self):
        """
        Returns list of JS files that this view depends on.

        The helper function that it uses to obtain the list of JS files
        works in conjunction with the Django pipeline to ensure that in development mode
        the files are loaded individually, but in production just the single bundle is loaded.
        """
        return self.get_js_dependencies('discussion')

    def css_dependencies(self):
        """
        Returns list of CSS files that this view depends on.

        The helper function that it uses to obtain the list of CSS files
        works in conjunction with the Django pipeline to ensure that in development mode
        the files are loaded individually, but in production just the single bundle is loaded.
        """
730 731 732 733 734 735 736
        is_right_to_left = get_language_bidi()
        if USE_BOOTSTRAP_FLAG.is_enabled():
            css_file = BOOTSTRAP_DISCUSSION_CSS_PATH
            if is_right_to_left:
                css_file = css_file.replace('.css', '-rtl.css')
            return [css_file]
        elif is_right_to_left:
737 738 739
            return self.get_css_dependencies('style-discussion-main-rtl')
        else:
            return self.get_css_dependencies('style-discussion-main')
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903


@expect_json
@login_required
def discussion_topics(request, course_key_string):
    """
    The handler for divided discussion categories requests.
    This will raise 404 if user is not staff.

    Returns the JSON representation of discussion topics w.r.t categories for the course.

    Example:
        >>> example = {
        >>>               "course_wide_discussions": {
        >>>                   "entries": {
        >>>                       "General": {
        >>>                           "sort_key": "General",
        >>>                           "is_divided": True,
        >>>                           "id": "i4x-edx-eiorguegnru-course-foobarbaz"
        >>>                       }
        >>>                   }
        >>>                   "children": ["General", "entry"]
        >>>               },
        >>>               "inline_discussions" : {
        >>>                   "subcategories": {
        >>>                       "Getting Started": {
        >>>                           "subcategories": {},
        >>>                           "children": [
        >>>                               ["Working with Videos", "entry"],
        >>>                               ["Videos on edX", "entry"]
        >>>                           ],
        >>>                           "entries": {
        >>>                               "Working with Videos": {
        >>>                                   "sort_key": None,
        >>>                                   "is_divided": False,
        >>>                                   "id": "d9f970a42067413cbb633f81cfb12604"
        >>>                               },
        >>>                               "Videos on edX": {
        >>>                                   "sort_key": None,
        >>>                                   "is_divided": False,
        >>>                                   "id": "98d8feb5971041a085512ae22b398613"
        >>>                               }
        >>>                           }
        >>>                       },
        >>>                       "children": ["Getting Started", "subcategory"]
        >>>                   },
        >>>               }
        >>>          }
    """
    course_key = CourseKey.from_string(course_key_string)
    course = get_course_with_access(request.user, 'staff', course_key)

    discussion_topics = {}
    discussion_category_map = utils.get_discussion_category_map(
        course, request.user, divided_only_if_explicit=True, exclude_unstarted=False
    )

    # We extract the data for the course wide discussions from the category map.
    course_wide_entries = discussion_category_map.pop('entries')

    course_wide_children = []
    inline_children = []

    for name, c_type in discussion_category_map['children']:
        if name in course_wide_entries and c_type == TYPE_ENTRY:
            course_wide_children.append([name, c_type])
        else:
            inline_children.append([name, c_type])

    discussion_topics['course_wide_discussions'] = {
        'entries': course_wide_entries,
        'children': course_wide_children
    }

    discussion_category_map['children'] = inline_children
    discussion_topics['inline_discussions'] = discussion_category_map

    return JsonResponse(discussion_topics)


@require_http_methods(("GET", "PATCH"))
@ensure_csrf_cookie
@expect_json
@login_required
def course_discussions_settings_handler(request, course_key_string):
    """
    The restful handler for divided discussion setting requests. Requires JSON.
    This will raise 404 if user is not staff.
    GET
        Returns the JSON representation of divided discussion settings for the course.
    PATCH
        Updates the divided discussion settings for the course. Returns the JSON representation of updated settings.
    """
    course_key = CourseKey.from_string(course_key_string)
    course = get_course_with_access(request.user, 'staff', course_key)
    discussion_settings = get_course_discussion_settings(course_key)

    if request.method == 'PATCH':
        divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
            course, discussion_settings
        )

        settings_to_change = {}

        if 'divided_course_wide_discussions' in request.json or 'divided_inline_discussions' in request.json:
            divided_course_wide_discussions = request.json.get(
                'divided_course_wide_discussions', divided_course_wide_discussions
            )
            divided_inline_discussions = request.json.get(
                'divided_inline_discussions', divided_inline_discussions
            )
            settings_to_change['divided_discussions'] = divided_course_wide_discussions + divided_inline_discussions

        if 'always_divide_inline_discussions' in request.json:
            settings_to_change['always_divide_inline_discussions'] = request.json.get(
                'always_divide_inline_discussions'
            )
        if 'division_scheme' in request.json:
            settings_to_change['division_scheme'] = request.json.get(
                'division_scheme'
            )

        if not settings_to_change:
            return JsonResponse({"error": unicode("Bad Request")}, 400)

        try:
            if settings_to_change:
                discussion_settings = set_course_discussion_settings(course_key, **settings_to_change)

        except ValueError as err:
            # Note: error message not translated because it is not exposed to the user (UI prevents this state).
            return JsonResponse({"error": unicode(err)}, 400)

    divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
        course, discussion_settings
    )

    return JsonResponse({
        'id': discussion_settings.id,
        'divided_inline_discussions': divided_inline_discussions,
        'divided_course_wide_discussions': divided_course_wide_discussions,
        'always_divide_inline_discussions': discussion_settings.always_divide_inline_discussions,
        'division_scheme': discussion_settings.division_scheme,
        'available_division_schemes': available_division_schemes(course_key)
    })


def get_divided_discussions(course, discussion_settings):
    """
    Returns the course-wide and inline divided discussion ids separately.
    """
    divided_course_wide_discussions = []
    divided_inline_discussions = []

    course_wide_discussions = [topic['id'] for __, topic in course.discussion_topics.items()]
    all_discussions = utils.get_discussion_categories_ids(course, None, include_all=True)

    for divided_discussion_id in discussion_settings.divided_discussions:
        if divided_discussion_id in course_wide_discussions:
            divided_course_wide_discussions.append(divided_discussion_id)
        elif divided_discussion_id in all_discussions:
            divided_inline_discussions.append(divided_discussion_id)

    return divided_course_wide_discussions, divided_inline_discussions