示例#1
0
    def create(self, request, *args, **kwargs):
        """
        Create a Course, Course Entitlement, and Entitlement.
        """
        course_run_creation_fields = request.data.pop('course_run', None)
        course_creation_fields = {
            'title': request.data.get('title'),
            'number': request.data.get('number'),
            'org': request.data.get('org'),
            'type': request.data.get('type'),
        }
        url_slug = request.data.get('url_slug', '')

        missing_values = [
            k for k, v in course_creation_fields.items() if v is None
        ]
        error_message = ''
        if missing_values:
            error_message += ''.join([
                _('Missing value for: [{name}]. ').format(name=name)
                for name in missing_values
            ])
        if not Organization.objects.filter(
                key=course_creation_fields['org']).exists():
            error_message += _('Organization [{org}] does not exist. ').format(
                org=course_creation_fields['org'])
        if not CourseType.objects.filter(
                uuid=course_creation_fields['type']).exists():
            error_message += _(
                'Course Type [{course_type}] does not exist. ').format(
                    course_type=course_creation_fields['type'])
        if error_message:
            return Response(
                (_('Incorrect data sent. ') + error_message).strip(),
                status=status.HTTP_400_BAD_REQUEST)

        partner = request.site.partner
        course_creation_fields['partner'] = partner.id
        course_creation_fields['key'] = self.get_course_key(
            course_creation_fields)

        validate_course_number(course_creation_fields['number'])

        serializer = self.get_serializer(data=course_creation_fields)
        serializer.is_valid(raise_exception=True)

        # Confirm that this course doesn't already exist in an official non-draft form
        if Course.objects.filter(partner=partner,
                                 key=course_creation_fields['key']).exists():
            raise Exception(
                _('A course with key [{key}] already exists.').format(
                    key=course_creation_fields['key']))

        # if a manually entered url_slug, ensure it's not already taken (auto-generated are guaranteed uniqueness)
        if url_slug:
            validators.validate_slug(url_slug)
            if CourseUrlSlug.objects.filter(url_slug=url_slug,
                                            partner=partner).exists():
                raise Exception(
                    _('Course creation was unsuccessful. The course URL slug ‘[{url_slug}]’ is already in '
                      'use. Please update this field and try again.').format(
                          url_slug=url_slug))

        course = serializer.save(draft=True)
        course.set_active_url_slug(url_slug)

        organization = Organization.objects.get(
            key=course_creation_fields['org'])
        course.authoring_organizations.add(organization)

        collaborators_uuid = request.data.get('collaborators')
        if collaborators_uuid:
            collaborators = Collaborator.objects.filter(
                uuid__in=collaborators_uuid)
            course.collaborators.add(*collaborators)

        entitlement_types = course.type.entitlement_types.all()
        prices = request.data.get('prices', {})
        for entitlement_type in entitlement_types:
            CourseEntitlement.objects.create(
                course=course,
                mode=entitlement_type,
                partner=partner,
                price=prices.get(entitlement_type.slug, 0),
                draft=True,
            )

        CourseEditor.objects.create(
            user=request.user,
            course=course,
        )

        # We want to create the course run here so it is captured as part of the atomic transaction.
        # Note: We have to send the request object as well because it is used for its metadata
        # (like request.user and is set as part of the serializer context)
        if course_run_creation_fields:
            course_run_creation_fields.update({
                'course': course.key,
                'prices': prices
            })
            run_response = CourseRunViewSet().create_run_helper(
                course_run_creation_fields, request)
            if run_response.status_code != 201:
                raise Exception(str(run_response.data))

        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data,
                        status=status.HTTP_201_CREATED,
                        headers=headers)
示例#2
0
    def update_course(self, data, partial=False):  # pylint: disable=too-many-statements
        """ Updates an existing course from incoming data. """
        # logging to help debug error around course url slugs incrementing
        logger.info('The raw course data coming from publisher is {}.'.format(data))

        changed = False
        # Sending draft=False means the course data is live and updates should be pushed out immediately
        draft = data.pop('draft', True)
        image_data = data.pop('image', None)
        video_data = data.pop('video', None)
        url_slug = data.pop('url_slug', '')

        # Get and validate object serializer
        course = self.get_object()
        course = ensure_draft_world(course)  # always work on drafts
        serializer = self.get_serializer(course, data=data, partial=partial)
        serializer.is_valid(raise_exception=True)

        # TEMPORARY - log incoming request (subject and prices) for all course updates, see Jira DISCO-1593
        self.log_request_subjects_and_prices(data, course)

        # First, update course entitlements
        if data.get('type') or data.get('prices'):
            entitlements = []
            prices = data.get('prices', {})
            course_type = CourseType.objects.get(uuid=data.get('type')) if data.get('type') else course.type
            entitlement_types = course_type.entitlement_types.all()
            for entitlement_type in entitlement_types:
                price = prices.get(entitlement_type.slug)
                if price is None:
                    continue
                entitlement, did_change = self.update_entitlement(course, entitlement_type, price, partial=partial)
                entitlements.append(entitlement)
                changed = changed or did_change
            # Deleting entitlements here since they would be orphaned otherwise.
            # One example of how this situation can happen is if a course team is switching between
            # "Verified and Audit" and "Audit Only" before actually publishing their course run.
            course.entitlements.exclude(mode__in=entitlement_types).delete()
            course.entitlements.set(entitlements)

        # Save video if a new video source is provided, also allow removing the video from course
        if video_data:
            video_url = video_data.get('src')
            if not video_url and course.video:
                course.video = None
            elif video_url and (not course.video or video_url != course.video.src):
                video, __ = Video.objects.get_or_create(src=video_data['src'])
                course.video = video

        # Save image and convert to the correct format
        if image_data and isinstance(image_data, str) and image_data.startswith('data:image'):
            # base64 encoded image - decode
            file_format, imgstr = image_data.split(';base64,')  # format ~= data:image/X;base64,/xxxyyyzzz/
            ext = file_format.split('/')[-1]  # guess file extension
            image_data = ContentFile(base64.b64decode(imgstr), name=f'tmp.{ext}')
            course.image.save(image_data.name, image_data)

        if data.get('collaborators'):
            collaborators_uuids = data.get('collaborators')
            collaborators = Collaborator.objects.filter(uuid__in=collaborators_uuids)
            course.collaborators.add(*collaborators)

        # If price didnt change, check the other fields on the course
        # (besides image and video, they are popped off above)
        changed_fields = reviewable_data_has_changed(course, serializer.validated_data.items())
        changed = changed or bool(changed_fields)

        if url_slug:
            validators.validate_slug(url_slug)
            all_course_historical_slugs_excluding_present = CourseUrlSlug.objects.filter(
                url_slug=url_slug, partner=course.partner).exclude(course__uuid=course.uuid)
            if all_course_historical_slugs_excluding_present.exists():
                raise Exception(
                    _('Course edit was unsuccessful. The course URL slug ‘[{url_slug}]’ is already in use. '
                      'Please update this field and try again.').format(url_slug=url_slug))

        # Then the course itself
        course = serializer.save()
        if url_slug:
            course.set_active_url_slug(url_slug)

        if not draft:
            for course_run in course.active_course_runs:
                if course_run.status == CourseRunStatus.Published:
                    # This will also update the course
                    course_run.update_or_create_official_version()
                    CourseRunViewSet.update_course_run_image_in_studio(course_run)

                    if settings.FIRE_UPDATE_COURSE_SKILLS_SIGNAL:
                        # If a skills relavant course field is updated than fire signal
                        # so that a background task in taxonomy update the course skills
                        if any(field in COURSE_FIELDS_FOR_SKILLS for field in changed_fields):
                            logger.info('Signal fired to update course skills. Course: [%s]', course.uuid)
                            UPDATE_COURSE_SKILLS.send(self.__class__, course_uuid=course.uuid)
                elif course.official_version:
                    # If there is an official version available but no active or published
                    # course run, update the slug for official version
                    course.official_version.set_active_url_slug(url_slug)

        # Revert any Reviewed course runs back to Unpublished
        if changed:
            for course_run in course.course_runs.filter(status=CourseRunStatus.Reviewed):
                course_run.status = CourseRunStatus.Unpublished
                course_run.save()
                course_run.official_version.status = CourseRunStatus.Unpublished
                course_run.official_version.save()

        # hack to get the correctly-updated url slug into the response
        return_dict = {'url_slug': course.active_url_slug}
        return_dict.update(serializer.data)
        return Response(return_dict)