def generate_data_for_studio_api(cls, publisher_course_run): course = publisher_course_run.course course_team_admin = course.course_team_admin team = [] if course_team_admin: team = [ { 'user': course_team_admin.username, 'role': 'instructor', }, ] else: logger.warning( 'No course team admin specified for course [%s]. This may result in a Studio ' 'course run being created without a course team.', course.number) return { 'title': publisher_course_run.title_override or course.title, 'org': course.organizations.first().key, 'number': course.number, 'run': cls.calculate_course_run_key_run_value(publisher_course_run), 'schedule': { 'start': serialize_datetime(publisher_course_run.start), 'end': serialize_datetime(publisher_course_run.end), }, 'team': team, 'pacing_type': publisher_course_run.pacing_type, }
def assert_data_generated_correctly(self, course_run, expected_team_data, creating=False): course = course_run.course expected = { 'title': course_run.title_override or course.title, 'org': course.organizations.first().key, 'number': course.number, 'run': StudioAPI.calculate_course_run_key_run_value( course.number, course_run.start_date_temporary), 'schedule': { 'start': serialize_datetime(course_run.start_date_temporary), 'end': serialize_datetime(course_run.end_date_temporary), }, 'team': expected_team_data, 'pacing_type': course_run.pacing_type_temporary, } assert StudioAPI.generate_data_for_studio_api( course_run, creating=creating) == expected
def generate_data_for_studio_api(cls, course_run, user=None): editors = cls._run_editors(course_run) org, number, run = cls._run_key_parts(course_run) start, end = cls._run_times(course_run) if user: editors.append(user) if editors: team = [{ 'user': user.username, 'role': 'instructor', } for user in editors] else: team = [] logger.warning( 'No course team admin specified for course [%s]. This may result in a Studio ' 'course run being created without a course team.', number) return { 'title': cls._run_title(course_run), 'org': org, 'number': number, 'run': run, 'schedule': { 'start': serialize_datetime(start), 'end': serialize_datetime(end), }, 'team': team, 'pacing_type': cls._run_pacing(course_run), }
def publish_to_ecommerce(self, partner, course_run): api = EdxRestApiClient(partner.ecommerce_api_url, jwt=partner.access_token) data = { 'id': course_run.lms_course_id, 'name': course_run.title_override or course_run.course.title, 'verification_deadline': serialize_datetime(course_run.end), 'create_or_activate_enrollment_code': False, # NOTE (CCB): We only order here to aid testing. The E-Commerce API does NOT care about ordering. 'products': [ serialize_seat_for_ecommerce_api(seat) for seat in course_run.seats.exclude( type=Seat.CREDIT).order_by('created') ], } try: api.publication.post(data) return self.PUBLICATION_SUCCESS_STATUS except SlumberBaseException as ex: content = ex.content.decode('utf8') logger.exception( 'Failed to publish course run [%d] to E-Commerce! Error was: [%s]', course_run.pk, content) return 'FAILED: ' + content
def publish_to_ecommerce(self, partner, course_run): course_key = self.get_course_key(course_run.course) discovery_course = Course.objects.get(partner=partner, key=course_key) api = EdxRestApiClient(partner.ecommerce_api_url, jwt=partner.access_token) data = { 'id': course_run.lms_course_id, 'uuid': str(discovery_course.uuid), 'name': course_run.title_override or course_run.course.title, 'verification_deadline': serialize_datetime(course_run.end), } # NOTE: We only order here to aid testing. The E-Commerce API does NOT care about ordering. products = [ serialize_seat_for_ecommerce_api(seat) for seat in course_run.seats.exclude( type=Seat.CREDIT).order_by('created') ] products.extend([ serialize_entitlement_for_ecommerce_api(entitlement) for entitlement in course_run.course.entitlements.order_by('created') ]) data['products'] = products try: api.publication.post(data) return self.PUBLICATION_SUCCESS_STATUS except SlumberBaseException as ex: content = ex.content.decode('utf8') logger.exception( 'Failed to publish course run [%d] to E-Commerce! Error was: [%s]', course_run.pk, content) return 'FAILED: ' + content
def make_studio_data(self, run, add_pacing=True, add_schedule=True, team=None): key = CourseKey.from_string(run.key) data = { 'title': run.title, 'org': key.org, 'number': key.course, 'run': key.run, 'team': team or [], } if add_pacing: data['pacing_type'] = run.pacing_type if add_schedule: data['schedule'] = { 'start': serialize_datetime(run.start), 'end': serialize_datetime(run.end), } return data
def assert_data_generated_correctly(course_run, expected_team_data): course = course_run.course expected = { 'title': course_run.title_override or course.title, 'org': course.organizations.first().key, 'number': course.number, 'run': StudioAPI.calculate_course_run_key_run_value(course_run), 'schedule': { 'start': serialize_datetime(course_run.start), 'end': serialize_datetime(course_run.end), 'enrollment_start': serialize_datetime(course_run.enrollment_start), 'enrollment_end': serialize_datetime(course_run.enrollment_end), }, 'team': expected_team_data, 'pacing_type': course_run.pacing_type, } assert StudioAPI.generate_data_for_studio_api(course_run) == expected
def generate_data_for_studio_api(cls, course_run, creating, user=None): editors = [editor.user for editor in course_run.course.editors.all()] key = CourseKey.from_string(course_run.key) # start, end, and pacing are not sent on updates - Studio is where users edit them start = course_run.start if creating else None end = course_run.end if creating else None pacing = course_run.pacing_type if creating else None if user: editors.append(user) if editors: team = [ { 'user': user.username, 'role': 'instructor', } for user in editors ] else: team = [] logger.warning('No course team admin specified for course [%s]. This may result in a Studio ' 'course run being created without a course team.', key.course) data = { 'title': course_run.title, 'org': key.org, 'number': key.course, 'run': key.run, 'team': team, } if pacing: data['pacing_type'] = pacing if start or end: data['schedule'] = { 'start': serialize_datetime(start), 'end': serialize_datetime(end), } return data
def assert_data_generated_correctly(course_run, expected_team_data): course = course_run.course expected = { 'title': course_run.title_override or course.title, 'org': course.organizations.first().key, 'number': course.number, 'run': StudioAPI.calculate_course_run_key_run_value( course.number, course_run.start_date_temporary), 'schedule': { 'start': serialize_datetime(course_run.start_date_temporary), 'end': serialize_datetime(course_run.end_date_temporary), }, 'team': expected_team_data, 'pacing_type': course_run.pacing_type_temporary, } # the publisher djangoapp doesn't care about the 'creating' flag passed below, so we just always set it False assert StudioAPI.generate_data_for_studio_api(course_run, creating=False) == expected
def serialize_seat_for_ecommerce_api(seat, mode): return { 'expires': serialize_datetime(calculated_seat_upgrade_deadline(seat)), 'price': str(seat.price), 'product_class': 'Seat', 'attribute_values': [{ 'name': 'certificate_type', 'value': mode.certificate_type, }, { 'name': 'id_verification_required', 'value': mode.is_id_verified, }] }
def serialize_seat_for_ecommerce_api(self, seat): return { 'expires': serialize_datetime(seat.upgrade_deadline or seat.course_run.end), 'price': str(seat.price), 'product_class': 'Seat', 'attribute_values': [ { 'name': 'certificate_type', 'value': None if seat.type is Seat.AUDIT else seat.type, }, { 'name': 'id_verification_required', 'value': seat.type in (Seat.VERIFIED, Seat.PROFESSIONAL), } ] }
def test_serialize_seat_for_ecommerce_api_with_audit_seat(self): seat = SeatFactory(type=Seat.AUDIT) actual = serialize_seat_for_ecommerce_api(seat) expected = { 'expires': serialize_datetime(seat.calculated_upgrade_deadline), 'price': str(seat.price), 'product_class': 'Seat', 'attribute_values': [{ 'name': 'certificate_type', 'value': '', }, { 'name': 'id_verification_required', 'value': False, }] } assert actual == expected
def test_publish(self, mock_access_token): # pylint: disable=unused-argument,too-many-statements publisher_course_run = self._create_course_run_for_publication() currency = Currency.objects.get(code='USD') common_seat_kwargs = { 'course_run': publisher_course_run, 'currency': currency, } audit_seat = SeatFactory(type=Seat.AUDIT, upgrade_deadline=None, **common_seat_kwargs) # The credit seat should NOT be published. SeatFactory(type=Seat.CREDIT, **common_seat_kwargs) professional_seat = SeatFactory(type=Seat.PROFESSIONAL, **common_seat_kwargs) verified_seat = SeatFactory(type=Seat.VERIFIED, **common_seat_kwargs) partner = publisher_course_run.course.organizations.first().partner self._set_test_client_domain_and_login(partner) self._mock_studio_api_success(publisher_course_run) self._mock_ecommerce_api(publisher_course_run) url = reverse('publisher:api:v1:course_run-publish', kwargs={'pk': publisher_course_run.pk}) response = self.client.post(url, {}) assert response.status_code == 200 assert len(responses.calls) == 3 expected = { 'discovery': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS, 'ecommerce': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS, 'studio': CourseRunViewSet.PUBLICATION_SUCCESS_STATUS, } assert response.data == expected # Verify the correct deadlines were sent to the E-Commerce API ecommerce_body = json.loads(responses.calls[2].request.body) expected = [ serialize_seat_for_ecommerce_api(audit_seat), serialize_seat_for_ecommerce_api(professional_seat), serialize_seat_for_ecommerce_api(verified_seat), ] assert ecommerce_body['products'] == expected assert ecommerce_body['verification_deadline'] == serialize_datetime( publisher_course_run.end) discovery_course_run = CourseRun.objects.get( key=publisher_course_run.lms_course_id) publisher_course = publisher_course_run.course discovery_course = discovery_course_run.course # pylint: disable=no-member assert discovery_course_run.title_override == publisher_course_run.title_override assert discovery_course_run.short_description_override is None assert discovery_course_run.full_description_override is None assert discovery_course_run.start == publisher_course_run.start assert discovery_course_run.end == publisher_course_run.end assert discovery_course_run.enrollment_start == publisher_course_run.enrollment_start assert discovery_course_run.enrollment_end == publisher_course_run.enrollment_end assert discovery_course_run.pacing_type == publisher_course_run.pacing_type assert discovery_course_run.min_effort == publisher_course_run.min_effort assert discovery_course_run.max_effort == publisher_course_run.max_effort assert discovery_course_run.language == publisher_course_run.language assert discovery_course_run.weeks_to_complete == publisher_course_run.length assert discovery_course_run.learner_testimonials == publisher_course.learner_testimonial expected = set(publisher_course_run.transcript_languages.all()) assert set(discovery_course_run.transcript_languages.all()) == expected assert set(discovery_course_run.staff.all()) == set( publisher_course_run.staff.all()) assert discovery_course.canonical_course_run == discovery_course_run assert discovery_course.partner == partner assert discovery_course.title == publisher_course.title assert discovery_course.short_description == publisher_course.short_description assert discovery_course.full_description == publisher_course.full_description assert discovery_course.level_type == publisher_course.level_type assert discovery_course.video == Video.objects.get( src=publisher_course.video_link) assert discovery_course.image.name is not None assert discovery_course.image.url is not None assert discovery_course.image.file is not None assert discovery_course.image.small.url is not None assert discovery_course.image.small.file is not None assert discovery_course.outcome == publisher_course.expected_learnings assert discovery_course.prerequisites_raw == publisher_course.prerequisites assert discovery_course.syllabus_raw == publisher_course.syllabus expected = list(publisher_course_run.course.organizations.all()) assert list(discovery_course.authoring_organizations.all()) == expected expected = { publisher_course.primary_subject, publisher_course.secondary_subject } assert set(discovery_course.subjects.all()) == expected common_seat_kwargs = { 'course_run': discovery_course_run, 'currency': currency, } DiscoverySeat.objects.get(type=DiscoverySeat.AUDIT, upgrade_deadline__isnull=True, **common_seat_kwargs) DiscoverySeat.objects.get(type=DiscoverySeat.PROFESSIONAL, upgrade_deadline__isnull=True, price=professional_seat.price, **common_seat_kwargs) DiscoverySeat.objects.get( type=DiscoverySeat.VERIFIED, upgrade_deadline=verified_seat.upgrade_deadline, price=verified_seat.price, **common_seat_kwargs)
def push_to_ecommerce_for_course_run(course_run): """ Args: course_run: Official version of a course_metadata CourseRun """ course = course_run.course if not course.partner.ecommerce_api_url: return False api = course.partner.oauth_api_client entitlements = course.entitlements.all() # Figure out which seats to send (skip ones that have no ecom products - like Masters - or are just misconfigured). # This is dumb and does basically a O(n^2) inner join here to match seats to modes. I feel like the Django # ORM has a better solution for this, but I couldn't find it easily. These lists are small anyway. tracks = course_run.type.tracks.all() seats_with_modes = [] for seat in course_run.seats.all(): for track in tracks: if track.seat_type and seat.type == track.seat_type: seats_with_modes.append((seat, track.mode)) break discovery_products = [] serialized_products = [] if seats_with_modes: serialized_products.extend([ serialize_seat_for_ecommerce_api(s[0], s[1]) for s in seats_with_modes ]) discovery_products.extend([s[0] for s in seats_with_modes]) if entitlements: serialized_products.extend( [serialize_entitlement_for_ecommerce_api(e) for e in entitlements]) discovery_products.extend(list(entitlements)) if not serialized_products: return False # nothing to do url = urljoin(course.partner.ecommerce_api_url, 'publication/') response = api.post(url, json={ 'id': course_run.key, 'uuid': str(course.uuid), 'name': course_run.title, 'verification_deadline': serialize_datetime(course_run.end), 'products': serialized_products, }) if 400 <= response.status_code < 600: error = response.json().get('error') if error: raise EcommerceSiteAPIClientException({'error': error}) response.raise_for_status() # Now save the returned SKU numbers locally ecommerce_products = response.json().get('products', []) if len(discovery_products) == len(ecommerce_products): with transaction.atomic(): for i, discovery_product in enumerate(discovery_products): ecommerce_product = ecommerce_products[i] sku = ecommerce_product.get('partner_sku') if not sku: continue discovery_product.sku = sku discovery_product.save() if discovery_product.draft_version: discovery_product.draft_version.sku = sku discovery_product.draft_version.save() return True
def get_last_state_change(self, course): return serialize_datetime(course.course_state.owner_role_modified)
def handle_datetime_field(value): if isinstance(value, str): value = parse_datetime(value) return serialize_datetime(value)