def test_errors(self, has_perm, post_params, error_msg, status_code, mock_has_perm): """ Test the error template is rendered on different types of errors. When the chosen CourseMode is 'honor' or 'audit' via POST, it redirects to dashboard, but if there's an error in the process, it shows the error template. If the user does not have permission to enroll, GET is called with error message, but it also redirects to dashboard. """ # Create course modes for mode in ('audit', 'honor', 'verified'): CourseModeFactory.create(mode_slug=mode, course_id=self.course.id) # Value Prop TODO (REV-2378): remove waffle flag from tests once flag is removed. with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True): mock_has_perm.return_value = has_perm url = reverse('course_modes_choose', args=[str(self.course.id)]) # Choose mode (POST request) response = self.client.post(url, post_params) self.assertEqual(response.status_code, status_code) if has_perm: self.assertContains(response, error_msg) self.assertContains(response, 'Sorry, we were unable to enroll you') # Check for CTA button on error page marketing_root = settings.MKTG_URLS.get('ROOT') search_courses_url = urljoin(marketing_root, '/search?tab=course') self.assertContains(response, search_courses_url) self.assertContains(response, '<span>Explore all courses</span>') else: self.assertTrue(CourseEnrollment.is_enrollment_closed(self.user, self.course))
def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=too-many-statements """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_client_ip(request)[0], url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) increment('track-selection.{}.{}'.format( enrollment_mode, 'active' if is_active else 'inactive')) increment('track-selection.views') if enrollment_mode is None: LOG.info( 'Rendering track selection for unenrolled user, referred by %s', request.META.get('HTTP_REFERER')) 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 'no-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") redirect_url = IDVerificationService.get_verify_location( course_id=course_key) 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.get_checkout_page_url( professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.bulk_sku) return redirect(redirect_url) course = modulestore().get_course(course_key) # 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 self._redirect_to_course_or_dashboard( course, course_key, request.user) # 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 self._redirect_to_course_or_dashboard( course, course_key, request.user) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(str(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 = six.moves.urllib.parse.urlencode( {'course_closed': enrollment_end_date}) return redirect('{}?{}'.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 university 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 = str(course_key) gated_content = ContentTypeGatingConfig.enabled_for_enrollment( user=request.user, course_key=course_key) 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, "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, "content_gating_enabled": gated_content, "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment( request.user, course), } context.update( get_experiment_user_metadata_context( course, request.user, )) title_content = '' if enrollment_mode: title_content = _( "Congratulations! You are now enrolled in {course_name}" ).format(course_name=course.display_name_with_default) 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() ] price_before_discount = verified_mode.min_price course_price = price_before_discount enterprise_customer = enterprise_customer_for_request(request) LOG.info( '[e-commerce calculate API] Going to hit the API for user [%s] linked to [%s] enterprise', request.user.username, enterprise_customer.get('name') if isinstance( enterprise_customer, dict) else None # Test Purpose ) if enterprise_customer and verified_mode.sku: course_price = get_course_final_price(request.user, verified_mode.sku, price_before_discount) context["currency"] = verified_mode.currency.upper() context["currency_symbol"] = get_currency_symbol( verified_mode.currency.upper()) context["min_price"] = course_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description # if course_price is equal to price_before_discount then user doesn't entitle to any discount. if course_price != price_before_discount: context["price_before_discount"] = price_before_discount 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 context['currency_data'] = [] if waffle.switch_is_active('local_currency'): if 'edx-price-l10n' not in request.COOKIES: currency_data = get_currency_data() try: context['currency_data'] = json.dumps(currency_data) except TypeError: pass language = get_language() context['track_links'] = get_verified_track_links(language) duration = get_user_course_duration(request.user, course) deadline = duration and get_user_course_expiration_date( request.user, course) if deadline: formatted_audit_access_date = strftime_localized_html( deadline, 'SHORT_DATE') context['audit_access_deadline'] = formatted_audit_access_date fbe_is_on = deadline and gated_content # Route to correct Track Selection page. # REV-2133 TODO Value Prop: remove waffle flag after testing is completed # and happy path version is ready to be rolled out to all users. if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled(): if not error: # TODO: Remove by executing REV-2355 if not enterprise_customer_for_request( request): # TODO: Remove by executing REV-2342 if fbe_is_on: return render_to_response("course_modes/fbe.html", context) else: return render_to_response("course_modes/unfbe.html", context) # If error or enterprise_customer, failover to old choose.html page return render_to_response("course_modes/choose.html", context)
def post(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument """ 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 = str(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('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) # Check to see if the User has an entitlement and enroll them if they have one for this course if CourseEntitlement.check_for_existing_entitlement_and_enroll( user=user, course_run_key=course_key): return JsonResponse( { 'redirect_destination': reverse('courseware', args=[str(course_id)]), }, ) # Accept either honor or audit as an enrollment mode to # maintain backwards compatibility with existing courses default_enrollment_mode = audit_mode or honor_mode course_name = None course_announcement = None if course is not None: course_name = course.display_name course_announcement = course.announcement 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, course_name=course_name, username=user.username, announcement=course_announcement) log.info(msg) self._enroll(course_key, user, default_enrollment_mode.slug) mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR # lint-amnesty, pylint: disable=unused-variable 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)
def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) masquerade_object, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) user_is_masquerading = is_masquerading( request.user, course_key, course_masquerade=masquerade_object) course_overview = get_course_overview_or_404(course_key) enrollment = CourseEnrollment.get_enrollment(request.user, course_key) enrollment_mode = getattr(enrollment, 'mode', None) allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled( course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] dates_tab_link = get_learning_mfe_home_url(course_key=course.id, url_fragment='dates') # Set all of the defaults access_expiration = None cert_data = None course_blocks = None course_goals = { 'selected_goal': None, 'weekly_learning_goal_enabled': False, } course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) dates_widget = { 'course_date_blocks': [], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } enroll_alert = { 'can_enroll': True, 'extra_text': None, } handouts_html = None offer_data = None resume_course = { 'has_visited_course': False, 'url': None, } welcome_message_html = None is_enrolled = enrollment and enrollment.is_active is_staff = bool(has_access(request.user, 'staff', course_key)) show_enrolled = is_enrolled or is_staff enable_proctored_exams = False if show_enrolled: course_blocks = get_course_outline_block_tree( request, course_key_string, request.user) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) dates_widget['course_date_blocks'] = [ block for block in date_blocks if not isinstance(block, TodaysDate) ] handouts_html = get_course_info_section(request, request.user, course, 'handouts') welcome_message_html = get_current_update_for_user(request, course) offer_data = generate_offer_data(request.user, course_overview) access_expiration = get_access_expiration_data( request.user, course_overview) cert_data = get_cert_data(request.user, course, enrollment.mode) if is_enrolled else None enable_proctored_exams = course_overview.enable_proctored_exams if (is_enrolled and ENABLE_COURSE_GOALS.is_enabled(course_key)): course_goals['weekly_learning_goal_enabled'] = True selected_goal = get_course_goal(request.user, course_key) if selected_goal: course_goals['selected_goal'] = { 'days_per_week': selected_goal.days_per_week, 'subscribed_to_reminders': selected_goal.subscribed_to_reminders, } try: resume_block = get_key_to_last_completed_block( request.user, course.id) resume_course['has_visited_course'] = True resume_path = reverse('jump_to', kwargs={ 'course_id': course_key_string, 'location': str(resume_block) }) resume_course['url'] = request.build_absolute_uri(resume_path) except UnavailableCompletionData: start_block = get_start_block(course_blocks) resume_course['url'] = start_block['lms_web_url'] elif allow_public_outline or allow_public or user_is_masquerading: course_blocks = get_course_outline_block_tree( request, course_key_string, None) if allow_public or user_is_masquerading: handouts_html = get_course_info_section( request, request.user, course, 'handouts') if not is_enrolled: if CourseMode.is_masters_only(course_key): enroll_alert['can_enroll'] = False enroll_alert['extra_text'] = _( 'Please contact your degree administrator or ' '{platform_name} Support if you have questions.').format( platform_name=settings.PLATFORM_NAME) elif CourseEnrollment.is_enrollment_closed(request.user, course_overview): enroll_alert['can_enroll'] = False elif CourseEnrollment.objects.is_course_full(course_overview): enroll_alert['can_enroll'] = False enroll_alert['extra_text'] = _('Course is full') # Sometimes there are sequences returned by Course Blocks that we # don't actually want to show to the user, such as when a sequence is # composed entirely of units that the user can't access. The Learning # Sequences API knows how to roll this up, so we use it determine which # sequences we should remove from course_blocks. # # The long term goal is to remove the Course Blocks API call entirely, # so this is a tiny first step in that migration. if course_blocks: user_course_outline = get_user_course_outline( course_key, request.user, datetime.now(tz=timezone.utc)) available_seq_ids = { str(usage_key) for usage_key in user_course_outline.sequences } # course_blocks is a reference to the root of the course, so we go # through the chapters (sections) to look for sequences to remove. for chapter_data in course_blocks.get('children', []): chapter_data['children'] = [ seq_data for seq_data in chapter_data['children'] if (seq_data['id'] in available_seq_ids or # Edge case: Sometimes we have weird course structures. # We expect only sequentials here, but if there is # another type, just skip it (don't filter it out). seq_data['type'] != 'sequential') ] if 'children' in chapter_data else [] user_has_passing_grade = False if not request.user.is_anonymous: user_grade = CourseGradeFactory().read(request.user, course) if user_grade: user_has_passing_grade = user_grade.passed data = { 'access_expiration': access_expiration, 'cert_data': cert_data, 'course_blocks': course_blocks, 'course_goals': course_goals, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enable_proctored_exams': enable_proctored_exams, 'enroll_alert': enroll_alert, 'enrollment_mode': enrollment_mode, 'handouts_html': handouts_html, 'has_ended': course.has_ended(), 'offer': offer_data, 'resume_course': resume_course, 'user_has_passing_grade': user_has_passing_grade, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_overview'] = course_overview context['enable_links'] = show_enrolled or allow_public context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)