示例#1
0
    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))
示例#2
0
    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,
        )
示例#3
0
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
示例#4
0
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,
        )
示例#5
0
    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
示例#6
0
    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()
        )
示例#7
0
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)
示例#8
0
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"))
示例#9
0
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))
示例#10
0
文件: api.py 项目: mitodl/mitxpro
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 "",
    }
示例#11
0
文件: api.py 项目: mitodl/mitxpro
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)}"
            )
示例#12
0
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
示例#13
0
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())
示例#14
0
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
示例#15
0
    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,
            },
        )
示例#16
0
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]
示例#17
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
示例#18
0
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,
    }
示例#19
0
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
示例#20
0
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])
示例#21
0
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)
示例#22
0
文件: api.py 项目: mitodl/mitxpro
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)
示例#23
0
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
示例#24
0
    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)
示例#25
0
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
示例#26
0
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]
示例#27
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,
    }
示例#28
0
    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"
            ))
示例#29
0
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]
示例#30
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)