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,
        }
예제 #2
0
 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
예제 #3
0
    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),
        }
예제 #4
0
    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
예제 #5
0
    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
예제 #6
0
 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
예제 #8
0
    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
예제 #9
0
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
예제 #10
0
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,
        }]
    }
예제 #11
0
 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),
             }
         ]
     }
예제 #12
0
    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
예제 #13
0
    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)
예제 #14
0
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
예제 #15
0
 def get_last_state_change(self, course):
     return serialize_datetime(course.course_state.owner_role_modified)
예제 #16
0
 def handle_datetime_field(value):
     if isinstance(value, str):
         value = parse_datetime(value)
     return serialize_datetime(value)