def programs(): """Fixture for a set of Programs in the database""" programs = ProgramFactory.create_batch(3) for program in programs: ProductVersionFactory.create(product=ProductFactory( content_object=program)) return programs
def test_course_runs_not_live_in_courses_api(client, live): """Course runs should be filtered out of the courses API if not live""" run = CourseRunFactory.create(live=live, course__live=True) ProductVersionFactory.create(product=ProductFactory(content_object=run)) resp = client.get(reverse("courses_api-list")) assert resp.status_code == status.HTTP_200_OK assert_drf_json_equal(resp.json()[0]["courseruns"], [CourseRunSerializer(run).data] if live else [])
def test_programs_not_live(client, live): """Programs should be filtered out if live=False""" program = ProgramFactory.create(live=live) ProductVersionFactory.create(product=ProductFactory( content_object=program)) resp = client.get(reverse("programs_api-list")) assert resp.status_code == status.HTTP_200_OK assert_drf_json_equal(resp.json(), [ProgramSerializer(program).data] if live else [])
def test_courses_not_live_in_programs_api(client, live): """Courses should be filtered out of the programs API if not live""" course = CourseFactory.create(live=live, program__live=True) ProductVersionFactory.create(product=ProductFactory( content_object=course.program)) resp = client.get(reverse("programs_api-list")) assert resp.status_code == status.HTTP_200_OK assert_drf_json_equal(resp.json()[0]["courses"], [CourseSerializer(course).data] if live else [])
def test_course_run_current_price(): """ current_price should return the price of the latest product version if it exists """ run = CourseRunFactory.create() assert run.current_price is None price = 10 ProductVersionFactory.create(product=ProductFactory(content_object=run), price=price) assert run.current_price == price
def test_program_current_price(): """ current_price should return the price of the latest product version if it exists """ program = ProgramFactory.create() assert program.current_price is None price = 10 ProductVersionFactory.create( product=ProductFactory(content_object=program), price=price) assert program.current_price == price
def test_program_without_product_in_programs_api(client, has_product): """Programs should be filtered out of the programs API if they don't have an associated product""" program = ProgramFactory.create(live=True) if has_product: ProductVersionFactory.create(product=ProductFactory( content_object=program)) resp = client.get(reverse("programs_api-list")) assert resp.status_code == status.HTTP_200_OK assert_drf_json_equal( resp.json(), [ProgramSerializer(program).data] if has_product else [])
def test_course_runs_without_product_in_courses_api(client, has_product): """Course runs should be filtered out of the courses API if they don't have an associated product""" run = CourseRunFactory.create(live=True, course__live=True) if has_product: ProductVersionFactory.create(product=ProductFactory( content_object=run)) resp = client.get(reverse("courses_api-list")) assert resp.status_code == status.HTTP_200_OK assert_drf_json_equal( resp.json()[0]["courseruns"], [CourseRunSerializer(run).data] if has_product else [], )
def test_course_runs_without_product_in_programs_api(client, has_product): """Regardless of whether course runs have a product, runs should **not** be filtered out of the programs API""" run = CourseRunFactory.create(live=True, course__live=True) ProductVersionFactory.create(product=ProductFactory( content_object=run.course.program)) if has_product: ProductVersionFactory.create(product=ProductFactory( content_object=run)) resp = client.get(reverse("programs_api-list")) assert resp.status_code == status.HTTP_200_OK assert_drf_json_equal(resp.json()[0]["courses"][0]["courseruns"], [CourseRunSerializer(run).data])
def test_serialize_basket_product_version_program(mock_context): """Test ProductVersion serialization for a Program""" program = ProgramFactory.create() courses = CourseFactory.create_batch(3, program=program) product_version = ProductVersionFactory.create( product=ProductFactory(content_object=program) ) data = FullProductVersionSerializer( instance=product_version, context=mock_context ).data assert data == { "id": product_version.id, "description": product_version.description, "content_title": product_version.product.content_object.title, "price": str(round_half_up(product_version.price)), "type": product_version.product.content_type.model, "courses": [ CourseSerializer(instance=course, context=mock_context).data for course in courses ], "thumbnail_url": program.catalog_image_url, "object_id": product_version.product.object_id, "product_id": product_version.product.id, "readable_id": get_readable_id(product_version.product.content_object), "run_tag": None, "created_on": product_version.created_on.strftime(datetime_millis_format), "start_date": product_version.product.content_object.next_run_date.isoformat() if product_version.product.content_object.next_run_date else None, }
def test_product_version_save_text_id_badproduct(mocker): """ProductVersion.text_id should None if ProductVersion.product is invalid""" mock_log = mocker.patch("ecommerce.models.log") product_version = ProductVersionFactory.create( product=ProductFactory.create(content_object=LineFactory())) assert product_version.text_id is None assert mock_log.called_once_with( f"The content object for this ProductVersion ({product_version.id}) does not have a `text_id` property" )
def test_program_view(client, user, home_page, is_enrolled, has_product, has_unexpired_run, is_anonymous): """ Test that the program detail view has the right context and shows the right HTML for the enroll/view button """ program = ProgramFactory.create(live=True, page__parent=home_page) if has_unexpired_run: now = now_in_utc() CourseRunFactory.create_batch( 3, course=CourseFactory.create(program=program, live=True, position_in_program=1), live=True, start_date=now + timedelta(hours=2), ) if has_product: product_id = ProductVersionFactory.create(product=ProductFactory( content_object=program)).product.id else: product_id = None if is_enrolled: ProgramEnrollmentFactory.create(user=user, program=program) if not is_anonymous: client.force_login(user) resp = client.get(program.page.get_url()) assert resp.context["user"] == user if not is_anonymous else AnonymousUser( ) assert resp.context["product_id"] == product_id assert resp.context["enrolled"] == (is_enrolled and not is_anonymous) # Anynoymous users don't see the enrolled/enroll-now button. # For logged in users: # a) product should exist, next courserun should be there, user not enrolled (enroll now button) # b) user is enrolled (enrolled button) has_button = ((has_product and has_unexpired_run and not is_enrolled) or is_enrolled) and not is_anonymous url = "" # make linter happy class_name = "" if not is_anonymous: if not is_enrolled and has_product and has_unexpired_run: url = f'{reverse("checkout-page")}?product={product_id}' class_name = "enroll-now" if is_enrolled: url = reverse("user-dashboard") class_name = "enrolled" assert ( f'<a class="enroll-button {class_name}" href="{url}">'.encode("utf-8") in resp.content) is has_button assert ("Please Sign In to MITx PRO to enroll in a course".encode("utf-8") in resp.content) is (is_anonymous and has_product and has_unexpired_run)
def test_serialize_basket_product_version_programrun(mock_context): """Test ProductVersion serialization for a Program with an associated ProgramRun""" program_run = ProgramRunFactory() product_version = ProductVersionFactory.create( product=ProductFactory(content_object=program_run.program) ) context = {**mock_context, **{"program_run": program_run}} data = FullProductVersionSerializer(instance=product_version, context=context).data assert data["object_id"] == program_run.program.id assert data["run_tag"] == program_run.run_tag
def test_serialize_b2b_order_with_coupon(client, mocker): """Test that B2BOrderToDealSerializer produces the correct serialized data for an order with coupon""" product_version = ProductVersionFactory.create(price=10) payload = {"a": "payload"} mocker.patch( "b2b_ecommerce.views.generate_b2b_cybersource_sa_payload", autospec=True, return_value=payload, ) coupon = B2BCouponFactory.create( product=product_version.product, discount_percent=Decimal(0.8) ) num_seats = 10 resp = client.post( reverse("b2b-checkout"), { "num_seats": num_seats, "email": "*****@*****.**", "product_version_id": product_version.id, "discount_code": coupon.coupon_code, "contract_number": "", }, ) assert resp.status_code == status.HTTP_200_OK assert B2BOrder.objects.count() == 1 order = B2BOrder.objects.first() discount = round(Decimal(coupon.discount_percent) * 100, 2) serialized_data = B2BOrderToDealSerializer(instance=order).data assert serialized_data == { "id": order.id, "name": f"XPRO-B2BORDER-{order.id}", "stage": ORDER_STATUS_MAPPING[order.status], "discount_amount": discount.to_eng_string(), "amount": order.total_price.to_eng_string(), "close_date": ( int(order.updated_on.timestamp() * 1000) if order.status == Order.FULFILLED else None ), "coupon_code": coupon.coupon_code, "company": coupon.company.name, "payment_type": None, "payment_transaction": None, "num_seats": num_seats, "discount_percent": round( Decimal(coupon.discount_percent) * 100, 2 ).to_eng_string(), "status": order.status, "purchaser": format_hubspot_id(order.email), }
def test_course_view(client, user, home_page, is_enrolled, has_unexpired_run, has_product, is_anonymous): """ Test that the course detail view has the right context and shows the right HTML for the enroll/view button """ course = CourseFactory.create(live=True, page__parent=home_page) if has_unexpired_run: run = CourseRunFactory.create(course=course, live=True) else: run = None if has_product and has_unexpired_run: product_id = ProductVersionFactory.create(product=ProductFactory( content_object=run)).product.id else: product_id = None if is_enrolled and has_unexpired_run: CourseRunEnrollmentFactory.create(user=user, run=run) if not is_anonymous: client.force_login(user) resp = client.get(course.page.get_url()) assert resp.context["user"] == user if not is_anonymous else AnonymousUser( ) assert resp.context["product_id"] == product_id assert resp.context["enrolled"] == (is_enrolled and has_unexpired_run and not is_anonymous) # Anynoymous users don't see the enrolled/enroll-now button. # For logged in users: # a) product should exist, next courserun should be there, user not enrolled (enroll now button) # b) user is enrolled (enrolled button) # NOTE: added `has_unexpired_run` to test for case (b) only because of the way the test is written, # enrollment isn't actually created unless the course has an unexpired run. has_button = ((has_product and has_unexpired_run and not is_enrolled) or (is_enrolled and has_unexpired_run)) and not is_anonymous url = "" # make linter happy class_name = "" if not is_anonymous: if not is_enrolled and has_product and has_unexpired_run: url = f'{reverse("checkout-page")}?product={product_id}' class_name = "enroll-now" if is_enrolled and has_unexpired_run: url = reverse("user-dashboard") class_name = "enrolled" assert ( f'<a class="enroll-button {class_name}" href="{url}">'.encode("utf-8") in resp.content) is has_button assert ("Please Sign In to MITx PRO to enroll in a course".encode("utf-8") in resp.content) is (is_anonymous and has_product and has_unexpired_run)
def test_program_page_checkout_url_product(client, wagtail_basics): """ The checkout URL in the program page context should include the product ID if a product exists for the given program """ program_page = ProgramPageFactory.create() program_page.save_revision().publish() product_version = ProductVersionFactory.create( product__content_object=program_page.program ) resp = client.get(program_page.get_url()) checkout_url = resp.context["checkout_url"] assert f"product={product_version.product.id}" in checkout_url
def test_serialize_product(text_id, expected): """ Test that ProductSerializer has correct data """ product_version = ProductVersionFactory.create( product=ProductFactory.create( content_object=CourseRunFactory.create(courseware_id=text_id) ) ) product = Product.objects.get(id=product_version.product.id) run = product.content_object serialized_data = ProductSerializer(instance=product).data assert serialized_data.get("title") == f"{run.title}: {expected}" assert serialized_data.get("product_type") == "courserun" assert serialized_data.get("id") == product.id assert serialized_data.get("price") == product.latest_version.price.to_eng_string() assert serialized_data.get("description") == product.latest_version.description
def test_basket_thumbnail_program(basket_and_coupons, mock_context): """Basket thumbnail should be serialized for a program""" thumbnail_filename = "abcde.jpg" program_page = ProgramPageFactory.create( thumbnail_image__file__filename=thumbnail_filename ) program = program_page.program product_version = ProductVersionFactory.create(product__content_object=program) data = FullProductVersionSerializer( instance=product_version, context=mock_context ).data assert ( data["thumbnail_url"] == program_page.thumbnail_image.get_rendition( CATALOG_COURSE_IMG_WAGTAIL_FILL ).url )
def test_basket_thumbnail_courserun(basket_and_coupons, mock_context): """Basket thumbnail should be serialized for a courserun""" thumbnail_filename = "abcde.jpg" course_page = CoursePageFactory.create( thumbnail_image__file__filename=thumbnail_filename ) run = CourseRunFactory.create(course=course_page.course) product_version = ProductVersionFactory.create(product__content_object=run) data = FullProductVersionSerializer( instance=product_version, context=mock_context ).data assert ( data["thumbnail_url"] == course_page.thumbnail_image.get_rendition( CATALOG_COURSE_IMG_WAGTAIL_FILL ).url )
def test_send_enrollment_failure_message(mocker, is_program): """Test that send_enrollment_failure_message sends a message with proper formatting""" patched_django_mail = mocker.patch("ecommerce.mail_api.mail") product_object = (ProgramFactory.create() if is_program else CourseRunFactory.create()) product_version = ProductVersionFactory.create( product=ProductFactory.create(content_object=product_object)) order = LineFactory.create(product_version=product_version).order details = "TestException on line 21" expected_message = "{name}({email}): Order #{order_id}, {error_obj} #{obj_id} ({obj_title})\n\n{details}".format( name=order.purchaser.username, email=order.purchaser.email, order_id=order.id, error_obj=("Program" if is_program else "Run"), obj_id=product_object.id, obj_title=product_object.title, details=details, ) send_enrollment_failure_message(order, product_object, details) patched_django_mail.send_mail.assert_called_once() send_mail_args = patched_django_mail.send_mail.call_args[0] assert send_mail_args[0] == ENROLL_ERROR_EMAIL_SUBJECT assert send_mail_args[1] == expected_message
def test_serialize_course_run(has_product): """Test CourseRun serialization""" faculty_names = ["Emma Jones", "Joe Smith"] course_run = CourseRunFactory.create() FacultyMembersPageFactory.create( parent=course_run.course.page, **{ f"members__{idx}__member__name": name for idx, name in enumerate(faculty_names) }, ) product_id = (ProductVersionFactory.create( product__content_object=course_run).product.id if has_product else None) course_run.refresh_from_db() data = CourseRunSerializer(course_run).data assert_drf_json_equal( data, { "title": course_run.title, "courseware_id": course_run.courseware_id, "run_tag": course_run.run_tag, "courseware_url": course_run.courseware_url, "start_date": drf_datetime(course_run.start_date), "end_date": drf_datetime(course_run.end_date), "enrollment_start": drf_datetime(course_run.enrollment_start), "enrollment_end": drf_datetime(course_run.enrollment_end), "expiration_date": drf_datetime(course_run.expiration_date), "current_price": course_run.current_price, "instructors": course_run.instructors, "id": course_run.id, "product_id": product_id, }, )
def courseware_objects(): """Database objects that CSV data depends on""" run = CourseRunFactory.create( courseware_id="course-v1:edX+DemoX+Demo_Course") ProductVersionFactory.create(product__content_object=run)
def test_serialize_course(mock_context, is_anonymous, all_runs): """Test Course serialization""" now = datetime.now(tz=pytz.UTC) if is_anonymous: mock_context["request"].user = AnonymousUser() if all_runs: mock_context["all_runs"] = True user = mock_context["request"].user course_run = CourseRunFactory.create(course__no_program=True, live=True) course = course_run.course topic = "a course topic" course.topics.set([CourseTopic.objects.create(name=topic)]) # Create expired, enrollment_ended, future, and enrolled course runs CourseRunFactory.create(course=course, end_date=now - timedelta(1), live=True) CourseRunFactory.create(course=course, enrollment_end=now - timedelta(1), live=True) CourseRunFactory.create(course=course, enrollment_start=now + timedelta(1), live=True) enrolled_run = CourseRunFactory.create(course=course, live=True) unexpired_runs = [enrolled_run, course_run] CourseRunEnrollmentFactory.create(run=enrolled_run, **({} if is_anonymous else { "user": user })) # create products for all courses so the serializer shows them for run in CourseRun.objects.all(): ProductVersionFactory.create(product__content_object=run) data = CourseSerializer(instance=course, context=mock_context).data if all_runs or is_anonymous: expected_runs = unexpired_runs else: expected_runs = [course_run] assert_drf_json_equal( data, { "title": course.title, "description": course.page.description, "readable_id": course.readable_id, "id": course.id, "courseruns": [ CourseRunSerializer(run).data for run in sorted(expected_runs, key=lambda run: run.start_date) ], "thumbnail_url": f"http://localhost:8053{course.page.thumbnail_image.file.url}", "next_run_id": course.first_unexpired_run.id, "topics": [{ "name": topic }], }, )
def test_serialize_program(mock_context, has_product): """Test Program serialization""" program = ProgramFactory.create() run1 = CourseRunFactory.create(course__program=program) course1 = run1.course run2 = CourseRunFactory.create(course__program=program) course2 = run2.course runs = ([run1, run2] + [CourseRunFactory.create(course=course1) for _ in range(2)] + [CourseRunFactory.create(course=course2) for _ in range(2)]) faculty_names = ["Teacher 1", "Teacher 2"] FacultyMembersPageFactory.create( parent=program.page, **{ f"members__{idx}__member__name": name for idx, name in enumerate(faculty_names) }, ) if has_product: ProductVersionFactory.create(product__content_object=program) topics = [ CourseTopic.objects.create(name=f"topic{num}") for num in range(3) ] course1.topics.set([topics[0], topics[1]]) course2.topics.set([topics[1], topics[2]]) data = ProgramSerializer(instance=program, context=mock_context).data assert_drf_json_equal( data, { "title": program.title, "readable_id": program.readable_id, "id": program.id, "description": program.page.description, "courses": [ CourseSerializer(instance=course, context={ **mock_context, "filter_products": False }).data for course in [course1, course2] ], "thumbnail_url": f"http://localhost:8053{program.page.thumbnail_image.file.url}", "current_price": program.current_price, "start_date": sorted(runs, key=lambda run: run.start_date) [0].start_date.strftime(datetime_format), "end_date": sorted(runs, key=lambda run: run.end_date)[-1].end_date.strftime( datetime_format), "enrollment_start": sorted(runs, key=lambda run: run.enrollment_start) [0].enrollment_start.strftime(datetime_format), "url": f"http://localhost{program.page.get_url()}", "instructors": [{ "name": name } for name in faculty_names], "topics": [{ "name": topic.name } for topic in topics], }, )
def test_product_version_save_text_id_courserun(): """ProductVersion.text_id should be set to CourseRun.courseware_id on save""" run = CourseRunFactory.create() product_version = ProductVersionFactory.create( product=ProductFactory.create(content_object=run)) assert product_version.text_id == run.courseware_id
def test_product_version_save_text_id_program(): """ProductVersion.text_id should be set to Program.readable_id on save""" program = ProgramFactory.create() product_version = ProductVersionFactory.create( product=ProductFactory.create(content_object=program)) assert product_version.text_id == program.readable_id