views.py 17 KB
Newer Older
Greg Price committed
1 2 3
"""
Discussion API views
"""
4
from django.core.exceptions import ValidationError
5 6
from rest_framework.exceptions import UnsupportedMediaType
from rest_framework.parsers import JSONParser
Greg Price committed
7 8 9

from rest_framework.response import Response
from rest_framework.views import APIView
10
from rest_framework.viewsets import ViewSet
Greg Price committed
11

12
from opaque_keys.edx.keys import CourseKey
13
from xmodule.modulestore.django import modulestore
Greg Price committed
14

15 16 17
from discussion_api.api import (
    create_comment,
    create_thread,
18
    delete_thread,
19
    delete_comment,
20
    get_comment_list,
21
    get_response_comments,
22
    get_course,
23
    get_course_topics,
24
    get_thread,
25
    get_thread_list,
26
    update_comment,
27
    update_thread,
28
)
29
from discussion_api.forms import CommentListGetForm, ThreadListGetForm, CommentGetForm
30
from openedx.core.lib.api.parsers import MergePatchParser
31
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
Greg Price committed
32 33


34 35
@view_auth_classes()
class CourseView(DeveloperErrorViewMixin, APIView):
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
    """
    **Use Cases**

        Retrieve general discussion metadata for a course.

    **Example Requests**:

        GET /api/discussion/v1/courses/course-v1:ExampleX+Subject101+2015

    **Response Values**:

        * id: The identifier of the course

        * blackouts: A list of objects representing blackout periods (during
            which discussions are read-only except for privileged users). Each
            item in the list includes:

            * start: The ISO 8601 timestamp for the start of the blackout period

            * end: The ISO 8601 timestamp for the end of the blackout period

        * thread_list_url: The URL of the list of all threads in the course.

        * topics_url: The URL of the topic listing for the course.
    """
    def get(self, request, course_id):
        """Implements the GET method as described in the class docstring."""
        course_key = CourseKey.from_string(course_id)  # TODO: which class is right?
        return Response(get_course(request, course_key))


67 68
@view_auth_classes()
class CourseTopicsView(DeveloperErrorViewMixin, APIView):
Greg Price committed
69 70 71 72 73 74 75 76
    """
    **Use Cases**

        Retrieve the topic listing for a course. Only topics accessible to the
        authenticated user are included.

    **Example Requests**:

77
        GET /api/discussion/v1/course_topics/course-v1:ExampleX+Subject101+2015
78
            ?topic_id={topic_id_1, topid_id_2}
Greg Price committed
79 80 81

    **Response Values**:
        * courseware_topics: The list of topic trees for courseware-linked
82
            topics. Each item in the list includes:
Greg Price committed
83 84 85 86 87 88 89 90 91 92 93 94

            * id: The id of the discussion topic (null for a topic that only
              has children but cannot contain threads itself).

            * name: The display name of the topic.

            * children: A list of child subtrees of the same format.

        * non_courseware_topics: The list of topic trees that are not linked to
              courseware. Items are of the same format as in courseware_topics.
    """
    def get(self, request, course_id):
95 96 97
        """
        Implements the GET method as described in the class docstring.
        """
98
        course_key = CourseKey.from_string(course_id)
99
        topic_ids = self.request.GET.get('topic_id')
100
        with modulestore().bulk_operations(course_key):
101 102 103 104 105
            response = get_course_topics(
                request,
                course_key,
                set(topic_ids.strip(',').split(',')) if topic_ids else None,
            )
106
        return Response(response)
107 108


109 110
@view_auth_classes()
class ThreadViewSet(DeveloperErrorViewMixin, ViewSet):
111 112 113
    """
    **Use Cases**

114 115
        Retrieve the list of threads for a course, retrieve thread details,
        post a new thread, or modify or delete an existing thread.
116 117 118 119 120

    **Example Requests**:

        GET /api/discussion/v1/threads/?course_id=ExampleX/Demo/2015

121
        GET /api/discussion/v1/threads/{thread_id}
122

123 124 125 126 127 128
        POST /api/discussion/v1/threads
        {
          "course_id": "foo/bar/baz",
          "topic_id": "quux",
          "type": "discussion",
          "title": "Title text",
129
          "raw_body": "Body text"
130 131
        }

132 133
        PATCH /api/discussion/v1/threads/thread_id
        {"raw_body": "Edited text"}
134
        Content Type: "application/merge-patch+json"
135

136 137
        DELETE /api/discussion/v1/threads/thread_id

138
    **GET Thread List Parameters**:
139 140 141 142 143 144 145

        * course_id (required): The course to retrieve threads for

        * page: The (1-indexed) page to retrieve (default is 1)

        * page_size: The number of items per page (default is 10, max is 100)

146 147 148 149
        * topic_id: The id of the topic to retrieve the threads. There can be
            multiple topic_id queries to retrieve threads from multiple topics
            at once.

150 151 152 153
        * text_search: A search string to match. Any thread whose content
            (including the bodies of comments in the thread) matches the search
            string will be returned.

154 155 156 157
        * order_by: Must be "last_activity_at", "comment_count", or
            "vote_count". The key to sort the threads by. The default is
            "last_activity_at".

158 159 160
        * order_direction: Must be "desc". The direction in which to sort the
            threads by. The default and only value is "desc". This will be
            removed in a future major version.
161

162 163 164
        * following: If true, retrieve only threads the requesting user is
            following

165 166 167
        * view: "unread" for threads the requesting user has not read, or
            "unanswered" for question threads with no marked answer. Only one
            can be selected.
168

169 170
        * requested_fields: (list) Indicates which additional fields to return
          for each thread. (supports 'profile_image')
171

172 173 174
        The topic_id, text_search, and following parameters are mutually
        exclusive (i.e. only one may be specified in a request)

175 176 177 178 179 180 181
    **GET Thread Parameters**:

        * thread_id (required): The id of the thread

        * requested_fields (optional parameter): (list) Indicates which additional
         fields to return for each thread. (supports 'profile_image')

182
    **POST Parameters**:
183

184
        * course_id (required): The course to create the thread in
185

186
        * topic_id (required): The topic to create the thread in
187

188
        * type (required): The thread's type (either "question" or "discussion")
189

190
        * title (required): The thread's title
191

192
        * raw_body (required): The thread's raw body text
193

194 195 196
        * following (optional): A boolean indicating whether the user should
            follow the thread upon its creation; defaults to false

197 198
    **PATCH Parameters**:

199 200 201 202 203 204 205
        * abuse_flagged (optional): A boolean to mark thread as abusive

        * voted (optional): A boolean to vote for thread

        * read (optional): A boolean to mark thread as read

        * topic_id, type, title, and raw_body are accepted with the same meaning
206 207
        as in a POST request

208 209 210
        If "application/merge-patch+json" is not the specified content type,
        a 415 error is returned.

211
    **GET Thread List Response Values**:
212

213
        * results: The list of threads; each item in the list has the same
214
            fields as the POST/PATCH response below
215

216
        * next: The URL of the next page (or null if first page)
217

218
        * previous: The URL of the previous page (or null if last page)
219

220 221 222 223
        * text_search_rewrite: The search string to which the text_search
            parameter was rewritten in order to match threads (e.g. for spelling
            correction)

224 225 226 227
    **GET Thread Details Response Values**:

        Same response fields as the POST/PATCH response below

228
    **POST/PATCH response values**:
229

230
        * id: The id of the thread
231

232
        * course_id: The id of the thread's course
233

234
        * topic_id: The id of the thread's topic
235

236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
        * created_at: The ISO 8601 timestamp for the creation of the thread

        * updated_at: The ISO 8601 timestamp for the last modification of
            the thread, which may not have been an update of the title/body

        * type: The thread's type (either "question" or "discussion")

        * title: The thread's title

        * raw_body: The thread's raw body text without any rendering applied

        * pinned: Boolean indicating whether the thread has been pinned

        * closed: Boolean indicating whether the thread has been closed

        * comment_count: The number of comments within the thread

        * unread_comment_count: The number of comments within the thread
            that were created or updated since the last time the user read
            the thread
256

257 258 259
        * editable_fields: The fields that the requesting user is allowed to
            modify with a PATCH request

260 261 262 263
        * read: Boolean indicating whether the user has read this thread

        * has_endorsed: Boolean indicating whether this thread has been answered

264 265
        * response_count: The number of direct responses for a thread

266 267 268 269
    **DELETE response values:

        No content is returned for a DELETE request

270
    """
271
    lookup_field = "thread_id"
272
    parser_classes = (JSONParser, MergePatchParser,)
273

274 275 276 277 278 279 280 281
    def list(self, request):
        """
        Implements the GET method for the list endpoint as described in the
        class docstring.
        """
        form = ThreadListGetForm(request.GET)
        if not form.is_valid():
            raise ValidationError(form.errors)
282 283 284 285 286 287 288 289 290 291 292
        return get_thread_list(
            request,
            form.cleaned_data["course_id"],
            form.cleaned_data["page"],
            form.cleaned_data["page_size"],
            form.cleaned_data["topic_id"],
            form.cleaned_data["text_search"],
            form.cleaned_data["following"],
            form.cleaned_data["view"],
            form.cleaned_data["order_by"],
            form.cleaned_data["order_direction"],
293
            form.cleaned_data["requested_fields"]
294
        )
295

296 297 298 299
    def retrieve(self, request, thread_id=None):
        """
        Implements the GET method for thread ID
        """
300 301
        requested_fields = request.GET.get('requested_fields')
        return Response(get_thread(request, thread_id, requested_fields))
302

303 304 305 306 307
    def create(self, request):
        """
        Implements the POST method for the list endpoint as described in the
        class docstring.
        """
308
        return Response(create_thread(request, request.data))
309

310 311 312 313 314
    def partial_update(self, request, thread_id):
        """
        Implements the PATCH method for the instance endpoint as described in
        the class docstring.
        """
315 316
        if request.content_type != MergePatchParser.media_type:
            raise UnsupportedMediaType(request.content_type)
317
        return Response(update_thread(request, thread_id, request.data))
318

319 320 321 322 323 324 325 326
    def destroy(self, request, thread_id):
        """
        Implements the DELETE method for the instance endpoint as described in
        the class docstring
        """
        delete_thread(request, thread_id)
        return Response(status=204)

327

328 329
@view_auth_classes()
class CommentViewSet(DeveloperErrorViewMixin, ViewSet):
330 331 332
    """
    **Use Cases**

333 334
        Retrieve the list of comments in a thread, retrieve the list of
        child comments for a response comment, create a comment, or modify
335
        or delete an existing comment.
336 337 338 339 340

    **Example Requests**:

        GET /api/discussion/v1/comments/?thread_id=0123456789abcdef01234567

341 342
        GET /api/discussion/v1/comments/2123456789abcdef01234555

343 344 345 346 347 348
        POST /api/discussion/v1/comments/
        {
            "thread_id": "0123456789abcdef01234567",
            "raw_body": "Body text"
        }

349 350
        PATCH /api/discussion/v1/comments/comment_id
        {"raw_body": "Edited text"}
351
        Content Type: "application/merge-patch+json"
352

353 354
        DELETE /api/discussion/v1/comments/comment_id

355
    **GET Comment List Parameters**:
356 357 358 359 360 361 362 363 364 365 366

        * thread_id (required): The thread to retrieve comments for

        * endorsed: If specified, only retrieve the endorsed or non-endorsed
          comments accordingly. Required for a question thread, must be absent
          for a discussion thread.

        * page: The (1-indexed) page to retrieve (default is 1)

        * page_size: The number of items per page (default is 10, max is 100)

367 368 369
        * requested_fields: (list) Indicates which additional fields to return
          for each thread. (supports 'profile_image')

370 371 372 373 374 375 376 377
    **GET Child Comment List Parameters**:

        * comment_id (required): The comment to retrieve child comments for

        * page: The (1-indexed) page to retrieve (default is 1)

        * page_size: The number of items per page (default is 10, max is 100)

378 379 380
        * requested_fields: (list) Indicates which additional fields to return
          for each thread. (supports 'profile_image')

381

382
    **POST Parameters**:
383

384
        * thread_id (required): The thread to post the comment in
385

386 387
        * parent_id: The parent comment of the new comment. Can be null or
          omitted for a comment that should be directly under the thread
388

389
        * raw_body: The comment's raw body text
390

391 392 393 394
    **PATCH Parameters**:

        raw_body is accepted with the same meaning as in a POST request

395 396 397
        If "application/merge-patch+json" is not the specified content type,
        a 415 error is returned.

398
    **GET Response Values**:
399

400 401
        * results: The list of comments; each item in the list has the same
            fields as the POST response below
402

403
        * next: The URL of the next page (or null if first page)
404

405
        * previous: The URL of the previous page (or null if last page)
406

407
    **POST/PATCH Response Values**:
408

409
        * id: The id of the comment
410

411
        * thread_id: The id of the comment's thread
412

413
        * parent_id: The id of the comment's parent
414

415 416
        * author: The username of the comment's author, or None if the
          comment is anonymous
417

418
        * author_label: A label indicating whether the author has a special
419 420
          role in the course, either "Staff" for moderators and
          administrators or "Community TA" for community TAs
421

422
        * created_at: The ISO 8601 timestamp for the creation of the comment
423

424 425
        * updated_at: The ISO 8601 timestamp for the last modification of
            the comment, which may not have been an update of the body
426

427
        * raw_body: The comment's raw body text without any rendering applied
428

429 430 431
        * endorsed: Boolean indicating whether the comment has been endorsed
            (by a privileged user or, for a question thread, the thread
            author)
432

433
        * endorsed_by: The username of the endorsing user, if available
434

435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
        * endorsed_by_label: A label indicating whether the endorsing user
            has a special role in the course (see author_label)

        * endorsed_at: The ISO 8601 timestamp for the endorsement, if
            available

        * abuse_flagged: Boolean indicating whether the requesting user has
          flagged the comment for abuse

        * voted: Boolean indicating whether the requesting user has voted
          for the comment

        * vote_count: The number of votes for the comment

        * children: The list of child comments (with the same format)
450

451 452 453
        * editable_fields: The fields that the requesting user is allowed to
            modify with a PATCH request

454 455 456 457
    **DELETE Response Value**

        No content is returned for a DELETE request

458
    """
459
    lookup_field = "comment_id"
460
    parser_classes = (JSONParser, MergePatchParser,)
461

462 463 464 465 466 467 468 469
    def list(self, request):
        """
        Implements the GET method for the list endpoint as described in the
        class docstring.
        """
        form = CommentListGetForm(request.GET)
        if not form.is_valid():
            raise ValidationError(form.errors)
470 471 472 473 474
        return get_comment_list(
            request,
            form.cleaned_data["thread_id"],
            form.cleaned_data["endorsed"],
            form.cleaned_data["page"],
475 476
            form.cleaned_data["page_size"],
            form.cleaned_data["requested_fields"],
477
        )
478

479 480 481 482
    def retrieve(self, request, comment_id=None):
        """
        Implements the GET method for comments against response ID
        """
483
        form = CommentGetForm(request.GET)
484 485
        if not form.is_valid():
            raise ValidationError(form.errors)
486 487 488 489
        return get_response_comments(
            request,
            comment_id,
            form.cleaned_data["page"],
490 491
            form.cleaned_data["page_size"],
            form.cleaned_data["requested_fields"],
492 493
        )

494 495 496 497 498
    def create(self, request):
        """
        Implements the POST method for the list endpoint as described in the
        class docstring.
        """
499
        return Response(create_comment(request, request.data))
500

501 502 503 504 505 506 507 508
    def destroy(self, request, comment_id):
        """
        Implements the DELETE method for the instance endpoint as described in
        the class docstring
        """
        delete_comment(request, comment_id)
        return Response(status=204)

509 510 511 512 513
    def partial_update(self, request, comment_id):
        """
        Implements the PATCH method for the instance endpoint as described in
        the class docstring.
        """
514 515
        if request.content_type != MergePatchParser.media_type:
            raise UnsupportedMediaType(request.content_type)
516
        return Response(update_comment(request, comment_id, request.data))