views.py 30.8 KB
Newer Older
1 2 3
""" API v0 views. """

import datetime
4
import json
5 6
import logging

7 8
import pytz
from ccx_keys.locator import CCXLocator
9 10 11
from django.contrib.auth.models import User
from django.db import transaction
from django.http import Http404
12 13 14
from edx_rest_framework_extensions.authentication import JwtAuthentication
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
15 16 17 18 19 20 21
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from courseware import courses
from lms.djangoapps.ccx.models import CcxFieldOverride, CustomCourseForEdX
22
from lms.djangoapps.ccx.overrides import override_field_for_ccx
23
from lms.djangoapps.ccx.utils import (
24
    add_master_course_staff_to_ccx,
25
    assign_staff_role_to_ccx,
26
    get_course_chapters,
27
    is_email
28
)
29 30 31 32 33 34 35
from lms.djangoapps.instructor.enrollment import enroll_email, get_email_params
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api import authentication, permissions
from student.models import CourseEnrollment
from student.roles import CourseCcxCoachRole
from xmodule.modulestore.django import SignalHandler

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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
from .paginators import CCXAPIPagination
from .serializers import CCXCourseSerializer

log = logging.getLogger(__name__)
TODAY = datetime.datetime.today  # for patching in tests


def get_valid_course(course_id, is_ccx=False, advanced_course_check=False):
    """
    Helper function used to validate and get a course from a course_id string.
    It works with both master and ccx course id.

    Args:
        course_id (str): A string representation of a Master or CCX Course ID.
        is_ccx (bool): Flag to perform the right validation
        advanced_course_check (bool): Flag to perform extra validations for the master course

    Returns:
        tuple: a tuple of course_object, course_key, error_code, http_status_code
    """
    if course_id is None:
        # the ccx detail view cannot call this function with a "None" value
        # so the following `error_code` should be never used, but putting it
        # to avoid a `NameError` exception in case this function will be used
        # elsewhere in the future
        error_code = 'course_id_not_provided'
        if not is_ccx:
            log.info('Master course ID not provided')
            error_code = 'master_course_id_not_provided'

        return None, None, error_code, status.HTTP_400_BAD_REQUEST

    try:
        course_key = CourseKey.from_string(course_id)
    except InvalidKeyError:
        log.info('Course ID string "%s" is not valid', course_id)
        return None, None, 'course_id_not_valid', status.HTTP_400_BAD_REQUEST

    if not is_ccx:
        try:
            course_object = courses.get_course_by_id(course_key)
        except Http404:
            log.info('Master Course with ID "%s" not found', course_id)
            return None, None, 'course_id_does_not_exist', status.HTTP_404_NOT_FOUND
        if advanced_course_check:
            if course_object.id.deprecated:
                return None, None, 'deprecated_master_course_id', status.HTTP_400_BAD_REQUEST
            if not course_object.enable_ccx:
                return None, None, 'ccx_not_enabled_for_master_course', status.HTTP_403_FORBIDDEN
        return course_object, course_key, None, None
    else:
        try:
            ccx_id = course_key.ccx
        except AttributeError:
            log.info('Course ID string "%s" is not a valid CCX ID', course_id)
            return None, None, 'course_id_not_valid_ccx_id', status.HTTP_400_BAD_REQUEST
        # get the master_course key
        master_course_key = course_key.to_course_locator()
        try:
            ccx_course = CustomCourseForEdX.objects.get(id=ccx_id, course_id=master_course_key)
            return ccx_course, course_key, None, None
        except CustomCourseForEdX.DoesNotExist:
            log.info('CCX Course with ID "%s" not found', course_id)
            return None, None, 'ccx_course_id_does_not_exist', status.HTTP_404_NOT_FOUND


def get_valid_input(request_data, ignore_missing=False):
    """
    Helper function to validate the data sent as input and to
    build field based errors.

    Args:
        request_data (OrderedDict): the request data object
        ignore_missing (bool): whether or not to ignore fields
            missing from the input data

    Returns:
        tuple: a tuple of two dictionaries for valid input and field errors
    """
    valid_input = {}
    field_errors = {}
    mandatory_fields = ('coach_email', 'display_name', 'max_students_allowed',)

    # checking first if all the fields are present and they are not null
    if not ignore_missing:
        for field in mandatory_fields:
            if field not in request_data:
                field_errors[field] = {'error_code': 'missing_field_{0}'.format(field)}
        if field_errors:
            return valid_input, field_errors

    # at this point I can assume that if the fields are present,
    # they must be validated, otherwise they can be skipped
    coach_email = request_data.get('coach_email')
    if coach_email is not None:
        if is_email(coach_email):
            valid_input['coach_email'] = coach_email
        else:
            field_errors['coach_email'] = {'error_code': 'invalid_coach_email'}
    elif 'coach_email' in request_data:
        field_errors['coach_email'] = {'error_code': 'null_field_coach_email'}

    display_name = request_data.get('display_name')
    if display_name is not None:
        if not display_name:
            field_errors['display_name'] = {'error_code': 'invalid_display_name'}
        else:
            valid_input['display_name'] = display_name
    elif 'display_name' in request_data:
        field_errors['display_name'] = {'error_code': 'null_field_display_name'}

    max_students_allowed = request_data.get('max_students_allowed')
    if max_students_allowed is not None:
        try:
            max_students_allowed = int(max_students_allowed)
            valid_input['max_students_allowed'] = max_students_allowed
        except (TypeError, ValueError):
            field_errors['max_students_allowed'] = {'error_code': 'invalid_max_students_allowed'}
    elif 'max_students_allowed' in request_data:
        field_errors['max_students_allowed'] = {'error_code': 'null_field_max_students_allowed'}
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175

    course_modules = request_data.get('course_modules')
    if course_modules is not None:
        if isinstance(course_modules, list):
            # de-duplicate list of modules
            course_modules = list(set(course_modules))
            for course_module_id in course_modules:
                try:
                    UsageKey.from_string(course_module_id)
                except InvalidKeyError:
                    field_errors['course_modules'] = {'error_code': 'invalid_course_module_keys'}
                    break
            else:
                valid_input['course_modules'] = course_modules
        else:
            field_errors['course_modules'] = {'error_code': 'invalid_course_module_list'}
    elif 'course_modules' in request_data:
        # case if the user actually passed null as input
        valid_input['course_modules'] = None

176 177 178
    return valid_input, field_errors


179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
def valid_course_modules(course_module_list, master_course_key):
    """
    Function to validate that each element in the course_module_list belongs
    to the master course structure.
    Args:
        course_module_list (list): A list of strings representing Block Usage Keys
        master_course_key (CourseKey): An object representing the master course key id

    Returns:
        bool: whether or not all the course module strings belong to the master course
    """
    course_chapters = get_course_chapters(master_course_key)
    if course_chapters is None:
        return False
    return set(course_module_list).intersection(set(course_chapters)) == set(course_module_list)


196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
def make_user_coach(user, master_course_key):
    """
    Makes an user coach on the master course.
    This function is needed because an user cannot become a coach of the CCX if s/he is not
    coach on the master course.

    Args:
        user (User): User object
        master_course_key (CourseKey): Key locator object for the course
    """
    coach_role_on_master_course = CourseCcxCoachRole(master_course_key)
    coach_role_on_master_course.add_users(user)


class CCXListView(GenericAPIView):
    """
        **Use Case**

            * Get the list of CCX courses for a given master course.

            * Creates a new CCX course for a given master course.

        **Example Request**

            GET /api/ccx/v0/ccx/?master_course_id={master_course_id}

            POST /api/ccx/v0/ccx {

                "master_course_id": "course-v1:Organization+EX101+RUN-FALL2099",
                "display_name": "CCX example title",
                "coach_email": "john@example.com",
227 228 229 230 231 232
                "max_students_allowed": 123,
                "course_modules" : [
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                ]
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

            }

        **GET Parameters**

            A GET request can include the following parameters.

            * master_course_id: A string representation of a Master Course ID. Note that this must be properly
              encoded by the client.

            * page: Optional. An integer representing the pagination instance number.

            * order_by: Optional. A string representing the field by which sort the results.

            * sort_order: Optional. A string (either "asc" or "desc") indicating the desired order.

        **POST Parameters**

            A POST request can include the following parameters.

            * master_course_id: A string representation of a Master Course ID.

            * display_name: A string representing the CCX Course title.

            * coach_email: A string representing the CCX owner email.

            * max_students_allowed: An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

262 263
            * course_modules: Optional. A list of course modules id keys.

264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        **GET Response Values**

            If the request for information about the course is successful, an HTTP 200 "OK" response
            is returned with a collection of CCX courses for the specified master course.

            The HTTP 200 response has the following values.

            * results: a collection of CCX courses. Each CCX course contains the following values:

                * ccx_course_id: A string representation of a CCX Course ID.

                * display_name: A string representing the CCX Course title.

                * coach_email: A string representing the CCX owner email.

                * start: A string representing the start date for the CCX Course.

                * due: A string representing the due date for the CCX Course.

                * max_students_allowed: An integer representing he maximum number of students that
                  can be enrolled in the CCX Course.

286 287
                * course_modules: A list of course modules id keys.

288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
            * count: An integer representing the total number of records that matched the request parameters.

            * next: A string representing the URL where to retrieve the next page of results. This can be `null`
              in case the response contains the complete list of results.

            * previous: A string representing the URL where to retrieve the previous page of results. This can be
              `null` in case the response contains the first page of results.

        **Example GET Response**

            {
                "count": 99,
                "next": "https://openedx-ccx-api-instance.org/api/ccx/v0/ccx/?course_id=<course_id>&page=2",
                "previous": null,
                "results": {
                    {
                        "ccx_course_id": "ccx-v1:Organization+EX101+RUN-FALL2099+ccx@1",
                        "display_name": "CCX example title",
                        "coach_email": "john@example.com",
                        "start": "2019-01-01",
                        "due": "2019-06-01",
309 310 311 312 313 314
                        "max_students_allowed": 123,
                        "course_modules" : [
                            "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                            "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                            "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                        ]
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
                    },
                    { ... }
                }
            }

        **POST Response Values**

            If the request for the creation of a CCX Course is successful, an HTTP 201 "Created" response
            is returned with the newly created CCX details.

            The HTTP 201 response has the following values.

            * ccx_course_id: A string representation of a CCX Course ID.

            * display_name: A string representing the CCX Course title.

            * coach_email: A string representing the CCX owner email.

            * start: A string representing the start date for the CCX Course.

            * due: A string representing the due date for the CCX Course.

            * max_students_allowed: An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

340 341
            * course_modules: A list of course modules id keys.

342 343 344 345 346 347 348 349
        **Example POST Response**

            {
                "ccx_course_id": "ccx-v1:Organization+EX101+RUN-FALL2099+ccx@1",
                "display_name": "CCX example title",
                "coach_email": "john@example.com",
                "start": "2019-01-01",
                "due": "2019-06-01",
350 351 352 353 354 355 356
                "max_students_allowed": 123,
                "course_modules" : [
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                ]
    }
357
    """
358 359 360 361 362
    authentication_classes = (
        JwtAuthentication,
        authentication.OAuth2AuthenticationAllowInactiveUser,
        authentication.SessionAuthenticationAllowInactiveUser,
    )
363
    permission_classes = (IsAuthenticated, permissions.IsMasterCourseStaffInstructor)
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
    serializer_class = CCXCourseSerializer
    pagination_class = CCXAPIPagination

    def get(self, request):
        """
        Gets a list of CCX Courses for a given Master Course.

        Additional parameters are allowed for pagination purposes.

        Args:
            request (Request): Django request object.

        Return:
            A JSON serialized representation of a list of CCX courses.
        """
        master_course_id = request.GET.get('master_course_id')
        master_course_object, master_course_key, error_code, http_status = get_valid_course(master_course_id)
        if master_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )

        queryset = CustomCourseForEdX.objects.filter(course_id=master_course_key)
        order_by_input = request.query_params.get('order_by')
        sort_order_input = request.query_params.get('sort_order')
        if order_by_input in ('id', 'display_name'):
            sort_direction = ''
            if sort_order_input == 'desc':
                sort_direction = '-'
            queryset = queryset.order_by('{0}{1}'.format(sort_direction, order_by_input))
        page = self.paginate_queryset(queryset)
        serializer = self.get_serializer(page, many=True)
        response = self.get_paginated_response(serializer.data)
        return response

    def post(self, request):
        """
        Creates a new CCX course for a given Master Course.

        Args:
            request (Request): Django request object.

        Return:
            A JSON serialized representation a newly created CCX course.
        """
        master_course_id = request.data.get('master_course_id')
        master_course_object, master_course_key, error_code, http_status = get_valid_course(
            master_course_id,
            advanced_course_check=True
        )
        if master_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )

        # validating the rest of the input
        valid_input, field_errors = get_valid_input(request.data)
        if field_errors:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    'field_errors': field_errors
                }
            )

        try:
            coach = User.objects.get(email=valid_input['coach_email'])
        except User.DoesNotExist:
            return Response(
                status=status.HTTP_404_NOT_FOUND,
                data={
                    'error_code': 'coach_user_does_not_exist'
                }
            )

445 446 447 448 449 450 451 452 453 454 455
        if valid_input.get('course_modules'):
            if not valid_course_modules(valid_input['course_modules'], master_course_key):
                return Response(
                    status=status.HTTP_400_BAD_REQUEST,
                    data={
                        'error_code': 'course_module_list_not_belonging_to_master_course'
                    }
                )
        # prepare the course_modules to be stored in a json stringified field
        course_modules_json = json.dumps(valid_input.get('course_modules'))

456 457 458 459
        with transaction.atomic():
            ccx_course_object = CustomCourseForEdX(
                course_id=master_course_object.id,
                coach=coach,
460 461 462
                display_name=valid_input['display_name'],
                structure_json=course_modules_json
            )
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490
            ccx_course_object.save()

            # Make sure start/due are overridden for entire course
            start = TODAY().replace(tzinfo=pytz.UTC)
            override_field_for_ccx(ccx_course_object, master_course_object, 'start', start)
            override_field_for_ccx(ccx_course_object, master_course_object, 'due', None)

            # Enforce a static limit for the maximum amount of students that can be enrolled
            override_field_for_ccx(
                ccx_course_object,
                master_course_object,
                'max_student_enrollments_allowed',
                valid_input['max_students_allowed']
            )

            # Hide anything that can show up in the schedule
            hidden = 'visible_to_staff_only'
            for chapter in master_course_object.get_children():
                override_field_for_ccx(ccx_course_object, chapter, hidden, True)
                for sequential in chapter.get_children():
                    override_field_for_ccx(ccx_course_object, sequential, hidden, True)
                    for vertical in sequential.get_children():
                        override_field_for_ccx(ccx_course_object, vertical, hidden, True)

            # make the coach user a coach on the master course
            make_user_coach(coach, master_course_key)

            # pull the ccx course key
491
            ccx_course_key = CCXLocator.from_course_locator(master_course_object.id, unicode(ccx_course_object.id))
492 493 494 495 496 497 498 499 500 501 502 503 504 505
            # enroll the coach in the newly created ccx
            email_params = get_email_params(
                master_course_object,
                auto_enroll=True,
                course_key=ccx_course_key,
                display_name=ccx_course_object.display_name
            )
            enroll_email(
                course_id=ccx_course_key,
                student_email=coach.email,
                auto_enroll=True,
                email_students=True,
                email_params=email_params,
            )
506 507
            # assign staff role for the coach to the newly created ccx
            assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)
508 509 510 511 512 513 514
            # assign staff role for all the staff and instructor of the master course to the newly created ccx
            add_master_course_staff_to_ccx(
                master_course_object,
                ccx_course_key,
                ccx_course_object.display_name,
                send_email=False
            )
515 516

        serializer = self.get_serializer(ccx_course_object)
517 518 519 520 521 522 523 524

        # using CCX object as sender here.
        responses = SignalHandler.course_published.send(
            sender=ccx_course_object,
            course_key=ccx_course_key
        )
        for rec, response in responses:
            log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
        return Response(
            status=status.HTTP_201_CREATED,
            data=serializer.data
        )


class CCXDetailView(GenericAPIView):
    """
        **Use Case**

            * Get the details of CCX course.

            * Modify a CCX course.

            * Delete a CCX course.

        **Example Request**

            GET /api/ccx/v0/ccx/{ccx_course_id}

            PATCH /api/ccx/v0/ccx/{ccx_course_id} {

                "display_name": "CCX example title modified",
                "coach_email": "joe@example.com",
549 550 551 552 553 554
                "max_students_allowed": 111,
                "course_modules" : [
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week1",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week4",
                    "block-v1:Organization+EX101+RUN-FALL2099+type@chapter+block@week5"
                ]
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577
            }

            DELETE /api/ccx/v0/ccx/{ccx_course_id}

        **GET and DELETE Parameters**

            A GET or DELETE request must include the following parameter.

            * ccx_course_id: A string representation of a CCX Course ID.

        **PATCH Parameters**

            A PATCH request can include the following parameters

            * ccx_course_id: A string representation of a CCX Course ID.

            * display_name: Optional. A string representing the CCX Course title.

            * coach_email: Optional. A string representing the CCX owner email.

            * max_students_allowed: Optional. An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

578 579
            * course_modules: Optional. A list of course modules id keys.

580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
        **GET Response Values**

            If the request for information about the CCX course is successful, an HTTP 200 "OK" response
            is returned.

            The HTTP 200 response has the following values.

            * ccx_course_id: A string representation of a CCX Course ID.

            * display_name: A string representing the CCX Course title.

            * coach_email: A string representing the CCX owner email.

            * start: A string representing the start date for the CCX Course.

            * due: A string representing the due date for the CCX Course.

            * max_students_allowed: An integer representing he maximum number of students that
              can be enrolled in the CCX Course.

600 601
            * course_modules: A list of course modules id keys.

602 603 604 605 606 607
        **PATCH and DELETE Response Values**

            If the request for modification or deletion of a CCX course is successful, an HTTP 204 "No Content"
            response is returned.
    """

608 609 610 611 612
    authentication_classes = (
        JwtAuthentication,
        authentication.OAuth2AuthenticationAllowInactiveUser,
        authentication.SessionAuthenticationAllowInactiveUser,
    )
613
    permission_classes = (IsAuthenticated, permissions.IsCourseStaffInstructor)
614 615
    serializer_class = CCXCourseSerializer

616 617 618 619 620 621 622 623
    def get_object(self, course_id, is_ccx=False):  # pylint: disable=arguments-differ
        """
        Override the default get_object to allow a custom getter for the CCX
        """
        course_object, course_key, error_code, http_status = get_valid_course(course_id, is_ccx)
        self.check_object_permissions(self.request, course_object)
        return course_object, course_key, error_code, http_status

624 625 626 627 628 629 630 631 632 633 634
    def get(self, request, ccx_course_id=None):
        """
        Gets a CCX Course information.

        Args:
            request (Request): Django request object.
            ccx_course_id (string): URI element specifying the CCX course location.

        Return:
            A JSON serialized representation of the CCX course.
        """
635
        ccx_course_object, _, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
        if ccx_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )
        serializer = self.get_serializer(ccx_course_object)
        return Response(serializer.data)

    def delete(self, request, ccx_course_id=None):  # pylint: disable=unused-argument
        """
        Deletes a CCX course.

        Args:
            request (Request): Django request object.
            ccx_course_id (string): URI element specifying the CCX course location.
        """
654
        ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681
        if ccx_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )
        ccx_course_overview = CourseOverview.get_from_id(ccx_course_key)
        # clean everything up with a single transaction
        with transaction.atomic():
            CcxFieldOverride.objects.filter(ccx=ccx_course_object).delete()
            # remove all users enrolled in the CCX from the CourseEnrollment model
            CourseEnrollment.objects.filter(course_id=ccx_course_key).delete()
            ccx_course_overview.delete()
            ccx_course_object.delete()
        return Response(
            status=status.HTTP_204_NO_CONTENT,
        )

    def patch(self, request, ccx_course_id=None):
        """
        Modifies a CCX course.

        Args:
            request (Request): Django request object.
            ccx_course_id (string): URI element specifying the CCX course location.
        """
682
        ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
        if ccx_course_object is None:
            return Response(
                status=http_status,
                data={
                    'error_code': error_code
                }
            )

        master_course_id = request.data.get('master_course_id')
        if master_course_id is not None and unicode(ccx_course_object.course_id) != master_course_id:
            return Response(
                status=status.HTTP_403_FORBIDDEN,
                data={
                    'error_code': 'master_course_id_change_not_allowed'
                }
            )

        valid_input, field_errors = get_valid_input(request.data, ignore_missing=True)
        if field_errors:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={
                    'field_errors': field_errors
                }
            )

709 710 711
        # get the master course key and master course object
        master_course_object, master_course_key, _, _ = get_valid_course(unicode(ccx_course_object.course_id))

712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
        with transaction.atomic():
            # update the display name
            if 'display_name' in valid_input:
                ccx_course_object.display_name = valid_input['display_name']
            # check if the coach has changed and in case update it
            old_coach = None
            if 'coach_email' in valid_input:
                try:
                    coach = User.objects.get(email=valid_input['coach_email'])
                except User.DoesNotExist:
                    return Response(
                        status=status.HTTP_404_NOT_FOUND,
                        data={
                            'error_code': 'coach_user_does_not_exist'
                        }
                    )
                if ccx_course_object.coach.id != coach.id:
                    old_coach = ccx_course_object.coach
                    ccx_course_object.coach = coach
731 732 733 734 735 736 737 738 739 740 741
            if 'course_modules' in valid_input:
                if valid_input.get('course_modules'):
                    if not valid_course_modules(valid_input['course_modules'], master_course_key):
                        return Response(
                            status=status.HTTP_400_BAD_REQUEST,
                            data={
                                'error_code': 'course_module_list_not_belonging_to_master_course'
                            }
                        )
                # course_modules to be stored in a json stringified field
                ccx_course_object.structure_json = json.dumps(valid_input.get('course_modules'))
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
            ccx_course_object.save()
            # update the overridden field for the maximum amount of students
            if 'max_students_allowed' in valid_input:
                override_field_for_ccx(
                    ccx_course_object,
                    ccx_course_object.course,
                    'max_student_enrollments_allowed',
                    valid_input['max_students_allowed']
                )
            # if the coach has changed, update the permissions
            if old_coach is not None:
                # make the new ccx coach a coach on the master course
                make_user_coach(coach, master_course_key)
                # enroll the coach in the ccx
                email_params = get_email_params(
                    master_course_object,
                    auto_enroll=True,
                    course_key=ccx_course_key,
                    display_name=ccx_course_object.display_name
                )
                enroll_email(
                    course_id=ccx_course_key,
                    student_email=coach.email,
                    auto_enroll=True,
                    email_students=True,
                    email_params=email_params,
                )
769 770
                # make the new coach staff on the CCX
                assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id)
771

772 773 774 775 776 777 778 779
        # using CCX object as sender here.
        responses = SignalHandler.course_published.send(
            sender=ccx_course_object,
            course_key=ccx_course_key
        )
        for rec, response in responses:
            log.info('Signal fired when course is published. Receiver: %s. Response: %s', rec, response)

780 781 782
        return Response(
            status=status.HTTP_204_NO_CONTENT,
        )