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

from functools import wraps
Arjun Singh committed
6
import json
7
import logging
Arjun Singh committed
8

9
from django.contrib.auth.decorators import login_required
10
from django.conf import settings
11
from django.core.context_processors import csrf
12
from django.core.urlresolvers import reverse
13
from django.contrib.auth.models import User
14
from django.http import Http404, HttpResponseBadRequest
15
from django.utils.translation import ugettext_noop
16
from django.views.decorators.http import require_GET
17
import newrelic.agent
18

David Baumgold committed
19
from edxmako.shortcuts import render_to_response
20
from courseware.courses import get_course_with_access
21 22 23 24 25
from openedx.core.djangoapps.course_groups.cohorts import (
    is_course_cohorted,
    get_cohort_id,
    get_course_cohorts,
)
26
from courseware.tabs import EnrolledTab
27
from courseware.access import has_access
28
from xmodule.modulestore.django import modulestore
29

30 31
from django_comment_common.utils import ThreadContext
from django_comment_client.permissions import has_permission, get_team
32 33 34 35 36
from django_comment_client.utils import (
    merge_dict,
    extract,
    strip_none,
    add_courseware_context,
37 38
    get_group_id_for_comments_service,
    is_commentable_cohorted
39
)
Rocky Duan committed
40
import django_comment_client.utils as utils
41
import lms.lib.comment_client as cc
Rocky Duan committed
42

43
from opaque_keys.edx.keys import CourseKey
44

45
THREADS_PER_PAGE = 20
46
INLINE_THREADS_PER_PAGE = 20
Rocky Duan committed
47
PAGES_NEARBY_DELTA = 2
Arjun Singh committed
48
log = logging.getLogger("edx.discussions")
49

Calen Pennington committed
50

51
class DiscussionTab(EnrolledTab):
52 53 54 55
    """
    A tab for the cs_comments_service forums.
    """

56
    type = 'discussion'
57
    title = ugettext_noop('Discussion')
58 59
    priority = None
    view_name = 'django_comment_client.forum.views.forum_form_discussion'
60
    is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False)
61
    is_default = False
62 63 64

    @classmethod
    def is_enabled(cls, course, user=None):
65
        if not super(DiscussionTab, cls).is_enabled(course, user):
66
            return False
67
        return utils.is_discussion_enabled(course.id)
68 69


70
@newrelic.agent.function_trace()
71
def make_course_settings(course, user):
72 73 74 75 76 77 78 79 80
    """
    Generate a JSON-serializable model for course settings, which will be used to initialize a
    DiscussionCourseSettings object on the client.
    """

    obj = {
        'is_cohorted': is_course_cohorted(course.id),
        'allow_anonymous': course.allow_anonymous,
        'allow_anonymous_to_peers': course.allow_anonymous_to_peers,
81
        'cohorts': [{"id": str(g.id), "name": g.name} for g in get_course_cohorts(course)],
82
        'category_map': utils.get_discussion_category_map(course, user)
83 84 85 86
    }

    return obj

87

88
@newrelic.agent.function_trace()
89
def get_threads(request, course, discussion_id=None, per_page=THREADS_PER_PAGE):
90
    """
91
    This may raise an appropriate subclass of cc.utils.CommentClientError
92
    if something goes wrong, or ValueError if the group_id is invalid.
93
    """
Rocky Duan committed
94 95
    default_query_params = {
        'page': 1,
96
        'per_page': per_page,
97
        'sort_key': 'date',
Rocky Duan committed
98 99
        'sort_order': 'desc',
        'text': '',
100
        'course_id': unicode(course.id),
Rocky Duan committed
101
        'user_id': request.user.id,
102
        'context': ThreadContext.COURSE,
103
        'group_id': get_group_id_for_comments_service(request, course.id, discussion_id),  # may raise ValueError
Rocky Duan committed
104
    }
Your Name committed
105

106 107 108 109
    # 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
110 111 112
        # 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
113

114 115
    if not request.GET.get('sort_key'):
        # If the user did not select a sort key, use their last used sort key
116 117
        cc_user = cc.User.from_django_user(request.user)
        cc_user.retrieve()
118
        # TODO: After the comment service is updated this can just be user.default_sort_key because the service returns the default value
119
        default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key']
120 121
    else:
        # If the user clicked a sort key, update their default sort key
122 123 124
        cc_user = cc.User.from_django_user(request.user)
        cc_user.default_sort_key = request.GET.get('sort_key')
        cc_user.save()
125

Your Name committed
126 127 128
    #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
129

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

149 150
    paginated_results = cc.Thread.search(query_params)
    threads = paginated_results.collection
151

152 153 154 155 156 157 158 159 160
    # 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
        ]

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

166 167 168
    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
169 170 171

    return threads, query_params

Calen Pennington committed
172

173 174 175 176 177 178 179 180 181 182 183 184 185 186
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
        course_key = CourseKey.from_string(course_id)
        with modulestore().bulk_operations(course_key):
            return view_func(request, course_key, *args, **kwargs)
    return wrapped_view


187
@login_required
188 189
@use_bulk_ops
def inline_discussion(request, course_key, discussion_id):
190 191 192
    """
    Renders JSON for DiscussionModules
    """
193 194
    nr_transaction = newrelic.agent.current_transaction()

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

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

204
    with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
205
        annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
206
    is_staff = has_permission(request.user, 'openclose_thread', course.id)
207 208
    threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
    with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
209
        add_courseware_context(threads, course, request.user)
Rocky Duan committed
210
    return utils.JsonResponse({
211
        'is_commentable_cohorted': is_commentable_cohorted(course_key, discussion_id),
212
        'discussion_data': threads,
213
        'user_info': user_info,
214 215
        'annotated_content_info': annotated_content_info,
        'page': query_params['page'],
216
        'num_pages': query_params['num_pages'],
217
        'roles': utils.get_role_ids(course_key),
218
        'course_settings': make_course_settings(course, request.user)
Rocky Duan committed
219
    })
220

221

222
@login_required
223 224
@use_bulk_ops
def forum_form_discussion(request, course_key):
225
    """
226
    Renders the main Discussion page, potentially filtered by a search query
227
    """
228
    nr_transaction = newrelic.agent.current_transaction()
229

230
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
231
    course_settings = make_course_settings(course, request.user)
Arjun Singh committed
232

233 234 235
    user = cc.User.from_django_user(request.user)
    user_info = user.to_dict()

236
    try:
237
        unsafethreads, query_params = get_threads(request, course)   # This might process a search query
238
        is_staff = has_permission(request.user, 'openclose_thread', course.id)
239
        threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
Sarina Canelake committed
240
    except cc.utils.CommentClientMaintenanceError:
241 242
        log.warning("Forum is in maintenance mode")
        return render_to_response('discussion/maintenance.html', {})
243 244
    except ValueError:
        return HttpResponseBadRequest("Invalid group_id")
245

246
    with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
247
        annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
248

249
    with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
250
        add_courseware_context(threads, course, request.user)
251

252
    if request.is_ajax():
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
        with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
262
            user_cohort_id = get_cohort_id(request.user, course_key)
263

264 265 266
        context = {
            'csrf': csrf(request)['csrf_token'],
            'course': course,
Arjun Singh committed
267
            #'recent_active_threads': recent_active_threads,
268
            'staff_access': bool(has_access(request.user, 'staff', course)),
269
            'threads': json.dumps(threads),
Matthew Mongeau committed
270
            'thread_pages': query_params['num_pages'],
271 272
            'user_info': json.dumps(user_info, default=lambda x: None),
            'can_create_comment': json.dumps(
273
                has_permission(request.user, "create_comment", course.id)),
274
            'can_create_subcomment': json.dumps(
275 276
                has_permission(request.user, "create_sub_comment", course.id)),
            'can_create_thread': has_permission(request.user, "create_thread", course.id),
277
            'flag_moderator': bool(
278 279 280
                has_permission(request.user, 'openclose_thread', course.id) or
                has_access(request.user, 'staff', course)
            ),
281
            'annotated_content_info': json.dumps(annotated_content_info),
282
            'course_id': course.id.to_deprecated_string(),
283
            'roles': json.dumps(utils.get_role_ids(course_key)),
284
            'is_moderator': has_permission(request.user, "see_all_cohorts", course_key),
285
            'cohorts': course_settings["cohorts"],  # still needed to render _thread_list_template
286
            'user_cohort': user_cohort_id,  # read from container in NewPostView
287
            'is_course_cohorted': is_course_cohorted(course_key),  # still needed to render _thread_list_template
288
            'sort_preference': user.default_sort_key,
289
            'category_map': course_settings["category_map"],
290
            'course_settings': json.dumps(course_settings)
291
        }
292
        # print "start rendering.."
293
        return render_to_response('discussion/index.html', context)
294

295

296
@require_GET
297
@login_required
298 299 300 301 302
@use_bulk_ops
def single_thread(request, course_key, discussion_id, thread_id):
    """
    Renders a response to display a single discussion thread.
    """
303 304
    nr_transaction = newrelic.agent.current_transaction()

305
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
306
    course_settings = make_course_settings(course, request.user)
307 308
    cc_user = cc.User.from_django_user(request.user)
    user_info = cc_user.to_dict()
309
    is_moderator = has_permission(request.user, "see_all_cohorts", course_key)
Rocky Duan committed
310

311 312 313
    # Currently, the front end always loads responses via AJAX, even for this
    # page; it would be a nice optimization to avoid that extra round trip to
    # the comments service.
314 315 316 317 318 319 320 321 322 323 324
    try:
        thread = cc.Thread.find(thread_id).retrieve(
            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 as e:
        if e.status_code == 404:
            raise Http404
        raise
Arjun Singh committed
325

326
    # Verify that the student has access to this thread if belongs to a course discussion module
327 328
    thread_context = getattr(thread, "context", "course")
    if thread_context == "course" and not utils.discussion_category_id_access(course, request.user, discussion_id):
329 330
        raise Http404

331 332 333
    # verify that the thread belongs to the requesting student's cohort
    if is_commentable_cohorted(course_key, discussion_id) and not is_moderator:
        user_group_id = get_cohort_id(request.user, course_key)
334
        if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id:
335 336
            raise Http404

337
    is_staff = has_permission(request.user, 'openclose_thread', course.id)
Rocky Duan committed
338
    if request.is_ajax():
339
        with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
340 341 342 343 344 345
            annotated_content_info = utils.get_annotated_content_infos(
                course_key,
                thread,
                request.user,
                user_info=user_info
            )
346
        content = utils.prepare_content(thread.to_dict(), course_key, is_staff)
347
        with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
348
            add_courseware_context([content], course, request.user)
349
        return utils.JsonResponse({
350
            'content': content,
Rocky Duan committed
351 352
            'annotated_content_info': annotated_content_info,
        })
353

Rocky Duan committed
354
    else:
355
        try:
356
            threads, query_params = get_threads(request, course)
357 358
        except ValueError:
            return HttpResponseBadRequest("Invalid group_id")
359
        threads.append(thread.to_dict())
Rocky Duan committed
360

361
        with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
362
            add_courseware_context(threads, course, request.user)
363 364

        for thread in threads:
David Baumgold committed
365 366
            # patch for backward compatibility with comments service
            if "pinned" not in thread:
367
                thread["pinned"] = False
368

369
        threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
370

371
        with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
372
            annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
373

374
        with newrelic.agent.FunctionTrace(nr_transaction, "get_cohort_info"):
375
            user_cohort = get_cohort_id(request.user, course_key)
376

Rocky Duan committed
377
        context = {
378
            'discussion_id': discussion_id,
Rocky Duan committed
379
            'csrf': csrf(request)['csrf_token'],
Calen Pennington committed
380
            'init': '',   # TODO: What is this?
381 382
            'user_info': json.dumps(user_info),
            'can_create_comment': json.dumps(
383
                has_permission(request.user, "create_comment", course.id)),
384
            'can_create_subcomment': json.dumps(
385 386
                has_permission(request.user, "create_sub_comment", course.id)),
            'can_create_thread': has_permission(request.user, "create_thread", course.id),
387
            'annotated_content_info': json.dumps(annotated_content_info),
Rocky Duan committed
388
            'course': course,
389
            #'recent_active_threads': recent_active_threads,
390
            'course_id': course.id.to_deprecated_string(),   # TODO: Why pass both course and course.id to template?
391
            'thread_id': thread_id,
392 393
            'threads': json.dumps(threads),
            'roles': json.dumps(utils.get_role_ids(course_key)),
394
            'is_moderator': is_moderator,
395
            'thread_pages': query_params['num_pages'],
396
            'is_course_cohorted': is_course_cohorted(course_key),
397
            'flag_moderator': bool(
398 399 400
                has_permission(request.user, 'openclose_thread', course.id) or
                has_access(request.user, 'staff', course)
            ),
401 402
            'cohorts': course_settings["cohorts"],
            'user_cohort': user_cohort,
403
            'sort_preference': cc_user.default_sort_key,
404
            'category_map': course_settings["category_map"],
405
            'course_settings': json.dumps(course_settings)
Rocky Duan committed
406
        }
407
        return render_to_response('discussion/index.html', context)
408

409

410
@require_GET
411
@login_required
412 413 414 415 416 417 418
@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).
    """

419 420
    nr_transaction = newrelic.agent.current_transaction()

421
    #TODO: Allow sorting?
422
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
423
    try:
424 425
        query_params = {
            'page': request.GET.get('page', 1),
Calen Pennington committed
426
            'per_page': THREADS_PER_PAGE,   # more than threads_per_page to show more activities
427
        }
428

429
        try:
430
            group_id = get_group_id_for_comments_service(request, course_key)
431 432 433 434
        except ValueError:
            return HttpResponseBadRequest("Invalid group_id")
        if group_id is not None:
            query_params['group_id'] = group_id
435 436 437
            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)
438

439
        threads, page, num_pages = profiled_user.active_threads(query_params)
440 441
        query_params['page'] = page
        query_params['num_pages'] = num_pages
442 443
        user_info = cc.User.from_django_user(request.user).to_dict()

444
        with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
445
            annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
446

447
        is_staff = has_permission(request.user, 'openclose_thread', course.id)
448
        threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
449 450
        if request.is_ajax():
            return utils.JsonResponse({
451
                'discussion_data': threads,
452 453
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
454
                'annotated_content_info': json.dumps(annotated_content_info),
455 456
            })
        else:
457
            django_user = User.objects.get(id=user_id)
458 459 460
            context = {
                'course': course,
                'user': request.user,
461
                'django_user': django_user,
462
                'profiled_user': profiled_user.to_dict(),
463 464 465
                'threads': json.dumps(threads),
                'user_info': json.dumps(user_info, default=lambda x: None),
                'annotated_content_info': json.dumps(annotated_content_info),
466 467
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
468
                'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username})
469 470 471
            }

            return render_to_response('discussion/user_profile.html', context)
472
    except User.DoesNotExist:
473
        raise Http404
474 475


476
@login_required
477 478 479 480 481 482
@use_bulk_ops
def followed_threads(request, course_key, user_id):
    """
    Ajax-only endpoint retrieving the threads followed by a specific user.
    """

483 484
    nr_transaction = newrelic.agent.current_transaction()

485
    course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
486
    try:
487
        profiled_user = cc.User(id=user_id, course_id=course_key)
488

489 490
        default_query_params = {
            'page': 1,
Calen Pennington committed
491
            'per_page': THREADS_PER_PAGE,   # more than threads_per_page to show more activities
492 493
            'sort_key': 'date',
            'sort_order': 'desc',
494
        }
495

496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
        query_params = merge_dict(
            default_query_params,
            strip_none(
                extract(
                    request.GET,
                    [
                        'page',
                        'sort_key',
                        'sort_order',
                        'flagged',
                        'unread',
                        'unanswered',
                    ]
                )
            )
        )

513
        try:
514
            group_id = get_group_id_for_comments_service(request, course_key)
515 516 517 518 519
        except ValueError:
            return HttpResponseBadRequest("Invalid group_id")
        if group_id is not None:
            query_params['group_id'] = group_id

520 521 522 523 524
        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
525 526
        user_info = cc.User.from_django_user(request.user).to_dict()

527
        with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
528 529 530 531 532
            annotated_content_info = utils.get_metadata_for_threads(
                course_key,
                paginated_results.collection,
                request.user, user_info
            )
533
        if request.is_ajax():
534
            is_staff = has_permission(request.user, 'openclose_thread', course.id)
535 536
            return utils.JsonResponse({
                'annotated_content_info': annotated_content_info,
537 538 539
                'discussion_data': [
                    utils.prepare_content(thread, course_key, is_staff) for thread in paginated_results.collection
                ],
540 541
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
542
            })
543
        #TODO remove non-AJAX support, it does not appear to be used and does not appear to work.
544 545 546 547 548 549
        else:
            context = {
                'course': course,
                'user': request.user,
                'django_user': User.objects.get(id=user_id),
                'profiled_user': profiled_user.to_dict(),
550
                'threads': json.dumps(paginated_results.collection),
551 552
                'user_info': json.dumps(user_info),
                'annotated_content_info': json.dumps(annotated_content_info),
553
                #                'content': content,
554
            }
555 556

            return render_to_response('discussion/user_profile.html', context)
557
    except User.DoesNotExist:
558
        raise Http404