コード例 #1
0
    def update(self, request, *args, **kwargs):
        """ Update one, or more, fields for a course run. """
        course_run = self.get_object()
        course_run = ensure_draft_world(course_run)  # always work on drafts

        # Disallow patch or put if the course run is in review.
        if course_run.in_review:
            return Response(
                _('Course run is in review. Editing disabled.'),
                status=status.HTTP_403_FORBIDDEN
            )

        # If changes are made after review and before publish,
        # revert status to unpublished.
        if course_run.status == CourseRunStatus.Reviewed:
            request.data['status'] = CourseRunStatus.Unpublished

        # Sending draft=False triggers the review process
        draft = request.data.pop('draft', True)  # Don't let draft parameter trickle down
        if not draft and course_run.status != CourseRunStatus.Published:
            request.data['status'] = CourseRunStatus.LegalReview

        response = super().update(request, *args, **kwargs)
        self.push_to_studio(request, course_run, create=False)

        return response
コード例 #2
0
    def create(self, request, *args, **kwargs):
        """ Create a course run object. """
        # Set a pacing default when creating (studio requires this to be set, even though discovery does not)
        request.data.setdefault('pacing_type', 'instructor_paced')

        # Guard against externally setting the draft state
        request.data.pop('draft', None)

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

        # Grab any existing course run for this course (we'll use it when talking to studio to form basis of rerun)
        course_key = request.data['course']  # required field
        course = Course.objects.filter_drafts().get(key=course_key)
        course = ensure_draft_world(course)
        old_course_run = course.canonical_course_run

        # And finally, save to database and push to studio
        course_run = serializer.save()
        course_run.draft = True
        course_run.save()
        self.push_to_studio(request, course_run, create=True, old_course_run=old_course_run)

        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
コード例 #3
0
 def _get_course_or_404(partner, course_uuid):
     try:
         course = Course.objects.filter_drafts().get(partner=partner,
                                                     uuid=course_uuid)
         return ensure_draft_world(course)
     except Course.DoesNotExist as course_does_not_exist:
         raise Http404 from course_does_not_exist
コード例 #4
0
    def test_ensure_draft_world_draft_obj_given(self):
        course_run = CourseRunFactory(draft=True)
        ensured_draft_course_run = utils.ensure_draft_world(course_run)

        self.assertEqual(ensured_draft_course_run, course_run)
        self.assertEqual(ensured_draft_course_run.id, course_run.id)
        self.assertEqual(ensured_draft_course_run.uuid, course_run.uuid)
        self.assertEqual(ensured_draft_course_run.draft, course_run.draft)
コード例 #5
0
    def test_published(self):
        person = PersonFactory()
        org = OrganizationFactory()
        primary = DiscoveryCourseRunFactory(key=self.course_run.lms_course_id, staff=[person],
                                            status=CourseRunStatus.Unpublished, announcement=None,
                                            course__partner=self.partner, end=None, enrollment_end=None)
        second = DiscoveryCourseRunFactory(course=primary.course, status=CourseRunStatus.Published, end=None,
                                           enrollment_end=None, start=(primary.start + datetime.timedelta(days=1)))
        third = DiscoveryCourseRunFactory(course=primary.course, status=CourseRunStatus.Published,
                                          end=datetime.datetime(2010, 1, 1, tzinfo=UTC), enrollment_end=None)
        primary.course.authoring_organizations.add(org)
        self.course.organizations.add(org)
        ensure_draft_world(DiscoveryCourse.objects.get(pk=primary.course.pk))

        pc = UserFactory()
        factories.CourseUserRoleFactory(course=self.course, role=PublisherUserRole.ProjectCoordinator, user=pc)
        factories.OrganizationUserRoleFactory(organization=org, role=InternalUserRole.ProjectCoordinator, user=pc)

        self.mock_api_client()

        lookup_value = getattr(primary, self.publisher.unique_field)
        self.mock_node_retrieval(self.publisher.node_lookup_field, lookup_value)
        lookup_value = getattr(third, self.publisher.unique_field)
        self.mock_node_retrieval(self.publisher.node_lookup_field, lookup_value)

        self.mock_get_redirect_form()
        self.mock_add_redirect()

        self.course_run.course_run_state.name = CourseRunStateChoices.Approved
        self.course_run.course_run_state.change_state(CourseRunStateChoices.Published, self.user, self.site)
        primary.refresh_from_db()
        second.refresh_from_db()
        third.refresh_from_db()

        self.assertIsNotNone(primary.announcement)
        self.assertEqual(primary.status, CourseRunStatus.Published)
        self.assertEqual(second.status, CourseRunStatus.Published)  # doesn't change end=None runs
        self.assertEqual(third.status, CourseRunStatus.Unpublished)  # does change archived runs

        # Check email was sent (only one - from old publisher, not new publisher flow)
        assert len(mail.outbox) == 1
        message = mail.outbox[0]
        self.assertTrue(message.subject.startswith('Publication complete: '))
        self.assertEqual(message.to, [self.user.email])
        self.assertEqual(message.cc, [pc.email])
コード例 #6
0
    def test_affects_drafts_too(self):
        draft_course = ensure_draft_world(Course.objects.get(pk=self.course.pk))

        self.run_command()
        draft_course.refresh_from_db()
        assert self.course.type == self.va_course_type
        assert self.audit_run.type == self.audit_run_type
        assert self.verified_run.type == self.va_run_type
        assert draft_course.type == self.va_course_type
        assert set(draft_course.course_runs.values_list('type', flat=True)) \
               == {self.audit_run_type.id, self.va_run_type.id}
コード例 #7
0
    def test_ensure_draft_world_not_draft_course_run_given(self):
        course = CourseFactory()
        course_run = CourseRunFactory(course=course)
        verified_seat = SeatFactory(type='verified', course_run=course_run)
        audit_seat = SeatFactory(type='audit', course_run=course_run)
        course_run.seats.add(verified_seat, audit_seat)

        ensured_draft_course_run = utils.ensure_draft_world(course_run)
        not_draft_course_run = CourseRun.objects.get(uuid=course_run.uuid)

        self.assertNotEqual(ensured_draft_course_run, not_draft_course_run)
        self.assertEqual(ensured_draft_course_run.uuid,
                         not_draft_course_run.uuid)
        self.assertTrue(ensured_draft_course_run.draft)
        self.assertNotEqual(ensured_draft_course_run.course,
                            not_draft_course_run.course)
        self.assertEqual(ensured_draft_course_run.course.uuid,
                         not_draft_course_run.course.uuid)

        # Check slugs are equal
        self.assertEqual(ensured_draft_course_run.slug,
                         not_draft_course_run.slug)

        # Seat checks
        draft_seats = ensured_draft_course_run.seats.all()
        not_draft_seats = not_draft_course_run.seats.all()
        self.assertNotEqual(draft_seats, not_draft_seats)
        self.assertEqual(len(draft_seats), len(not_draft_seats))
        for i, __ in enumerate(draft_seats):
            self.assertEqual(draft_seats[i].price, not_draft_seats[i].price)
            self.assertEqual(draft_seats[i].sku, not_draft_seats[i].sku)
            self.assertNotEqual(draft_seats[i].course_run,
                                not_draft_seats[i].course_run)
            self.assertEqual(draft_seats[i].course_run.uuid,
                             not_draft_seats[i].course_run.uuid)
            self.assertEqual(draft_seats[i].official_version,
                             not_draft_seats[i])
            self.assertEqual(not_draft_seats[i].draft_version, draft_seats[i])

        # Check draft course is also created
        draft_course = ensured_draft_course_run.course
        not_draft_course = Course.objects.get(uuid=course.uuid)
        self.assertNotEqual(draft_course, not_draft_course)
        self.assertEqual(draft_course.uuid, not_draft_course.uuid)
        self.assertTrue(draft_course.draft)

        # Check official and draft versions match up
        self.assertEqual(ensured_draft_course_run.official_version,
                         not_draft_course_run)
        self.assertEqual(not_draft_course_run.draft_version,
                         ensured_draft_course_run)
コード例 #8
0
    def update(self, request, **kwargs):
        # logging to help debug error around course url slugs incrementing
        log.info('The raw course run data coming from publisher is {}.'.format(
            request.data))

        # Update one, or more, fields for a course run.
        course_run = self.get_object()
        course_run = ensure_draft_world(course_run)  # always work on drafts
        partial = kwargs.pop('partial', False)
        # Sending draft=False triggers the review process for unpublished courses
        draft = request.data.pop(
            'draft', True)  # Don't let draft parameter trickle down
        prices = request.data.pop('prices', {})

        serializer = self.get_serializer(course_run,
                                         data=request.data,
                                         partial=partial)
        serializer.is_valid(raise_exception=True)

        # Handle staff update on course run in review with valid status transition
        if (request.user.is_staff and course_run.in_review
                and 'status' in request.data and request.data['status']
                in CourseRunStatus.INTERNAL_STATUS_TRANSITIONS):
            return self.handle_internal_review(request, serializer)

        # Handle regular non-internal update
        request.data.pop('status',
                         None)  # Status management is handled in the model
        serializer.validated_data.pop(
            'status', None)  # Status management is handled in the model
        # Disallow patch or put if the course run is in review.
        if course_run.in_review:
            return Response(_('Course run is in review. Editing disabled.'),
                            status=status.HTTP_403_FORBIDDEN)
        # Disallow internal review fields when course run is not in review
        for key in request.data.keys():
            if key in CourseRun.INTERNAL_REVIEW_FIELDS:
                return Response(_('Invalid parameter'),
                                status=status.HTTP_400_BAD_REQUEST)

        changed_fields = reviewable_data_has_changed(
            course_run, serializer.validated_data.items(),
            CourseRun.STATUS_CHANGE_EXEMPT_FIELDS)
        response = self._update_course_run(course_run, draft,
                                           bool(changed_fields), serializer,
                                           request, prices)

        self.update_course_run_image_in_studio(course_run)

        return response
コード例 #9
0
    def update_course(self, data, partial=False):
        """ Updates an existing course from incoming data. """
        # Pop nested writables that we will handle ourselves (the serializer won't handle them)
        entitlements_data = data.pop('entitlements', [])
        image_data = data.pop('image', None)
        video_data = data.pop('video', None)

        # 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)

        # First, update nested entitlements
        entitlements = []
        for entitlement_data in entitlements_data:
            entitlements.append(
                self.update_entitlement(course,
                                        entitlement_data,
                                        partial=partial))

        # Save video if a new video source is provided
        if (video_data and video_data.get('src') and
            (not course.video or video_data.get('src') != 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='tmp.' + ext)
            # The image requires a name in order to save; however, we don't do anything with that name so
            # we are passing in an empty string so it doesn't break. None is not supported.
            course.image.save('', image_data)

        # Then the course itself
        serializer.save()

        return Response(serializer.data)
コード例 #10
0
ファイル: courses.py プロジェクト: aszykm/course-discovery
    def update_course(self, data, partial=False):  # pylint: disable=too-many-statements
        """ Updates an existing course from incoming 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
        if (video_data and video_data.get('src') and
            (not course.video or video_data.get('src') != 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='tmp.{extension}'.format(extension=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 = changed or reviewable_data_has_changed(
            course, serializer.validated_data.items())

        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()

        # 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)
コード例 #11
0
    def create_run_helper(self, run_data, request=None):
        # These are both required to be part of self because when we call self.get_serializer, it tries
        # to set these two variables as part of the serializer context. When the endpoint is hit directly,
        # self.request should exist, but when this function is called from the Course POST endpoint in courses.py
        # we have to manually set these values.
        if not hasattr(self, 'request'):
            self.request = request  # pylint: disable=attribute-defined-outside-init
        if not hasattr(self, 'format_kwarg'):
            self.format_kwarg = None  # pylint: disable=attribute-defined-outside-init

        # Set a pacing default when creating (studio requires this to be set, even though discovery does not)
        run_data.setdefault('pacing_type', 'instructor_paced')

        # Guard against externally setting the draft state
        run_data.pop('draft', None)

        prices = run_data.pop('prices', {})

        # Grab any existing course run for this course (we'll use it when talking to studio to form basis of rerun)
        course_key = run_data.get('course', None)  # required field
        if not course_key:
            raise ValidationError({'course': ['This field is required.']})

        # Before creating the serializer we need to ensure the course has draft rows as expected
        # The serializer will attempt to retrieve the draft version of the Course
        course = Course.objects.filter_drafts().get(key=course_key)
        course = ensure_draft_world(course)
        old_course_run_key = run_data.pop('rerun', None)

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

        # Save run to database
        course_run = serializer.save(draft=True)

        course_run.update_or_create_seats(course_run.type, prices)

        # Set canonical course run if needed (done this way to match historical behavior - but shouldn't this be
        # updated *each* time we make a new run?)
        if not course.canonical_course_run:
            course.canonical_course_run = course_run
            course.save()
        elif not old_course_run_key:
            # On a rerun, only set the old course run key to the canonical key if a rerun hasn't been provided
            # This will prevent a breaking change if users of this endpoint don't choose to provide a key on rerun
            old_course_run_key = course.canonical_course_run.key

        if old_course_run_key:
            old_course_run = CourseRun.objects.filter_drafts().get(key=old_course_run_key)
            course_run.language = old_course_run.language
            course_run.min_effort = old_course_run.min_effort
            course_run.max_effort = old_course_run.max_effort
            course_run.weeks_to_complete = old_course_run.weeks_to_complete
            course_run.save()
            course_run.staff.set(old_course_run.staff.all())
            course_run.transcript_languages.set(old_course_run.transcript_languages.all())

        # And finally, push run to studio
        self.push_to_studio(self.request, course_run, create=True, old_course_run_key=old_course_run_key)

        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
コード例 #12
0
    def test_ensure_draft_world_not_draft_course_given(self):
        course = CourseFactory()
        entitlement = CourseEntitlementFactory(course=course)
        course.entitlements.add(entitlement)
        course_runs = CourseRunFactory.create_batch(3, course=course)
        for run in course_runs:
            course.course_runs.add(run)
        course.canonical_course_run = course_runs[0]
        course.save()
        org = OrganizationFactory()
        course.authoring_organizations.add(org)  # pylint: disable=no-member

        ensured_draft_course = utils.ensure_draft_world(course)
        not_draft_course = Course.objects.get(uuid=course.uuid)

        self.assertNotEqual(ensured_draft_course, not_draft_course)
        self.assertEqual(ensured_draft_course.uuid, not_draft_course.uuid)
        self.assertTrue(ensured_draft_course.draft)

        # Check slugs are equal
        self.assertEqual(ensured_draft_course.slug, not_draft_course.slug)

        # Check authoring orgs are equal
        self.assertEqual(
            list(ensured_draft_course.authoring_organizations.all()),
            list(not_draft_course.authoring_organizations.all()))

        # Check canonical course run was updated
        self.assertNotEqual(ensured_draft_course.canonical_course_run,
                            not_draft_course.canonical_course_run)
        self.assertTrue(ensured_draft_course.canonical_course_run.draft)
        self.assertEqual(ensured_draft_course.canonical_course_run.uuid,
                         not_draft_course.canonical_course_run.uuid)

        # Check course runs all share the same UUIDs, but are now all drafts
        not_draft_course_runs_uuids = [run.uuid for run in course_runs]
        draft_course_runs_uuids = [
            run.uuid for run in ensured_draft_course.course_runs.all()
        ]
        self.assertListEqual(draft_course_runs_uuids,
                             not_draft_course_runs_uuids)

        # Entitlement checks
        draft_entitlement = ensured_draft_course.entitlements.first()
        not_draft_entitlement = not_draft_course.entitlements.first()
        self.assertNotEqual(draft_entitlement, not_draft_entitlement)
        self.assertEqual(draft_entitlement.price, not_draft_entitlement.price)
        self.assertEqual(draft_entitlement.sku, not_draft_entitlement.sku)
        self.assertNotEqual(draft_entitlement.course,
                            not_draft_entitlement.course)
        self.assertEqual(draft_entitlement.course.uuid,
                         not_draft_entitlement.course.uuid)

        # Check official and draft versions match up
        self.assertEqual(ensured_draft_course.official_version,
                         not_draft_course)
        self.assertEqual(not_draft_course.draft_version, ensured_draft_course)

        self.assertEqual(draft_entitlement.official_version,
                         not_draft_entitlement)
        self.assertEqual(not_draft_entitlement.draft_version,
                         draft_entitlement)