views.py 25.5 KB
Newer Older
1
import functools
2
import logging
3
import json
4 5
import random
import time
6 7
import urlparse

8
from django.contrib.auth.decorators import login_required
9 10
from django.contrib.auth.models import User
from django.core import exceptions
11
from django.http import Http404, HttpResponseBadRequest, HttpResponse
12
from django.utils.translation import ugettext as _
13 14 15
from django.views.decorators import csrf
from django.views.decorators.http import require_GET, require_POST
from opaque_keys.edx.keys import CourseKey
16

17
from courseware.access import has_access
18
from util.file import store_uploaded_file
19
from courseware.courses import get_course_with_access, get_course_overview_with_access, get_course_by_id
20
import django_comment_client.settings as cc_settings
21 22 23 24 25 26 27 28 29
from django_comment_common.signals import (
    thread_created,
    thread_edited,
    thread_voted,
    thread_deleted,
    comment_created,
    comment_edited,
    comment_voted,
    comment_deleted,
30
    comment_endorsed,
31
)
32
from django_comment_common.utils import ThreadContext
33 34 35 36
from django_comment_client.utils import (
    add_courseware_context,
    get_annotated_content_info,
    get_ability,
37
    is_comment_too_deep,
38 39
    JsonError,
    JsonResponse,
40
    prepare_content,
41
    get_group_id_for_comments_service,
42
    discussion_category_id_access,
43
    get_cached_discussion_id_map,
44
)
45
from django_comment_client.permissions import check_permissions_by_view, has_permission, get_team
46
from eventtracking import tracker
47
import lms.lib.comment_client as cc
Mike Chen committed
48

49 50
log = logging.getLogger(__name__)

51
TRACKING_MAX_FORUM_BODY = 2000
52
_EVENT_NAME_TEMPLATE = 'edx.forum.{obj_type}.{action_name}'
53

54

55 56 57 58 59 60 61
def track_forum_event(request, event_name, course, obj, data, id_map=None):
    """
    Send out an analytics event when a forum event happens. Works for threads,
    responses to threads, and comments on those responses.
    """
    user = request.user
    data['id'] = obj.id
62 63
    commentable_id = data['commentable_id']

64 65 66 67
    team = get_team(commentable_id)
    if team is not None:
        data.update(team_id=team.team_id)

68
    if id_map is None:
69
        id_map = get_cached_discussion_id_map(course, [commentable_id], user)
70 71 72 73 74 75 76 77 78 79 80 81 82 83
    if commentable_id in id_map:
        data['category_name'] = id_map[commentable_id]["title"]
        data['category_id'] = commentable_id
    data['url'] = request.META.get('HTTP_REFERER', '')
    data['user_forums_roles'] = [
        role.name for role in user.roles.filter(course_id=course.id)
    ]
    data['user_course_roles'] = [
        role.role for role in user.courseaccessrole_set.filter(course_id=course.id)
    ]

    tracker.emit(event_name, data)


84
def track_created_event(request, event_name, course, obj, data):
85 86 87
    """
    Send analytics event for a newly created thread, response or comment.
    """
88 89 90 91 92 93 94 95 96
    if len(obj.body) > TRACKING_MAX_FORUM_BODY:
        data['truncated'] = True
    else:
        data['truncated'] = False
    data['body'] = obj.body[:TRACKING_MAX_FORUM_BODY]
    track_forum_event(request, event_name, course, obj, data)


def track_thread_created_event(request, course, thread, followed):
97 98 99
    """
    Send analytics event for a newly created thread.
    """
100 101
    event_name = _EVENT_NAME_TEMPLATE.format(obj_type='thread', action_name='created')
    event_data = {
102 103 104 105 106 107 108 109 110 111 112 113
        'commentable_id': thread.commentable_id,
        'group_id': thread.get("group_id"),
        'thread_type': thread.thread_type,
        'title': thread.title,
        'anonymous': thread.anonymous,
        'anonymous_to_peers': thread.anonymous_to_peers,
        'options': {'followed': followed},
        # There is a stated desire for an 'origin' property that will state
        # whether this thread was created via courseware or the forum.
        # However, the view does not contain that data, and including it will
        # likely require changes elsewhere.
    }
114
    track_created_event(request, event_name, course, thread, event_data)
115 116


117
def track_comment_created_event(request, course, comment, commentable_id, followed):
118 119 120
    """
    Send analytics event for a newly created response or comment.
    """
121 122
    obj_type = 'comment' if comment.get("parent_id") else 'response'
    event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='created')
123 124 125 126 127
    event_data = {
        'discussion': {'id': comment.thread_id},
        'commentable_id': commentable_id,
        'options': {'followed': followed},
    }
128
    parent_id = comment.get('parent_id')
129 130
    if parent_id:
        event_data['response'] = {'id': parent_id}
131 132 133 134
    track_created_event(request, event_name, course, comment, event_data)


def track_voted_event(request, course, obj, vote_value, undo_vote=False):
135 136 137
    """
    Send analytics event for a vote on a thread or response.
    """
138 139 140 141 142 143 144 145 146 147 148 149 150 151
    if isinstance(obj, cc.Thread):
        obj_type = 'thread'
    else:
        obj_type = 'response'
    event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='voted')
    event_data = {
        'commentable_id': obj.commentable_id,
        'target_username': obj.get('username'),
        'undo_vote': undo_vote,
        'vote_value': vote_value,
    }
    track_forum_event(request, event_name, course, obj, event_data)


152 153 154 155 156
def permitted(func):
    """
    View decorator to verify the user is authorized to access this endpoint.
    """
    @functools.wraps(func)
157
    def wrapper(request, *args, **kwargs):
158 159 160
        """
        Wrapper for the view that only calls the view if the user is authorized.
        """
161
        def fetch_content():
162 163 164
            """
            Extract the forum object from the keyword arguments to the view.
            """
165 166 167 168 169 170 171 172 173
            if "thread_id" in kwargs:
                content = cc.Thread.find(kwargs["thread_id"]).to_dict()
            elif "comment_id" in kwargs:
                content = cc.Comment.find(kwargs["comment_id"]).to_dict()
            elif "commentable_id" in kwargs:
                content = cc.Commentable.find(kwargs["commentable_id"]).to_dict()
            else:
                content = None
            return content
174
        course_key = CourseKey.from_string(kwargs['course_id'])
175
        if check_permissions_by_view(request.user, course_key, fetch_content(), request.view_name):
176
            return func(request, *args, **kwargs)
177 178 179
        else:
            return JsonError("unauthorized", status=401)
    return wrapper
180

181 182

def ajax_content_response(request, course_key, content):
183 184 185
    """
    Standard AJAX response returning the content hierarchy of the current thread.
    """
186 187 188 189 190 191
    user_info = cc.User.from_django_user(request.user).to_dict()
    annotated_content_info = get_annotated_content_info(course_key, content, request.user, user_info)
    return JsonResponse({
        'content': prepare_content(content, course_key),
        'annotated_content_info': annotated_content_info,
    })
192 193


194
@require_POST
195
@login_required
196
@permitted
197
def create_thread(request, course_id, commentable_id):
198 199 200
    """
    Given a course and commentble ID, create the thread
    """
201

202
    log.debug("Creating new thread in %r, id %r", course_id, commentable_id)
203
    course_key = CourseKey.from_string(course_id)
204
    course = get_course_with_access(request.user, 'load', course_key)
205
    post = request.POST
206
    user = request.user
207

208
    if course.allow_anonymous:
209 210 211 212
        anonymous = post.get('anonymous', 'false').lower() == 'true'
    else:
        anonymous = False

213
    if course.allow_anonymous_to_peers:
214 215 216 217
        anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
    else:
        anonymous_to_peers = False

218 219 220 221 222
    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"))

223 224 225 226 227
    params = {
        'anonymous': anonymous,
        'anonymous_to_peers': anonymous_to_peers,
        'commentable_id': commentable_id,
        'course_id': course_key.to_deprecated_string(),
228
        'user_id': user.id,
229 230 231 232 233
        'thread_type': post["thread_type"],
        'body': post["body"],
        'title': post["title"],
    }

234 235 236 237 238
    # Check for whether this commentable belongs to a team, and add the right context
    if get_team(commentable_id) is not None:
        params['context'] = ThreadContext.STANDALONE
    else:
        params['context'] = ThreadContext.COURSE
239 240

    thread = cc.Thread(**params)
241

242 243 244 245 246 247 248
    # Cohort the thread if required
    try:
        group_id = get_group_id_for_comments_service(request, course_key, commentable_id)
    except ValueError:
        return HttpResponseBadRequest("Invalid cohort id")
    if group_id is not None:
        thread.group_id = group_id
249

250
    thread.save()
251

252 253
    thread_created.send(sender=None, user=user, post=thread)

David Baumgold committed
254 255
    # patch for backward compatibility to comments service
    if 'pinned' not in thread.attributes:
256
        thread['pinned'] = False
257

258 259 260
    follow = post.get('auto_subscribe', 'false').lower() == 'true'

    if follow:
261 262
        cc_user = cc.User.from_django_user(user)
        cc_user.follow(thread)
263

264
    data = thread.to_dict()
265

266
    add_courseware_context([data], course, user)
267

268
    track_thread_created_event(request, course, thread, follow)
269

270
    if request.is_ajax():
271
        return ajax_content_response(request, course_key, data)
272
    else:
273
        return JsonResponse(prepare_content(data, course_key))
274

Calen Pennington committed
275

276
@require_POST
277
@login_required
278
@permitted
279
def update_thread(request, course_id, thread_id):
280 281 282
    """
    Given a course id and thread id, update a existing thread, used for both static and ajax submissions
    """
283 284 285 286
    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"))
287

288
    course_key = CourseKey.from_string(course_id)
289
    thread = cc.Thread.find(thread_id)
290 291
    # Get thread context first in order to be safe from reseting the values of thread object later
    thread_context = getattr(thread, "context", "course")
292 293
    thread.body = request.POST["body"]
    thread.title = request.POST["title"]
294
    user = request.user
295 296 297 298
    # The following checks should avoid issues we've seen during deploys, where end users are hitting an updated server
    # while their browser still has the old client code. This will avoid erasing present values in those cases.
    if "thread_type" in request.POST:
        thread.thread_type = request.POST["thread_type"]
299
    if "commentable_id" in request.POST:
300
        commentable_id = request.POST["commentable_id"]
301 302
        course = get_course_with_access(user, 'load', course_key)
        if thread_context == "course" and not discussion_category_id_access(course, user, commentable_id):
303
            return JsonError(_("Topic doesn't exist"))
304 305
        else:
            thread.commentable_id = commentable_id
306 307

    thread.save()
308 309 310

    thread_edited.send(sender=None, user=user, post=thread)

311
    if request.is_ajax():
312
        return ajax_content_response(request, course_key, thread.to_dict())
313
    else:
314
        return JsonResponse(prepare_content(thread.to_dict(), course_key))
315

Calen Pennington committed
316

317
def _create_comment(request, course_key, thread_id=None, parent_id=None):
318
    """
319
    given a course_key, thread_id, and parent_id, create a comment,
320 321
    called from create_comment to do the actual creation
    """
322
    assert isinstance(course_key, CourseKey)
323
    post = request.POST
324
    user = request.user
325 326 327

    if 'body' not in post or not post['body'].strip():
        return JsonError(_("Body can't be empty"))
328

329
    course = get_course_with_access(user, 'load', course_key)
330
    if course.allow_anonymous:
331 332 333 334
        anonymous = post.get('anonymous', 'false').lower() == 'true'
    else:
        anonymous = False

335
    if course.allow_anonymous_to_peers:
336 337 338 339
        anonymous_to_peers = post.get('anonymous_to_peers', 'false').lower() == 'true'
    else:
        anonymous_to_peers = False

340 341 342
    comment = cc.Comment(
        anonymous=anonymous,
        anonymous_to_peers=anonymous_to_peers,
343
        user_id=user.id,
344
        course_id=course_key.to_deprecated_string(),
345 346 347 348
        thread_id=thread_id,
        parent_id=parent_id,
        body=post["body"]
    )
349
    comment.save()
350

351 352
    comment_created.send(sender=None, user=user, post=comment)

353 354 355
    followed = post.get('auto_subscribe', 'false').lower() == 'true'

    if followed:
356 357
        cc_user = cc.User.from_django_user(request.user)
        cc_user.follow(comment.thread)
358

359
    track_comment_created_event(request, course, comment, comment.thread.commentable_id, followed)
360

361
    if request.is_ajax():
362
        return ajax_content_response(request, course_key, comment.to_dict())
363
    else:
364
        return JsonResponse(prepare_content(comment.to_dict(), course.id))
365

Calen Pennington committed
366

367
@require_POST
368
@login_required
369
@permitted
370
def create_comment(request, course_id, thread_id):
371 372 373 374
    """
    given a course_id and thread_id, test for comment depth. if not too deep,
    call _create_comment to create the actual comment.
    """
375 376
    if is_comment_too_deep(parent=None):
        return JsonError(_("Comment level too deep"))
377
    return _create_comment(request, CourseKey.from_string(course_id), thread_id=thread_id)
378

Calen Pennington committed
379

380
@require_POST
381
@login_required
382
@permitted
383
def delete_thread(request, course_id, thread_id):
384 385 386 387
    """
    given a course_id and thread_id, delete this thread
    this is ajax only
    """
388
    course_key = CourseKey.from_string(course_id)
389
    thread = cc.Thread.find(thread_id)
390
    thread.delete()
391
    thread_deleted.send(sender=None, user=request.user, post=thread)
392
    return JsonResponse(prepare_content(thread.to_dict(), course_key))
393

Calen Pennington committed
394

395
@require_POST
396
@login_required
397
@permitted
398
def update_comment(request, course_id, comment_id):
399 400 401 402
    """
    given a course_id and comment_id, update the comment with payload attributes
    handles static and ajax submissions
    """
403
    course_key = CourseKey.from_string(course_id)
404
    comment = cc.Comment.find(comment_id)
405 406
    if 'body' not in request.POST or not request.POST['body'].strip():
        return JsonError(_("Body can't be empty"))
407
    comment.body = request.POST["body"]
408
    comment.save()
409 410 411

    comment_edited.send(sender=None, user=request.user, post=comment)

412
    if request.is_ajax():
413
        return ajax_content_response(request, course_key, comment.to_dict())
414
    else:
415
        return JsonResponse(prepare_content(comment.to_dict(), course_key))
416

Calen Pennington committed
417

418
@require_POST
419
@login_required
420
@permitted
421
def endorse_comment(request, course_id, comment_id):
422 423 424 425
    """
    given a course_id and comment_id, toggle the endorsement of this comment,
    ajax only
    """
426
    course_key = CourseKey.from_string(course_id)
427
    comment = cc.Comment.find(comment_id)
428
    user = request.user
429
    comment.endorsed = request.POST.get('endorsed', 'false').lower() == 'true'
430
    comment.endorsement_user_id = user.id
431
    comment.save()
432
    comment_endorsed.send(sender=None, user=user, post=comment)
433
    return JsonResponse(prepare_content(comment.to_dict(), course_key))
434

Calen Pennington committed
435

436
@require_POST
437
@login_required
438
@permitted
Mike Chen committed
439
def openclose_thread(request, course_id, thread_id):
440 441 442 443
    """
    given a course_id and thread_id, toggle the status of this thread
    ajax only
    """
444
    course_key = CourseKey.from_string(course_id)
Rocky Duan committed
445 446 447
    thread = cc.Thread.find(thread_id)
    thread.closed = request.POST.get('closed', 'false').lower() == 'true'
    thread.save()
448

449
    return JsonResponse({
450 451
        'content': prepare_content(thread.to_dict(), course_key),
        'ability': get_ability(course_key, thread.to_dict(), request.user),
452
    })
Mike Chen committed
453

Calen Pennington committed
454

Mike Chen committed
455 456
@require_POST
@login_required
457
@permitted
458
def create_sub_comment(request, course_id, comment_id):
459 460 461 462
    """
    given a course_id and comment_id, create a response to a comment
    after checking the max depth allowed, if allowed
    """
463 464
    if is_comment_too_deep(parent=cc.Comment(comment_id)):
        return JsonError(_("Comment level too deep"))
465
    return _create_comment(request, CourseKey.from_string(course_id), parent_id=comment_id)
466

Calen Pennington committed
467

468
@require_POST
469
@login_required
470
@permitted
471
def delete_comment(request, course_id, comment_id):
472 473 474 475
    """
    given a course_id and comment_id delete this comment
    ajax only
    """
476
    course_key = CourseKey.from_string(course_id)
477
    comment = cc.Comment.find(comment_id)
478
    comment.delete()
479
    comment_deleted.send(sender=None, user=request.user, post=comment)
480
    return JsonResponse(prepare_content(comment.to_dict(), course_key))
481

Calen Pennington committed
482

483 484 485 486
def _vote_or_unvote(request, course_id, obj, value='up', undo_vote=False):
    """
    Vote or unvote for a thread or a response.
    """
487
    course_key = CourseKey.from_string(course_id)
488 489 490 491 492 493 494 495 496 497 498 499 500
    course = get_course_with_access(request.user, 'load', course_key)
    user = cc.User.from_django_user(request.user)
    if undo_vote:
        user.unvote(obj)
        # TODO(smarnach): Determine the value of the vote that is undone.  Currently, you can
        # only cast upvotes in the user interface, so it is assumed that the vote value is 'up'.
        # (People could theoretically downvote by handcrafting AJAX requests.)
    else:
        user.vote(obj, value)
    track_voted_event(request, course, obj, value, undo_vote)
    return JsonResponse(prepare_content(obj.to_dict(), course_key))


501
@require_POST
502
@login_required
503
@permitted
504
def vote_for_comment(request, course_id, comment_id, value):
505
    """
506
    Given a course_id and comment_id, vote for this response.  AJAX only.
507
    """
508
    comment = cc.Comment.find(comment_id)
509 510 511
    result = _vote_or_unvote(request, course_id, comment, value)
    comment_voted.send(sender=None, user=request.user, post=comment)
    return result
512

Calen Pennington committed
513

514
@require_POST
515
@login_required
516
@permitted
Rocky Duan committed
517
def undo_vote_for_comment(request, course_id, comment_id):
518 519 520 521
    """
    given a course id and comment id, remove vote
    ajax only
    """
522
    return _vote_or_unvote(request, course_id, cc.Comment.find(comment_id), undo_vote=True)
Rocky Duan committed
523

Calen Pennington committed
524

Rocky Duan committed
525
@require_POST
526
@login_required
527
@permitted
528
def vote_for_thread(request, course_id, thread_id, value):
529 530 531 532
    """
    given a course id and thread id vote for this thread
    ajax only
    """
533
    thread = cc.Thread.find(thread_id)
534 535 536 537 538 539 540 541 542 543 544 545 546 547
    result = _vote_or_unvote(request, course_id, thread, value)
    thread_voted.send(sender=None, user=request.user, post=thread)
    return result


@require_POST
@login_required
@permitted
def undo_vote_for_thread(request, course_id, thread_id):
    """
    given a course id and thread id, remove users vote for thread
    ajax only
    """
    return _vote_or_unvote(request, course_id, cc.Thread.find(thread_id), undo_vote=True)
548

Calen Pennington committed
549

550
@require_POST
551
@login_required
552
@permitted
553
def flag_abuse_for_thread(request, course_id, thread_id):
554 555 556 557
    """
    given a course_id and thread_id flag this thread for abuse
    ajax only
    """
558
    course_key = CourseKey.from_string(course_id)
559 560 561
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
    thread.flagAbuse(user, thread)
562 563

    return JsonResponse(prepare_content(thread.to_dict(), course_key))
564 565 566 567 568 569


@require_POST
@login_required
@permitted
def un_flag_abuse_for_thread(request, course_id, thread_id):
570 571 572 573
    """
    given a course id and thread id, remove abuse flag for this thread
    ajax only
    """
574
    user = cc.User.from_django_user(request.user)
575
    course_key = CourseKey.from_string(course_id)
576
    course = get_course_by_id(course_key)
577
    thread = cc.Thread.find(thread_id)
578
    remove_all = bool(
579 580 581
        has_permission(request.user, 'openclose_thread', course_key) or
        has_access(request.user, 'staff', course)
    )
582
    thread.unFlagAbuse(user, thread, remove_all)
583 584

    return JsonResponse(prepare_content(thread.to_dict(), course_key))
585 586 587 588 589 590


@require_POST
@login_required
@permitted
def flag_abuse_for_comment(request, course_id, comment_id):
591 592 593 594
    """
    given a course and comment id, flag comment for abuse
    ajax only
    """
595
    course_key = CourseKey.from_string(course_id)
596 597 598
    user = cc.User.from_django_user(request.user)
    comment = cc.Comment.find(comment_id)
    comment.flagAbuse(user, comment)
599
    return JsonResponse(prepare_content(comment.to_dict(), course_key))
600 601 602 603 604 605


@require_POST
@login_required
@permitted
def un_flag_abuse_for_comment(request, course_id, comment_id):
606 607 608 609
    """
    given a course_id and comment id, unflag comment for abuse
    ajax only
    """
610
    user = cc.User.from_django_user(request.user)
611
    course_key = CourseKey.from_string(course_id)
612
    course = get_course_by_id(course_key)
613
    remove_all = bool(
614 615 616
        has_permission(request.user, 'openclose_thread', course_key) or
        has_access(request.user, 'staff', course)
    )
617
    comment = cc.Comment.find(comment_id)
618
    comment.unFlagAbuse(user, comment, remove_all)
619
    return JsonResponse(prepare_content(comment.to_dict(), course_key))
620 621 622 623 624


@require_POST
@login_required
@permitted
Your Name committed
625
def pin_thread(request, course_id, thread_id):
626 627 628 629
    """
    given a course id and thread id, pin this thread
    ajax only
    """
630
    course_key = CourseKey.from_string(course_id)
Your Name committed
631 632
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
633
    thread.pin(user, thread_id)
634 635

    return JsonResponse(prepare_content(thread.to_dict(), course_key))
Your Name committed
636

637

638 639 640
@require_POST
@login_required
@permitted
Your Name committed
641
def un_pin_thread(request, course_id, thread_id):
642 643 644 645
    """
    given a course id and thread id, remove pin from this thread
    ajax only
    """
646
    course_key = CourseKey.from_string(course_id)
Your Name committed
647 648
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
649
    thread.un_pin(user, thread_id)
650 651

    return JsonResponse(prepare_content(thread.to_dict(), course_key))
Your Name committed
652

Rocky Duan committed
653 654

@require_POST
655
@login_required
656
@permitted
Rocky Duan committed
657
def follow_thread(request, course_id, thread_id):
658 659
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
660 661
    user.follow(thread)
    return JsonResponse({})
662

Calen Pennington committed
663

664
@require_POST
665
@login_required
666
@permitted
Rocky Duan committed
667
def follow_commentable(request, course_id, commentable_id):
668 669 670 671
    """
    given a course_id and commentable id, follow this commentable
    ajax only
    """
672 673
    user = cc.User.from_django_user(request.user)
    commentable = cc.Commentable.find(commentable_id)
674 675
    user.follow(commentable)
    return JsonResponse({})
676

Calen Pennington committed
677

678
@require_POST
679
@login_required
680
@permitted
Rocky Duan committed
681
def unfollow_thread(request, course_id, thread_id):
682 683 684 685
    """
    given a course id and thread id, stop following this thread
    ajax only
    """
686 687
    user = cc.User.from_django_user(request.user)
    thread = cc.Thread.find(thread_id)
688 689
    user.unfollow(thread)
    return JsonResponse({})
690

Calen Pennington committed
691

692
@require_POST
693
@login_required
694
@permitted
Rocky Duan committed
695
def unfollow_commentable(request, course_id, commentable_id):
696 697 698 699
    """
    given a course id and commentable id stop following commentable
    ajax only
    """
700 701
    user = cc.User.from_django_user(request.user)
    commentable = cc.Commentable.find(commentable_id)
702 703
    user.unfollow(commentable)
    return JsonResponse({})
704

Calen Pennington committed
705

706
@require_POST
707 708
@login_required
@csrf.csrf_exempt
Calen Pennington committed
709
def upload(request, course_id):  # ajax upload file to a question or answer
710 711 712 713 714 715 716 717
    """view that handles file upload via Ajax
    """

    # check upload permission
    error = ''
    new_file_name = ''
    try:
        # TODO authorization
718
        #may raise exceptions.PermissionDenied
719 720 721 722 723 724
        #if request.user.is_anonymous():
        #    msg = _('Sorry, anonymous users cannot upload files')
        #    raise exceptions.PermissionDenied(msg)

        #request.user.assert_can_upload_file()

725 726 727 728 729
        base_file_name = str(time.time()).replace('.', str(random.randint(0, 100000)))
        file_storage, new_file_name = store_uploaded_file(
            request, 'file-upload', cc_settings.ALLOWED_UPLOAD_FILE_TYPES, base_file_name,
            max_file_size=cc_settings.MAX_UPLOAD_FILE_SIZE
        )
730

731
    except exceptions.PermissionDenied, err:
e0d committed
732
        error = unicode(err)
733 734 735
    except Exception, err:
        print err
        logging.critical(unicode(err))
736 737 738
        error = _('Error uploading file. Please contact the site administrator. Thank you.')

    if error == '':
739
        result = _('Good')
740 741 742 743
        file_url = file_storage.url(new_file_name)
        parsed_url = urlparse.urlparse(file_url)
        file_url = urlparse.urlunparse(
            urlparse.ParseResult(
744
                parsed_url.scheme,
745 746 747 748 749 750 751 752 753
                parsed_url.netloc,
                parsed_url.path,
                '', '', ''
            )
        )
    else:
        result = ''
        file_url = ''

754 755 756 757
    # Using content-type of text/plain here instead of JSON because
    # IE doesn't know how to handle the JSON response and prompts the
    # user to save the JSON as a file instead of passing it to the callback.
    return HttpResponse(json.dumps({
758 759 760 761 762
        'result': {
            'msg': result,
            'error': error,
            'file_url': file_url,
        }
763
    }), content_type="text/plain")
764

765

766 767 768 769 770 771 772 773 774
@require_GET
@login_required
def users(request, course_id):
    """
    Given a `username` query parameter, find matches for users in the forum for this course.

    Only exact matches are supported here, so the length of the result set will either be 0 or 1.
    """

775
    course_key = CourseKey.from_string(course_id)
776
    try:
777
        get_course_overview_with_access(request.user, 'load', course_key, check_if_enrolled=True)
778 779 780 781 782 783 784 785 786 787 788 789 790 791
    except Http404:
        # course didn't exist, or requesting user does not have access to it.
        return JsonError(status=404)

    try:
        username = request.GET['username']
    except KeyError:
        # 400 is default status for JsonError
        return JsonError(["username parameter is required"])

    user_objs = []
    try:
        matched_user = User.objects.get(username=username)
        cc_user = cc.User.from_django_user(matched_user)
792
        cc_user.course_id = course_key
793 794 795 796 797 798 799 800 801
        cc_user.retrieve(complete=False)
        if (cc_user['threads_count'] + cc_user['comments_count']) > 0:
            user_objs.append({
                'id': matched_user.id,
                'username': matched_user.username,
            })
    except User.DoesNotExist:
        pass
    return JsonResponse({"users": user_objs})