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