Add new course_module field which describes what the catalog visibility should be (both, about, none)

This commit is contained in:
Chris Dodge
2014-11-04 19:15:46 -05:00
parent 01c029aa9f
commit 3c36f7cbad
10 changed files with 379 additions and 24 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 = "<form id=\"class_enroll_form\" method=\"post\" data-remote=\"true\" action=\"/change_enrollment\">"
@@ -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)

View File

@@ -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):
"""

View File

@@ -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)

View File

@@ -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(