views.py 17.3 KB
Newer Older
Arjun Singh committed
1
import json
2
import logging
3
import xml.sax.saxutils as saxutils
Arjun Singh committed
4

5
from django.contrib.auth.decorators import login_required
6
from django.http import Http404
7
from django.core.context_processors import csrf
8
from django.contrib.auth.models import User
9

10
from mitxmako.shortcuts import render_to_response
11
from courseware.courses import get_course_with_access
12
from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_commentable_cohorted,
13
                                   get_cohorted_commentables, get_course_cohorts, get_cohort_by_id)
14
from courseware.access import has_access
15

16 17
from django_comment_client.permissions import cached_has_permission
from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context)
Rocky Duan committed
18 19
import django_comment_client.utils as utils
import comment_client as cc
Rocky Duan committed
20

21
THREADS_PER_PAGE = 20
22
INLINE_THREADS_PER_PAGE = 20
Rocky Duan committed
23
PAGES_NEARBY_DELTA = 2
24
escapedict = {'"': '"'}
Arjun Singh committed
25
log = logging.getLogger("edx.discussions")
26

Calen Pennington committed
27

28
@login_required
29
def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE):
30 31 32 33
    """
    This may raise cc.utils.CommentClientError or
    cc.utils.CommentClientUnknownError if something goes wrong.
    """
Rocky Duan committed
34 35
    default_query_params = {
        'page': 1,
36
        'per_page': per_page,
37
        'sort_key': 'date',
Rocky Duan committed
38 39 40
        'sort_order': 'desc',
        'text': '',
        'tags': '',
41 42
        'commentable_id': discussion_id,
        'course_id': course_id,
Rocky Duan committed
43
        'user_id': request.user.id,
Rocky Duan committed
44
    }
Your Name committed
45

46 47
    if not request.GET.get('sort_key'):
        # If the user did not select a sort key, use their last used sort key
48 49
        cc_user = cc.User.from_django_user(request.user)
        cc_user.retrieve()
50
        # TODO: After the comment service is updated this can just be user.default_sort_key because the service returns the default value
51
        default_query_params['sort_key'] = cc_user.get('default_sort_key') or default_query_params['sort_key']
52 53
    else:
        # If the user clicked a sort key, update their default sort key
54 55 56
        cc_user = cc.User.from_django_user(request.user)
        cc_user.default_sort_key = request.GET.get('sort_key')
        cc_user.save()
57

Your Name committed
58 59 60
    #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
61

Your Name committed
62
    #if the user requested a group explicitly, give them that group, othewrise, if mod, show all, else if student, use cohort
63

Your Name committed
64
    group_id = request.GET.get('group_id')
65

Your Name committed
66 67
    if group_id == "all":
        group_id = None
68

Your Name committed
69 70 71
    if not group_id:
        if not cached_has_permission(request.user, "see_all_cohorts", course_id):
            group_id = get_cohort_id(request.user, course_id)
72

Your Name committed
73
    if group_id:
74 75
        default_query_params["group_id"] = group_id

Your Name committed
76
    #so by default, a moderator sees all items, and a student sees his cohort
77

Rocky Duan committed
78
    query_params = merge_dict(default_query_params,
79 80 81
                              strip_none(extract(request.GET,
                                                 ['page', 'sort_key',
                                                  'sort_order', 'text',
82
                                                  'tags', 'commentable_ids', 'flagged'])))
Rocky Duan committed
83

84
    threads, page, num_pages = cc.Thread.search(query_params)
85

86 87
    #now add the group name if the thread has a group id
    for thread in threads:
88

Your Name committed
89 90
        if thread.get('group_id'):
            thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
Kevin Chugh committed
91
            thread['group_string'] = "This post visible only to Group %s." % (thread['group_name'])
Your Name committed
92 93 94
        else:
            thread['group_name'] = ""
            thread['group_string'] = "This post visible to everyone."
95

96 97 98
        #patch for backward compatibility to comments service
        if not 'pinned' in thread:
            thread['pinned'] = False
Your Name committed
99

Rocky Duan committed
100 101 102 103 104
    query_params['page'] = page
    query_params['num_pages'] = num_pages

    return threads, query_params

Calen Pennington committed
105

106
@login_required
107
def inline_discussion(request, course_id, discussion_id):
108 109 110
    """
    Renders JSON for DiscussionModules
    """
111
    course = get_course_with_access(request.user, course_id, 'load_forum')
112

113
    try:
Arjun Singh committed
114
        threads, query_params = get_threads(request, course_id, discussion_id, per_page=INLINE_THREADS_PER_PAGE)
115 116
        cc_user = cc.User.from_django_user(request.user)
        user_info = cc_user.to_dict()
Sarina Canelake committed
117
    except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError):
118 119 120
        # TODO (vshnayder): since none of this code seems to be aware of the fact that
        # sometimes things go wrong, I suspect that the js client is also not
        # checking for errors on request.  Check and fix as needed.
121
        log.error("Error loading inline discussion threads.")
122 123
        raise Http404

124
    annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
125

126 127
    allow_anonymous = course.allow_anonymous
    allow_anonymous_to_peers = course.allow_anonymous_to_peers
128

Kevin Chugh committed
129 130 131
    #since inline is all one commentable, only show or allow the choice of cohorts
    #if the commentable is cohorted, otherwise everything is not cohorted
    #and no one has the option of choosing a cohort
Your Name committed
132
    is_cohorted = is_course_cohorted(course_id) and is_commentable_cohorted(course_id, discussion_id)
Your Name committed
133
    is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_id)
134

Your Name committed
135
    cohorts_list = list()
136

Your Name committed
137
    if is_cohorted:
138
        cohorts_list.append({'name': 'All Groups', 'id': None})
139

140
        #if you're a mod, send all cohorts and let you pick
141

142 143
        if is_moderator:
            cohorts = get_course_cohorts(course_id)
Sarina Canelake committed
144 145
            for cohort in cohorts:
                cohorts_list.append({'name': cohort.name, 'id': cohort.id})
146

147 148 149
        else:
            #students don't get to choose
            cohorts_list = None
150

Rocky Duan committed
151
    return utils.JsonResponse({
152
        'discussion_data': map(utils.safe_content, threads),
153
        'user_info': user_info,
154 155
        'annotated_content_info': annotated_content_info,
        'page': query_params['page'],
156 157
        'num_pages': query_params['num_pages'],
        'roles': utils.get_role_ids(course_id),
158 159
        'allow_anonymous_to_peers': allow_anonymous_to_peers,
        'allow_anonymous': allow_anonymous,
Your Name committed
160
        'cohorts': cohorts_list,
Your Name committed
161
        'is_moderator': is_moderator,
Kevin Chugh committed
162
        'is_cohorted': is_cohorted
Rocky Duan committed
163
    })
164

Calen Pennington committed
165

166
@login_required
167
def forum_form_discussion(request, course_id):
168
    """
169
    Renders the main Discussion page, potentially filtered by a search query
170
    """
171

172
    course = get_course_with_access(request.user, course_id, 'load_forum')
173
    category_map = utils.get_discussion_category_map(course)
Arjun Singh committed
174

175
    try:
176
        unsafethreads, query_params = get_threads(request, course_id)   # This might process a search query
177
        threads = [utils.safe_content(thread) for thread in unsafethreads]
Sarina Canelake committed
178
    except cc.utils.CommentClientMaintenanceError:
179 180
        log.warning("Forum is in maintenance mode")
        return render_to_response('discussion/maintenance.html', {})
181
    except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
Sarina Canelake committed
182
        log.error("Error loading forum discussion threads: %s", str(err))
183 184
        raise Http404

185 186
    user = cc.User.from_django_user(request.user)
    user_info = user.to_dict()
187

188
    annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
189

190 191 192
    for thread in threads:
        courseware_context = get_courseware_context(thread, course)
        if courseware_context:
193
            thread.update(courseware_context)
194
    if request.is_ajax():
195
        return utils.JsonResponse({
Calen Pennington committed
196
            'discussion_data': threads,   # TODO: Standardize on 'discussion_data' vs 'threads'
197
            'annotated_content_info': annotated_content_info,
198 199
            'num_pages': query_params['num_pages'],
            'page': query_params['page'],
200
        })
201
    else:
Arjun Singh committed
202 203 204 205 206 207 208 209 210
        #recent_active_threads = cc.search_recent_active_threads(
        #    course_id,
        #    recursive=False,
        #    query_params={'follower_id': request.user.id},
        #)

        #trending_tags = cc.search_trending_tags(
        #    course_id,
        #)
211
        cohorts = get_course_cohorts(course_id)
Your Name committed
212
        cohorted_commentables = get_cohorted_commentables(course_id)
213

Your Name committed
214
        user_cohort_id = get_cohort_id(request.user, course_id)
215

216 217 218
        context = {
            'csrf': csrf(request)['csrf_token'],
            'course': course,
Arjun Singh committed
219 220
            #'recent_active_threads': recent_active_threads,
            #'trending_tags': trending_tags,
Calen Pennington committed
221 222
            'staff_access': has_access(request.user, course, 'staff'),
            'threads': saxutils.escape(json.dumps(threads), escapedict),
Matthew Mongeau committed
223
            'thread_pages': query_params['num_pages'],
Calen Pennington committed
224
            'user_info': saxutils.escape(json.dumps(user_info), escapedict),
225
            'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'),
Calen Pennington committed
226
            'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
227 228
            'course_id': course.id,
            'category_map': category_map,
229
            'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
230
            'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
231
            'cohorts': cohorts,
Your Name committed
232 233
            'user_cohort': user_cohort_id,
            'cohorted_commentables': cohorted_commentables,
234
            'is_course_cohorted': is_course_cohorted(course_id)
235
        }
236
        # print "start rendering.."
237
        return render_to_response('discussion/index.html', context)
238

239

240
@login_required
Rocky Duan committed
241
def single_thread(request, course_id, discussion_id, thread_id):
242
    course = get_course_with_access(request.user, course_id, 'load_forum')
243 244
    cc_user = cc.User.from_django_user(request.user)
    user_info = cc_user.to_dict()
Rocky Duan committed
245

Arjun Singh committed
246
    try:
Your Name committed
247
        thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
Sarina Canelake committed
248
    except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError):
Your Name committed
249 250
        log.error("Error loading single thread.")
        raise Http404
Arjun Singh committed
251

Rocky Duan committed
252
    if request.is_ajax():
253
        courseware_context = get_courseware_context(thread, course)
254
        annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
255
        context = {'thread': thread.to_dict(), 'course_id': course_id}
256
        # TODO: Remove completely or switch back to server side rendering
257
        # html = render_to_string('discussion/_ajax_single_thread.html', context)
258
        content = utils.safe_content(thread.to_dict())
259 260
        if courseware_context:
            content.update(courseware_context)
261
        return utils.JsonResponse({
262
            #'html': html,
263
            'content': content,
Rocky Duan committed
264 265
            'annotated_content_info': annotated_content_info,
        })
266

Rocky Duan committed
267
    else:
Arjun Singh committed
268
        category_map = utils.get_discussion_category_map(course)
Rocky Duan committed
269

Arjun Singh committed
270 271
        try:
            threads, query_params = get_threads(request, course_id)
272
            threads.append(thread.to_dict())
Sarina Canelake committed
273
        except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError):
274
            log.error("Error loading single thread.")
Arjun Singh committed
275
            raise Http404
Rocky Duan committed
276

277
        course = get_course_with_access(request.user, course_id, 'load_forum')
278 279 280 281

        for thread in threads:
            courseware_context = get_courseware_context(thread, course)
            if courseware_context:
282
                thread.update(courseware_context)
Kevin Chugh committed
283
            if thread.get('group_id') and not thread.get('group_name'):
284
                thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
285

286 287 288
            #patch for backward compatibility with comments service
            if not "pinned" in thread:
                thread["pinned"] = False
289

290 291
        threads = [utils.safe_content(thread) for thread in threads]

292 293 294 295 296
        #recent_active_threads = cc.search_recent_active_threads(
        #    course_id,
        #    recursive=False,
        #    query_params={'follower_id': request.user.id},
        #)
297

298 299 300
        #trending_tags = cc.search_trending_tags(
        #    course_id,
        #)
301

302
        annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
303

Your Name committed
304 305 306
        cohorts = get_course_cohorts(course_id)
        cohorted_commentables = get_cohorted_commentables(course_id)
        user_cohort = get_cohort_id(request.user, course_id)
307

Rocky Duan committed
308
        context = {
309
            'discussion_id': discussion_id,
Rocky Duan committed
310
            'csrf': csrf(request)['csrf_token'],
Calen Pennington committed
311 312
            'init': '',   # TODO: What is this?
            'user_info': saxutils.escape(json.dumps(user_info), escapedict),
313
            'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
Rocky Duan committed
314
            'course': course,
315 316
            #'recent_active_threads': recent_active_threads,
            #'trending_tags': trending_tags,
Calen Pennington committed
317
            'course_id': course.id,   # TODO: Why pass both course and course.id to template?
318
            'thread_id': thread_id,
319
            'threads': saxutils.escape(json.dumps(threads), escapedict),
Arjun Singh committed
320
            'category_map': category_map,
321
            'roles': saxutils.escape(json.dumps(utils.get_role_ids(course_id)), escapedict),
322
            'thread_pages': query_params['num_pages'],
Your Name committed
323
            'is_course_cohorted': is_course_cohorted(course_id),
Kevin Chugh committed
324
            'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_id),
325
            'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, course, 'staff'),
Your Name committed
326 327 328
            'cohorts': cohorts,
            'user_cohort': get_cohort_id(request.user, course_id),
            'cohorted_commentables': cohorted_commentables
Rocky Duan committed
329 330
        }

331
        return render_to_response('discussion/single_thread.html', context)
332

Calen Pennington committed
333

334
@login_required
335
def user_profile(request, course_id, user_id):
336
    #TODO: Allow sorting?
337
    course = get_course_with_access(request.user, course_id, 'load_forum')
338 339
    try:
        profiled_user = cc.User(id=user_id, course_id=course_id)
340

341 342
        query_params = {
            'page': request.GET.get('page', 1),
Calen Pennington committed
343
            'per_page': THREADS_PER_PAGE,   # more than threads_per_page to show more activities
344
        }
345

346
        threads, page, num_pages = profiled_user.active_threads(query_params)
347 348
        query_params['page'] = page
        query_params['num_pages'] = num_pages
349 350 351
        user_info = cc.User.from_django_user(request.user).to_dict()

        annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
352 353 354 355

        if request.is_ajax():
            return utils.JsonResponse({
                'discussion_data': map(utils.safe_content, threads),
356 357
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
Calen Pennington committed
358
                'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
359 360 361 362 363 364 365
            })
        else:
            context = {
                'course': course,
                'user': request.user,
                'django_user': User.objects.get(id=user_id),
                'profiled_user': profiled_user.to_dict(),
366
                'threads': saxutils.escape(json.dumps(threads), escapedict),
Calen Pennington committed
367 368
                'user_info': saxutils.escape(json.dumps(user_info), escapedict),
                'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
369
#                'content': content,
370 371 372
            }

            return render_to_response('discussion/user_profile.html', context)
Sarina Canelake committed
373
    except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError, User.DoesNotExist):
374
        raise Http404
375 376


377
def followed_threads(request, course_id, user_id):
378
    course = get_course_with_access(request.user, course_id, 'load_forum')
379 380 381 382 383
    try:
        profiled_user = cc.User(id=user_id, course_id=course_id)

        query_params = {
            'page': request.GET.get('page', 1),
Calen Pennington committed
384
            'per_page': THREADS_PER_PAGE,   # more than threads_per_page to show more activities
385 386
            'sort_key': request.GET.get('sort_key', 'date'),
            'sort_order': request.GET.get('sort_order', 'desc'),
387
        }
388

389 390 391 392 393 394 395 396 397 398
        threads, page, num_pages = profiled_user.subscribed_threads(query_params)
        query_params['page'] = page
        query_params['num_pages'] = num_pages
        user_info = cc.User.from_django_user(request.user).to_dict()

        annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
        if request.is_ajax():
            return utils.JsonResponse({
                'annotated_content_info': annotated_content_info,
                'discussion_data': map(utils.safe_content, threads),
399 400
                'page': query_params['page'],
                'num_pages': query_params['num_pages'],
401
            })
402 403 404 405 406 407 408 409
        else:

            context = {
                'course': course,
                'user': request.user,
                'django_user': User.objects.get(id=user_id),
                'profiled_user': profiled_user.to_dict(),
                'threads': saxutils.escape(json.dumps(threads), escapedict),
Calen Pennington committed
410 411
                'user_info': saxutils.escape(json.dumps(user_info), escapedict),
                'annotated_content_info': saxutils.escape(json.dumps(annotated_content_info), escapedict),
412
                #                'content': content,
413
            }
414 415

            return render_to_response('discussion/user_profile.html', context)
Sarina Canelake committed
416
    except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError, User.DoesNotExist):
417
        raise Http404