diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index f8b287858c..3a829d37ca 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -24,6 +24,9 @@ _ = lambda text: text DEFAULT_START_DATE = datetime(2030, 1, 1, tzinfo=UTC()) +CATALOG_VISIBILITY_CATALOG_AND_ABOUT = "both" +CATALOG_VISIBILITY_ABOUT = "about" +CATALOG_VISIBILITY_NONE = "none" class StringOrDate(Date): def from_json(self, value): @@ -575,6 +578,17 @@ class CourseFields(object): deprecated=True ) + catalog_visibility = String( + display_name=_("Course Visibility In Catalog"), + help=_("Defines the access permissions for showing the course in the course catalog. This can be set to one of three values: 'both' (show in catalog and allow access to about page), 'about' (only allow access to about page), 'none' (do not show in catalog and do not allow access to an about page)."), + default=CATALOG_VISIBILITY_CATALOG_AND_ABOUT, + scope=Scope.settings, + values=[ + {"display_name": _("Both"), "value": CATALOG_VISIBILITY_CATALOG_AND_ABOUT}, + {"display_name": _("About"), "value": CATALOG_VISIBILITY_ABOUT}, + {"display_name": _("None"), "value": CATALOG_VISIBILITY_NONE}] + ) + class CourseDescriptor(CourseFields, SequenceDescriptor): module_class = SequenceModule diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index c29d76e533..93bcbf7c1f 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -8,7 +8,9 @@ import pytz from django.conf import settings from django.contrib.auth.models import AnonymousUser -from xmodule.course_module import CourseDescriptor +from xmodule.course_module import ( + CourseDescriptor, CATALOG_VISIBILITY_CATALOG_AND_ABOUT, + CATALOG_VISIBILITY_ABOUT) from xmodule.error_module import ErrorDescriptor from xmodule.x_module import XModule @@ -110,6 +112,8 @@ def _has_access_course_desc(user, action, course): ACCESS_REQUIRE_STAFF_FOR_COURSE, 'see_exists' -- can see that the course exists. 'staff' -- staff access to course. + 'see_in_catalog' -- user is able to see the course listed in the course catalog. + 'see_about_page' -- user is able to see the course about page. """ def can_load(): """ @@ -204,6 +208,29 @@ def _has_access_course_desc(user, action, course): return can_enroll() or can_load() + def can_see_in_catalog(): + """ + Implements the "can see course in catalog" logic if a course should be visible in the main course catalog + In this case we use the catalog_visibility property on the course descriptor + but also allow course staff to see this. + """ + return ( + course.catalog_visibility == CATALOG_VISIBILITY_CATALOG_AND_ABOUT or + _has_staff_access_to_descriptor(user, course, course.id) + ) + + def can_see_about_page(): + """ + Implements the "can see course about page" logic if a course about page should be visible + In this case we use the catalog_visibility property on the course descriptor + but also allow course staff to see this. + """ + return ( + course.catalog_visibility == CATALOG_VISIBILITY_CATALOG_AND_ABOUT or + course.catalog_visibility == CATALOG_VISIBILITY_ABOUT or + _has_staff_access_to_descriptor(user, course, course.id) + ) + checkers = { 'load': can_load, 'load_forum': can_load_forum, @@ -211,6 +238,8 @@ def _has_access_course_desc(user, action, course): 'see_exists': see_exists, 'staff': lambda: _has_staff_access_to_descriptor(user, course, course.id), 'instructor': lambda: _has_instructor_access_to_descriptor(user, course, course.id), + 'see_in_catalog': can_see_in_catalog, + 'see_about_page': can_see_about_page, } return _dispatch(checkers, action, user, course) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 27ceb530f3..33b8548067 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -16,6 +16,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from static_replace import replace_static_urls from xmodule.modulestore import ModuleStoreEnum from xmodule.x_module import STUDENT_VIEW +from microsite_configuration import microsite from courseware.access import has_access from courseware.model_data import FieldDataCache @@ -256,7 +257,7 @@ def get_course_info_section_module(request, course, section_key): log_if_not_found=False, wrap_xmodule_display=False, static_asset_path=course.static_asset_path - ) + ) def get_course_info_section(request, course, section_key): """ @@ -345,7 +346,13 @@ def get_courses(user, domain=None): Returns a list of courses available, sorted by course.number ''' courses = branding.get_visible_courses() - courses = [c for c in courses if has_access(user, 'see_exists', c)] + + permission_name = microsite.get_value( + 'COURSE_CATALOG_VISIBILITY_PERMISSION', + settings.COURSE_CATALOG_VISIBILITY_PERMISSION + ) + + courses = [c for c in courses if has_access(user, permission_name, c)] courses = sorted(courses, key=lambda course: course.number) diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py index 164c5be340..bd4cb51013 100644 --- a/lms/djangoapps/courseware/tests/test_about.py +++ b/lms/djangoapps/courseware/tests/test_about.py @@ -15,6 +15,12 @@ from courseware.tests.modulestore_config import TEST_DATA_MONGO_MODULESTORE, TES from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from opaque_keys.edx.locations import SlashSeparatedCourseKey from student.tests.factories import UserFactory, CourseEnrollmentAllowedFactory +from course_modes.models import CourseMode +from student.models import CourseEnrollment + +from shoppingcart.models import Order, PaidCourseRegistration + +from xmodule.course_module import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE # HTML for registration button REG_STR = "
" @@ -33,15 +39,28 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): 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" + ) - def test_logged_in(self): - 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) + 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) @@ -50,6 +69,43 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): # 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() @@ -261,3 +317,155 @@ class AboutWithClosedEnrollment(ModuleStoreTestCase): # Check that registration button is not present self.assertNotIn(REG_STR, resp.content) + + +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +@patch.dict(settings.FEATURES, {'ENABLE_SHOPPING_CART': True}) +@patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True}) +class AboutPurchaseCourseTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): + """ + This test class runs through a suite of verifications regarding + purchaseable courses + """ + def setUp(self): + super(AboutPurchaseCourseTestCase, self).setUp() + self.course = CourseFactory.create(org='MITx', number='buyme', display_name='Course To Buy') + self._set_ecomm(self.course) + + 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() + + def test_anonymous_user(self): + """ + Make sure an anonymous user sees the purchase button + """ + url = reverse('about_course', args=[self.course.id.to_deprecated_string()]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn("Add buyme to Cart ($10)", resp.content) + + def test_logged_in(self): + """ + Make sure a logged in user sees the purchase button + """ + 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("Add buyme to Cart ($10)", resp.content) + + def test_already_in_cart(self): + """ + This makes sure if a user has this course in the cart, that the expected message + appears + """ + self.setup_user() + cart = Order.get_cart_for_user(self.user) + PaidCourseRegistration.add_to_order(cart, self.course.id) + + url = reverse('about_course', args=[self.course.id.to_deprecated_string()]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn("This course is in your", resp.content) + self.assertNotIn("Add buyme to Cart ($10)", resp.content) + + def test_already_enrolled(self): + """ + This makes sure that the already enrolled message appears for paywalled courses + """ + self.setup_user() + + # note that we can't call self.enroll here since that goes through + # the Django student views, which doesn't allow for enrollments + # for paywalled courses + CourseEnrollment.enroll(self.user, self.course.id) + + 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) + self.assertNotIn("Add buyme to Cart ($10)", resp.content) + + def test_closed_enrollment(self): + """ + This makes sure that paywalled courses also honor the registration + window + """ + self.setup_user() + now = datetime.datetime.now(pytz.UTC) + tomorrow = now + datetime.timedelta(days=1) + nextday = tomorrow + datetime.timedelta(days=1) + + self.course.enrollment_start = tomorrow + self.course.enrollment_end = nextday + self.course = self.update_course(self.course, self.user.id) + + url = reverse('about_course', args=[self.course.id.to_deprecated_string()]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn("Enrollment is Closed", resp.content) + self.assertNotIn("Add buyme to Cart ($10)", resp.content) + + def test_invitation_only(self): + """ + This makes sure that the invitation only restirction takes prescendence over + any purchase enablements + """ + course = CourseFactory.create(metadata={"invitation_only": True}) + self._set_ecomm(course) + self.setup_user() + + url = reverse('about_course', args=[course.id.to_deprecated_string()]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn("Enrollment in this course is by invitation only", resp.content) + + def test_enrollment_cap(self): + """ + Make sure that capped enrollments work even with + paywalled courses + """ + course = CourseFactory.create( + metadata={ + "max_student_enrollments_allowed": 1, + "display_coursenumber": "buyme", + } + ) + self._set_ecomm(course) + + self.setup_user() + url = reverse('about_course', args=[course.id.to_deprecated_string()]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn("Add buyme to Cart ($10)", resp.content) + + # note that we can't call self.enroll here since that goes through + # the Django student views, which doesn't allow for enrollments + # for paywalled courses + CourseEnrollment.enroll(self.user, course.id) + + # create a new account since the first account is already registered for the course + email = 'foo_second@test.com' + password = 'bar' + username = 'test_second' + self.create_account(username, + email, password) + self.activate_user(email) + self.login(email, password) + + # Get the about page again and make sure that the page says that the course is full + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertIn("Course is full", resp.content) + self.assertNotIn("Add buyme to Cart ($10)", resp.content) diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py index 3516b3911c..928fcb6c26 100644 --- a/lms/djangoapps/courseware/tests/test_access.py +++ b/lms/djangoapps/courseware/tests/test_access.py @@ -11,8 +11,12 @@ from student.tests.factories import AnonymousUserFactory, CourseEnrollmentAllowe import pytz from opaque_keys.edx.locations import SlashSeparatedCourseKey -# pylint: disable=C0111 +from xmodule.course_module import ( + CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CATALOG_VISIBILITY_ABOUT, + CATALOG_VISIBILITY_NONE) +# pylint: disable=C0111 +# pylint: disable=W0212 class AccessTestCase(TestCase): """ @@ -202,6 +206,43 @@ class AccessTestCase(TestCase): """Ensure has_access handles a user being passed as null""" access.has_access(None, 'staff', 'global', None) + def test__catalog_visibility(self): + """ + Tests the catalog visibility tri-states + """ + user = UserFactory.create() + course_id = SlashSeparatedCourseKey('edX', 'test', '2012_Fall') + staff = StaffFactory.create(course_key=course_id) + + course = Mock( + id=course_id, + catalog_visibility=CATALOG_VISIBILITY_CATALOG_AND_ABOUT + ) + self.assertTrue(access._has_access_course_desc(user, 'see_in_catalog', course)) + self.assertTrue(access._has_access_course_desc(user, 'see_about_page', course)) + self.assertTrue(access._has_access_course_desc(staff, 'see_in_catalog', course)) + self.assertTrue(access._has_access_course_desc(staff, 'see_about_page', course)) + + # Now set visibility to just about page + course = Mock( + id=SlashSeparatedCourseKey('edX', 'test', '2012_Fall'), + catalog_visibility=CATALOG_VISIBILITY_ABOUT + ) + self.assertFalse(access._has_access_course_desc(user, 'see_in_catalog', course)) + self.assertTrue(access._has_access_course_desc(user, 'see_about_page', course)) + self.assertTrue(access._has_access_course_desc(staff, 'see_in_catalog', course)) + self.assertTrue(access._has_access_course_desc(staff, 'see_about_page', course)) + + # Now set visibility to none, which means neither in catalog nor about pages + course = Mock( + id=SlashSeparatedCourseKey('edX', 'test', '2012_Fall'), + catalog_visibility=CATALOG_VISIBILITY_NONE + ) + self.assertFalse(access._has_access_course_desc(user, 'see_in_catalog', course)) + self.assertFalse(access._has_access_course_desc(user, 'see_about_page', course)) + self.assertTrue(access._has_access_course_desc(staff, 'see_in_catalog', course)) + self.assertTrue(access._has_access_course_desc(staff, 'see_about_page', course)) + class UserRoleTestCase(TestCase): """ diff --git a/lms/djangoapps/courseware/tests/test_microsites.py b/lms/djangoapps/courseware/tests/test_microsites.py index 8f58828cb5..2f34de8e14 100644 --- a/lms/djangoapps/courseware/tests/test_microsites.py +++ b/lms/djangoapps/courseware/tests/test_microsites.py @@ -11,6 +11,8 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from helpers import LoginEnrollmentTestCase from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE +from xmodule.course_module import ( + CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CATALOG_VISIBILITY_NONE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase): @@ -42,6 +44,20 @@ class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase): self.course_outside_microsite = CourseFactory.create(display_name='Robot_Course_Outside_Microsite', org='FooX') + # have a course which explicitly sets visibility in catalog to False + self.course_hidden_visibility = CourseFactory.create( + display_name='Hidden_course', + org='TestMicrositeX', + catalog_visibility=CATALOG_VISIBILITY_NONE, + ) + + # have a course which explicitly sets visibility in catalog and about to true + self.course_with_visibility = CourseFactory.create( + display_name='visible_course', + org='TestMicrositeX', + catalog_visibility=CATALOG_VISIBILITY_CATALOG_AND_ABOUT, + ) + def setup_users(self): # Create student accounts and activate them. for i in range(len(self.STUDENT_INFO)): @@ -71,9 +87,15 @@ class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase): # assert that test course display name is visible self.assertContains(resp, 'Robot_Super_Course') + # assert that test course with 'visible_in_catalog' to True is showing up + self.assertContains(resp, 'visible_course') + # assert that test course that is outside microsite is not visible self.assertNotContains(resp, 'Robot_Course_Outside_Microsite') + # assert that a course that has visible_in_catalog=False is not visible + self.assertNotContains(resp, 'Hidden_course') + # assert that footer template has been properly overriden on homepage self.assertContains(resp, 'This is a Test Microsite footer') @@ -153,3 +175,17 @@ class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase): resp = self.client.get(reverse('dashboard')) self.assertNotContains(resp, 'Robot_Super_Course') self.assertContains(resp, 'Robot_Course_Outside_Microsite') + + @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME) + def test_visible_about_page_settings(self): + """ + Make sure the Microsite is honoring the visible_about_page permissions that is + set in configuration + """ + 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) + + url = reverse('about_course', args=[self.course_hidden_visibility.id.to_deprecated_string()]) + resp = self.client.get(url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME) + self.assertEqual(resp.status_code, 404) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index f337c7f180..6cb05bd0a9 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -701,7 +701,12 @@ def course_about(request, course_id): """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - course = get_course_with_access(request.user, 'see_exists', course_key) + + permission_name = microsite.get_value( + 'COURSE_ABOUT_VISIBILITY_PERMISSION', + settings.COURSE_ABOUT_VISIBILITY_PERMISSION + ) + course = get_course_with_access(request.user, permission_name, course_key) if microsite.get_value( 'ENABLE_MKTG_SITE', @@ -710,6 +715,7 @@ def course_about(request, course_id): return redirect(reverse('info', args=[course.id.to_deprecated_string()])) registered = registered_for_course(course, request.user) + staff_access = has_access(request.user, 'staff', course) studio_url = get_studio_url(course, 'settings/details') @@ -782,8 +788,12 @@ def mktg_course_about(request, course_id): course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) try: - course = get_course_with_access(request.user, 'see_exists', course_key) - except (ValueError, Http404) as e: + permission_name = microsite.get_value( + 'COURSE_ABOUT_VISIBILITY_PERMISSION', + settings.COURSE_ABOUT_VISIBILITY_PERMISSION + ) + course = get_course_with_access(request.user, permission_name, course_key) + except (ValueError, Http404): # if a course does not exist yet, display a coming # soon button return render_to_response( diff --git a/lms/envs/common.py b/lms/envs/common.py index fa35232b12..c23e6c1556 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1851,3 +1851,11 @@ INVOICE_PAYMENT_INSTRUCTIONS = "This is where you can\nput directions on how peo COUNTRIES_OVERRIDE = { "TW": _("Taiwan"), } + +# which access.py permission name to check in order to determine if a course is visible in +# the course catalog. We default this to the legacy permission 'see_exists'. +COURSE_CATALOG_VISIBILITY_PERMISSION = 'see_exists' + +# which access.py permission name to check in order to determine if a course about page is +# visible. We default this to the legacy permission 'see_exists'. +COURSE_ABOUT_VISIBILITY_PERMISSION = 'see_exists' diff --git a/lms/envs/test.py b/lms/envs/test.py index dbfe8b66b8..049fdf7b89 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -350,6 +350,8 @@ MICROSITE_CONFIGURATION = { "course_index_overlay_logo_file": "test_microsite/images/header-logo.png", "homepage_overlay_html": "

This is a Test Microsite Overlay HTML

", "ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER": False, + "COURSE_CATALOG_VISIBILITY_PERMISSION": "see_in_catalog", + "COURSE_ABOUT_VISIBILITY_PERMISSION": "see_about_page", }, "default": { "university": "default_university", diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index 620de57eb8..77063c099c 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -151,6 +151,17 @@ ${_('This course is in your cart.').format(cart_link=cart_link)} + % elif is_course_full: + + ${_("Course is full")} + + % elif invitation_only and not can_enroll: + ${_("Enrollment in this course is by invitation only")} + ## Shib courses need the enrollment button to be displayed even when can_enroll is False, + ## because AnonymousUsers cause can_enroll for shib courses to be False, but we need them to be able to click + ## so that they can register and become a real user that can enroll. + % elif not is_shib_course and not can_enroll: + ${_("Enrollment is Closed")} %elif settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION') and registration_price: <% if user.is_authenticated(): @@ -166,17 +177,6 @@ cost=registration_price)}
- % elif is_course_full: - - ${_("Course is full")} - - % elif invitation_only and not can_enroll: - ${_("Enrollment in this course is by invitation only")} - ## Shib courses need the enrollment button to be displayed even when can_enroll is False, - ## because AnonymousUsers cause can_enroll for shib courses to be False, but we need them to be able to click - ## so that they can register and become a real user that can enroll. - % elif not is_shib_course and not can_enroll: - ${_("Enrollment is Closed")} %else: ${_("Register for {course.display_number_with_default}").format(course=course) | h}