views.py 18.1 KB
Newer Older
1 2 3 4 5 6
import time
import random
import os
import os.path
import logging
import urlparse
Rocky Duan committed
7
import functools
8

9
import lms.lib.comment_client as cc
Rocky Duan committed
10
import django_comment_client.utils as utils
Arjun Singh committed
11 12
import django_comment_client.settings as cc_settings

13 14

from django.core import exceptions
15 16
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET
17 18 19
from django.views.decorators import csrf
from django.core.files.storage import get_storage_class
from django.utils.translation import ugettext as _
20

David Baumgold committed
21
from edxmako.shortcuts import render_to_string
22
from courseware.courses import get_course_with_access, get_course_by_id
23
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
24

25
from django_comment_client.utils import JsonResponse, JsonError, extract, add_courseware_context
26

27
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
Kevin Chugh committed
28
from courseware.access import has_access
Mike Chen committed
29

30 31
log = logging.getLogger(__name__)

Calen Pennington committed
32

33 34 35 36 37
def permitted(fn):
    @functools.wraps(fn)
    def wrapper(request, *args, **kwargs):
        def fetch_content():
            if "thread_id" in kwargs:
38
                content = cc.Thread.find(kwargs["thread_id"]).to_dict()
39
            elif "comment_id" in kwargs:
40
                content = cc.Comment.find(kwargs["comment_id"]).to_dict()
Mike Chen committed
41
            else:
42 43
                content = None
            return content
44
        if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name):
45 46
            return fn(request, *args, **kwargs)
        else:
47
            return JsonError("unauthorized", status=401)
48
    return wrapper
Mike Chen committed
49

Calen Pennington committed
50

51
def ajax_content_response(request, course_id, content):
52 53 54 55
    context = {
        'course_id': course_id,
        'content': content,
    }
56
    user_info = cc.User.from_django_user(request.user).to_dict()
57
    annotated_content_info = utils.get_annotated_content_info(course_id, content, request.user, user_info)
58
    return JsonResponse({
59
        'content': utils.safe_content(content),
60 61 62
        'annotated_content_info': annotated_content_info,
    })

63

64
@require_POST
65
@login_required
66
@permitted
67
def create_thread(request, course_id, commentable_id):
68 69 70
    """
    Given a course and commentble ID, create the thread
    """
71

72
    log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
73
    course = get_course_with_access(request.user, course_id, 'load')
74
    post = request.POST
75

76
    if course.allow_anonymous:
77 78 79 80
        anonymous = post.get('anonymous', 'false').lower() == 'true'
    else:
        anonymous = False

81
    if course.allow_anonymous_to_peers:
82 83 84 85
        anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
    else:
        anonymous_to_peers = False

86 87 88 89 90
    if 'title' not in post or not post['title'].strip():
        return JsonError(_("Title can't be empty"))
    if 'body' not in post or not post['body'].strip():
        return JsonError(_("Body can't be empty"))

91
    thread = cc.Thread(**extract(post, ['body', 'title']))
Rocky Duan committed
92
    thread.update_attributes(**{
Calen Pennington committed
93 94 95 96 97
        'anonymous': anonymous,
        'anonymous_to_peers': anonymous_to_peers,
        'commentable_id': commentable_id,
        'course_id': course_id,
        'user_id': request.user.id,
Rocky Duan committed
98
    })
99

100
    user = cc.User.from_django_user(request.user)
101

102 103
    #kevinchugh because the new requirement is that all groups will be determined
    #by the group id in the request this all goes away
Your Name committed
104
    #not anymore, only for admins
105

106
    # Cohort the thread if the commentable is cohorted.
Your Name committed
107 108 109
    if is_commentable_cohorted(course_id, commentable_id):
        user_group_id = get_cohort_id(user, course_id)

110 111 112
        # TODO (vshnayder): once we have more than just cohorts, we'll want to
        # change this to a single get_group_for_user_and_commentable function
        # that can do different things depending on the commentable_id
Your Name committed
113
        if cached_has_permission(request.user, "see_all_cohorts", course_id):
114
            # admins can optionally choose what group to post as
Your Name committed
115 116
            group_id = post.get('group_id', user_group_id)
        else:
117
            # regular users always post with their own id.
Your Name committed
118 119
            group_id = user_group_id

Kevin Chugh committed
120
        if group_id:
Your Name committed
121
            thread.update_attributes(group_id=group_id)
122

123
    thread.save()
124

125 126 127
    #patch for backward compatibility to comments service
    if not 'pinned' in thread.attributes:
        thread['pinned'] = False
128

129
    if post.get('auto_subscribe', 'false').lower() == 'true':
130 131
        user = cc.User.from_django_user(request.user)
        user.follow(thread)
132
    data = thread.to_dict()
133
    add_courseware_context([data], course)
134
    if request.is_ajax():
135
        return ajax_content_response(request, course_id, data)
136
    else:
137
        return JsonResponse(utils.safe_content(data))
138

Calen Pennington committed
139

140
@require_POST
141
@login_required
142
@permitted
143
def update_thread(request, course_id, thread_id):
144 145 146
    """
    Given a course id and thread id, update a existing thread, used for both static and ajax submissions
    """
147 148 149 150
    if 'title' not in request.POST or not request.POST['title'].strip():
        return JsonError(_("Title can't be empty"))
    if 'body' not in request.POST or not request.POST['body'].strip():
        return JsonError(_("Body can't be empty"))
151
    thread = cc.Thread.find(thread_id)
152
    thread.update_attributes(**extract(request.POST, ['body', 'title']))
153
    thread.save()
154
    if request.is_ajax():
155
        return ajax_content_response(request, course_id, thread.to_dict())
156
    else:
157
        return JsonResponse(utils.safe_content(thread.to_dict()))
158

Calen Pennington committed
159

160
def _create_comment(request, course_id, thread_id=None, parent_id=None):
161 162 163 164
    """
    given a course_id, thread_id, and parent_id, create a comment,
    called from create_comment to do the actual creation
    """
165
    post = request.POST
166 167 168

    if 'body' not in post or not post['body'].strip():
        return JsonError(_("Body can't be empty"))
169
    comment = cc.Comment(**extract(post, ['body']))
170

171
    course = get_course_with_access(request.user, course_id, 'load')
172
    if course.allow_anonymous:
173 174 175 176
        anonymous = post.get('anonymous', 'false').lower() == 'true'
    else:
        anonymous = False

177
    if course.allow_anonymous_to_peers:
178 179 180 181
        anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
    else:
        anonymous_to_peers = False

Rocky Duan committed
182
    comment.update_attributes(**{
Calen Pennington committed
183 184 185 186 187 188
        'anonymous': anonymous,
        'anonymous_to_peers': anonymous_to_peers,
        'user_id': request.user.id,
        'course_id': course_id,
        'thread_id': thread_id,
        'parent_id': parent_id,
Rocky Duan committed
189
    })
190 191
    comment.save()
    if post.get('auto_subscribe', 'false').lower() == 'true':
192 193
        user = cc.User.from_django_user(request.user)
        user.follow(comment.thread)
194
    if request.is_ajax():
195
        return ajax_content_response(request, course_id, comment.to_dict())
196
    else:
197
        return JsonResponse(utils.safe_content(comment.to_dict()))
198

Calen Pennington committed
199

200
@require_POST
201
@login_required
202
@permitted
203
def create_comment(request, course_id, thread_id):
204 205 206 207
    """
    given a course_id and thread_id, test for comment depth. if not too deep,
    call _create_comment to create the actual comment.
    """
Arjun Singh committed
208 209
    if cc_settings.MAX_COMMENT_DEPTH is not None:
        if cc_settings.MAX_COMMENT_DEPTH < 0:
210
            return JsonError(_("Comment level too deep"))
211
    return _create_comment(request, course_id, thread_id=thread_id)
212

Calen Pennington committed
213

214
@require_POST
215
@login_required
216
@permitted
217
def delete_thread(request, course_id, thread_id):
218 219 220 221
    """
    given a course_id and thread_id, delete this thread
    this is ajax only
    """
222
    thread = cc.Thread.find(thread_id)
223
    thread.delete()
224
    return JsonResponse(utils.safe_content(thread.to_dict()))
225

Calen Pennington committed
226

227
@require_POST
228
@login_required
229
@permitted
230
def update_comment(request, course_id, comment_id):
231 232 233 234
    """
    given a course_id and comment_id, update the comment with payload attributes
    handles static and ajax submissions
    """
235
    comment = cc.Comment.find(comment_id)
236 237
    if 'body' not in request.POST or not request.POST['body'].strip():
        return JsonError(_("Body can't be empty"))
238 239
    comment.update_attributes(**extract(request.POST, ['body']))
    comment.save()
240
    if request.is_ajax():
241
        return ajax_content_response(request, course_id, comment.to_dict())
242
    else:
243
        return JsonResponse(utils.safe_content(comment.to_dict()))
244

Calen Pennington committed
245

246
@require_POST
247
@login_required
248
@permitted
249
def endorse_comment(request, course_id, comment_id):
250 251 252 253
    """
    given a course_id and comment_id, toggle the endorsement of this comment,
    ajax only
    """
254
    comment = cc.Comment.find(comment_id)
255 256
    comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
    comment.save()
257
    return JsonResponse(utils.safe_content(comment.to_dict()))
258

Calen Pennington committed
259

260
@require_POST
261
@login_required
262
@permitted
Mike Chen committed
263
def openclose_thread(request, course_id, thread_id):
264 265 266 267
    """
    given a course_id and thread_id, toggle the status of this thread
    ajax only
    """
Rocky Duan committed
268 269 270
    thread = cc.Thread.find(thread_id)
    thread.closed = request.POST.get('closed', 'false').lower() == 'true'
    thread.save()
271 272
    thread = thread.to_dict()
    return JsonResponse({
273
        'content': utils.safe_content(thread),
274 275
        'ability': utils.get_ability(course_id, thread, request.user),
    })
Mike Chen committed
276

Calen Pennington committed
277

Mike Chen committed
278 279
@require_POST
@login_required
280
@permitted
281
def create_sub_comment(request, course_id, comment_id):
282 283 284 285
    """
    given a course_id and comment_id, create a response to a comment
    after checking the max depth allowed, if allowed
    """
Arjun Singh committed
286 287
    if cc_settings.MAX_COMMENT_DEPTH is not None:
        if cc_settings.MAX_COMMENT_DEPTH <= cc.Comment.find(comment_id).depth:
288
            return JsonError(_("Comment level too deep"))
289
    return _create_comment(request, course_id, parent_id=comment_id)
290

Calen Pennington committed
291

292
@require_POST
293
@login_required
294
@permitted
295
def delete_comment(request, course_id, comment_id):
296 297 298 299
    """
    given a course_id and comment_id delete this comment
    ajax only
    """
300
    comment = cc.Comment.find(comment_id)
301
    comment.delete()
302
    return JsonResponse(utils.safe_content(comment.to_dict()))
303

Calen Pennington committed
304

305
@require_POST
306
@login_required
307
@permitted
308
def vote_for_comment(request, course_id, comment_id, value):
309 310 311
    """
    given a course_id and comment_id,
    """
312 313
    user = cc.User.from_django_user(request.user)
    comment = cc.Comment.find(comment_id)
314
    user.vote(comment, value)
315
    return JsonResponse(utils.safe_content(comment.to_dict()))
316

Calen Pennington committed
317

318
@require_POST
319
@login_required
320
@permitted
Rocky Duan committed
321
def undo_vote_for_comment(request, course_id, comment_id):
322 323 324 325
    """
    given a course id and comment id, remove vote
    ajax only
    """
326 327
    user = cc.User.from_django_user(request.user)
    comment = cc.Comment.find(comment_id)
328
    user.unvote(comment)
329
    return JsonResponse(utils.safe_content(comment.to_dict()))
Rocky Duan committed
330

Calen Pennington committed
331

Rocky Duan committed
332
@require_POST
333
@login_required
334
@permitted
335
def vote_for_thread(request, course_id, thread_id, value):
336 337 338 339
    """
    given a course id and thread id vote for this thread
    ajax only
    """
340 341
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
342
    user.vote(thread, value)
343
    return JsonResponse(utils.safe_content(thread.to_dict()))
344

Calen Pennington committed
345

346
@require_POST
347
@login_required
348
@permitted
349
def flag_abuse_for_thread(request, course_id, thread_id):
350 351 352 353
    """
    given a course_id and thread_id flag this thread for abuse
    ajax only
    """
354 355 356 357 358 359 360 361 362 363
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
    thread.flagAbuse(user, thread)
    return JsonResponse(utils.safe_content(thread.to_dict()))


@require_POST
@login_required
@permitted
def un_flag_abuse_for_thread(request, course_id, thread_id):
364 365 366 367
    """
    given a course id and thread id, remove abuse flag for this thread
    ajax only
    """
368 369 370 371 372 373 374 375 376 377 378 379
    user = cc.User.from_django_user(request.user)
    course = get_course_by_id(course_id)
    thread = cc.Thread.find(thread_id)
    removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
    thread.unFlagAbuse(user, thread, removeAll)
    return JsonResponse(utils.safe_content(thread.to_dict()))


@require_POST
@login_required
@permitted
def flag_abuse_for_comment(request, course_id, comment_id):
380 381 382 383
    """
    given a course and comment id, flag comment for abuse
    ajax only
    """
384 385 386 387 388 389 390 391 392 393
    user = cc.User.from_django_user(request.user)
    comment = cc.Comment.find(comment_id)
    comment.flagAbuse(user, comment)
    return JsonResponse(utils.safe_content(comment.to_dict()))


@require_POST
@login_required
@permitted
def un_flag_abuse_for_comment(request, course_id, comment_id):
394 395 396 397
    """
    given a course_id and comment id, unflag comment for abuse
    ajax only
    """
398 399 400 401 402 403 404 405 406 407 408
    user = cc.User.from_django_user(request.user)
    course = get_course_by_id(course_id)
    removeAll = cached_has_permission(request.user, 'openclose_thread', course_id) or has_access(request.user, course, 'staff')
    comment = cc.Comment.find(comment_id)
    comment.unFlagAbuse(user, comment, removeAll)
    return JsonResponse(utils.safe_content(comment.to_dict()))


@require_POST
@login_required
@permitted
Rocky Duan committed
409
def undo_vote_for_thread(request, course_id, thread_id):
410 411 412 413
    """
    given a course id and thread id, remove users vote for thread
    ajax only
    """
414 415
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
416
    user.unvote(thread)
417
    return JsonResponse(utils.safe_content(thread.to_dict()))
418

419

Your Name committed
420 421 422 423
@require_POST
@login_required
@permitted
def pin_thread(request, course_id, thread_id):
424 425 426 427
    """
    given a course id and thread id, pin this thread
    ajax only
    """
Your Name committed
428 429
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
430
    thread.pin(user, thread_id)
Your Name committed
431 432
    return JsonResponse(utils.safe_content(thread.to_dict()))

433

434 435 436
@require_POST
@login_required
@permitted
Your Name committed
437
def un_pin_thread(request, course_id, thread_id):
438 439 440 441
    """
    given a course id and thread id, remove pin from this thread
    ajax only
    """
Your Name committed
442 443
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
444
    thread.un_pin(user, thread_id)
Your Name committed
445 446
    return JsonResponse(utils.safe_content(thread.to_dict()))

Rocky Duan committed
447 448

@require_POST
449
@login_required
450
@permitted
Rocky Duan committed
451
def follow_thread(request, course_id, thread_id):
452 453
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
454 455
    user.follow(thread)
    return JsonResponse({})
456

Calen Pennington committed
457

458
@require_POST
459
@login_required
460
@permitted
Rocky Duan committed
461
def follow_commentable(request, course_id, commentable_id):
462 463 464 465
    """
    given a course_id and commentable id, follow this commentable
    ajax only
    """
466 467
    user = cc.User.from_django_user(request.user)
    commentable = cc.Commentable.find(commentable_id)
468 469
    user.follow(commentable)
    return JsonResponse({})
470

Calen Pennington committed
471

472
@require_POST
473
@login_required
474
@permitted
Rocky Duan committed
475
def follow_user(request, course_id, followed_user_id):
476 477
    user = cc.User.from_django_user(request.user)
    followed_user = cc.User.find(followed_user_id)
478 479
    user.follow(followed_user)
    return JsonResponse({})
480

Calen Pennington committed
481

482
@require_POST
483
@login_required
484
@permitted
Rocky Duan committed
485
def unfollow_thread(request, course_id, thread_id):
486 487 488 489
    """
    given a course id and thread id, stop following this thread
    ajax only
    """
490 491
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
492 493
    user.unfollow(thread)
    return JsonResponse({})
494

Calen Pennington committed
495

496
@require_POST
497
@login_required
498
@permitted
Rocky Duan committed
499
def unfollow_commentable(request, course_id, commentable_id):
500 501 502 503
    """
    given a course id and commentable id stop following commentable
    ajax only
    """
504 505
    user = cc.User.from_django_user(request.user)
    commentable = cc.Commentable.find(commentable_id)
506 507
    user.unfollow(commentable)
    return JsonResponse({})
508

Calen Pennington committed
509

510
@require_POST
511
@login_required
512
@permitted
Rocky Duan committed
513
def unfollow_user(request, course_id, followed_user_id):
514 515 516 517
    """
    given a course id and user id, stop following this user
    ajax only
    """
518 519
    user = cc.User.from_django_user(request.user)
    followed_user = cc.User.find(followed_user_id)
520 521
    user.unfollow(followed_user)
    return JsonResponse({})
522

Calen Pennington committed
523

524
@require_POST
525 526
@login_required
@csrf.csrf_exempt
Calen Pennington committed
527
def upload(request, course_id):  # ajax upload file to a question or answer
528 529 530 531 532 533 534 535 536
    """view that handles file upload via Ajax
    """

    # check upload permission
    result = ''
    error = ''
    new_file_name = ''
    try:
        # TODO authorization
537
        #may raise exceptions.PermissionDenied
538 539 540 541 542 543 544 545 546
        #if request.user.is_anonymous():
        #    msg = _('Sorry, anonymous users cannot upload files')
        #    raise exceptions.PermissionDenied(msg)

        #request.user.assert_can_upload_file()

        # check file type
        f = request.FILES['file-upload']
        file_extension = os.path.splitext(f.name)[1].lower()
Rocky Duan committed
547 548
        if not file_extension in cc_settings.ALLOWED_UPLOAD_FILE_TYPES:
            file_types = "', '".join(cc_settings.ALLOWED_UPLOAD_FILE_TYPES)
549
            msg = _("allowed file types are '%(file_types)s'") % \
550
                {'file_types': file_types}
551 552 553
            raise exceptions.PermissionDenied(msg)

        # generate new file name
554
        new_file_name = str(time.time()).replace('.', str(random.randint(0, 100000))) + file_extension
555 556 557 558 559 560 561

        file_storage = get_storage_class()()
        # use default storage to store file
        file_storage.save(new_file_name, f)
        # check file size
        # byte
        size = file_storage.size(new_file_name)
Rocky Duan committed
562
        if size > cc_settings.MAX_UPLOAD_FILE_SIZE:
563 564
            file_storage.delete(new_file_name)
            msg = _("maximum upload file size is %(file_size)sK") % \
565
                {'file_size': cc_settings.MAX_UPLOAD_FILE_SIZE}
566 567
            raise exceptions.PermissionDenied(msg)

568
    except exceptions.PermissionDenied, err:
e0d committed
569
        error = unicode(err)
570 571 572
    except Exception, err:
        print err
        logging.critical(unicode(err))
573 574 575
        error = _('Error uploading file. Please contact the site administrator. Thank you.')

    if error == '':
576
        result = _('Good')
577 578 579 580
        file_url = file_storage.url(new_file_name)
        parsed_url = urlparse.urlparse(file_url)
        file_url = urlparse.urlunparse(
            urlparse.ParseResult(
581
                parsed_url.scheme,
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
                parsed_url.netloc,
                parsed_url.path,
                '', '', ''
            )
        )
    else:
        result = ''
        file_url = ''

    return JsonResponse({
        'result': {
            'msg': result,
            'error': error,
            'file_url': file_url,
        }
    })