def is_not_beyond_enrollment(self): """ Checks if the course is not beyond its enrollment period Returns: boolean: True if enrollment period has begun but not ended """ now = now_in_utc() return ((self.end_date is None or self.end_date > now) and (self.enrollment_end is None or self.enrollment_end > now) and (self.enrollment_start is None or self.enrollment_start <= now))
def process_row(self, row_index, row_data): # pylint: disable=too-many-return-statements coupon_gen_request, request_created, request_updated = self.get_or_create_request( row_data) try: coupon_req_row = CouponRequestRow.parse_raw_data( row_index, row_data) except SheetRowParsingException as exc: return RowResult( row_index=row_index, row_db_record=coupon_gen_request, row_object=None, result_type=ResultType.FAILED, message="Parsing failure: {}".format(str(exc)), ) is_unchanged_error_row = (coupon_req_row.errors and not request_created and not request_updated) if is_unchanged_error_row: return RowResult( row_index=row_index, row_db_record=coupon_gen_request, row_object=None, result_type=ResultType.IGNORED, message=None, ) elif (coupon_gen_request.date_completed and coupon_req_row.date_processed is None): return RowResult( row_index=row_index, row_db_record=coupon_gen_request, row_object=None, result_type=ResultType.OUT_OF_SYNC, message=None, ) company, created = Company.objects.get_or_create( name__iexact=coupon_req_row.company_name, defaults=dict(name=coupon_req_row.company_name), ) if created: log.info("Created new Company '%s'...", coupon_req_row.company_name) create_coupons_for_request_row(coupon_req_row, company.id) coupon_gen_request.date_completed = now_in_utc() coupon_gen_request.save() return RowResult( row_index=row_index, row_db_record=coupon_gen_request, row_object=coupon_req_row, result_type=ResultType.PROCESSED, message=None, )
def test_program_page_checkout_url_program_run(client, wagtail_basics): """ The checkout URL in the program page context should include the program run text ID if a program run exists """ program_page = ProgramPageFactory.create() program_page.save_revision().publish() program_run = ProgramRunFactory.create( program=program_page.program, start_date=(now_in_utc() - timedelta(days=1)) ) resp = client.get(program_page.get_url()) checkout_url = resp.context["checkout_url"] assert checkout_url is None program_run.start_date = now_in_utc() + timedelta(days=1) program_run.save() # If multiple future program runs exist, the one with the earliest start date should be used ProgramRunFactory.create( program=program_page.program, start_date=(program_run.start_date + timedelta(days=1)), ) resp = client.get(program_page.get_url()) checkout_url = resp.context["checkout_url"] assert f"product={program_run.full_readable_id}" in checkout_url
def generate_course_certificates(): """ Task to generate certificates for courses. """ now = now_in_utc() course_runs = (CourseRun.objects.live().filter( end_date__lt=now - timedelta(hours=settings.CERTIFICATE_CREATION_DELAY_IN_HOURS)).exclude( id__in=CourseRunCertificate.objects.values_list("course_run__id", flat=True))) for run in course_runs: edx_grade_user_iter = exception_logging_generator( get_edx_grades_with_users(run)) created_grades_count, updated_grades_count, generated_certificates_count = ( 0, 0, 0, ) for edx_grade, user in edx_grade_user_iter: course_run_grade, created, updated = ensure_course_run_grade( user=user, course_run=run, edx_grade=edx_grade, should_update=True) if created: created_grades_count += 1 elif updated: updated_grades_count += 1 _, created, deleted = process_course_run_grade_certificate( course_run_grade=course_run_grade) if deleted: log.warning( "Certificate deleted for user %s and course_run %s", user, run) elif created: generated_certificates_count += 1 log.info( "Finished processing course run %s: created grades for %d users, " "updated grades for %d users, generated certificates for %d users", run, created_grades_count, updated_grades_count, generated_certificates_count, )
def update_coupon_delivery_statuses(self, assignment_status_map): """ Updates product coupon assignment records and their corresponding rows in the spreadsheet based on the contents of the assigment status map. Args: assignment_status_map (AssignmentStatusMap): An object representing the status of messages sent for bulk coupon assignments. Returns: List[ProductCouponAssignment]: Product coupon assignment objects that were updated """ bulk_assignment_id = self.bulk_assignment.id # Update product coupon assignment statuses and dates in database updated_assignments = update_product_coupon_assignments( bulk_assignment_id, assignment_status_map) # Set the BulkCouponAssignment to complete if every coupon has been assigned and # all of the coupon messages have been delivered. unsent_assignments_exist = ProductCouponAssignment.objects.filter( bulk_assignment_id=self.bulk_assignment.id, message_status__in=UNSENT_EMAIL_STATUSES, ).exists() if (not unsent_assignments_exist and not assignment_status_map.has_unassigned_codes( bulk_assignment_id)): now = now_in_utc() BulkCouponAssignment.objects.filter(id=bulk_assignment_id).update( message_delivery_completed_date=now, updated_on=now) # Update spreadsheet metadata to reflect the status try: self.set_spreadsheet_completed(now) except Exception: # pylint: disable=broad-except log.exception( "The BulkCouponAssignment has been updated to indicate that message delivery is complete, " "but the request to update spreadsheet properties to indicate this status failed " "(spreadsheet id: %s)", self.spreadsheet.id, ) # Update delivery dates in Sheet if assignment_status_map.has_new_statuses(bulk_assignment_id): row_updates = assignment_status_map.get_row_updates( bulk_assignment_id) self.update_sheet_with_new_statuses(row_updates, zero_based_index=False) return updated_assignments
def get_queryset(self): now = now_in_utc() expired_courseruns = CourseRun.objects.filter( enrollment_end__lt=now ).values_list("id", flat=True) expired_courses = ( Course.objects.annotate( runs=Count("courseruns", filter=~Q(courseruns__in=expired_courseruns)) ) .filter(runs=0) .values_list("id", flat=True) ) expired_programs = ( Program.objects.annotate( valid_runs=Count( "programruns", filter=Q(programruns__end_date__gt=now) | Q(programruns__end_date=None), ) ) .filter( Q(programruns__isnull=True) | Q(valid_runs=0) | Q(courses__in=expired_courses) ) .values_list("id", flat=True) .distinct() ) return ( Product.objects.exclude( Q(productversions=None) | ( Q(object_id__in=expired_courseruns) & Q(content_type__model="courserun") ) | (Q(object_id__in=expired_programs) & Q(content_type__model="program")) ) .order_by("programs__title", "course_run__course__title") .select_related("content_type") .prefetch_related("content_object") .prefetch_generic_related( "content_type", {CourseRun: ["content_object__course"]} ) .with_ordered_versions() )
def test_course_run_not_beyond_enrollment(end_days, enroll_start_days, enroll_end_days, expected): """ Test that CourseRun.is_beyond_enrollment returns the expected boolean value """ now = now_in_utc() end_date = None if end_days is None else now + timedelta(days=end_days) enr_end_date = (None if enroll_end_days is None else now + timedelta(days=enroll_end_days)) enr_start_date = (None if enroll_start_days is None else now + timedelta(days=enroll_start_days)) assert (CourseRunFactory.create( end_date=end_date, enrollment_end=enr_end_date, enrollment_start=enr_start_date, ).is_not_beyond_enrollment is expected)
def fetch_update_eligible_bulk_assignments(): """ Fetches bulk assignment records that should be considered for a message status update Returns: List[BulkCouponAssignment]: Bulk assignment records that should be considered for a message status update """ now = now_in_utc() min_last_assignment_date = now - timedelta( days=ASSIGNMENT_SHEET_MAX_AGE_DAYS) # Fetch all bulk assignments that are eligible to have message statuses updated return list( BulkCouponAssignment.objects.exclude(assignment_sheet_id=None). exclude(assignments_started_date=None).filter( message_delivery_completed_date=None, last_assignment_date__gt=min_last_assignment_date, ).order_by("assignments_started_date").prefetch_related("assignments"))
def fetch_webhook_eligible_assign_sheet_ids(): """ Fetches the file ids of coupon assignment sheets with enough recent activity that they can have a file watch created/renewed. Returns: iterable of str: File ids for assignment sheets that can have a file watch created/renewed """ min_last_activity_date = now_in_utc() - timedelta( days=settings.DRIVE_WEBHOOK_ASSIGNMENT_MAX_AGE_DAYS) return (BulkCouponAssignment.objects.exclude( assignment_sheet_id=None).exclude( assignments_started_date=None, created_on__lt=min_last_activity_date).filter( message_delivery_completed_date=None, last_assignment_date__gte=min_last_activity_date, ).values_list("assignment_sheet_id", flat=True))
def _generate_b2b_cybersource_sa_payload(*, order, receipt_url, cancel_url): """ Generates a payload dict to send to CyberSource for Secure Acceptance for a B2BOrder Args: order (B2BOrder): An order for purchasing enrollment codes receipt_url (str): The URL to be used by Cybersource to redirect the user after completion of the purchase cancel_url (str): The URL to be used by Cybersource to redirect the user after they click cancel Returns: dict: the payload to send to CyberSource via Secure Acceptance """ # http://apps.cybersource.com/library/documentation/dev_guides/Secure_Acceptance_WM/Secure_Acceptance_WM.pdf # Section: API Fields # NOTE: be careful about max length here, many (all?) string fields have a max # length of 255. At the moment none of these fields should go over that, due to database # constraints or other reasons product_version = order.product_version content_object = product_version.product.content_object content_type = str(product_version.product.content_type) price = order.total_price return { "access_key": settings.CYBERSOURCE_ACCESS_KEY, "amount": str(price), "currency": "USD", "locale": "en-us", "item_0_code": "enrollment_code", "item_0_name": f"Enrollment codes for {product_version.description}"[:254], "item_0_quantity": order.num_seats, "item_0_sku": f"enrollment_code-{content_type}-{content_object.id}"[:254], "item_0_tax_amount": "0", "item_0_unit_price": str(price), "line_item_count": 1, "reference_number": order.reference_number, "profile_id": settings.CYBERSOURCE_PROFILE_ID, "signed_date_time": now_in_utc().strftime(ISO_8601_FORMAT), "override_custom_receipt_page": receipt_url, "override_custom_cancel_page": cancel_url, "transaction_type": "sale", "transaction_uuid": uuid.uuid4().hex, "unsigned_field_names": "", "merchant_defined_data1": order.contract_number or "", }
def create_edx_user(user): """ Makes a request to create an equivalent user in Open edX Args: user (user.models.User): the application user """ application = Application.objects.get(name=settings.OPENEDX_OAUTH_APP_NAME) expiry_date = now_in_utc() + timedelta( hours=settings.OPENEDX_TOKEN_EXPIRES_HOURS) access_token = AccessToken.objects.create(user=user, application=application, token=generate_token(), expires=expiry_date) with transaction.atomic(): _, created = CoursewareUser.objects.select_for_update().get_or_create( user=user, platform=PLATFORM_EDX) if not created: return # a non-200 status here will ensure we rollback creation of the CoursewareUser and try again req_session = requests.Session() if settings.MITXPRO_REGISTRATION_ACCESS_TOKEN is not None: req_session.headers.update({ ACCESS_TOKEN_HEADER_NAME: settings.MITXPRO_REGISTRATION_ACCESS_TOKEN }) resp = req_session.post( edx_url(OPENEDX_REGISTER_USER_PATH), data=dict( username=user.username, email=user.email, name=user.name, provider=settings.MITXPRO_OAUTH_PROVIDER, access_token=access_token.token, **OPENEDX_REQUEST_DEFAULTS, ), ) # edX responds with 200 on success, not 201 if resp.status_code != status.HTTP_200_OK: raise CoursewareUserCreateError( f"Error creating Open edX user. {get_error_response_summary(resp)}" )
def test_enrollment_is_ended(): """Verify that is_ended returns True, if all of course runs in a program/course are ended.""" past_date = now_in_utc() - timedelta(days=1) past_program = ProgramFactory.create() past_course = CourseFactory.create() past_course_runs = CourseRunFactory.create_batch( 3, end_date=past_date, course=past_course, course__program=past_program) program_enrollment = ProgramEnrollmentFactory.create(program=past_program) course_enrollment = CourseRunEnrollmentFactory.create( run=past_course_runs[0]) assert program_enrollment.is_ended assert course_enrollment.is_ended
def _validate_assignment_delivery(parsed_row, assignment, delivery_date): """ Returns True if the email for the given enrollment code was never sent, and it's old enough that we would expect it to have been sent. Args: parsed_row (CouponAssignmentRow): assignment (ProductCouponAssignment): delivery_date (Optional[datetime.datetime]): The datetime when the enrollment code email was sent Returns: bool: True if the email for the given enrollment code was never sent, and it's old enough that we would expect it to have been sent """ return (assignment and delivery_date is None and parsed_row.status in {None, ASSIGNMENT_SHEET_ASSIGNED_STATUS} and (assignment.updated_on + timedelta( minutes=ASSIGNMENT_SHEET_EMAIL_RETRY_MINUTES)) < now_in_utc())
def test_program_first_course_unexpired_runs(): """ first_course_unexpired_runs should return the unexpired course runs of the first course in the program (position_in_program=1) """ program = ProgramFactory.create() now = now_in_utc() past_start_dates = [ now + timedelta(days=-10), now + timedelta(days=-11), now + timedelta(days=-12), ] past_end_dates = [now + timedelta(days=-5), now + timedelta(days=-6)] future_end_dates = [ now + timedelta(days=10), now + timedelta(days=11), now + timedelta(days=12), ] first_course = CourseFactory.create(live=True, program=program, position_in_program=1) second_course = CourseFactory.create(live=True, program=program, position_in_program=2) CourseRunFactory.create_batch( 2, course=second_course, start_date=factory.Iterator(past_start_dates), end_date=factory.Iterator(past_end_dates), live=True, ) CourseRunFactory.create_batch( 3, course=first_course, start_date=factory.Iterator(past_start_dates), end_date=factory.Iterator(future_end_dates), enrollment_end=factory.Iterator(future_end_dates), live=True, ) assert len(program.first_course_unexpired_runs) == 3
def set_spreadsheet_completed(self, completed_dt=None): """ Sets spreadsheet metadata to indicate that all coupon assignments have been completed and enrollment messages have all been sent. Args: completed_dt (datetime.datetime or None): A datetime indicating completion date (defaults to UTC now) Returns: dict: Google Drive API results from the files.update endpoint """ date_str = format_datetime_for_google_api(completed_dt or now_in_utc()) return self.expanded_sheets_client.update_spreadsheet_properties( self.spreadsheet.id, { ASSIGNMENT_MESSAGES_COMPLETED_KEY: GOOGLE_API_TRUE_VAL, ASSIGNMENT_MESSAGES_COMPLETED_DATE_KEY: date_str, }, )
def test_course_next_run_date(): """ next_run_date should return the date of the CourseRun with the nearest future start date """ course = CourseFactory.create() CourseRunFactory.create_batch(2, course=course, past_start=True, live=True) assert course.next_run_date is None now = now_in_utc() future_dates = [now + timedelta(hours=1), now + timedelta(hours=2)] CourseRunFactory.create_batch(2, course=course, start_date=factory.Iterator(future_dates), live=True) # invlidate cached property del course.next_run_date assert course.next_run_date == future_dates[0]
def test_program_page_for_program_run(client): """ Test that prgram page URL works with program run id """ program_page = ProgramPageFactory.create() program_page.save_revision().publish() program_run = ProgramRunFactory.create( program=program_page.program, run_tag="R1", start_date=(now_in_utc() + timedelta(days=10)), ) page_base_url = program_page.get_url().rstrip("/") good_url = "{}+{}/".format(page_base_url, program_run.run_tag) resp = client.get(good_url) assert resp.status_code == 200 bad_url = "{}+R2/".format(page_base_url) resp = client.get(bad_url) assert resp.status_code == 404
def make_sync_message(object_id, properties): """ Create data for sync message Args: object_id (ObjectID): Internal ID to match with Hubspot object properties (dict): dict of properties to be synced Returns: dict: serialized sync-message """ properties = sanitize_properties(properties) return { "integratorObjectId": format_hubspot_id(object_id), "action": "UPSERT", "changeOccurredTimestamp": hubspot_timestamp(now_in_utc()), "propertyNameToValues": properties, }
def set_assignment_rows_to_enrolled(sheet_update_map): """ Sets the status to "enrolled" (along with status date) for the specified rows in a coupon assignment sheet. Args: sheet_update_map (dict): A dict of dicts that maps assignment sheet id's a dict of coupon codes and emails representing the rows that need to be set to enrolled. Example: {"sheet-id-1": {"couponcode1": "*****@*****.**", "couponcode2": "*****@*****.**"}} Returns: dict: A summary of execution results. The id of each provided sheet is mapped to the number of updated assignments in that sheet. """ now = now_in_utc() result_summary = {} for sheet_id, assignment_code_email_dict in sheet_update_map.items(): bulk_assignment = BulkCouponAssignment.objects.get( assignment_sheet_id=sheet_id) coupon_assignment_handler = coupon_assign_api.CouponAssignmentHandler( spreadsheet_id=sheet_id, bulk_assignment=bulk_assignment) assignment_rows = coupon_assignment_handler.parsed_rows() assignment_row_updates = [] for assignment_row in assignment_rows: if not assignment_row.code or not assignment_row.email: continue if assignment_row.code in assignment_code_email_dict: redeemed_email = assignment_code_email_dict[ assignment_row.code] alternate_email = (None if case_insensitive_equal( redeemed_email, assignment_row.email) else redeemed_email) assignment_row_updates.append( AssignmentRowUpdate( row_index=assignment_row.row_index, status=ASSIGNMENT_SHEET_ENROLLED_STATUS, status_date=now, alternate_email=alternate_email, )) coupon_assignment_handler.update_sheet_with_new_statuses( assignment_row_updates, zero_based_index=False) result_summary[sheet_id] = len(assignment_code_email_dict) return result_summary
def test_retry_failed_enroll_grace_period(mocker): """ Tests that retry_failed_edx_enrollments does not attempt to repair any enrollments that were recently created """ now = now_in_utc() with freeze_time(now - timedelta( minutes=COURSEWARE_REPAIR_GRACE_PERIOD_MINS - 1)): CourseRunEnrollmentFactory.create(edx_enrolled=False, user__is_active=True) with freeze_time(now - timedelta( minutes=COURSEWARE_REPAIR_GRACE_PERIOD_MINS + 1)): older_enrollment = CourseRunEnrollmentFactory.create( edx_enrolled=False, user__is_active=True) patched_enroll_in_edx = mocker.patch( "courseware.api.enroll_in_edx_course_runs") successful_enrollments = retry_failed_edx_enrollments() assert successful_enrollments == [older_enrollment] patched_enroll_in_edx.assert_called_once_with(older_enrollment.user, [older_enrollment.run])
def test_retry_users_grace_period(mocker): """ Tests that repair_faulty_courseware_users does not attempt to repair any users that were recently created """ now = now_in_utc() with freeze_time(now - timedelta( minutes=COURSEWARE_REPAIR_GRACE_PERIOD_MINS - 1)): UserFactory.create() with freeze_time(now - timedelta( minutes=COURSEWARE_REPAIR_GRACE_PERIOD_MINS + 1)): user_to_repair = UserFactory.create() patched_faulty_user_qset = mocker.patch( "users.models.FaultyCoursewareUserManager.get_queryset", return_value=User.objects.all(), ) patched_repair_user = mocker.patch("courseware.api.repair_faulty_edx_user", return_value=(True, True)) repaired_users = repair_faulty_courseware_users() assert repaired_users == [user_to_repair] patched_faulty_user_qset.assert_called_once() patched_repair_user.assert_called_once_with(user_to_repair)
def _generate_channel_id(sheet_metadata, sheet_file_id=None, dt=None): """ Generates a string channel id based on several spreadsheet attributes. The channel id is an identifier used in the Google file watch API. Args: sheet_metadata (SheetMetadata): sheet_file_id (str): The file id of the spreadsheet dt (Optional[datetime.datetime]): The date and time when the file watch was created Returns: str: The channel id to be used in the Google file watch API """ dt = dt or now_in_utc() new_channel_id_segments = [ settings.DRIVE_WEBHOOK_CHANNEL_ID, sheet_metadata.sheet_type, dt.strftime("%Y%m%d-%H%M%S"), ] if isinstance(sheet_metadata, CouponAssignSheetMetadata): new_channel_id_segments.insert(2, sheet_file_id) return "-".join(new_channel_id_segments)
def test_program_is_catalog_visible(): """ is_catalog_visible should return True if a program has any course run that has a start date or enrollment end date in the future """ program = ProgramFactory.create() runs = CourseRunFactory.create_batch(2, course__program=program, past_start=True, past_enrollment_end=True) assert program.is_catalog_visible is False now = now_in_utc() run = runs[0] run.start_date = now + timedelta(hours=1) run.save() assert program.is_catalog_visible is True run.start_date = now - timedelta(hours=1) run.enrollment_end = now + timedelta(hours=1) run.save() assert program.is_catalog_visible is True
def report_invalid_emails(self, assignment_rows, invalid_emails): """ Updates the status column for each row in an assignment sheet with an invalid email Args: assignment_rows (iterable of CouponAssignmentRow): The parsed rows in the given assignment sheet invalid_emails (set of str): Email addresses that failed validation """ now = now_in_utc() row_updates = [ AssignmentRowUpdate( row_index=row.row_index, status=ASSIGNMENT_SHEET_INVALID_STATUS, status_date=now, alternate_email=None, ) for row in assignment_rows if row.email in {email.lower() for email in invalid_emails} ] self.update_sheet_with_new_statuses(row_updates=row_updates, zero_based_index=False)
def test_course_first_unexpired_run(): """ Test that the first unexpired run of a course is returned """ course = CourseFactory.create() now = now_in_utc() end_date = now + timedelta(days=100) enr_end_date = now + timedelta(days=100) first_run = CourseRunFactory.create( start_date=now, course=course, end_date=end_date, enrollment_end=enr_end_date, live=True, ) CourseRunFactory.create( start_date=now + timedelta(days=50), course=course, end_date=end_date, enrollment_end=enr_end_date, ) assert course.first_unexpired_run == first_run
def test_program_next_run_date(): """ next_run_date should return the date of the CourseRun with the nearest future start date and first position in program (course__position_in_program=1) """ program = ProgramFactory.create() CourseRunFactory.create_batch( 2, course=CourseFactory.create(program=program, position_in_program=3), past_start=True, ) assert program.next_run_date is None now = now_in_utc() second_course_future_dates = [ now + timedelta(hours=1), now + timedelta(hours=3) ] CourseRunFactory.create_batch( 2, course=CourseFactory.create(program=program, position_in_program=2), start_date=factory.Iterator(second_course_future_dates), live=True, ) first_course_future_dates = [ now + timedelta(hours=2), now + timedelta(hours=4) ] CourseRunFactory.create_batch( 2, course=CourseFactory.create(program=program, position_in_program=1), start_date=factory.Iterator(first_course_future_dates), live=True, ) # invalidate cached property del program.next_run_date assert program.next_run_date == first_course_future_dates[0]
def process_coupon_assignment_sheet(*, file_id, change_date=None): """ Processes a single coupon assignment spreadsheet Args: file_id (str): The file id of the assignment spreadsheet (visible in the spreadsheet URL) change_date (str): ISO-8601-formatted string indicating the datetime when this spreadsheet was changed """ change_dt = datetime.fromisoformat( change_date) if change_date else now_in_utc() bulk_assignment, _ = BulkCouponAssignment.objects.update_or_create( assignment_sheet_id=file_id, defaults=dict(sheet_last_modified_date=change_dt)) coupon_assignment_handler = coupon_assign_api.CouponAssignmentHandler( spreadsheet_id=file_id, bulk_assignment=bulk_assignment) _, num_created, num_removed = ( coupon_assignment_handler.process_assignment_spreadsheet()) return { "sheet_id": file_id, "assignments_created": num_created, "assignments_removed": num_removed, }
def handle(self, *args, **options): # pylint: disable=too-many-locals """Handle command execution""" runs = [] if options["run"]: try: runs = [CourseRun.objects.get(courseware_id=options["run"])] except CourseRun.DoesNotExist: raise CommandError( "Could not find run with courseware_id={}".format( options["run"])) else: # We pick up all the course runs that do not have an expiration date (implies not having # an end_date) or those that are not expired yet, in case the user has not specified any # course run id. now = now_in_utc() runs = CourseRun.objects.live().filter( Q(expiration_date__isnull=True) | Q(expiration_date__gt=now)) success_count, error_count = sync_course_runs(runs) self.stdout.write( self.style.SUCCESS( f"Sync complete: {success_count} updated, {error_count} failures" ))
def test_course_unexpired_runs(): """unexpired_runs should return expected value""" course = CourseFactory.create() now = now_in_utc() start_dates = [now, now + timedelta(days=-3)] end_dates = [now + timedelta(hours=1), now + timedelta(days=-2)] CourseRunFactory.create_batch( 2, course=course, start_date=factory.Iterator(start_dates), end_date=factory.Iterator(end_dates), live=True, ) # Add a run that is not live and shouldn't show up in unexpired list CourseRunFactory.create(course=course, start_date=start_dates[0], end_date=end_dates[0], live=False) assert len(course.unexpired_runs) == 1 course_run = course.unexpired_runs[0] assert course_run.start_date == start_dates[0] assert course_run.end_date == end_dates[0]
def test_create_edx_user(user, settings, application, access_token_count): """Test that create_edx_user makes a request to create an edX user""" responses.add( responses.POST, f"{settings.OPENEDX_API_BASE_URL}/user_api/v1/account/registration/", json=dict(success=True), status=status.HTTP_200_OK, ) for _ in range(access_token_count): AccessToken.objects.create( user=user, application=application, token=generate_token(), expires=now_in_utc() + timedelta(hours=1), ) create_edx_user(user) # An AccessToken should be created during execution created_access_token = AccessToken.objects.filter( application=application).last() assert (responses.calls[0].request.headers[ACCESS_TOKEN_HEADER_NAME] == settings.MITXPRO_REGISTRATION_ACCESS_TOKEN) assert dict(parse_qsl(responses.calls[0].request.body)) == { "username": user.username, "email": user.email, "name": user.name, "provider": settings.MITXPRO_OAUTH_PROVIDER, "access_token": created_access_token.token, "country": "US", "honor_code": "True", } assert (CoursewareUser.objects.filter( user=user, platform=PLATFORM_EDX, has_been_synced=True).exists() is True)