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