Exemplo n.º 1
0
    def setUp(self):
        patcher = patch('student.models.tracker')
        self.mock_tracker = patcher.start()
        self.user = UserFactory.create()
        self.user.set_password('password')
        self.user.save()
        self.instructor = AdminFactory.create()
        self.cost = 40
        self.coupon_code = 'abcde'
        self.reg_code = 'qwerty'
        self.percentage_discount = 10
        self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
        self.course_key = self.course.id
        self.course_mode = CourseMode(course_id=self.course_key,
                                      mode_slug="honor",
                                      mode_display_name="honor cert",
                                      min_price=self.cost)
        self.course_mode.save()

        #Saving another testing course mode
        self.testing_cost = 20
        self.testing_course = CourseFactory.create(org='edX', number='888', display_name='Testing Super Course')
        self.testing_course_mode = CourseMode(course_id=self.testing_course.id,
                                              mode_slug="honor",
                                              mode_display_name="testing honor cert",
                                              min_price=self.testing_cost)
        self.testing_course_mode.save()

        verified_course = CourseFactory.create(org='org', number='test', display_name='Test Course')
        self.verified_course_key = verified_course.id
        self.cart = Order.get_cart_for_user(self.user)
        self.addCleanup(patcher.stop)
Exemplo n.º 2
0
    def setUp(self):
        patcher = patch("student.models.tracker")
        self.mock_tracker = patcher.start()
        self.user = UserFactory.create()
        self.user.set_password("password")
        self.user.save()
        self.instructor = AdminFactory.create()
        self.cost = 40
        self.coupon_code = "abcde"
        self.reg_code = "qwerty"
        self.percentage_discount = 10
        self.course = CourseFactory.create(org="MITx", number="999", display_name="Robot Super Course")
        self.course_key = self.course.id
        self.course_mode = CourseMode(
            course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost
        )
        self.course_mode.save()

        # Saving another testing course mode
        self.testing_cost = 20
        self.testing_course = CourseFactory.create(org="edX", number="888", display_name="Testing Super Course")
        self.testing_course_mode = CourseMode(
            course_id=self.testing_course.id,
            mode_slug="honor",
            mode_display_name="testing honor cert",
            min_price=self.testing_cost,
        )
        self.testing_course_mode.save()

        verified_course = CourseFactory.create(org="org", number="test", display_name="Test Course")
        self.verified_course_key = verified_course.id
        self.cart = Order.get_cart_for_user(self.user)
        self.addCleanup(patcher.stop)
    def test_paid_course_registration(self):
        """
        Make sure that Microsite overrides on the ENABLE_SHOPPING_CART and
        ENABLE_PAID_COURSE_ENROLLMENTS are honored
        """
        course_mode = CourseMode(
            course_id=self.course_with_visibility.id,
            mode_slug="honor",
            mode_display_name="honor cert",
            min_price=10,
        )
        course_mode.save()

        # first try on the non microsite, which
        # should pick up the global configuration (where ENABLE_PAID_COURSE_REGISTRATIONS = False)
        url = reverse('about_course', args=[self.course_with_visibility.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("Register for {}".format(self.course_with_visibility.id.course), resp.content)
        self.assertNotIn("Add {} to Cart ($10)".format(self.course_with_visibility.id.course), resp.content)

        # now try on the microsite
        url = reverse('about_course', args=[self.course_with_visibility.id.to_deprecated_string()])
        resp = self.client.get(url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
        self.assertEqual(resp.status_code, 200)
        self.assertNotIn("Register for {}".format(self.course_with_visibility.id.course), resp.content)
        self.assertIn("Add {} to Cart ($10)".format(self.course_with_visibility.id.course), resp.content)
        self.assertIn('$("#add_to_cart_post").click', resp.content)
    def get_group_for_user(cls, course_key, user, user_partition, **kwargs):  # pylint: disable=unused-argument
        """
        Returns the Group from the specified user partition to which the user
        is assigned, via enrollment mode. If a user is in a Credit mode, the Verified or
        Professional mode for the course is returned instead.

        If a course is using the Verified Track Cohorting pilot feature, this method
        returns None regardless of the user's enrollment mode.
        """
        if is_course_using_cohort_instead(course_key):
            return None

        # First, check if we have to deal with masquerading.
        # If the current user is masquerading as a specific student, use the
        # same logic as normal to return that student's group. If the current
        # user is masquerading as a generic student in a specific group, then
        # return that group.
        if get_course_masquerade(user, course_key) and not is_masquerading_as_specific_student(user, course_key):
            return get_masquerading_user_group(course_key, user, user_partition)

        mode_slug, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_key)
        if mode_slug and is_active:
            course_mode = CourseMode.mode_for_course(
                course_key,
                mode_slug,
                modes=CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False),
            )
            if course_mode and CourseMode.is_credit_mode(course_mode):
                course_mode = CourseMode.verified_mode_for_course(course_key)
            if not course_mode:
                course_mode = CourseMode.DEFAULT_MODE
            return Group(ENROLLMENT_GROUP_IDS[course_mode.slug], unicode(course_mode.name))
        else:
            return None
Exemplo n.º 5
0
    def test_refund_cert_callback_before_expiration_email(self):
        """ Test that refund emails are being sent correctly. """
        course = CourseFactory.create()
        course_key = course.id
        many_days = datetime.timedelta(days=60)

        course_mode = CourseMode(course_id=course_key,
                                 mode_slug="verified",
                                 mode_display_name="verified cert",
                                 min_price=self.cost,
                                 expiration_datetime=datetime.datetime.now(pytz.utc) + many_days)
        course_mode.save()

        CourseEnrollment.enroll(self.user, course_key, 'verified')
        cart = Order.get_cart_for_user(user=self.user)
        CertificateItem.add_to_order(cart, course_key, self.cost, 'verified')
        cart.purchase()

        mail.outbox = []
        with patch('shoppingcart.models.log.error') as mock_error_logger:
            CourseEnrollment.unenroll(self.user, course_key)
            self.assertFalse(mock_error_logger.called)
            self.assertEquals(len(mail.outbox), 1)
            self.assertEquals('[Refund] User-Requested Refund', mail.outbox[0].subject)
            self.assertEquals(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].from_email)
            self.assertIn('has requested a refund on Order', mail.outbox[0].body)
Exemplo n.º 6
0
    def test_refund_cert_callback_before_expiration(self):
        # If the expiration date has not yet passed on a verified mode, the user can be refunded
        many_days = datetime.timedelta(days=60)

        course = CourseFactory.create()
        self.course_key = course.id
        course_mode = CourseMode(course_id=self.course_key,
                                 mode_slug="verified",
                                 mode_display_name="verified cert",
                                 min_price=self.cost,
                                 expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days))
        course_mode.save()

        # need to prevent analytics errors from appearing in stderr
        with patch('sys.stderr', sys.stdout.write):
            CourseEnrollment.enroll(self.user, self.course_key, 'verified')
            cart = Order.get_cart_for_user(user=self.user)
            CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified')
            cart.purchase()
            CourseEnrollment.unenroll(self.user, self.course_key)

        target_certs = CertificateItem.objects.filter(course_id=self.course_key, user_id=self.user, status='refunded', mode='verified')
        self.assertTrue(target_certs[0])
        self.assertTrue(target_certs[0].refund_requested_time)
        self.assertEquals(target_certs[0].order.status, 'refunded')
        self._assert_refund_tracked()
Exemplo n.º 7
0
    def get_payment_type(self, obj):
        try:
            course_id = obj.course_id
            cache_key = "api_course_payment_cache.{}".format(course_id)
            payment_type = cache.get(cache_key, "")
            if payment_type:
                return payment_type.split("|")
            course_key = CourseKey.from_string(course_id)
            course_modes = CourseMode.modes_for_course(course_key)
            course_modes_dict = CourseMode.modes_for_course_dict(course_id, course_modes)
            has_verified_mode = CourseMode.has_verified_mode(course_modes_dict)
            SA = True
            ST = True if has_verified_mode else False
            if ST and not "honor" in course_modes_dict:
                SA = False
            if SA and ST:
                payment_type = "SA|ST"
            elif SA:
                payment_type = "SA"
            else:
                payment_type = "ST"
            cache.set(cache_key, payment_type, 60 * 60 * 6)
        except Exception as ex:
            # import traceback
            # traceback.print_exc()
            log.error(ex)
            payment_type = "SA"

        return payment_type.split("|")
Exemplo n.º 8
0
    def _get_paid_mode(self, course_key):
        """
        Retrieve the paid course mode for a course.

        The returned course mode may or may not be expired.
        Unexpired modes are preferred to expired modes.

        Arguments:
            course_key (CourseKey): The location of the course.

        Returns:
            CourseMode tuple

        """
        # Retrieve all the modes at once to reduce the number of database queries
        all_modes, unexpired_modes = CourseMode.all_and_unexpired_modes_for_courses([course_key])

        # Retrieve the first mode that matches the following criteria:
        #  * Unexpired
        #  * Price > 0
        #  * Not credit
        for mode in unexpired_modes[course_key]:
            if mode.min_price > 0 and not CourseMode.is_credit_mode(mode):
                return mode

        # Otherwise, find the first non credit expired paid mode
        for mode in all_modes[course_key]:
            if mode.min_price > 0 and not CourseMode.is_credit_mode(mode):
                return mode

        # Otherwise, return None and so the view knows to respond with a 404.
        return None
Exemplo n.º 9
0
    def test_refund_cert_callback_before_expiration(self):
        # If the expiration date has not yet passed on a verified mode, the user can be refunded
        many_days = datetime.timedelta(days=60)

        course = CourseFactory.create(org="refund_before_expiration", number="test", display_name="one")
        course_key = course.id
        course_mode = CourseMode(
            course_id=course_key,
            mode_slug="verified",
            mode_display_name="verified cert",
            min_price=self.cost,
            expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days),
        )
        course_mode.save()

        CourseEnrollment.enroll(self.user, course_key, "verified")
        cart = Order.get_cart_for_user(user=self.user)
        CertificateItem.add_to_order(cart, course_key, self.cost, "verified")
        cart.purchase()

        CourseEnrollment.unenroll(self.user, course_key)
        target_certs = CertificateItem.objects.filter(
            course_id=course_key, user_id=self.user, status="refunded", mode="verified"
        )
        self.assertTrue(target_certs[0])
        self.assertTrue(target_certs[0].refund_requested_time)
        self.assertEquals(target_certs[0].order.status, "refunded")
Exemplo n.º 10
0
    def get_verification_context(self, request, course):
        course_key = CourseKey.from_string(unicode(course.id))

        # Establish whether the course has a verified mode
        available_modes = CourseMode.modes_for_course_dict(unicode(course.id))
        has_verified_mode = CourseMode.has_verified_mode(available_modes)

        # Establish whether the user is already enrolled
        is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user.id, course_key)

        # Establish whether the verification deadline has already passed
        verification_deadline = VerifiedUpgradeDeadlineDate(course, request.user)
        deadline_has_passed = verification_deadline.deadline_has_passed()

        show_course_sock = has_verified_mode and not is_already_verified and not deadline_has_passed

        # Get the price of the course and format correctly
        course_price = get_cosmetic_verified_display_price(course)

        context = {
            'show_course_sock': show_course_sock,
            'course_price': course_price,
            'course_id': course.id
        }

        return context
Exemplo n.º 11
0
    def test_modes_for_course_expired(self):
        expired_mode, _status = self.create_mode('verified', 'Verified Certificate', 10)
        expired_mode.expiration_datetime = now() + timedelta(days=-1)
        expired_mode.save()
        modes = CourseMode.modes_for_course(self.course_key)
        self.assertEqual([CourseMode.DEFAULT_MODE], modes)

        mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
        self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices)
        modes = CourseMode.modes_for_course(self.course_key)
        self.assertEqual([mode1], modes)

        expiration_datetime = now() + timedelta(days=1)
        expired_mode.expiration_datetime = expiration_datetime
        expired_mode.save()
        expired_mode_value = Mode(
            u'verified',
            u'Verified Certificate',
            10,
            '',
            'usd',
            expiration_datetime,
            None,
            None,
            None
        )
        modes = CourseMode.modes_for_course(self.course_key)
        self.assertEqual([expired_mode_value, mode1], modes)

        modes = CourseMode.modes_for_course(CourseLocator('TestOrg', 'TestCourse', 'TestRun'))
        self.assertEqual([CourseMode.DEFAULT_MODE], modes)
Exemplo n.º 12
0
    def _get_expired_verified_and_paid_mode(self, course_key):  # pylint: disable=invalid-name
        """Retrieve expired verified mode and unexpired paid mode(with min_price>0) for a course.

        Arguments:
            course_key (CourseKey): The location of the course.

        Returns:
            Tuple of `(expired_verified_mode, unexpired_paid_mode)`.  If provided,
                `expired_verified_mode` is an *expired* verified mode for the course.
                If provided, `unexpired_paid_mode` is an *unexpired* paid(with min_price>0)
                mode for the course.  Either of these may be None.

        """
        # Retrieve all the modes at once to reduce the number of database queries
        all_modes, unexpired_modes = CourseMode.all_and_unexpired_modes_for_courses([course_key])

        # Unexpired paid modes
        unexpired_paid_modes = [mode for mode in unexpired_modes[course_key] if mode.min_price]
        if len(unexpired_paid_modes) > 1:
            # There is more than one paid mode defined,
            # so choose the first one.
            log.warn(
                u"More than one paid modes are defined for course '%s' choosing the first one %s",
                course_key, unexpired_paid_modes[0]
            )
        unexpired_paid_mode = unexpired_paid_modes[0] if unexpired_paid_modes else None

        # Find an unexpired verified mode
        verified_mode = CourseMode.verified_mode_for_course(course_key, modes=unexpired_modes[course_key])
        expired_verified_mode = None

        if verified_mode is None:
            expired_verified_mode = CourseMode.verified_mode_for_course(course_key, modes=all_modes[course_key])

        return (expired_verified_mode, unexpired_paid_mode)
Exemplo n.º 13
0
    def test_update_course_price_check(self):
        price = 200
        # course B
        course2 = CourseFactory.create(org="EDX", display_name="test_course", number="100")
        mode = CourseMode(
            course_id=course2.id.to_deprecated_string(),
            mode_slug="honor",
            mode_display_name="honor",
            min_price=30,
            currency="usd",
        )
        mode.save()
        # course A update
        CourseMode.objects.filter(course_id=self.course.id).update(min_price=price)

        set_course_price_url = reverse(
            "set_course_mode_price", kwargs={"course_id": self.course.id.to_deprecated_string()}
        )
        data = {"course_price": price, "currency": "usd"}
        response = self.client.post(set_course_price_url, data)
        self.assertTrue("CourseMode price updated successfully" in response.content)

        # Course A updated total amount should be visible in e-commerce page if the user is finance admin
        url = reverse("instructor_dashboard", kwargs={"course_id": self.course.id.to_deprecated_string()})
        response = self.client.get(url)

        self.assertTrue("Course Price: <span>$" + str(price) + "</span>" in response.content)
Exemplo n.º 14
0
    def get_verification_context(self, request, course):
        course_key = CourseKey.from_string(unicode(course.id))

        # Establish whether the course has a verified mode
        available_modes = CourseMode.modes_for_course_dict(unicode(course.id))
        has_verified_mode = CourseMode.has_verified_mode(available_modes)

        # Establish whether the user is already enrolled
        is_already_verified = CourseEnrollment.is_enrolled_as_verified(request.user, course_key)

        # Establish whether the verification deadline has already passed
        verification_deadline = VerifiedUpgradeDeadlineDate(course, request.user)
        deadline_has_passed = verification_deadline.deadline_has_passed()

        # If this proves its worth, we can internationalize and display for more than English speakers.
        show_course_sock = (
            has_verified_mode and not is_already_verified and
            not deadline_has_passed and get_language() == 'en'
        )

        # Get information about the upgrade
        course_price = get_cosmetic_verified_display_price(course)
        upgrade_url = EcommerceService().upgrade_url(request.user, course_key)

        context = {
            'show_course_sock': show_course_sock,
            'course_price': course_price,
            'course_id': course.id,
            'upgrade_url': upgrade_url,
        }

        return context
Exemplo n.º 15
0
    def test_course_is_professional_mode(self, mode):
        # check that tuple has professional mode

        course_mode, __ = self.create_mode(mode, 'course mode', 10)
        if mode in ['professional', 'no-id-professional']:
            self.assertTrue(CourseMode.is_professional_mode(course_mode.to_tuple()))
        else:
            self.assertFalse(CourseMode.is_professional_mode(course_mode.to_tuple()))
Exemplo n.º 16
0
    def post(self, request, *args, **kwargs):
        """
        Attempt to enroll the user.
        """
        user = request.user
        valid, course_key, error = self._is_data_valid(request)
        if not valid:
            return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)

        embargo_response = embargo_api.get_embargo_response(request, course_key, user)

        if embargo_response:
            return embargo_response

        # Don't do anything if an enrollment already exists
        course_id = unicode(course_key)
        enrollment = CourseEnrollment.get_enrollment(user, course_key)
        if enrollment and enrollment.is_active:
            msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
            return DetailResponse(msg, status=HTTP_409_CONFLICT)

        # Check to see if enrollment for this course is closed.
        course = courses.get_course(course_key)
        if CourseEnrollment.is_enrollment_closed(user, course):
            msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id)
            log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)

        # If there is no audit or honor course mode, this most likely
        # a Prof-Ed course. Return an error so that the JS redirects
        # to track selection.
        honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
        audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT)

        # Accept either honor or audit as an enrollment mode to
        # maintain backwards compatibility with existing courses
        default_enrollment_mode = audit_mode or honor_mode
        if default_enrollment_mode:
            msg = Messages.ENROLL_DIRECTLY.format(
                username=user.username,
                course_id=course_id
            )
            if not default_enrollment_mode.sku:
                # If there are no course modes with SKUs, return a different message.
                msg = Messages.NO_SKU_ENROLLED.format(
                    enrollment_mode=default_enrollment_mode.slug,
                    course_id=course_id,
                    username=user.username
                )
            log.info(msg)
            self._enroll(course_key, user, default_enrollment_mode.slug)
            self._handle_marketing_opt_in(request, course_key, user)
            return DetailResponse(msg)
        else:
            msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(course_id=course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
Exemplo n.º 17
0
 def _create_and_purchase_verified(self, student, course_id):
     """ Creates a verified mode for the course and purchases it for the student. """
     course_mode = CourseMode(
         course_id=course_id, mode_slug="verified", mode_display_name="verified cert", min_price=50
     )
     course_mode.save()
     # When there is no expiration date on a verified mode, the user can always get a refund
     cart = Order.get_cart_for_user(user=student)
     CertificateItem.add_to_order(cart, course_id, 50, "verified")
     cart.purchase()
Exemplo n.º 18
0
    def test_course_has_professional_mode(self, mode):
        # check the professional mode.

        self.create_mode(mode, 'course mode', 10)
        modes_dict = CourseMode.modes_for_course_dict(self.course_key)

        if mode in ['professional', 'no-id-professional']:
            self.assertTrue(CourseMode.has_professional_mode(modes_dict))
        else:
            self.assertFalse(CourseMode.has_professional_mode(modes_dict))
Exemplo n.º 19
0
 def setUp(self):
     self.user = UserFactory.create(username="******", password="******")
     self.client.login(username="******", password="******")
     self.course_id = 'Robot/999/Test_Course'
     CourseFactory.create(org='Robot', number='999', display_name='Test Course')
     verified_mode = CourseMode(course_id=self.course_id,
                                mode_slug="verified",
                                mode_display_name="Verified Certificate",
                                min_price=50)
     verified_mode.save()
def _register_course_home_messages(request, course, user_access, course_start_data):
    """
    Register messages to be shown in the course home content page.
    """
    allow_anonymous = allow_public_access(course, [COURSE_VISIBILITY_PUBLIC])

    if user_access['is_anonymous'] and not allow_anonymous:
        sign_in_or_register_text = (_(u'{sign_in_link} or {register_link} and then enroll in this course.')
                                    if not CourseMode.is_masters_only(course.id)
                                    else _(u'{sign_in_link} or {register_link}.'))
        CourseHomeMessages.register_info_message(
            request,
            Text(sign_in_or_register_text).format(
                sign_in_link=HTML(u'<a href="/login?next={current_url}">{sign_in_label}</a>').format(
                    sign_in_label=_('Sign in'),
                    current_url=urlquote_plus(request.path),
                ),
                register_link=HTML(u'<a href="/register?next={current_url}">{register_label}</a>').format(
                    register_label=_('register'),
                    current_url=urlquote_plus(request.path),
                )
            ),
            title=Text(_('You must be enrolled in the course to see course content.'))
        )
    if not user_access['is_anonymous'] and not user_access['is_staff'] and \
            not user_access['is_enrolled']:

        title = Text(_(u'Welcome to {course_display_name}')).format(
            course_display_name=course.display_name
        )

        if CourseMode.is_masters_only(course.id):
            # if a course is a Master's only course, we will not offer user ability to self-enroll
            CourseHomeMessages.register_info_message(
                request,
                Text(_('You must be enrolled in the course to see course content. '
                       'Please contact your degree administrator or edX Support if you have questions.')),
                title=title
            )
        elif not course.invitation_only:
            CourseHomeMessages.register_info_message(
                request,
                Text(_(
                    u'{open_enroll_link}Enroll now{close_enroll_link} to access the full course.'
                )).format(
                    open_enroll_link=HTML('<button class="enroll-btn btn-link">'),
                    close_enroll_link=HTML('</button>')
                ),
                title=title
            )
        else:
            CourseHomeMessages.register_info_message(
                request,
                Text(_('You must be enrolled in the course to see course content.')),
            )
Exemplo n.º 21
0
 def _set_ecomm(self, course):
     """
     Helper method to turn on ecommerce on the course
     """
     course_mode = CourseMode(
         course_id=course.id,
         mode_slug=CourseMode.DEFAULT_MODE_SLUG,
         mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
         min_price=10,
     )
     course_mode.save()
Exemplo n.º 22
0
    def test_modes_for_course_expired(self):
        expired_mode, _status = self.create_mode('verified', 'Verified Certificate')
        expired_mode.expiration_date = datetime.now(pytz.UTC) + timedelta(days=-1)
        expired_mode.save()
        modes = CourseMode.modes_for_course(self.course_id)
        self.assertEqual([CourseMode.DEFAULT_MODE], modes)

        mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd')
        self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices)
        modes = CourseMode.modes_for_course(self.course_id)
        self.assertEqual([mode1], modes)
Exemplo n.º 23
0
 def setUp(self):
     self.user = UserFactory.create(username="******", password="******")
     self.client.login(username="******", password="******")
     self.course_key = SlashSeparatedCourseKey('Robot', '999', 'Test_Course')
     CourseFactory.create(org='Robot', number='999', display_name='Test Course')
     verified_mode = CourseMode(course_id=self.course_key,
                                mode_slug="verified",
                                mode_display_name="Verified Certificate",
                                min_price=50,
                                suggested_prices="50.0,100.0")
     verified_mode.save()
Exemplo n.º 24
0
 def _set_ecomm(self, course):
     """
     Helper method to turn on ecommerce on the course
     """
     course_mode = CourseMode(
         course_id=course.id,
         mode_slug="honor",
         mode_display_name="honor cert",
         min_price=10,
     )
     course_mode.save()
Exemplo n.º 25
0
def change_enrollment(strategy, user=None, *args, **kwargs):
    """Enroll a user in a course.

    If a user entered the authentication flow when trying to enroll
    in a course, then attempt to enroll the user.
    We will try to do this if the pipeline was started with the
    querystring param `enroll_course_id`.

    In the following cases, we can't enroll the user:
        * The course does not have an honor mode.
        * The course has an honor mode with a minimum price.
        * The course is not yet open for enrollment.
        * The course does not exist.

    If we can't enroll the user now, then skip this step.
    For paid courses, users will be redirected to the payment flow
    upon completion of the authentication pipeline
    (configured using the ?next parameter to the third party auth login url).

    """
    enroll_course_id = strategy.session_get('enroll_course_id')
    if enroll_course_id:
        course_id = CourseKey.from_string(enroll_course_id)
        modes = CourseMode.modes_for_course_dict(course_id)
        # If the email opt in parameter is found, set the preference.
        email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
        if email_opt_in:
            opt_in = email_opt_in.lower() == 'true'
            profile.update_email_opt_in(user.username, course_id.org, opt_in)
        if CourseMode.can_auto_enroll(course_id, modes_dict=modes):
            try:
                CourseEnrollment.enroll(user, course_id, check_access=True)
            except CourseEnrollmentException:
                pass
            except Exception as ex:
                logger.exception(ex)

        # Handle white-label courses as a special case
        # If a course is white-label, we should add it to the shopping cart.
        elif CourseMode.is_white_label(course_id, modes_dict=modes):
            try:
                cart = Order.get_cart_for_user(user)
                PaidCourseRegistration.add_to_order(cart, course_id)
            except (
                CourseDoesNotExistException,
                ItemAlreadyInCartException,
                AlreadyEnrolledInCourseException
            ):
                pass
            # It's more important to complete login than to
            # ensure that the course was added to the shopping cart.
            # Log errors, but don't stop the authentication pipeline.
            except Exception as ex:
                logger.exception(ex)
Exemplo n.º 26
0
def has_course_goal_permission(request, course_id, user_access):
    """
    Returns whether the user can access the course goal functionality.

    Only authenticated users that are enrolled in a verifiable course
    can use this feature.
    """
    course_key = CourseKey.from_string(course_id)
    has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(course_key))
    return user_access['is_enrolled'] and has_verified_mode and ENABLE_COURSE_GOALS.is_enabled(course_key) \
        and settings.FEATURES.get('ENABLE_COURSE_GOALS')
Exemplo n.º 27
0
    def setUp(self):
        super(TestECommerceDashboardViews, self).setUp()

        # Create instructor account
        self.instructor = AdminFactory.create()
        self.client.login(username=self.instructor.username, password="******")
        mode = CourseMode(
            course_id=text_type(self.course.id), mode_slug='honor',
            mode_display_name='honor', min_price=10, currency='usd'
        )
        mode.save()
        CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
Exemplo n.º 28
0
def create_order(request):
    """
    This endpoint is named 'create_order' for backward compatibility, but its
    actual use is to add a single product to the user's cart and request
    immediate checkout.
    """
    course_id = request.POST['course_id']
    course_id = CourseKey.from_string(course_id)
    donation_for_course = request.session.get('donation_for_course', {})
    contribution = request.POST.get("contribution", donation_for_course.get(unicode(course_id), 0))
    try:
        amount = decimal.Decimal(contribution).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
    except decimal.InvalidOperation:
        return HttpResponseBadRequest(_("Selected price is not valid number."))

    current_mode = None
    paid_modes = CourseMode.paid_modes_for_course(course_id)
    # Check if there are more than 1 paid(mode with min_price>0 e.g verified/professional/no-id-professional) modes
    # for course exist then choose the first one
    if paid_modes:
        if len(paid_modes) > 1:
            log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id)
        current_mode = paid_modes[0]

    # Make sure this course has a paid mode
    if not current_mode:
        log.warn(u"Create order requested for course '%s' without a paid mode.", course_id)
        return HttpResponseBadRequest(_("This course doesn't support paid certificates"))

    if CourseMode.is_professional_mode(current_mode):
        amount = current_mode.min_price

    if amount < current_mode.min_price:
        return HttpResponseBadRequest(_("No selected price or selected price is below minimum."))

    if current_mode.sku:
        # if request.POST doesn't contain 'processor' then the service's default payment processor will be used.
        payment_data = checkout_with_ecommerce_service(
            request.user,
            course_id,
            current_mode,
            request.POST.get('processor')
        )
    else:
        payment_data = checkout_with_shoppingcart(request, request.user, course_id, current_mode, amount)

    if 'processor' not in request.POST:
        # (XCOM-214) To be removed after release.
        # the absence of this key in the POST payload indicates that the request was initiated from
        # a stale js client, which expects a response containing only the 'payment_form_data' part of
        # the payment data result.
        payment_data = payment_data['payment_form_data']
    return HttpResponse(json.dumps(payment_data), content_type="application/json")
Exemplo n.º 29
0
    def test_nodes_for_course_single(self):
        """
        Find the modes for a course with only one mode
        """

        self.create_mode("verified", "Verified Certificate")
        modes = CourseMode.modes_for_course(self.course_key)
        mode = Mode(u"verified", u"Verified Certificate", 0, "", "usd", None, None)
        self.assertEqual([mode], modes)

        modes_dict = CourseMode.modes_for_course_dict(self.course_key)
        self.assertEqual(modes_dict["verified"], mode)
        self.assertEqual(CourseMode.mode_for_course(self.course_key, "verified"), mode)
Exemplo n.º 30
0
    def test_verified_mode_for_course(self):
        self.create_mode('verified', 'Verified Certificate')

        mode = CourseMode.verified_mode_for_course(self.course_key)

        self.assertEqual(mode.slug, 'verified')

        # verify that the professional mode is preferred
        self.create_mode('professional', 'Professional Education Verified Certificate')

        mode = CourseMode.verified_mode_for_course(self.course_key)

        self.assertEqual(mode.slug, 'professional')
Exemplo n.º 31
0
def certificate_status(generated_certificate):
    """
    This returns a dictionary with a key for status, and other information.
    The status is one of the following:

    unavailable  - No entry for this student--if they are actually in
                   the course, they probably have not been graded for
                   certificate generation yet.
    generating   - A request has been made to generate a certificate,
                   but it has not been generated yet.
    deleting     - A request has been made to delete a certificate.

    deleted      - The certificate has been deleted.
    downloadable - The certificate is available for download.
    notpassing   - The student was graded but is not passing
    restricted   - The student is on the restricted embargo list and
                   should not be issued a certificate. This will
                   be set if allow_certificate is set to False in
                   the userprofile table
    unverified   - The student is in verified enrollment track and
                   the student did not have their identity verified,
                   even though they should be eligible for the cert otherwise.

    If the status is "downloadable", the dictionary also contains
    "download_url".

    If the student has been graded, the dictionary also contains their
    grade for the course with the key "grade".
    """
    # Import here instead of top of file since this module gets imported before
    # the course_modes app is loaded, resulting in a Django deprecation warning.
    from course_modes.models import CourseMode

    if generated_certificate:
        cert_status = {
            'status': generated_certificate.status,
            'mode': generated_certificate.mode,
            'uuid': generated_certificate.verify_uuid,
        }
        if generated_certificate.grade:
            cert_status['grade'] = generated_certificate.grade

        if generated_certificate.mode == 'audit':
            course_mode_slugs = [
                mode.slug for mode in CourseMode.modes_for_course(
                    generated_certificate.course_id)
            ]
            # Short term fix to make sure old audit users with certs still see their certs
            # only do this if there if no honor mode
            if 'honor' not in course_mode_slugs:
                cert_status['status'] = CertificateStatuses.auditing
                return cert_status

        if generated_certificate.status == CertificateStatuses.downloadable:
            cert_status['download_url'] = generated_certificate.download_url

        return cert_status
    else:
        return {
            'status': CertificateStatuses.unavailable,
            'mode': GeneratedCertificate.MODES.honor,
            'uuid': None
        }
Exemplo n.º 32
0
    def post(self, request, *args, **kwargs):
        """
        Attempt to create the basket and enroll the user.
        """
        user = request.user
        valid, course_key, error = self._is_data_valid(request)
        if not valid:
            return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)

        embargo_response = embargo_api.get_embargo_response(
            request, course_key, user)

        if embargo_response:
            return embargo_response

        # Don't do anything if an enrollment already exists
        course_id = unicode(course_key)
        enrollment = CourseEnrollment.get_enrollment(user, course_key)
        if enrollment and enrollment.is_active:
            msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id,
                                                    username=user.username)
            return DetailResponse(msg, status=HTTP_409_CONFLICT)

        # Check to see if enrollment for this course is closed.
        course = courses.get_course(course_key)
        if CourseEnrollment.is_enrollment_closed(user, course):
            msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id)
            log.info(u'Unable to enroll user %s in closed course %s.', user.id,
                     course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)

        # If there is no audit or honor course mode, this most likely
        # a Prof-Ed course. Return an error so that the JS redirects
        # to track selection.
        honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
        audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT)

        # Accept either honor or audit as an enrollment mode to
        # maintain backwards compatibility with existing courses
        default_enrollment_mode = audit_mode or honor_mode

        if not default_enrollment_mode:
            msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(
                course_id=course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
        elif default_enrollment_mode and not default_enrollment_mode.sku:
            # If there are no course modes with SKUs, enroll the user without contacting the external API.
            msg = Messages.NO_SKU_ENROLLED.format(
                enrollment_mode=default_enrollment_mode.slug,
                course_id=course_id,
                username=user.username)
            log.info(msg)
            self._enroll(course_key, user, default_enrollment_mode.slug)
            self._handle_marketing_opt_in(request, course_key, user)
            return DetailResponse(msg)

        # Setup the API

        try:
            api_session = requests.Session()
            api = ecommerce_api_client(user, session=api_session)
        except ValueError:
            self._enroll(course_key, user)
            msg = Messages.NO_ECOM_API.format(username=user.username,
                                              course_id=unicode(course_key))
            log.debug(msg)
            return DetailResponse(msg)

        response = None

        # Make the API call
        try:
            # Pass along Sailthru campaign id
            campaign_cookie = request.COOKIES.get(SAILTHRU_CAMPAIGN_COOKIE)
            if campaign_cookie:
                cookie = {SAILTHRU_CAMPAIGN_COOKIE: campaign_cookie}
                if api_session.cookies:
                    requests.utils.add_dict_to_cookiejar(
                        api_session.cookies, cookie)
                else:
                    api_session.cookies = requests.utils.cookiejar_from_dict(
                        cookie)

            response_data = api.baskets.post({
                'products': [{
                    'sku': default_enrollment_mode.sku
                }],
                'checkout':
                True,
            })

            payment_data = response_data["payment_data"]
            if payment_data:
                # Pass data to the client to begin the payment flow.
                response = JsonResponse(payment_data)
            elif response_data['order']:
                # The order was completed immediately because there is no charge.
                msg = Messages.ORDER_COMPLETED.format(
                    order_number=response_data['order']['number'])
                log.debug(msg)
                response = DetailResponse(msg)
            else:
                msg = u'Unexpected response from basket endpoint.'
                log.error(
                    msg +
                    u' Could not enroll user %(username)s in course %(course_id)s.',
                    {
                        'username': user.id,
                        'course_id': course_id
                    },
                )
                raise InvalidResponseError(msg)
        except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
            log.exception(ex.message)
            return InternalRequestErrorResponse(ex.message)
        finally:
            audit_log('checkout_requested',
                      course_id=course_id,
                      mode=default_enrollment_mode.slug,
                      processor_name=None,
                      user_id=user.id)

        self._handle_marketing_opt_in(request, course_key, user)
        return response
Exemplo n.º 33
0
    def add_cert(self,
                 student,
                 course_id,
                 course=None,
                 forced_grade=None,
                 template_file=None,
                 generate_pdf=True):
        """
        Request a new certificate for a student.

        Arguments:
          student   - User.object
          course_id - courseenrollment.course_id (CourseKey)
          forced_grade - a string indicating a grade parameter to pass with
                         the certificate request. If this is given, grading
                         will be skipped.
          generate_pdf - Boolean should a message be sent in queue to generate certificate PDF

        Will change the certificate status to 'generating' or
        `downloadable` in case of web view certificates.

        The course must not be a CCX.

        Certificate must be in the 'unavailable', 'error',
        'deleted' or 'generating' state.

        If a student has a passing grade or is in the whitelist
        table for the course a request will be made for a new cert.

        If a student has allow_certificate set to False in the
        userprofile table the status will change to 'restricted'

        If a student does not have a passing grade the status
        will change to status.notpassing

        Returns the newly created certificate instance
        """

        if hasattr(course_id, 'ccx'):
            LOGGER.warning(
                (u"Cannot create certificate generation task for user %s "
                 u"in the course '%s'; "
                 u"certificates are not allowed for CCX courses."), student.id,
                six.text_type(course_id))
            return None

        valid_statuses = [
            status.generating,
            status.unavailable,
            status.deleted,
            status.error,
            status.notpassing,
            status.downloadable,
            status.auditing,
            status.audit_passing,
            status.audit_notpassing,
            status.unverified,
        ]

        cert_status_dict = certificate_status_for_student(student, course_id)
        cert_status = cert_status_dict.get('status')
        download_url = cert_status_dict.get('download_url')
        cert = None
        if download_url:
            self._log_pdf_cert_generation_discontinued_warning(
                student.id, course_id, cert_status, download_url)
            return None

        if cert_status not in valid_statuses:
            LOGGER.warning(
                (u"Cannot create certificate generation task for user %s "
                 u"in the course '%s'; "
                 u"the certificate status '%s' is not one of %s."), student.id,
                six.text_type(course_id), cert_status,
                six.text_type(valid_statuses))
            return None

        # The caller can optionally pass a course in to avoid
        # re-fetching it from Mongo. If they have not provided one,
        # get it from the modulestore.
        if course is None:
            course = modulestore().get_course(course_id, depth=0)

        profile = UserProfile.objects.get(user=student)
        profile_name = profile.name

        # Needed for access control in grading.
        self.request.user = student
        self.request.session = {}

        is_whitelisted = self.whitelist.filter(user=student,
                                               course_id=course_id,
                                               whitelist=True).exists()
        course_grade = CourseGradeFactory().read(student, course)
        enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(
            student, course_id)
        mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
        user_is_verified = IDVerificationService.user_is_verified(student)
        cert_mode = enrollment_mode
        is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(
            enrollment_mode)
        unverified = False
        # For credit mode generate verified certificate
        if cert_mode in (CourseMode.CREDIT_MODE, CourseMode.MASTERS):
            cert_mode = CourseMode.VERIFIED

        if template_file is not None:
            template_pdf = template_file
        elif mode_is_verified and user_is_verified:
            template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(
                id=course_id)
        elif mode_is_verified and not user_is_verified:
            template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                id=course_id)
            if CourseMode.mode_for_course(course_id, CourseMode.HONOR):
                cert_mode = GeneratedCertificate.MODES.honor
            else:
                unverified = True
        else:
            # honor code and audit students
            template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(
                id=course_id)

        LOGGER.info((
            u"Certificate generated for student %s in the course: %s with template: %s. "
            u"given template: %s, "
            u"user is verified: %s, "
            u"mode is verified: %s,"
            u"generate_pdf is: %s"), student.username,
                    six.text_type(course_id), template_pdf, template_file,
                    user_is_verified, mode_is_verified, generate_pdf)

        cert, created = GeneratedCertificate.objects.get_or_create(
            user=student, course_id=course_id)

        cert.mode = cert_mode
        cert.user = student
        cert.grade = course_grade.percent
        cert.course_id = course_id
        cert.name = profile_name
        cert.download_url = ''

        # Strip HTML from grade range label
        grade_contents = forced_grade or course_grade.letter_grade
        try:
            grade_contents = lxml.html.fromstring(
                grade_contents).text_content()
            passing = True
        except (TypeError, XMLSyntaxError, ParserError) as exc:
            LOGGER.info((u"Could not retrieve grade for student %s "
                         u"in the course '%s' "
                         u"because an exception occurred while parsing the "
                         u"grade contents '%s' as HTML. "
                         u"The exception was: '%s'"), student.id,
                        six.text_type(course_id), grade_contents,
                        six.text_type(exc))

            # Log if the student is whitelisted
            if is_whitelisted:
                LOGGER.info(u"Student %s is whitelisted in '%s'", student.id,
                            six.text_type(course_id))
                passing = True
            else:
                passing = False

        # If this user's enrollment is not eligible to receive a
        # certificate, mark it as such for reporting and
        # analytics. Only do this if the certificate is new, or
        # already marked as ineligible -- we don't want to mark
        # existing audit certs as ineligible.
        cutoff = settings.AUDIT_CERT_CUTOFF_DATE
        if (cutoff and cert.created_date >= cutoff
            ) and not is_eligible_for_certificate:
            cert.status = status.audit_passing if passing else status.audit_notpassing
            cert.save()
            LOGGER.info(
                u"Student %s with enrollment mode %s is not eligible for a certificate.",
                student.id, enrollment_mode)
            return cert
        # If they are not passing, short-circuit and don't generate cert
        elif not passing:
            cert.status = status.notpassing
            cert.save()

            LOGGER.info(
                (u"Student %s does not have a grade for '%s', "
                 u"so their certificate status has been set to '%s'. "
                 u"No certificate generation task was sent to the XQueue."),
                student.id, six.text_type(course_id), cert.status)
            return cert

        # Check to see whether the student is on the the embargoed
        # country restricted list. If so, they should not receive a
        # certificate -- set their status to restricted and log it.
        if self.restricted.filter(user=student).exists():
            cert.status = status.restricted
            cert.save()

            LOGGER.info(
                (u"Student %s is in the embargoed country restricted "
                 u"list, so their certificate status has been set to '%s' "
                 u"for the course '%s'. "
                 u"No certificate generation task was sent to the XQueue."),
                student.id, cert.status, six.text_type(course_id))
            return cert

        if unverified:
            cert.status = status.unverified
            cert.save()
            LOGGER.info(
                (u"User %s has a verified enrollment in course %s "
                 u"but is missing ID verification. "
                 u"Certificate status has been set to unverified"),
                student.id,
                six.text_type(course_id),
            )
            return cert

        # Finally, generate the certificate and send it off.
        return self._generate_cert(cert, course, student, grade_contents,
                                   template_pdf, generate_pdf)
Exemplo n.º 34
0
def student_dashboard(request):
    """
    Provides the LMS dashboard view

    TODO: This is lms specific and does not belong in common code.

    Arguments:
        request: The request object.

    Returns:
        The dashboard response.

    """
    user = request.user
    if not UserProfile.objects.filter(user=user).exists():
        return redirect(reverse('account_settings'))

    platform_name = configuration_helpers.get_value("platform_name",
                                                    settings.PLATFORM_NAME)

    enable_verified_certificates = configuration_helpers.get_value(
        'ENABLE_VERIFIED_CERTIFICATES',
        settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'))
    display_course_modes_on_dashboard = configuration_helpers.get_value(
        'DISPLAY_COURSE_MODES_ON_DASHBOARD',
        settings.FEATURES.get('DISPLAY_COURSE_MODES_ON_DASHBOARD', True))
    activation_email_support_link = configuration_helpers.get_value(
        'ACTIVATION_EMAIL_SUPPORT_LINK',
        settings.ACTIVATION_EMAIL_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK
    hide_dashboard_courses_until_activated = configuration_helpers.get_value(
        'HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED',
        settings.FEATURES.get('HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', False))
    empty_dashboard_message = configuration_helpers.get_value(
        'EMPTY_DASHBOARD_MESSAGE', None)

    # Get the org whitelist or the org blacklist for the current site
    site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site(
    )
    course_enrollments = list(
        get_course_enrollments(user, site_org_whitelist, site_org_blacklist))

    # Get the entitlements for the user and a mapping to all available sessions for that entitlement
    # If an entitlement has no available sessions, pass through a mock course overview object
    (course_entitlements, course_entitlement_available_sessions,
     unfulfilled_entitlement_pseudo_sessions
     ) = get_filtered_course_entitlements(user, site_org_whitelist,
                                          site_org_blacklist)

    # Record how many courses there are so that we can get a better
    # understanding of usage patterns on prod.
    monitoring_utils.accumulate('num_courses', len(course_enrollments))

    # Sort the enrollment pairs by the enrollment date
    course_enrollments.sort(key=lambda x: x.created, reverse=True)

    # Retrieve the course modes for each course
    enrolled_course_ids = [
        enrollment.course_id for enrollment in course_enrollments
    ]
    __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(
        enrolled_course_ids)
    course_modes_by_course = {
        course_id: {mode.slug: mode
                    for mode in modes}
        for course_id, modes in iteritems(unexpired_course_modes)
    }

    # Check to see if the student has recently enrolled in a course.
    # If so, display a notification message confirming the enrollment.
    enrollment_message = _create_recent_enrollment_message(
        course_enrollments, course_modes_by_course)
    course_optouts = Optout.objects.filter(user=user).values_list('course_id',
                                                                  flat=True)

    # Display activation message
    activate_account_message = ''
    if not user.is_active:
        activate_account_message = Text(
            _("Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. "
              "If you need help, contact {link_start}{platform_name} Support{link_end}."
              )
        ).format(
            platform_name=platform_name,
            email_start=HTML("<strong>"),
            email_end=HTML("</strong>"),
            email=user.email,
            link_start=HTML(
                "<a target='_blank' href='{activation_email_support_link}'>").
            format(
                activation_email_support_link=activation_email_support_link, ),
            link_end=HTML("</a>"),
        )

    enterprise_message = get_dashboard_consent_notification(
        request, user, course_enrollments)

    # Disable lookup of Enterprise consent_required_course due to ENT-727
    # Will re-enable after fixing WL-1315
    consent_required_courses = set()
    enterprise_customer_name = None

    # Account activation message
    account_activation_messages = [
        message for message in messages.get_messages(request)
        if 'account-activation' in message.tags
    ]

    # Global staff can see what courses encountered an error on their dashboard
    staff_access = False
    errored_courses = {}
    if has_access(user, 'staff', 'global'):
        # Show any courses that encountered an error on load
        staff_access = True
        errored_courses = modulestore().get_errored_courses()

    show_courseware_links_for = {
        enrollment.course_id: has_access(request.user, 'load',
                                         enrollment.course_overview)
        for enrollment in course_enrollments
    }

    # Find programs associated with course runs being displayed. This information
    # is passed in the template context to allow rendering of program-related
    # information on the dashboard.
    meter = ProgramProgressMeter(request.site,
                                 user,
                                 enrollments=course_enrollments)
    ecommerce_service = EcommerceService()
    inverted_programs = meter.invert_programs()

    urls, programs_data = {}, {}
    bundles_on_dashboard_flag = WaffleFlag(
        WaffleFlagNamespace(name=u'student.experiments'),
        u'bundles_on_dashboard')

    # TODO: Delete this code and the relevant HTML code after testing LEARNER-3072 is complete
    if bundles_on_dashboard_flag.is_enabled(
    ) and inverted_programs and inverted_programs.items():
        if len(course_enrollments) < 4:
            for program in inverted_programs.values():
                try:
                    program_uuid = program[0]['uuid']
                    program_data = get_programs(request.site,
                                                uuid=program_uuid)
                    program_data = ProgramDataExtender(program_data,
                                                       request.user).extend()
                    skus = program_data.get('skus')
                    checkout_page_url = ecommerce_service.get_checkout_page_url(
                        *skus)
                    program_data[
                        'completeProgramURL'] = checkout_page_url + '&bundle=' + program_data.get(
                            'uuid')
                    programs_data[program_uuid] = program_data
                except:  # pylint: disable=bare-except
                    pass

    # Construct a dictionary of course mode information
    # used to render the course list.  We re-use the course modes dict
    # we loaded earlier to avoid hitting the database.
    course_mode_info = {
        enrollment.course_id: complete_course_mode_info(
            enrollment.course_id,
            enrollment,
            modes=course_modes_by_course[enrollment.course_id])
        for enrollment in course_enrollments
    }

    # Determine the per-course verification status
    # This is a dictionary in which the keys are course locators
    # and the values are one of:
    #
    # VERIFY_STATUS_NEED_TO_VERIFY
    # VERIFY_STATUS_SUBMITTED
    # VERIFY_STATUS_APPROVED
    # VERIFY_STATUS_MISSED_DEADLINE
    #
    # Each of which correspond to a particular message to display
    # next to the course on the dashboard.
    #
    # If a course is not included in this dictionary,
    # there is no verification messaging to display.
    verify_status_by_course = check_verify_status_by_course(
        user, course_enrollments)
    cert_statuses = {
        enrollment.course_id: cert_info(request.user,
                                        enrollment.course_overview)
        for enrollment in course_enrollments
    }

    # only show email settings for Mongo course and when bulk email is turned on
    show_email_settings_for = frozenset(
        enrollment.course_id for enrollment in course_enrollments
        if (BulkEmailFlag.feature_enabled(enrollment.course_id)))

    # Verification Attempts
    # Used to generate the "you must reverify for course x" banner
    verification_status = IDVerificationService.user_status(user)
    verification_errors = get_verification_error_reasons_for_display(
        verification_status['error'])

    # Gets data for midcourse reverifications, if any are necessary or have failed
    statuses = ["approved", "denied", "pending", "must_reverify"]
    reverifications = reverification_info(statuses)

    block_courses = frozenset(
        enrollment.course_id for enrollment in course_enrollments
        if is_course_blocked(
            request,
            CourseRegistrationCode.objects.filter(
                course_id=enrollment.course_id,
                registrationcoderedemption__redeemed_by=request.user),
            enrollment.course_id))

    enrolled_courses_either_paid = frozenset(
        enrollment.course_id for enrollment in course_enrollments
        if enrollment.is_paid_course())

    # If there are *any* denied reverifications that have not been toggled off,
    # we'll display the banner
    denied_banner = any(item.display for item in reverifications["denied"])

    # Populate the Order History for the side-bar.
    order_history_list = order_history(user,
                                       course_org_filter=site_org_whitelist,
                                       org_filter_out_set=site_org_blacklist)

    # get list of courses having pre-requisites yet to be completed
    courses_having_prerequisites = frozenset(
        enrollment.course_id for enrollment in course_enrollments
        if enrollment.course_overview.pre_requisite_courses)
    courses_requirements_not_met = get_pre_requisite_courses_not_completed(
        user, courses_having_prerequisites)

    if 'notlive' in request.GET:
        redirect_message = _(
            "The course you are looking for does not start until {date}."
        ).format(date=request.GET['notlive'])
    elif 'course_closed' in request.GET:
        redirect_message = _(
            "The course you are looking for is closed for enrollment as of {date}."
        ).format(date=request.GET['course_closed'])
    elif 'access_response_error' in request.GET:
        # This can be populated in a generalized way with fields from access response errors
        redirect_message = request.GET['access_response_error']
    else:
        redirect_message = ''

    valid_verification_statuses = [
        'approved', 'must_reverify', 'pending', 'expired'
    ]
    display_sidebar_on_dashboard = (
        len(order_history_list)
        or (verification_status['status'] in valid_verification_statuses
            and verification_status['should_display']))

    # Filter out any course enrollment course cards that are associated with fulfilled entitlements
    for entitlement in [
            e for e in course_entitlements
            if e.enrollment_course_run is not None
    ]:
        course_enrollments = [
            enr for enr in course_enrollments
            if entitlement.enrollment_course_run.course_id != enr.course_id
        ]

    context = {
        'urls':
        urls,
        'programs_data':
        programs_data,
        'enterprise_message':
        enterprise_message,
        'consent_required_courses':
        consent_required_courses,
        'enterprise_customer_name':
        enterprise_customer_name,
        'enrollment_message':
        enrollment_message,
        'redirect_message':
        redirect_message,
        'account_activation_messages':
        account_activation_messages,
        'activate_account_message':
        activate_account_message,
        'course_enrollments':
        course_enrollments,
        'course_entitlements':
        course_entitlements,
        'course_entitlement_available_sessions':
        course_entitlement_available_sessions,
        'unfulfilled_entitlement_pseudo_sessions':
        unfulfilled_entitlement_pseudo_sessions,
        'course_optouts':
        course_optouts,
        'staff_access':
        staff_access,
        'errored_courses':
        errored_courses,
        'show_courseware_links_for':
        show_courseware_links_for,
        'all_course_modes':
        course_mode_info,
        'cert_statuses':
        cert_statuses,
        'credit_statuses':
        _credit_statuses(user, course_enrollments),
        'show_email_settings_for':
        show_email_settings_for,
        'reverifications':
        reverifications,
        'verification_display':
        verification_status['should_display'],
        'verification_status':
        verification_status['status'],
        'verification_status_by_course':
        verify_status_by_course,
        'verification_errors':
        verification_errors,
        'block_courses':
        block_courses,
        'denied_banner':
        denied_banner,
        'billing_email':
        settings.PAYMENT_SUPPORT_EMAIL,
        'user':
        user,
        'logout_url':
        reverse('logout'),
        'platform_name':
        platform_name,
        'enrolled_courses_either_paid':
        enrolled_courses_either_paid,
        'provider_states': [],
        'order_history_list':
        order_history_list,
        'courses_requirements_not_met':
        courses_requirements_not_met,
        'nav_hidden':
        True,
        'inverted_programs':
        inverted_programs,
        'show_program_listing':
        ProgramsApiConfig.is_enabled(),
        'show_journal_listing':
        journals_enabled(),  # TODO: Dashboard Plugin required
        'show_dashboard_tabs':
        True,
        'disable_courseware_js':
        True,
        'display_course_modes_on_dashboard':
        enable_verified_certificates and display_course_modes_on_dashboard,
        'display_sidebar_on_dashboard':
        display_sidebar_on_dashboard,
        'display_sidebar_account_activation_message':
        not (user.is_active or hide_dashboard_courses_until_activated),
        'display_dashboard_courses':
        (user.is_active or not hide_dashboard_courses_until_activated),
        'empty_dashboard_message':
        empty_dashboard_message,
    }

    if ecommerce_service.is_enabled(request.user):
        context.update({
            'use_ecommerce_payment_flow':
            True,
            'ecommerce_payment_page':
            ecommerce_service.payment_page_url(),
        })

    # Gather urls for course card resume buttons.
    resume_button_urls = ['' for entitlement in course_entitlements]
    for url in _get_urls_for_resume_buttons(user, course_enrollments):
        resume_button_urls.append(url)
    # There must be enough urls for dashboard.html. Template creates course
    # cards for "enrollments + entitlements".
    context.update({'resume_button_urls': resume_button_urls})

    response = render_to_response('dashboard.html', context)
    set_deprecated_user_info_cookie(response, request, user)  # pylint: disable=protected-access
    return response
Exemplo n.º 35
0
    def get(self, request, course_id, error=None):
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path)
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)
        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to non-id-professional.
        has_enrolled_professional = (
            CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(
                modes) and not has_enrolled_professional:
            purchase_workflow = request.GET.get("purchase_workflow", "single")
            verify_url = reverse('verify_student_start_flow',
                                 kwargs={'course_id': unicode(course_key)})
            redirect_url = "{url}?purchase_workflow={workflow}".format(
                url=verify_url, workflow=purchase_workflow)
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(
                    CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(
                        CourseMode.PROFESSIONAL)
                if purchase_workflow == "single" and professional_mode.sku:
                    redirect_url = ecommerce_service.checkout_page_url(
                        professional_mode.sku)
                if purchase_workflow == "bulk" and professional_mode.bulk_sku:
                    redirect_url = ecommerce_service.checkout_page_url(
                        professional_mode.bulk_sku)
            return redirect(redirect_url)

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  Send the user to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return redirect(reverse('dashboard'))

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES +
                          [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            return redirect(reverse('dashboard'))

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(unicode(course_key), None)

        course = modulestore().get_course(course_key)
        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end,
                                                  'short',
                                                  locale=locale)
            params = urllib.urlencode({'course_closed': enrollment_end_date})
            return redirect('{0}?{1}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for univerity credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode)
            for mode in CourseMode.modes_for_course(course_key,
                                                    only_selectable=False))
        course_id = course_key.to_deprecated_string()
        context = {
            "course_modes_choose_url":
            reverse("course_modes_choose", kwargs={'course_id': course_id}),
            "modes":
            modes,
            "has_credit_upsell":
            has_credit_upsell,
            "course_name":
            course.display_name_with_default_escaped,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "chosen_price":
            chosen_price,
            "error":
            error,
            "responsive":
            True,
            "nav_hidden":
            True,
        }

        title_content = _(
            "Congratulations!  You are now enrolled in {course_name}").format(
                course_name=course.display_name_with_default_escaped)
        enterprise_learner_data = enterprise_api.get_enterprise_learner_data(
            site=request.site, user=request.user)
        if enterprise_learner_data:
            is_course_in_enterprise_catalog = enterprise_api.is_course_in_enterprise_catalog(
                site=request.site,
                course_id=course_id,
                user=request.user,
                enterprise_catalog_id=enterprise_learner_data[0]
                ['enterprise_customer']['catalog'])

            if is_course_in_enterprise_catalog:
                partner_names = partner_name = course.display_organization \
                    if course.display_organization else course.org
                enterprise_name = enterprise_learner_data[0][
                    'enterprise_customer']['name']
                organizations = organization_api.get_course_organizations(
                    course_id=course.id)
                if organizations:
                    partner_names = ' and '.join([
                        org.get('name', partner_name) for org in organizations
                    ])

                title_content = _(
                    "Welcome, {username}! You are about to enroll in {course_name},"
                    " from {partner_names}, sponsored by {enterprise_name}. Please select your enrollment"
                    " information below.").format(
                        username=request.user.username,
                        course_name=course.display_name_with_default_escaped,
                        partner_names=partner_names,
                        enterprise_name=enterprise_name)
        context["title_content"] = title_content

        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            context["currency"] = verified_mode.currency.upper()
            context["min_price"] = verified_mode.min_price
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description

            if verified_mode.sku:
                context[
                    "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(
                        request.user)
                context[
                    "ecommerce_payment_page"] = ecommerce_service.payment_page_url(
                    )
                context["sku"] = verified_mode.sku
                context["bulk_sku"] = verified_mode.bulk_sku

        return render_to_response("course_modes/choose.html", context)
Exemplo n.º 36
0
    def post(self, request, course_id):
        """Takes the form submission from the page and parses it.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Returns:
            Status code 400 when the requested mode is unsupported. When the honor mode
            is selected, redirects to the dashboard. When the verified mode is selected,
            returns error messages if the indicated contribution amount is invalid or
            below the minimum, otherwise redirects to the verification flow.

        """
        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
        user = request.user

        # This is a bit redundant with logic in student.views.change_enrollment,
        # but I don't really have the time to refactor it more nicely and test.
        course = modulestore().get_course(course_key)
        if not has_access(user, 'enroll', course):
            error_msg = _("Enrollment is closed")
            return self.get(request, course_id, error=error_msg)

        requested_mode = self._get_requested_mode(request.POST)

        allowed_modes = CourseMode.modes_for_course_dict(course_key)
        if requested_mode not in allowed_modes:
            return HttpResponseBadRequest(_("Enrollment mode not supported"))

        # TODO: Move out this piece of code as we are making changes in edx code.
        course = modulestore().get_course(course_key)
        today_date = timezone.now()
        course_start_date = course.start
        if course_start_date > today_date:
            course_target = reverse('about_course', args=[unicode(course_id)])
        else:
            first_chapter_url, first_section = get_course_related_keys(
                request, course)
            course_target = reverse('courseware_section',
                                    args=[
                                        course.id.to_deprecated_string(),
                                        first_chapter_url, first_section
                                    ])

        if requested_mode == 'audit':
            # The user will have already been enrolled in the audit mode at this
            # point, so we just redirect them to the dashboard, thereby avoiding
            # hitting the database a second time attempting to enroll them.
            return redirect(course_target)

        if requested_mode == 'honor':
            CourseEnrollment.enroll(user, course_key, mode=requested_mode)
            return redirect(course_target)

        mode_info = allowed_modes[requested_mode]

        if requested_mode == 'verified':
            amount = request.POST.get("contribution") or \
                request.POST.get("contribution-other-amt") or 0

            try:
                # Validate the amount passed in and force it into two digits
                amount_value = decimal.Decimal(amount).quantize(
                    decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
            except decimal.InvalidOperation:
                error_msg = _("Invalid amount selected.")
                return self.get(request, course_id, error=error_msg)

            # Check for minimum pricing
            if amount_value < mode_info.min_price:
                error_msg = _(
                    "No selected price or selected price is too low.")
                return self.get(request, course_id, error=error_msg)

            donation_for_course = request.session.get("donation_for_course",
                                                      {})
            donation_for_course[unicode(course_key)] = amount_value
            request.session["donation_for_course"] = donation_for_course

            return redirect(
                reverse('verify_student_start_flow',
                        kwargs={'course_id': unicode(course_key)}))
Exemplo n.º 37
0
    def test_is_masters_only(self, available_modes, expected_is_masters_only):
        for mode in available_modes:
            self.create_mode(mode, mode, 10)

        self.assertEqual(CourseMode.is_masters_only(self.course_key),
                         expected_is_masters_only)
Exemplo n.º 38
0
class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase,
                    EventTrackingTestCase, MilestonesTestCaseMixin):
    """
    Tests about xblock.
    """
    @classmethod
    def setUpClass(cls):
        super(AboutTestCase, cls).setUpClass()
        cls.course = CourseFactory.create()
        cls.course_without_about = CourseFactory.create(
            catalog_visibility=CATALOG_VISIBILITY_NONE)
        cls.course_with_about = CourseFactory.create(
            catalog_visibility=CATALOG_VISIBILITY_ABOUT)
        cls.purchase_course = CourseFactory.create(
            org='MITx', number='buyme', display_name='Course To Buy')
        cls.about = ItemFactory.create(category="about",
                                       parent_location=cls.course.location,
                                       data="OOGIE BLOOGIE",
                                       display_name="overview")
        cls.about = ItemFactory.create(
            category="about",
            parent_location=cls.course_without_about.location,
            data="WITHOUT ABOUT",
            display_name="overview")
        cls.about = ItemFactory.create(
            category="about",
            parent_location=cls.course_with_about.location,
            data="WITH ABOUT",
            display_name="overview")

    def setUp(self):
        super(AboutTestCase, self).setUp()

        self.course_mode = CourseMode(
            course_id=self.purchase_course.id,
            mode_slug=CourseMode.DEFAULT_MODE_SLUG,
            mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
            min_price=10)
        self.course_mode.save()

    def test_anonymous_user(self):
        """
        This test asserts that a non-logged in user can visit the course about page
        """
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("OOGIE BLOOGIE", resp.content)

        # Check that registration button is present
        self.assertIn(REG_STR, resp.content)

    def test_logged_in(self):
        """
        This test asserts that a logged-in user can visit the course about page
        """
        self.setup_user()
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("OOGIE BLOOGIE", resp.content)

    def test_already_enrolled(self):
        """
        Asserts that the end user sees the appropriate messaging
        when he/she visits the course about page, but is already enrolled
        """
        self.setup_user()
        self.enroll(self.course, True)
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("You are enrolled in this course", resp.content)
        self.assertIn("View Course", resp.content)

    @override_settings(COURSE_ABOUT_VISIBILITY_PERMISSION="see_about_page")
    def test_visible_about_page_settings(self):
        """
        Verify that the About Page honors the permission settings in the course module
        """
        url = reverse('about_course',
                      args=[self.course_with_about.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("WITH ABOUT", resp.content)

        url = reverse(
            'about_course',
            args=[self.course_without_about.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 404)

    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
    def test_logged_in_marketing(self):
        self.setup_user()
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        # should be redirected
        self.assertEqual(resp.status_code, 302)
        # follow this time, and check we're redirected to the course info page
        resp = self.client.get(url, follow=True)
        target_url = resp.redirect_chain[-1][0]
        info_url = reverse('info',
                           args=[self.course.id.to_deprecated_string()])
        self.assertTrue(target_url.endswith(info_url))

    @patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True})
    def test_pre_requisite_course(self):
        pre_requisite_course = CourseFactory.create(
            org='edX', course='900', display_name='pre requisite course')
        course = CourseFactory.create(
            pre_requisite_courses=[unicode(pre_requisite_course.id)])
        self.setup_user()
        url = reverse('about_course', args=[unicode(course.id)])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        pre_requisite_courses = get_prerequisite_courses_display(course)
        pre_requisite_course_about_url = reverse(
            'about_course', args=[unicode(pre_requisite_courses[0]['key'])])
        self.assertIn(
            "<span class=\"important-dates-item-text pre-requisite\"><a href=\"{}\">{}</a></span>"
            .format(pre_requisite_course_about_url,
                    pre_requisite_courses[0]['display']),
            resp.content.strip('\n'))

    @patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True})
    def test_about_page_unfulfilled_prereqs(self):
        pre_requisite_course = CourseFactory.create(
            org='edX',
            course='901',
            display_name='pre requisite course',
        )

        pre_requisite_courses = [unicode(pre_requisite_course.id)]

        # for this failure to occur, the enrollment window needs to be in the past
        course = CourseFactory.create(
            org='edX',
            course='1000',
            # closed enrollment
            enrollment_start=datetime.datetime(2013, 1, 1),
            enrollment_end=datetime.datetime(2014, 1, 1),
            start=datetime.datetime(2013, 1, 1),
            end=datetime.datetime(2030, 1, 1),
            pre_requisite_courses=pre_requisite_courses,
        )
        set_prerequisite_courses(course.id, pre_requisite_courses)

        self.setup_user()
        self.enroll(self.course, True)
        self.enroll(pre_requisite_course, True)

        url = reverse('about_course', args=[unicode(course.id)])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        pre_requisite_courses = get_prerequisite_courses_display(course)
        pre_requisite_course_about_url = reverse(
            'about_course', args=[unicode(pre_requisite_courses[0]['key'])])
        self.assertIn(
            "<span class=\"important-dates-item-text pre-requisite\"><a href=\"{}\">{}</a></span>"
            .format(pre_requisite_course_about_url,
                    pre_requisite_courses[0]['display']),
            resp.content.strip('\n'))

        url = reverse('about_course', args=[unicode(pre_requisite_course.id)])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
Exemplo n.º 39
0
    def get(self, request, course_id, error=None):
        """Displays the course mode choice page.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Keyword Args:
            error (unicode): If provided, display this error message
                on the page.

        Returns:
            Response

        """
        course_key = CourseKey.from_string(course_id)

        # Check whether the user has access to this course
        # based on country access rules.
        embargo_redirect = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path)
        if embargo_redirect:
            return redirect(embargo_redirect)

        enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(
            request.user, course_key)
        modes = CourseMode.modes_for_course_dict(course_key)
        ecommerce_service = EcommerceService()

        # We assume that, if 'professional' is one of the modes, it should be the *only* mode.
        # If there are both modes, default to non-id-professional.
        has_enrolled_professional = (
            CourseMode.is_professional_slug(enrollment_mode) and is_active)
        if CourseMode.has_professional_mode(
                modes) and not has_enrolled_professional:
            purchase_workflow = request.GET.get("purchase_workflow", "single")
            verify_url = reverse('verify_student_start_flow',
                                 kwargs={'course_id': unicode(course_key)})
            redirect_url = "{url}?purchase_workflow={workflow}".format(
                url=verify_url, workflow=purchase_workflow)
            if ecommerce_service.is_enabled(request.user):
                professional_mode = modes.get(
                    CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(
                        CourseMode.PROFESSIONAL)
                if purchase_workflow == "single" and professional_mode.sku:
                    redirect_url = ecommerce_service.checkout_page_url(
                        professional_mode.sku)
                if purchase_workflow == "bulk" and professional_mode.bulk_sku:
                    redirect_url = ecommerce_service.checkout_page_url(
                        professional_mode.bulk_sku)
            return redirect(redirect_url)

        # TODO: Move out this piece of code as we are making changes in edx code.
        course = modulestore().get_course(course_key)
        today_date = timezone.now()
        course_start_date = course.start
        if course_start_date > today_date:
            course_target = reverse('about_course', args=[unicode(course_id)])
        else:
            first_chapter_url, first_section = get_course_related_keys(
                request, course)
            course_target = reverse('courseware_section',
                                    args=[
                                        course.id.to_deprecated_string(),
                                        first_chapter_url, first_section
                                    ])

        # If there isn't a verified mode available, then there's nothing
        # to do on this page.  The user has almost certainly been auto-registered
        # in the "honor" track by this point, so we send the user
        # to the dashboard.
        if not CourseMode.has_verified_mode(modes):
            return redirect(course_target)

        # If a user has already paid, redirect them to the dashboard.
        if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES +
                          [CourseMode.NO_ID_PROFESSIONAL_MODE]):
            return redirect(course_target)

        donation_for_course = request.session.get("donation_for_course", {})
        chosen_price = donation_for_course.get(unicode(course_key), None)

        if CourseEnrollment.is_enrollment_closed(request.user, course):
            locale = to_locale(get_language())
            enrollment_end_date = format_datetime(course.enrollment_end,
                                                  'short',
                                                  locale=locale)
            params = urllib.urlencode({'course_closed': enrollment_end_date})
            return redirect('{0}?{1}'.format(reverse('dashboard'), params))

        # When a credit mode is available, students will be given the option
        # to upgrade from a verified mode to a credit mode at the end of the course.
        # This allows students who have completed photo verification to be eligible
        # for univerity credit.
        # Since credit isn't one of the selectable options on the track selection page,
        # we need to check *all* available course modes in order to determine whether
        # a credit mode is available.  If so, then we show slightly different messaging
        # for the verified track.
        has_credit_upsell = any(
            CourseMode.is_credit_mode(mode)
            for mode in CourseMode.modes_for_course(course_key,
                                                    only_selectable=False))

        context = {
            "course_modes_choose_url":
            reverse("course_modes_choose",
                    kwargs={'course_id': course_key.to_deprecated_string()}),
            "modes":
            modes,
            "has_credit_upsell":
            has_credit_upsell,
            "course_name":
            course.display_name_with_default_escaped,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "chosen_price":
            chosen_price,
            "error":
            error,
            "responsive":
            True,
            "nav_hidden":
            True,
        }
        if "verified" in modes:
            verified_mode = modes["verified"]
            context["suggested_prices"] = [
                decimal.Decimal(x.strip())
                for x in verified_mode.suggested_prices.split(",")
                if x.strip()
            ]
            context["currency"] = verified_mode.currency.upper()
            context["min_price"] = verified_mode.min_price
            context["verified_name"] = verified_mode.name
            context["verified_description"] = verified_mode.description

            if verified_mode.sku:
                context[
                    "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(
                        request.user)
                context[
                    "ecommerce_payment_page"] = ecommerce_service.payment_page_url(
                    )
                context["sku"] = verified_mode.sku
                context["bulk_sku"] = verified_mode.bulk_sku

        return render_to_response("course_modes/choose.html", context)
Exemplo n.º 40
0
 def test_get_mode_display_name(self, slug, expected_display_name):
     """ Verify the method properly maps mode slugs to display names. """
     mode = CourseMode(mode_slug=slug)
     self.assertEqual(self.course.get_mode_display_name(mode), expected_display_name)
Exemplo n.º 41
0
 def test_get_mode_display_name_unknown_slug(self):
     """ Verify the method returns the slug if it has no known mapping. """
     mode = CourseMode(mode_slug='Blah!')
     self.assertEqual(self.course.get_mode_display_name(mode), mode.mode_slug)
Exemplo n.º 42
0
class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase,
                              XssTestMixin):
    """
    Tests for the instructor dashboard (not legacy).
    """
    def setUp(self):
        """
        Set up tests
        """
        super(TestInstructorDashboard, self).setUp()
        self.course = CourseFactory.create(
            grading_policy={
                "GRADE_CUTOFFS": {
                    "A": 0.75,
                    "B": 0.63,
                    "C": 0.57,
                    "D": 0.5
                }
            },
            display_name='<script>alert("XSS")</script>')

        self.course_mode = CourseMode(
            course_id=self.course.id,
            mode_slug=CourseMode.DEFAULT_MODE_SLUG,
            mode_display_name=CourseMode.DEFAULT_MODE.name,
            min_price=40)
        self.course_mode.save()
        # Create instructor account
        self.instructor = AdminFactory.create()
        self.client.login(username=self.instructor.username, password="******")

        # URL for instructor dash
        self.url = reverse(
            'instructor_dashboard',
            kwargs={'course_id': self.course.id.to_deprecated_string()})

    def get_dashboard_enrollment_message(self):
        """
        Returns expected dashboard enrollment message with link to Insights.
        """
        return 'Enrollment data is now available in <a href="http://example.com/courses/{}" ' \
               'target="_blank">Example</a>.'.format(unicode(self.course.id))

    def get_dashboard_analytics_message(self):
        """
        Returns expected dashboard demographic message with link to Insights.
        """
        return 'For analytics about your course, go to <a href="http://example.com/courses/{}" ' \
               'target="_blank">Example</a>.'.format(unicode(self.course.id))

    def test_instructor_tab(self):
        """
        Verify that the instructor tab appears for staff only.
        """
        def has_instructor_tab(user, course):
            """Returns true if the "Instructor" tab is shown."""
            request = RequestFactory().request()
            request.user = user
            tabs = get_course_tab_list(request, course)
            return len([tab for tab in tabs if tab.name == 'Instructor']) == 1

        self.assertTrue(has_instructor_tab(self.instructor, self.course))
        student = UserFactory.create()
        self.assertFalse(has_instructor_tab(student, self.course))

    def test_default_currency_in_the_html_response(self):
        """
        Test that checks the default currency_symbol ($) in the response
        """
        CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
        total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(
            self.course.id)
        response = self.client.get(self.url)
        self.assertIn('${amount}'.format(amount=total_amount),
                      response.content)

    def test_course_name_xss(self):
        """Test that the instructor dashboard correctly escapes course names
        with script tags.
        """
        response = self.client.get(self.url)
        self.assert_no_xss(response, '<script>alert("XSS")</script>')

    @override_settings(PAID_COURSE_REGISTRATION_CURRENCY=['PKR', 'Rs'])
    def test_override_currency_settings_in_the_html_response(self):
        """
        Test that checks the default currency_symbol ($) in the response
        """
        CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
        total_amount = PaidCourseRegistration.get_total_amount_of_purchased_item(
            self.course.id)
        response = self.client.get(self.url)
        self.assertIn(
            '{currency}{amount}'.format(currency='Rs', amount=total_amount),
            response.content)

    @patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_ENROLLMENTS': False})
    @override_settings(ANALYTICS_DASHBOARD_URL='')
    def test_no_enrollments(self):
        """
        Test enrollment section is hidden.
        """
        response = self.client.get(self.url)
        # no enrollment information should be visible
        self.assertNotIn('<h3 class="hd hd-3">Enrollment Information</h3>',
                         response.content)

    @patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_ENROLLMENTS': True})
    @override_settings(ANALYTICS_DASHBOARD_URL='')
    def test_show_enrollments_data(self):
        """
        Test enrollment data is shown.
        """
        response = self.client.get(self.url)

        # enrollment information visible
        self.assertIn('<h3 class="hd hd-3">Enrollment Information</h3>',
                      response.content)
        self.assertIn('<th scope="row">Verified</th>', response.content)
        self.assertIn('<th scope="row">Audit</th>', response.content)
        self.assertIn('<th scope="row">Honor</th>', response.content)
        self.assertIn('<th scope="row">Professional</th>', response.content)

        # dashboard link hidden
        self.assertNotIn(self.get_dashboard_enrollment_message(),
                         response.content)

    @patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_ENROLLMENTS': True})
    @override_settings(ANALYTICS_DASHBOARD_URL='')
    def test_show_enrollment_data_for_prof_ed(self):
        # Create both "professional" (meaning professional + verification)
        # and "no-id-professional" (meaning professional without verification)
        # These should be aggregated for display purposes.
        users = [UserFactory() for _ in range(2)]
        CourseEnrollment.enroll(users[0], self.course.id, mode="professional")
        CourseEnrollment.enroll(users[1],
                                self.course.id,
                                mode="no-id-professional")
        response = self.client.get(self.url)

        # Check that the number of professional enrollments is two
        self.assertContains(response,
                            '<th scope="row">Professional</th><td>2</td>')

    @patch.dict(settings.FEATURES, {'DISPLAY_ANALYTICS_ENROLLMENTS': False})
    @override_settings(ANALYTICS_DASHBOARD_URL='http://example.com')
    @override_settings(ANALYTICS_DASHBOARD_NAME='Example')
    def test_show_dashboard_enrollment_message(self):
        """
        Test enrollment dashboard message is shown and data is hidden.
        """
        response = self.client.get(self.url)

        # enrollment information hidden
        self.assertNotIn('<th scope="row">Verified</th>', response.content)
        self.assertNotIn('<th scope="row">Audit</th>', response.content)
        self.assertNotIn('<th scope="row">Honor</th>', response.content)
        self.assertNotIn('<th scope="row">Professional</th>', response.content)

        # link to dashboard shown
        expected_message = self.get_dashboard_enrollment_message()
        self.assertIn(expected_message, response.content)

    @override_settings(ANALYTICS_DASHBOARD_URL='')
    @override_settings(ANALYTICS_DASHBOARD_NAME='')
    def test_dashboard_analytics_tab_not_shown(self):
        """
        Test dashboard analytics tab isn't shown if insights isn't configured.
        """
        response = self.client.get(self.url)
        analytics_section = '<li class="nav-item"><a href="" data-section="instructor_analytics">Analytics</a></li>'
        self.assertNotIn(analytics_section, response.content)

    @override_settings(ANALYTICS_DASHBOARD_URL='http://example.com')
    @override_settings(ANALYTICS_DASHBOARD_NAME='Example')
    def test_dashboard_analytics_points_at_insights(self):
        """
        Test analytics dashboard message is shown
        """
        response = self.client.get(self.url)
        analytics_section = '<li class="nav-item"><button type="button" class="btn-link" data-section="instructor_analytics">Analytics</button></li>'  # pylint: disable=line-too-long
        self.assertIn(analytics_section, response.content)

        # link to dashboard shown
        expected_message = self.get_dashboard_analytics_message()
        self.assertIn(expected_message, response.content)

    def add_course_to_user_cart(self, cart, course_key):
        """
        adding course to user cart
        """
        reg_item = PaidCourseRegistration.add_to_order(cart, course_key)
        return reg_item

    @patch.dict('django.conf.settings.FEATURES',
                {'ENABLE_PAID_COURSE_REGISTRATION': True})
    def test_total_credit_cart_sales_amount(self):
        """
        Test to check the total amount for all the credit card purchases.
        """
        student = UserFactory.create()
        self.client.login(username=student.username, password="******")
        student_cart = Order.get_cart_for_user(student)
        item = self.add_course_to_user_cart(student_cart, self.course.id)
        resp = self.client.post(reverse('shoppingcart.views.update_user_cart'),
                                {
                                    'ItemId': item.id,
                                    'qty': 4
                                })
        self.assertEqual(resp.status_code, 200)
        student_cart.purchase()

        self.client.login(username=self.instructor.username, password="******")
        CourseFinanceAdminRole(self.course.id).add_users(self.instructor)
        single_purchase_total = PaidCourseRegistration.get_total_amount_of_purchased_item(
            self.course.id)
        bulk_purchase_total = CourseRegCodeItem.get_total_amount_of_purchased_item(
            self.course.id)
        total_amount = single_purchase_total + bulk_purchase_total
        response = self.client.get(self.url)
        self.assertIn(
            '{currency}{amount}'.format(currency='$', amount=total_amount),
            response.content)

    @ddt.data(
        (True, True, True),
        (True, False, False),
        (True, None, False),
        (False, True, False),
        (False, False, False),
        (False, None, False),
    )
    @ddt.unpack
    def test_ccx_coaches_option_on_admin_list_management_instructor(
            self, ccx_feature_flag, enable_ccx, expected_result):
        """
        Test whether the "CCX Coaches" option is visible or hidden depending on the value of course.enable_ccx.
        """
        with patch.dict(settings.FEATURES,
                        {'CUSTOM_COURSES_EDX': ccx_feature_flag}):
            self.course.enable_ccx = enable_ccx
            self.store.update_item(self.course, self.instructor.id)

            response = self.client.get(self.url)

            self.assertEquals(
                expected_result,
                'CCX Coaches are able to create their own Custom Courses based on this course'
                in response.content)

    def test_grade_cutoffs(self):
        """
        Verify that grade cutoffs are displayed in the correct order.
        """
        response = self.client.get(self.url)
        self.assertIn('D: 0.5, C: 0.57, B: 0.63, A: 0.75', response.content)

    @patch('instructor.views.gradebook_api.MAX_STUDENTS_PER_PAGE_GRADE_BOOK',
           2)
    def test_calculate_page_info(self):
        page = calculate_page_info(offset=0, total_students=2)
        self.assertEqual(page["offset"], 0)
        self.assertEqual(page["page_num"], 1)
        self.assertEqual(page["next_offset"], None)
        self.assertEqual(page["previous_offset"], None)
        self.assertEqual(page["total_pages"], 1)

    @patch('instructor.views.gradebook_api.render_to_response',
           intercept_renderer)
    @patch('instructor.views.gradebook_api.MAX_STUDENTS_PER_PAGE_GRADE_BOOK',
           1)
    def test_spoc_gradebook_pages(self):
        for i in xrange(2):
            username = "******" % i
            student = UserFactory.create(username=username)
            CourseEnrollmentFactory.create(user=student,
                                           course_id=self.course.id)
        url = reverse('spoc_gradebook', kwargs={'course_id': self.course.id})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        # Max number of student per page is one.  Patched setting MAX_STUDENTS_PER_PAGE_GRADE_BOOK = 1
        self.assertEqual(len(response.mako_context['students']), 1)  # pylint: disable=no-member
Exemplo n.º 43
0
 def test_auto_enroll_mode(self, modes, result):
     # Verify that the proper auto enroll mode is returned
     self.assertEqual(CourseMode.auto_enroll_mode(self.course_key, modes),
                      result)
Exemplo n.º 44
0
class TestCourseSaleRecordsAnalyticsBasic(ModuleStoreTestCase):
    """ Test basic course sale records analytics functions. """
    def setUp(self):
        """
        Fixtures.
        """
        super(TestCourseSaleRecordsAnalyticsBasic, self).setUp()
        self.course = CourseFactory.create()
        self.cost = 40
        self.course_mode = CourseMode(course_id=self.course.id,
                                      mode_slug="honor",
                                      mode_display_name="honor cert",
                                      min_price=self.cost)
        self.course_mode.save()
        self.instructor = InstructorFactory(course_key=self.course.id)
        self.client.login(username=self.instructor.username, password='******')

    def test_course_sale_features(self):

        query_features = [
            'company_name', 'company_contact_name', 'company_contact_email',
            'total_codes', 'total_used_codes', 'total_amount', 'created',
            'customer_reference_number', 'recipient_name', 'recipient_email',
            'created_by', 'internal_reference', 'invoice_number', 'codes',
            'course_id'
        ]

        #create invoice
        sale_invoice = Invoice.objects.create(
            total_amount=1234.32,
            company_name='Test1',
            company_contact_name='TestName',
            company_contact_email='*****@*****.**',
            recipient_name='Testw_1',
            recipient_email='*****@*****.**',
            customer_reference_number='2Fwe23S',
            internal_reference="ABC",
            course_id=self.course.id)
        invoice_item = CourseRegistrationCodeInvoiceItem.objects.create(
            invoice=sale_invoice,
            qty=1,
            unit_price=1234.32,
            course_id=self.course.id)
        for i in range(5):
            course_code = CourseRegistrationCode(
                code="test_code{}".format(i),
                course_id=self.course.id.to_deprecated_string(),
                created_by=self.instructor,
                invoice=sale_invoice,
                invoice_item=invoice_item,
                mode_slug='honor')
            course_code.save()

        course_sale_records_list = sale_record_features(
            self.course.id, query_features)

        for sale_record in course_sale_records_list:
            self.assertEqual(sale_record['total_amount'],
                             sale_invoice.total_amount)
            self.assertEqual(sale_record['recipient_email'],
                             sale_invoice.recipient_email)
            self.assertEqual(sale_record['recipient_name'],
                             sale_invoice.recipient_name)
            self.assertEqual(sale_record['company_name'],
                             sale_invoice.company_name)
            self.assertEqual(sale_record['company_contact_name'],
                             sale_invoice.company_contact_name)
            self.assertEqual(sale_record['company_contact_email'],
                             sale_invoice.company_contact_email)
            self.assertEqual(sale_record['internal_reference'],
                             sale_invoice.internal_reference)
            self.assertEqual(sale_record['customer_reference_number'],
                             sale_invoice.customer_reference_number)
            self.assertEqual(sale_record['invoice_number'], sale_invoice.id)
            self.assertEqual(sale_record['created_by'], self.instructor)
            self.assertEqual(sale_record['total_used_codes'], 0)
            self.assertEqual(sale_record['total_codes'], 5)

    def test_course_sale_no_codes(self):

        query_features = [
            'company_name', 'company_contact_name', 'company_contact_email',
            'total_codes', 'total_used_codes', 'total_amount', 'created',
            'customer_reference_number', 'recipient_name', 'recipient_email',
            'created_by', 'internal_reference', 'invoice_number', 'codes',
            'course_id'
        ]

        #create invoice
        sale_invoice = Invoice.objects.create(
            total_amount=0.00,
            company_name='Test1',
            company_contact_name='TestName',
            company_contact_email='*****@*****.**',
            recipient_name='Testw_1',
            recipient_email='*****@*****.**',
            customer_reference_number='2Fwe23S',
            internal_reference="ABC",
            course_id=self.course.id)
        CourseRegistrationCodeInvoiceItem.objects.create(
            invoice=sale_invoice,
            qty=0,
            unit_price=0.00,
            course_id=self.course.id)

        course_sale_records_list = sale_record_features(
            self.course.id, query_features)

        for sale_record in course_sale_records_list:
            self.assertEqual(sale_record['total_amount'],
                             sale_invoice.total_amount)
            self.assertEqual(sale_record['recipient_email'],
                             sale_invoice.recipient_email)
            self.assertEqual(sale_record['recipient_name'],
                             sale_invoice.recipient_name)
            self.assertEqual(sale_record['company_name'],
                             sale_invoice.company_name)
            self.assertEqual(sale_record['company_contact_name'],
                             sale_invoice.company_contact_name)
            self.assertEqual(sale_record['company_contact_email'],
                             sale_invoice.company_contact_email)
            self.assertEqual(sale_record['internal_reference'],
                             sale_invoice.internal_reference)
            self.assertEqual(sale_record['customer_reference_number'],
                             sale_invoice.customer_reference_number)
            self.assertEqual(sale_record['invoice_number'], sale_invoice.id)
            self.assertEqual(sale_record['created_by'], None)
            self.assertEqual(sale_record['total_used_codes'], 0)
            self.assertEqual(sale_record['total_codes'], 0)

    def test_sale_order_features_with_discount(self):
        """
         Test Order Sales Report CSV
        """
        query_features = [('id', 'Order Id'), ('company_name', 'Company Name'),
                          ('company_contact_name', 'Company Contact Name'),
                          ('company_contact_email', 'Company Contact Email'),
                          ('total_amount', 'Total Amount'),
                          ('total_codes', 'Total Codes'),
                          ('total_used_codes', 'Total Used Codes'),
                          ('logged_in_username', 'Login Username'),
                          ('logged_in_email', 'Login User Email'),
                          ('purchase_time', 'Date of Sale'),
                          ('customer_reference_number',
                           'Customer Reference Number'),
                          ('recipient_name', 'Recipient Name'),
                          ('recipient_email', 'Recipient Email'),
                          ('bill_to_street1', 'Street 1'),
                          ('bill_to_street2', 'Street 2'),
                          ('bill_to_city', 'City'), ('bill_to_state', 'State'),
                          ('bill_to_postalcode', 'Postal Code'),
                          ('bill_to_country', 'Country'),
                          ('order_type', 'Order Type'),
                          ('status', 'Order Item Status'),
                          ('coupon_code', 'Coupon Code'),
                          ('unit_cost', 'Unit Price'),
                          ('list_price', 'List Price'),
                          ('codes', 'Registration Codes'),
                          ('course_id', 'Course Id')]
        # add the coupon code for the course
        coupon = Coupon(code='test_code',
                        description='test_description',
                        course_id=self.course.id,
                        percentage_discount='10',
                        created_by=self.instructor,
                        is_active=True)
        coupon.save()
        order = Order.get_cart_for_user(self.instructor)
        order.order_type = 'business'
        order.save()
        order.add_billing_details(company_name='Test Company',
                                  company_contact_name='Test',
                                  company_contact_email='test@123',
                                  recipient_name='R1',
                                  recipient_email='',
                                  customer_reference_number='PO#23')
        CourseRegCodeItem.add_to_order(order, self.course.id, 4)
        # apply the coupon code to the item in the cart
        resp = self.client.post(reverse('shoppingcart.views.use_code'),
                                {'code': coupon.code})
        self.assertEqual(resp.status_code, 200)
        order.purchase()

        # get the updated item
        item = order.orderitem_set.all().select_subclasses()[0]
        # get the redeemed coupon information
        coupon_redemption = CouponRedemption.objects.select_related(
            'coupon').filter(order=order)

        db_columns = [x[0] for x in query_features]
        sale_order_records_list = sale_order_record_features(
            self.course.id, db_columns)

        for sale_order_record in sale_order_records_list:
            self.assertEqual(sale_order_record['recipient_email'],
                             order.recipient_email)
            self.assertEqual(sale_order_record['recipient_name'],
                             order.recipient_name)
            self.assertEqual(sale_order_record['company_name'],
                             order.company_name)
            self.assertEqual(sale_order_record['company_contact_name'],
                             order.company_contact_name)
            self.assertEqual(sale_order_record['company_contact_email'],
                             order.company_contact_email)
            self.assertEqual(sale_order_record['customer_reference_number'],
                             order.customer_reference_number)
            self.assertEqual(sale_order_record['unit_cost'], item.unit_cost)
            self.assertEqual(sale_order_record['list_price'], item.list_price)
            self.assertEqual(sale_order_record['status'], item.status)
            self.assertEqual(sale_order_record['coupon_code'],
                             coupon_redemption[0].coupon.code)

    def test_sale_order_features_without_discount(self):
        """
         Test Order Sales Report CSV
        """
        query_features = [
            ('id', 'Order Id'),
            ('company_name', 'Company Name'),
            ('company_contact_name', 'Company Contact Name'),
            ('company_contact_email', 'Company Contact Email'),
            ('total_amount', 'Total Amount'),
            ('total_codes', 'Total Codes'),
            ('total_used_codes', 'Total Used Codes'),
            ('logged_in_username', 'Login Username'),
            ('logged_in_email', 'Login User Email'),
            ('purchase_time', 'Date of Sale'),
            ('customer_reference_number', 'Customer Reference Number'),
            ('recipient_name', 'Recipient Name'),
            ('recipient_email', 'Recipient Email'),
            ('bill_to_street1', 'Street 1'),
            ('bill_to_street2', 'Street 2'),
            ('bill_to_city', 'City'),
            ('bill_to_state', 'State'),
            ('bill_to_postalcode', 'Postal Code'),
            ('bill_to_country', 'Country'),
            ('order_type', 'Order Type'),
            ('status', 'Order Item Status'),
            ('coupon_code', 'Coupon Code'),
            ('unit_cost', 'Unit Price'),
            ('list_price', 'List Price'),
            ('codes', 'Registration Codes'),
            ('course_id', 'Course Id'),
            ('quantity', 'Quantity'),
            ('total_discount', 'Total Discount'),
            ('total_amount', 'Total Amount Paid'),
        ]
        # add the coupon code for the course
        order = Order.get_cart_for_user(self.instructor)
        order.order_type = 'business'
        order.save()
        order.add_billing_details(company_name='Test Company',
                                  company_contact_name='Test',
                                  company_contact_email='test@123',
                                  recipient_name='R1',
                                  recipient_email='',
                                  customer_reference_number='PO#23')
        CourseRegCodeItem.add_to_order(order, self.course.id, 4)
        order.purchase()

        # get the updated item
        item = order.orderitem_set.all().select_subclasses()[0]

        db_columns = [x[0] for x in query_features]
        sale_order_records_list = sale_order_record_features(
            self.course.id, db_columns)

        for sale_order_record in sale_order_records_list:
            self.assertEqual(sale_order_record['recipient_email'],
                             order.recipient_email)
            self.assertEqual(sale_order_record['recipient_name'],
                             order.recipient_name)
            self.assertEqual(sale_order_record['company_name'],
                             order.company_name)
            self.assertEqual(sale_order_record['company_contact_name'],
                             order.company_contact_name)
            self.assertEqual(sale_order_record['company_contact_email'],
                             order.company_contact_email)
            self.assertEqual(sale_order_record['customer_reference_number'],
                             order.customer_reference_number)
            self.assertEqual(sale_order_record['unit_cost'], item.unit_cost)
            # Make sure list price is not None and matches the unit price since no discount was applied.
            self.assertIsNotNone(sale_order_record['list_price'])
            self.assertEqual(sale_order_record['list_price'], item.unit_cost)
            self.assertEqual(sale_order_record['status'], item.status)
            self.assertEqual(sale_order_record['coupon_code'], 'N/A')
            self.assertEqual(sale_order_record['total_amount'],
                             item.unit_cost * item.qty)
            self.assertEqual(sale_order_record['total_discount'], 0)
            self.assertEqual(sale_order_record['quantity'], item.qty)
Exemplo n.º 45
0
 def test_course_has_payment_options_with_no_id_professional(self):
     # Has payment options.
     self.create_mode('no-id-professional',
                      'no-id-professional',
                      min_price=5)
     self.assertTrue(CourseMode.has_payment_options(self.course_key))
Exemplo n.º 46
0
    def get(self, request, course_id):
        """
        Displays the main verification view, which contains three separate steps:
            - Taking the standard face photo
            - Taking the id photo
            - Confirming that the photos and payment price are correct
              before proceeding to payment
        """
        upgrade = request.GET.get('upgrade', False)

        # If the user has already been verified within the given time period,
        # redirect straight to the payment -- no need to verify again.
        if SoftwareSecurePhotoVerification.user_has_valid_or_pending(
                request.user):
            return redirect(
                reverse('verify_student_verified',
                        kwargs={'course_id': course_id}) +
                "?upgrade={}".format(upgrade))
        elif CourseEnrollment.enrollment_mode_for_user(
                request.user, course_id) == 'verified':
            return redirect(reverse('dashboard'))
        else:
            # If they haven't completed a verification attempt, we have to
            # restart with a new one. We can't reuse an older one because we
            # won't be able to show them their encrypted photo_id -- it's easier
            # bookkeeping-wise just to start over.
            progress_state = "start"

        verify_mode = CourseMode.mode_for_course(course_id, "verified")
        # if the course doesn't have a verified mode, we want to kick them
        # from the flow
        if not verify_mode:
            return redirect(reverse('dashboard'))
        if course_id in request.session.get("donation_for_course", {}):
            chosen_price = request.session["donation_for_course"][course_id]
        else:
            chosen_price = verify_mode.min_price

        course = course_from_id(course_id)
        context = {
            "progress_state":
            progress_state,
            "user_full_name":
            request.user.profile.name,
            "course_id":
            course_id,
            "course_name":
            course.display_name_with_default,
            "course_org":
            course.display_org_with_default,
            "course_num":
            course.display_number_with_default,
            "purchase_endpoint":
            get_purchase_endpoint(),
            "suggested_prices": [
                decimal.Decimal(price)
                for price in verify_mode.suggested_prices.split(",")
            ],
            "currency":
            verify_mode.currency.upper(),
            "chosen_price":
            chosen_price,
            "min_price":
            verify_mode.min_price,
            "upgrade":
            upgrade,
        }

        return render_to_response('verify_student/photo_verification.html',
                                  context)
Exemplo n.º 47
0
def register_code_redemption(request, registration_code):
    """
    This view allows the student to redeem the registration code
    and enroll in the course.
    """

    # Add some rate limiting here by re-using the RateLimitMixin as a helper class
    site_name = configuration_helpers.get_value('SITE_NAME',
                                                settings.SITE_NAME)
    limiter = BadRequestRateLimiter()
    if limiter.is_rate_limit_exceeded(request):
        AUDIT_LOG.warning(
            "Rate limit exceeded in registration code redemption.")
        return HttpResponseForbidden()

    template_to_render = 'shoppingcart/registration_code_redemption.html'
    if request.method == "GET":
        reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(
            registration_code, request, limiter)
        course = get_course_by_id(course_registration.course_id, depth=0)

        # Restrict the user from enrolling based on country access rules
        embargo_redirect = embargo_api.redirect_if_blocked(
            course.id,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path)
        if embargo_redirect is not None:
            return redirect(embargo_redirect)

        context = {
            'reg_code_already_redeemed':
            reg_code_already_redeemed,
            'reg_code_is_valid':
            reg_code_is_valid,
            'reg_code':
            registration_code,
            'site_name':
            site_name,
            'course':
            course,
            'registered_for_course':
            not _is_enrollment_code_an_update(course, request.user,
                                              course_registration)
        }
        return render_to_response(template_to_render, context)
    elif request.method == "POST":
        reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(
            registration_code, request, limiter)
        course = get_course_by_id(course_registration.course_id, depth=0)

        # Restrict the user from enrolling based on country access rules
        embargo_redirect = embargo_api.redirect_if_blocked(
            course.id,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path)
        if embargo_redirect is not None:
            return redirect(embargo_redirect)

        context = {
            'reg_code': registration_code,
            'site_name': site_name,
            'course': course,
            'reg_code_is_valid': reg_code_is_valid,
            'reg_code_already_redeemed': reg_code_already_redeemed,
        }
        if reg_code_is_valid and not reg_code_already_redeemed:
            # remove the course from the cart if it was added there.
            cart = Order.get_cart_for_user(request.user)
            try:
                cart_items = cart.find_item_by_course_id(
                    course_registration.course_id)

            except ItemNotFoundInCartException:
                pass
            else:
                for cart_item in cart_items:
                    if isinstance(cart_item,
                                  PaidCourseRegistration) or isinstance(
                                      cart_item, CourseRegCodeItem):
                        # Reload the item directly to prevent select_subclasses() hackery from interfering with
                        # deletion of all objects in the model inheritance hierarchy
                        cart_item = cart_item.__class__.objects.get(
                            id=cart_item.id)
                        cart_item.delete()

            #now redeem the reg code.
            redemption = RegistrationCodeRedemption.create_invoice_generated_registration_redemption(
                course_registration, request.user)
            try:
                kwargs = {}
                if course_registration.mode_slug is not None:
                    if CourseMode.mode_for_course(
                            course.id, course_registration.mode_slug):
                        kwargs['mode'] = course_registration.mode_slug
                    else:
                        raise RedemptionCodeError()
                redemption.course_enrollment = CourseEnrollment.enroll(
                    request.user, course.id, **kwargs)
                redemption.save()
                context['redemption_success'] = True
            except RedemptionCodeError:
                context['redeem_code_error'] = True
                context['redemption_success'] = False
            except EnrollmentClosedError:
                context['enrollment_closed'] = True
                context['redemption_success'] = False
            except CourseFullError:
                context['course_full'] = True
                context['redemption_success'] = False
            except AlreadyEnrolledError:
                context['registered_for_course'] = True
                context['redemption_success'] = False
        else:
            context['redemption_success'] = False
        return render_to_response(template_to_render, context)
Exemplo n.º 48
0
def _section_send_email(course, access):
    """ Provide data for the corresponding bulk email section """
    course_key = course.id

    # Monkey-patch applicable_aside_types to return no asides for the duration of this render
    with patch.object(course.runtime, 'applicable_aside_types',
                      null_applicable_aside_types):
        # This HtmlDescriptor is only being used to generate a nice text editor.
        html_module = HtmlDescriptor(
            course.system, DictFieldData({'data': ''}),
            ScopeIds(None, None, None,
                     course_key.make_usage_key('html', 'fake')))
        fragment = course.system.render(html_module, 'studio_view')
    fragment = wrap_xblock(
        'LmsRuntime',
        html_module,
        'studio_view',
        fragment,
        None,
        extra_data={"course-id": unicode(course_key)},
        usage_id_serializer=lambda usage_id: quote_slashes(unicode(usage_id)),
        # Generate a new request_token here at random, because this module isn't connected to any other
        # xblock rendering.
        request_token=uuid.uuid1().get_hex())
    cohorts = []
    if is_course_cohorted(course_key):
        cohorts = get_course_cohorts(course)
    course_modes = []
    if not VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(
            course_key):
        course_modes = CourseMode.modes_for_course(course_key,
                                                   include_expired=True,
                                                   only_selectable=False)
    email_editor = fragment.content
    section_data = {
        'section_key':
        'send_email',
        'section_display_name':
        _('Email'),
        'access':
        access,
        'send_email':
        reverse('send_email', kwargs={'course_id': unicode(course_key)}),
        'editor':
        email_editor,
        'cohorts':
        cohorts,
        'course_modes':
        course_modes,
        'default_cohort_name':
        DEFAULT_COHORT_NAME,
        'list_instructor_tasks_url':
        reverse('list_instructor_tasks',
                kwargs={'course_id': unicode(course_key)}),
        'email_background_tasks_url':
        reverse('list_background_email_tasks',
                kwargs={'course_id': unicode(course_key)}),
        'email_content_history_url':
        reverse('list_email_content',
                kwargs={'course_id': unicode(course_key)}),
    }
    return section_data
Exemplo n.º 49
0
def _register_course_home_messages(request, course, user_access,
                                   course_start_data):
    """
    Register messages to be shown in the course home content page.
    """
    allow_anonymous = allow_public_access(course, [COURSE_VISIBILITY_PUBLIC])

    if user_access['is_anonymous'] and not allow_anonymous:
        sign_in_or_register_text = (_(
            u'{sign_in_link} or {register_link} and then enroll in this course.'
        ) if not CourseMode.is_masters_only(course.id) else
                                    _(u'{sign_in_link} or {register_link}.'))
        CourseHomeMessages.register_info_message(
            request,
            Text(sign_in_or_register_text).format(
                sign_in_link=HTML(
                    u'<a href="/login?next={current_url}">{sign_in_label}</a>'
                ).format(
                    sign_in_label=_('Sign in'),
                    current_url=urlquote_plus(request.path),
                ),
                register_link=HTML(
                    u'<a href="/register?next={current_url}">{register_label}</a>'
                ).format(
                    register_label=_('register'),
                    current_url=urlquote_plus(request.path),
                )),
            title=Text(
                _('You must be enrolled in the course to see course content.')
            ))
    if not user_access['is_anonymous'] and not user_access['is_staff'] and \
            not user_access['is_enrolled']:

        title = Text(_(u'Welcome to {course_display_name}')).format(
            course_display_name=course.display_name)

        if CourseMode.is_masters_only(course.id):
            # if a course is a Master's only course, we will not offer user ability to self-enroll
            CourseHomeMessages.register_info_message(
                request,
                Text(
                    _('You must be enrolled in the course to see course content. '
                      'Please contact your degree administrator or edX Support if you have questions.'
                      )),
                title=title)
        elif not course.invitation_only:
            CourseHomeMessages.register_info_message(
                request,
                Text(
                    _(u'{open_enroll_link}Enroll now{close_enroll_link} to access the full course.'
                      )).format(open_enroll_link=HTML(
                          '<button class="enroll-btn btn-link">'),
                                close_enroll_link=HTML('</button>')),
                title=title)
        else:
            CourseHomeMessages.register_info_message(
                request,
                Text(
                    _('You must be enrolled in the course to see course content.'
                      )),
            )
Exemplo n.º 50
0
def instructor_dashboard_2(request, course_id):
    """ Display the instructor dashboard for a course. """
    try:
        course_key = CourseKey.from_string(course_id)
    except InvalidKeyError:
        log.error(
            u"Unable to find course with course key %s while loading the Instructor Dashboard.",
            course_id)
        return HttpResponseServerError()

    course = get_course_by_id(course_key, depth=0)

    access = {
        'admin':
        request.user.is_staff,
        'instructor':
        bool(has_access(request.user, 'instructor', course)),
        'finance_admin':
        CourseFinanceAdminRole(course_key).has_user(request.user),
        'sales_admin':
        CourseSalesAdminRole(course_key).has_user(request.user),
        'staff':
        bool(has_access(request.user, 'staff', course)),
        'forum_admin':
        has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
    }

    if not access['staff']:
        raise Http404()

    is_white_label = CourseMode.is_white_label(course_key)

    sections = [
        _section_course_info(course, access),
        _section_membership(course, access, is_white_label),
        _section_cohort_management(course, access),
        _section_student_admin(course, access),
        _section_data_download(course, access),
    ]

    analytics_dashboard_message = None
    if settings.ANALYTICS_DASHBOARD_URL:
        # Construct a URL to the external analytics dashboard
        analytics_dashboard_url = '{0}/courses/{1}'.format(
            settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
        link_start = "<a href=\"{}\" target=\"_blank\">".format(
            analytics_dashboard_url)
        analytics_dashboard_message = _(
            "To gain insights into student enrollment and participation {link_start}"
            "visit {analytics_dashboard_name}, our new course analytics product{link_end}."
        )
        analytics_dashboard_message = analytics_dashboard_message.format(
            link_start=link_start,
            link_end="</a>",
            analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)

        # Temporarily show the "Analytics" section until we have a better way of linking to Insights
        sections.append(_section_analytics(course, access))

    # Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
    course_mode_has_price = False
    paid_modes = CourseMode.paid_modes_for_course(course_key)
    if len(paid_modes) == 1:
        course_mode_has_price = True
    elif len(paid_modes) > 1:
        log.error(
            u"Course %s has %s course modes with payment options. Course must only have "
            u"one paid course mode to enable eCommerce options.",
            unicode(course_key), len(paid_modes))

    if settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']:
        sections.insert(3, _section_extensions(course))

    # Gate access to course email by feature flag & by course-specific authorization
    if bulk_email_is_enabled_for_course(course_key):
        sections.append(_section_send_email(course, access))

    # Gate access to Metrics tab by featue flag and staff authorization
    if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
        sections.append(_section_metrics(course, access))

    # Gate access to Ecommerce tab
    if course_mode_has_price and (access['finance_admin']
                                  or access['sales_admin']):
        sections.append(
            _section_e_commerce(course, access, paid_modes[0], is_white_label,
                                is_white_label))

    # Gate access to Special Exam tab depending if either timed exams or proctored exams
    # are enabled in the course

    # NOTE: For now, if we only have procotred exams enabled, then only platform Staff
    # (user.is_staff) will be able to view the special exams tab. This may
    # change in the future
    can_see_special_exams = (
        ((course.enable_proctored_exams and request.user.is_staff)
         or course.enable_timed_exams)
        and settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False))
    if can_see_special_exams:
        sections.append(_section_special_exams(course, access))

    # Certificates panel
    # This is used to generate example certificates
    # and enable self-generated certificates for a course.
    certs_enabled = CertificateGenerationConfiguration.current().enabled
    if certs_enabled and access['admin']:
        sections.append(_section_certificates(course))

    disable_buttons = not _is_small_course(course_key)

    certificate_white_list = CertificateWhitelist.get_certificate_white_list(
        course_key)
    certificate_exception_url = reverse('create_certificate_exception',
                                        kwargs={
                                            'course_id': unicode(course_key),
                                            'white_list_student': ''
                                        })

    context = {
        'course':
        course,
        'old_dashboard_url':
        reverse('instructor_dashboard_legacy',
                kwargs={'course_id': unicode(course_key)}),
        'studio_url':
        get_studio_url(course, 'course'),
        'sections':
        sections,
        'disable_buttons':
        disable_buttons,
        'analytics_dashboard_message':
        analytics_dashboard_message,
        'certificate_white_list':
        certificate_white_list,
        'certificate_exception_url':
        certificate_exception_url
    }
    return render_to_response(
        'instructor/instructor_dashboard_2/instructor_dashboard_2.html',
        context)
Exemplo n.º 51
0
    def post(self, request, course_id):
        """Takes the form submission from the page and parses it.

        Args:
            request (`Request`): The Django Request object.
            course_id (unicode): The slash-separated course key.

        Returns:
            Status code 400 when the requested mode is unsupported. When the honor mode
            is selected, redirects to the dashboard. When the verified mode is selected,
            returns error messages if the indicated contribution amount is invalid or
            below the minimum, otherwise redirects to the verification flow.

        """
        course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
        user = request.user

        # This is a bit redundant with logic in student.views.change_enrollment,
        # but I don't really have the time to refactor it more nicely and test.
        course = modulestore().get_course(course_key)
        if not has_access(user, 'enroll', course):
            error_msg = _("Enrollment is closed")
            return self.get(request, course_id, error=error_msg)

        requested_mode = self._get_requested_mode(request.POST)

        allowed_modes = CourseMode.modes_for_course_dict(course_key)
        if requested_mode not in allowed_modes:
            return HttpResponseBadRequest(_("Enrollment mode not supported"))

        if requested_mode == 'audit':
            # If the learner has arrived at this screen via the traditional enrollment workflow,
            # then they should already be enrolled in an audit mode for the course, assuming one has
            # been configured.  However, alternative enrollment workflows have been introduced into the
            # system, such as third-party discovery.  These workflows result in learners arriving
            # directly at this screen, and they will not necessarily be pre-enrolled in the audit mode.
            CourseEnrollment.enroll(request.user, course_key, CourseMode.AUDIT)
            return redirect(reverse('dashboard'))

        if requested_mode == 'honor':
            CourseEnrollment.enroll(user, course_key, mode=requested_mode)
            return redirect(reverse('dashboard'))

        mode_info = allowed_modes[requested_mode]

        if requested_mode == 'verified':
            amount = request.POST.get("contribution") or \
                request.POST.get("contribution-other-amt") or 0

            try:
                # Validate the amount passed in and force it into two digits
                amount_value = decimal.Decimal(amount).quantize(
                    decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN)
            except decimal.InvalidOperation:
                error_msg = _("Invalid amount selected.")
                return self.get(request, course_id, error=error_msg)

            # Check for minimum pricing
            if amount_value < mode_info.min_price:
                error_msg = _(
                    "No selected price or selected price is too low.")
                return self.get(request, course_id, error=error_msg)

            donation_for_course = request.session.get("donation_for_course",
                                                      {})
            donation_for_course[unicode(course_key)] = amount_value
            request.session["donation_for_course"] = donation_for_course

            return redirect(
                reverse('verify_student_start_flow',
                        kwargs={'course_id': unicode(course_key)}))
Exemplo n.º 52
0
 def test_eligible_for_cert(self, mode_slug, expected_eligibility):
     """Verify that non-audit modes are eligible for a cert."""
     self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug),
                      expected_eligibility)
Exemplo n.º 53
0
class PaidCourseRegistrationTest(ModuleStoreTestCase):
    def setUp(self):
        self.user = UserFactory.create()
        self.course_id = "MITx/999/Robot_Super_Course"
        self.cost = 40
        self.course = CourseFactory.create(org='MITx',
                                           number='999',
                                           display_name='Robot Super Course')
        self.course_mode = CourseMode(course_id=self.course_id,
                                      mode_slug="honor",
                                      mode_display_name="honor cert",
                                      min_price=self.cost)
        self.course_mode.save()
        self.cart = Order.get_cart_for_user(self.user)

    def test_add_to_order(self):
        reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id)

        self.assertEqual(reg1.unit_cost, self.cost)
        self.assertEqual(reg1.line_cost, self.cost)
        self.assertEqual(reg1.unit_cost, self.course_mode.min_price)
        self.assertEqual(reg1.mode, "honor")
        self.assertEqual(reg1.user, self.user)
        self.assertEqual(reg1.status, "cart")
        self.assertTrue(
            PaidCourseRegistration.contained_in_order(self.cart,
                                                      self.course_id))
        self.assertFalse(
            PaidCourseRegistration.contained_in_order(self.cart,
                                                      self.course_id + "abcd"))
        self.assertEqual(self.cart.total_cost, self.cost)

    def test_add_with_default_mode(self):
        """
        Tests add_to_cart where the mode specified in the argument is NOT in the database
        and NOT the default "honor".  In this case it just adds the user in the CourseMode.DEFAULT_MODE, 0 price
        """
        reg1 = PaidCourseRegistration.add_to_order(self.cart,
                                                   self.course_id,
                                                   mode_slug="DNE")

        self.assertEqual(reg1.unit_cost, 0)
        self.assertEqual(reg1.line_cost, 0)
        self.assertEqual(reg1.mode, "honor")
        self.assertEqual(reg1.user, self.user)
        self.assertEqual(reg1.status, "cart")
        self.assertEqual(self.cart.total_cost, 0)
        self.assertTrue(
            PaidCourseRegistration.contained_in_order(self.cart,
                                                      self.course_id))

    def test_purchased_callback(self):
        reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
        self.cart.purchase()
        self.assertTrue(CourseEnrollment.is_enrolled(self.user,
                                                     self.course_id))
        reg1 = PaidCourseRegistration.objects.get(
            id=reg1.id)  # reload from DB to get side-effect
        self.assertEqual(reg1.status, "purchased")

    def test_generate_receipt_instructions(self):
        """
        Add 2 courses to the order and make sure the instruction_set only contains 1 element (no dups)
        """
        course2 = CourseFactory.create(org='MITx',
                                       number='998',
                                       display_name='Robot Duper Course')
        course_mode2 = CourseMode(course_id=course2.id,
                                  mode_slug="honor",
                                  mode_display_name="honor cert",
                                  min_price=self.cost)
        course_mode2.save()
        pr1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
        pr2 = PaidCourseRegistration.add_to_order(self.cart, course2.id)
        self.cart.purchase()
        inst_dict, inst_set = self.cart.generate_receipt_instructions()
        self.assertEqual(2, len(inst_dict))
        self.assertEqual(1, len(inst_set))
        self.assertIn("dashboard", inst_set.pop())
        self.assertIn(pr1.pk_with_subclass, inst_dict)
        self.assertIn(pr2.pk_with_subclass, inst_dict)

    def test_purchased_callback_exception(self):
        reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_id)
        reg1.course_id = "changedforsomereason"
        reg1.save()
        with self.assertRaises(PurchasedCallbackException):
            reg1.purchased_callback()
        self.assertFalse(
            CourseEnrollment.is_enrolled(self.user, self.course_id))

        reg1.course_id = "abc/efg/hij"
        reg1.save()
        with self.assertRaises(PurchasedCallbackException):
            reg1.purchased_callback()
        self.assertFalse(
            CourseEnrollment.is_enrolled(self.user, self.course_id))
Exemplo n.º 54
0
 def test_is_verified_slug(self, mode_slug, is_verified):
     # check that mode slug is verified or not
     if is_verified:
         self.assertTrue(CourseMode.is_verified_slug(mode_slug))
     else:
         self.assertFalse(CourseMode.is_verified_slug(mode_slug))
Exemplo n.º 55
0
class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
    """
    Tests about xblock.
    """
    def setUp(self):
        self.course = CourseFactory.create()
        self.about = ItemFactory.create(category="about",
                                        parent_location=self.course.location,
                                        data="OOGIE BLOOGIE",
                                        display_name="overview")
        self.course_without_about = CourseFactory.create(
            catalog_visibility=CATALOG_VISIBILITY_NONE)
        self.about = ItemFactory.create(
            category="about",
            parent_location=self.course_without_about.location,
            data="WITHOUT ABOUT",
            display_name="overview")
        self.course_with_about = CourseFactory.create(
            catalog_visibility=CATALOG_VISIBILITY_ABOUT)
        self.about = ItemFactory.create(
            category="about",
            parent_location=self.course_with_about.location,
            data="WITH ABOUT",
            display_name="overview")

        self.purchase_course = CourseFactory.create(
            org='MITx', number='buyme', display_name='Course To Buy')
        self.course_mode = CourseMode(course_id=self.purchase_course.id,
                                      mode_slug="honor",
                                      mode_display_name="honor cert",
                                      min_price=10)
        self.course_mode.save()

    def test_anonymous_user(self):
        """
        This test asserts that a non-logged in user can visit the course about page
        """
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("OOGIE BLOOGIE", resp.content)

        # Check that registration button is present
        self.assertIn(REG_STR, resp.content)

    def test_logged_in(self):
        """
        This test asserts that a logged-in user can visit the course about page
        """
        self.setup_user()
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("OOGIE BLOOGIE", resp.content)

    def test_already_enrolled(self):
        """
        Asserts that the end user sees the appropriate messaging
        when he/she visits the course about page, but is already enrolled
        """
        self.setup_user()
        self.enroll(self.course, True)
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("You are registered for this course", resp.content)
        self.assertIn("View Courseware", resp.content)

    @override_settings(COURSE_ABOUT_VISIBILITY_PERMISSION="see_about_page")
    def test_visible_about_page_settings(self):
        """
        Verify that the About Page honors the permission settings in the course module
        """
        url = reverse('about_course',
                      args=[self.course_with_about.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertIn("WITH ABOUT", resp.content)

        url = reverse(
            'about_course',
            args=[self.course_without_about.id.to_deprecated_string()])
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 404)

    @patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
    def test_logged_in_marketing(self):
        self.setup_user()
        url = reverse('about_course',
                      args=[self.course.id.to_deprecated_string()])
        resp = self.client.get(url)
        # should be redirected
        self.assertEqual(resp.status_code, 302)
        # follow this time, and check we're redirected to the course info page
        resp = self.client.get(url, follow=True)
        target_url = resp.redirect_chain[-1][0]
        info_url = reverse('info',
                           args=[self.course.id.to_deprecated_string()])
        self.assertTrue(target_url.endswith(info_url))
Exemplo n.º 56
0
 def test_course_is_professional_mode_with_invalid_tuple(self):
     # check that tuple has professional mode with None
     self.assertFalse(CourseMode.is_professional_mode(None))
Exemplo n.º 57
0
def change_enrollment(request, check_access=True):
    """
    Modify the enrollment status for the logged-in user.

    TODO: This is lms specific and does not belong in common code.

    The request parameter must be a POST request (other methods return 405)
    that specifies course_id and enrollment_action parameters. If course_id or
    enrollment_action is not specified, if course_id is not valid, if
    enrollment_action is something other than "enroll" or "unenroll", if
    enrollment_action is "enroll" and enrollment is closed for the course, or
    if enrollment_action is "unenroll" and the user is not enrolled in the
    course, a 400 error will be returned. If the user is not logged in, 403
    will be returned; it is important that only this case return 403 so the
    front end can redirect the user to a registration or login page when this
    happens. This function should only be called from an AJAX request, so
    the error messages in the responses should never actually be user-visible.

    Args:
        request (`Request`): The Django request object

    Keyword Args:
        check_access (boolean): If True, we check that an accessible course actually
            exists for the given course_key before we enroll the student.
            The default is set to False to avoid breaking legacy code or
            code with non-standard flows (ex. beta tester invitations), but
            for any standard enrollment flow you probably want this to be True.

    Returns:
        Response

    """
    # Get the user
    user = request.user

    # Ensure the user is authenticated
    if not user.is_authenticated:
        return HttpResponseForbidden()

    # Ensure we received a course_id
    action = request.POST.get("enrollment_action")
    if 'course_id' not in request.POST:
        return HttpResponseBadRequest(_("Course id not specified"))

    try:
        course_id = CourseKey.from_string(request.POST.get("course_id"))
    except InvalidKeyError:
        log.warning(
            u"User %s tried to %s with invalid course id: %s",
            user.username,
            action,
            request.POST.get("course_id"),
        )
        return HttpResponseBadRequest(_("Invalid course id"))

    # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features
    # on a per-course basis.
    monitoring_utils.set_custom_metric('course_id', text_type(course_id))

    if action == "enroll":
        # Make sure the course exists
        # We don't do this check on unenroll, or a bad course id can't be unenrolled from
        if not modulestore().has_course(course_id):
            log.warning(u"User %s tried to enroll in non-existent course %s",
                        user.username, course_id)
            return HttpResponseBadRequest(_("Course id is invalid"))

        # Record the user's email opt-in preference
        if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'):
            _update_email_opt_in(request, course_id.org)

        available_modes = CourseMode.modes_for_course_dict(course_id)

        # Check whether the user is blocked from enrolling in this course
        # This can occur if the user's IP is on a global blacklist
        # or if the user is enrolling in a country in which the course
        # is not available.
        redirect_url = embargo_api.redirect_if_blocked(
            course_id, user=user, ip_address=get_ip(request), url=request.path)
        if redirect_url:
            return HttpResponse(redirect_url)

        if CourseEntitlement.check_for_existing_entitlement_and_enroll(
                user=user, course_run_key=course_id):
            return HttpResponse(
                reverse('courseware', args=[unicode(course_id)]))

        # Check that auto enrollment is allowed for this course
        # (= the course is NOT behind a paywall)
        if CourseMode.can_auto_enroll(course_id):
            # Enroll the user using the default mode (audit)
            # We're assuming that users of the course enrollment table
            # will NOT try to look up the course enrollment model
            # by its slug.  If they do, it's possible (based on the state of the database)
            # for no such model to exist, even though we've set the enrollment type
            # to "audit".
            try:
                enroll_mode = CourseMode.auto_enroll_mode(
                    course_id, available_modes)
                if enroll_mode:
                    CourseEnrollment.enroll(user,
                                            course_id,
                                            check_access=check_access,
                                            mode=enroll_mode)
            except Exception:  # pylint: disable=broad-except
                return HttpResponseBadRequest(_("Could not enroll"))

        # If we have more than one course mode or professional ed is enabled,
        # then send the user to the choose your track page.
        # (In the case of no-id-professional/professional ed, this will redirect to a page that
        # funnels users directly into the verification / payment flow)
        if CourseMode.has_verified_mode(
                available_modes) or CourseMode.has_professional_mode(
                    available_modes):
            return HttpResponse(
                reverse("course_modes_choose",
                        kwargs={'course_id': text_type(course_id)}))

        # Otherwise, there is only one mode available (the default)
        return HttpResponse()
    elif action == "unenroll":
        enrollment = CourseEnrollment.get_enrollment(user, course_id)
        if not enrollment:
            return HttpResponseBadRequest(
                _("You are not enrolled in this course"))

        certificate_info = cert_info(user, enrollment.course_overview)
        if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES:
            return HttpResponseBadRequest(
                _("Your certificate prevents you from unenrolling from this course"
                  ))

        CourseEnrollment.unenroll(user, course_id)
        REFUND_ORDER.send(sender=None, course_enrollment=enrollment)
        return HttpResponse()
    else:
        return HttpResponseBadRequest(_("Enrollment action is invalid"))
Exemplo n.º 58
0
def instructor_dashboard_2(request, course_id):
    """ Display the instructor dashboard for a course. """
    try:
        course_key = CourseKey.from_string(course_id)
    except InvalidKeyError:
        log.error(u"Unable to find course with course key %s while loading the Instructor Dashboard.", course_id)
        return HttpResponseServerError()

    course = get_course_by_id(course_key, depth=0)

    access = {
        'admin': request.user.is_staff,
        'instructor': bool(has_access(request.user, 'instructor', course)),
        'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
        'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user),
        'staff': bool(has_access(request.user, 'staff', course)),
        'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR),
    }

    if not access['staff']:
        raise Http404()

    is_white_label = CourseMode.is_white_label(course_key)

    reports_enabled = configuration_helpers.get_value('SHOW_ECOMMERCE_REPORTS', False)

    sections = [
        _section_course_info(course, access),
        _section_membership(course, access),
        _section_cohort_management(course, access),
        _section_discussions_management(course, access),
        _section_student_admin(course, access),
        _section_data_download(course, access),
    ]

    analytics_dashboard_message = None
    if show_analytics_dashboard_message(course_key):
        # Construct a URL to the external analytics dashboard
        analytics_dashboard_url = '{0}/courses/{1}'.format(settings.ANALYTICS_DASHBOARD_URL, unicode(course_key))
        link_start = HTML(u"<a href=\"{}\" target=\"_blank\">").format(analytics_dashboard_url)
        analytics_dashboard_message = _(
            u"To gain insights into student enrollment and participation {link_start}"
            u"visit {analytics_dashboard_name}, our new course analytics product{link_end}."
        )
        analytics_dashboard_message = Text(analytics_dashboard_message).format(
            link_start=link_start, link_end=HTML("</a>"), analytics_dashboard_name=settings.ANALYTICS_DASHBOARD_NAME)

        # Temporarily show the "Analytics" section until we have a better way of linking to Insights
        sections.append(_section_analytics(course, access))

    # Check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course
    course_mode_has_price = False
    paid_modes = CourseMode.paid_modes_for_course(course_key)
    if len(paid_modes) == 1:
        course_mode_has_price = True
    elif len(paid_modes) > 1:
        log.error(
            u"Course %s has %s course modes with payment options. Course must only have "
            u"one paid course mode to enable eCommerce options.",
            unicode(course_key), len(paid_modes)
        )

    if settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']:
        sections.insert(3, _section_extensions(course))

    # Gate access to course email by feature flag & by course-specific authorization
    if BulkEmailFlag.feature_enabled(course_key):
        sections.append(_section_send_email(course, access))

    # Gate access to Metrics tab by featue flag and staff authorization
    if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
        sections.append(_section_metrics(course, access))

    # Gate access to Ecommerce tab
    if course_mode_has_price and (access['finance_admin'] or access['sales_admin']):
        sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label, reports_enabled))

    # Gate access to Special Exam tab depending if either timed exams or proctored exams
    # are enabled in the course

    user_has_access = any([
        request.user.is_staff,
        CourseStaffRole(course_key).has_user(request.user),
        CourseInstructorRole(course_key).has_user(request.user)
    ])
    course_has_special_exams = course.enable_proctored_exams or course.enable_timed_exams
    can_see_special_exams = course_has_special_exams and user_has_access and settings.FEATURES.get(
        'ENABLE_SPECIAL_EXAMS', False)

    if can_see_special_exams:
        sections.append(_section_special_exams(course, access))

    # Certificates panel
    # This is used to generate example certificates
    # and enable self-generated certificates for a course.
    # Note: This is hidden for all CCXs
    certs_enabled = CertificateGenerationConfiguration.current().enabled and not hasattr(course_key, 'ccx')
    if certs_enabled and access['admin']:
        sections.append(_section_certificates(course))

    openassessment_blocks = modulestore().get_items(
        course_key, qualifiers={'category': 'openassessment'}
    )
    # filter out orphaned openassessment blocks
    openassessment_blocks = [
        block for block in openassessment_blocks if block.parent is not None
    ]
    if len(openassessment_blocks) > 0:
        sections.append(_section_open_response_assessment(request, course, openassessment_blocks, access))

    disable_buttons = not _is_small_course(course_key)

    certificate_white_list = CertificateWhitelist.get_certificate_white_list(course_key)
    generate_certificate_exceptions_url = reverse(
        'generate_certificate_exceptions',
        kwargs={'course_id': unicode(course_key), 'generate_for': ''}
    )
    generate_bulk_certificate_exceptions_url = reverse(
        'generate_bulk_certificate_exceptions',
        kwargs={'course_id': unicode(course_key)}
    )
    certificate_exception_view_url = reverse(
        'certificate_exception_view',
        kwargs={'course_id': unicode(course_key)}
    )

    certificate_invalidation_view_url = reverse(
        'certificate_invalidation_view',
        kwargs={'course_id': unicode(course_key)}
    )

    certificate_invalidations = CertificateInvalidation.get_certificate_invalidations(course_key)

    context = {
        'course': course,
        'studio_url': get_studio_url(course, 'course'),
        'sections': sections,
        'disable_buttons': disable_buttons,
        'analytics_dashboard_message': analytics_dashboard_message,
        'certificate_white_list': certificate_white_list,
        'certificate_invalidations': certificate_invalidations,
        'generate_certificate_exceptions_url': generate_certificate_exceptions_url,
        'generate_bulk_certificate_exceptions_url': generate_bulk_certificate_exceptions_url,
        'certificate_exception_view_url': certificate_exception_view_url,
        'certificate_invalidation_view_url': certificate_invalidation_view_url,
    }

    return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
Exemplo n.º 59
0
class PaidCourseRegistrationTest(ModuleStoreTestCase):
    def setUp(self):
        self.user = UserFactory.create()
        self.cost = 40
        self.course = CourseFactory.create()
        self.course_key = self.course.id
        self.course_mode = CourseMode(course_id=self.course_key,
                                      mode_slug="honor",
                                      mode_display_name="honor cert",
                                      min_price=self.cost)
        self.course_mode.save()
        self.cart = Order.get_cart_for_user(self.user)

    def test_add_to_order(self):
        reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)

        self.assertEqual(reg1.unit_cost, self.cost)
        self.assertEqual(reg1.line_cost, self.cost)
        self.assertEqual(reg1.unit_cost, self.course_mode.min_price)
        self.assertEqual(reg1.mode, "honor")
        self.assertEqual(reg1.user, self.user)
        self.assertEqual(reg1.status, "cart")
        self.assertTrue(
            PaidCourseRegistration.contained_in_order(self.cart,
                                                      self.course_key))
        self.assertFalse(
            PaidCourseRegistration.contained_in_order(
                self.cart,
                CourseLocator(org="MITx",
                              course="999",
                              run="Robot_Super_Course_abcd")))

        self.assertEqual(self.cart.total_cost, self.cost)

    def test_cart_type_business(self):
        self.cart.order_type = 'business'
        self.cart.save()
        item = CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
        self.cart.purchase()
        self.assertFalse(
            CourseEnrollment.is_enrolled(self.user, self.course_key))
        # check that the registration codes are generated against the order
        self.assertEqual(
            len(CourseRegistrationCode.objects.filter(order=self.cart)),
            item.qty)

    def test_add_with_default_mode(self):
        """
        Tests add_to_cart where the mode specified in the argument is NOT in the database
        and NOT the default "honor".  In this case it just adds the user in the CourseMode.DEFAULT_MODE, 0 price
        """
        reg1 = PaidCourseRegistration.add_to_order(self.cart,
                                                   self.course_key,
                                                   mode_slug="DNE")

        self.assertEqual(reg1.unit_cost, 0)
        self.assertEqual(reg1.line_cost, 0)
        self.assertEqual(reg1.mode, "honor")
        self.assertEqual(reg1.user, self.user)
        self.assertEqual(reg1.status, "cart")
        self.assertEqual(self.cart.total_cost, 0)
        self.assertTrue(
            PaidCourseRegistration.contained_in_order(self.cart,
                                                      self.course_key))

        course_reg_code_item = CourseRegCodeItem.add_to_order(self.cart,
                                                              self.course_key,
                                                              2,
                                                              mode_slug="DNE")

        self.assertEqual(course_reg_code_item.unit_cost, 0)
        self.assertEqual(course_reg_code_item.line_cost, 0)
        self.assertEqual(course_reg_code_item.mode, "honor")
        self.assertEqual(course_reg_code_item.user, self.user)
        self.assertEqual(course_reg_code_item.status, "cart")
        self.assertEqual(self.cart.total_cost, 0)
        self.assertTrue(
            CourseRegCodeItem.contained_in_order(self.cart, self.course_key))

    def test_add_course_reg_item_with_no_course_item(self):
        fake_course_id = CourseLocator(org="edx", course="fake", run="course")
        with self.assertRaises(CourseDoesNotExistException):
            CourseRegCodeItem.add_to_order(self.cart, fake_course_id, 2)

    def test_course_reg_item_already_in_cart(self):
        CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)
        with self.assertRaises(ItemAlreadyInCartException):
            CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)

    def test_course_reg_item_already_enrolled_in_course(self):
        CourseEnrollment.enroll(self.user, self.course_key)
        with self.assertRaises(AlreadyEnrolledInCourseException):
            CourseRegCodeItem.add_to_order(self.cart, self.course_key, 2)

    def test_purchased_callback(self):
        reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
        self.cart.purchase()
        self.assertTrue(
            CourseEnrollment.is_enrolled(self.user, self.course_key))
        reg1 = PaidCourseRegistration.objects.get(
            id=reg1.id)  # reload from DB to get side-effect
        self.assertEqual(reg1.status, "purchased")

    def test_generate_receipt_instructions(self):
        """
        Add 2 courses to the order and make sure the instruction_set only contains 1 element (no dups)
        """
        course2 = CourseFactory.create()
        course_mode2 = CourseMode(course_id=course2.id,
                                  mode_slug="honor",
                                  mode_display_name="honor cert",
                                  min_price=self.cost)
        course_mode2.save()
        pr1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
        pr2 = PaidCourseRegistration.add_to_order(self.cart, course2.id)
        self.cart.purchase()
        inst_dict, inst_set = self.cart.generate_receipt_instructions()
        self.assertEqual(2, len(inst_dict))
        self.assertEqual(1, len(inst_set))
        self.assertIn("dashboard", inst_set.pop())
        self.assertIn(pr1.pk_with_subclass, inst_dict)
        self.assertIn(pr2.pk_with_subclass, inst_dict)

    def test_purchased_callback_exception(self):
        reg1 = PaidCourseRegistration.add_to_order(self.cart, self.course_key)
        reg1.course_id = CourseLocator(org="changed",
                                       course="forsome",
                                       run="reason")
        reg1.save()
        with self.assertRaises(PurchasedCallbackException):
            reg1.purchased_callback()
        self.assertFalse(
            CourseEnrollment.is_enrolled(self.user, self.course_key))

        reg1.course_id = CourseLocator(org="abc", course="efg", run="hij")
        reg1.save()
        with self.assertRaises(PurchasedCallbackException):
            reg1.purchased_callback()
        self.assertFalse(
            CourseEnrollment.is_enrolled(self.user, self.course_key))

        course_reg_code_item = CourseRegCodeItem.add_to_order(
            self.cart, self.course_key, 2)
        course_reg_code_item.course_id = CourseLocator(org="changed1",
                                                       course="forsome1",
                                                       run="reason1")
        course_reg_code_item.save()
        with self.assertRaises(PurchasedCallbackException):
            course_reg_code_item.purchased_callback()

    def test_user_cart_has_both_items(self):
        """
        This test exists b/c having both CertificateItem and PaidCourseRegistration in an order used to break
        PaidCourseRegistration.contained_in_order
        """
        cart = Order.get_cart_for_user(self.user)
        CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor')
        PaidCourseRegistration.add_to_order(self.cart, self.course_key)
        self.assertTrue(
            PaidCourseRegistration.contained_in_order(cart, self.course_key))
Exemplo n.º 60
0
class TestInstructorDashboardPerformance(ModuleStoreTestCase,
                                         LoginEnrollmentTestCase,
                                         XssTestMixin):
    """
    Tests for the instructor dashboard from the performance point of view.
    """
    MODULESTORE = TEST_DATA_SPLIT_MODULESTORE

    def setUp(self):
        """
        Set up tests
        """
        super(TestInstructorDashboardPerformance, self).setUp()
        self.course = CourseFactory.create(
            grading_policy={
                "GRADE_CUTOFFS": {
                    "A": 0.75,
                    "B": 0.63,
                    "C": 0.57,
                    "D": 0.5
                }
            },
            display_name='<script>alert("XSS")</script>',
            default_store=ModuleStoreEnum.Type.split)

        self.course_mode = CourseMode(
            course_id=self.course.id,
            mode_slug=CourseMode.DEFAULT_MODE_SLUG,
            mode_display_name=CourseMode.DEFAULT_MODE.name,
            min_price=40)
        self.course_mode.save()
        # Create instructor account
        self.instructor = AdminFactory.create()
        self.client.login(username=self.instructor.username, password="******")

    def test_spoc_gradebook_mongo_calls(self):
        """
        Test that the MongoDB cache is used in API to return grades
        """
        # prepare course structure
        course = ItemFactory.create(
            parent_location=self.course.location,
            category="course",
            display_name="Test course",
        )

        students = []
        for i in xrange(20):
            username = "******" % i
            student = UserFactory.create(username=username)
            CourseEnrollmentFactory.create(user=student,
                                           course_id=self.course.id)
            students.append(student)

        chapter = ItemFactory.create(
            parent=course,
            category='chapter',
            display_name="Chapter",
            publish_item=True,
            start=datetime.datetime(2015, 3, 1, tzinfo=UTC),
        )
        sequential = ItemFactory.create(
            parent=chapter,
            category='sequential',
            display_name="Lesson",
            publish_item=True,
            start=datetime.datetime(2015, 3, 1, tzinfo=UTC),
            metadata={
                'graded': True,
                'format': 'Homework'
            },
        )
        vertical = ItemFactory.create(
            parent=sequential,
            category='vertical',
            display_name='Subsection',
            publish_item=True,
            start=datetime.datetime(2015, 4, 1, tzinfo=UTC),
        )
        for i in xrange(10):
            problem = ItemFactory.create(
                category="problem",
                parent=vertical,
                display_name="A Problem Block %d" % i,
                weight=1,
                publish_item=False,
                metadata={'rerandomize': 'always'},
            )
            for j in students:
                grade = i % 2
                StudentModuleFactory.create(grade=grade,
                                            max_grade=1,
                                            student=j,
                                            course_id=self.course.id,
                                            module_state_key=problem.location)

        # check MongoDB calls count
        url = reverse('spoc_gradebook', kwargs={'course_id': self.course.id})
        with check_mongo_calls(7):
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)