diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index f74da9564c..65a1934391 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -272,6 +272,7 @@ def create_mode(request, course_id): `min_price` (int): The minimum price a user must pay to enroll in the new course mode `suggested_prices` (str): Comma-separated prices to suggest to the user. `currency` (str): The currency in which to list prices. + `sku` (str): The product SKU value. By default, this endpoint will create an 'honor' mode for the given course with display name 'Honor Code', a minimum price of 0, no suggested prices, and using USD as the currency. @@ -289,6 +290,7 @@ def create_mode(request, course_id): 'min_price': 0, 'suggested_prices': u'', 'currency': u'usd', + 'sku': None, } # Try pulling querystring parameters out of the request diff --git a/common/test/acceptance/pages/lms/create_mode.py b/common/test/acceptance/pages/lms/create_mode.py index 7c0d9982cd..412035ed99 100644 --- a/common/test/acceptance/pages/lms/create_mode.py +++ b/common/test/acceptance/pages/lms/create_mode.py @@ -14,7 +14,8 @@ class ModeCreationPage(PageObject): created for an existing course. """ - def __init__(self, browser, course_id, mode_slug=None, mode_display_name=None, min_price=None, suggested_prices=None, currency=None): + def __init__(self, browser, course_id, mode_slug=None, mode_display_name=None, min_price=None, + suggested_prices=None, currency=None, sku=None): """The mode creation page is an endpoint for HTTP GET requests. By default, it will create an 'honor' mode for the given course with display name @@ -30,6 +31,7 @@ class ModeCreationPage(PageObject): min_price (int): The minimum price a user must pay to enroll in the new course mode suggested_prices (str): Comma-separated prices to suggest to the user. currency (str): The currency in which to list prices. + sku (str): The product SKU value. """ super(ModeCreationPage, self).__init__(browser) @@ -51,6 +53,9 @@ class ModeCreationPage(PageObject): if currency is not None: self._parameters['currency'] = currency + if sku is not None: + self._parameters['sku'] = sku + @property def url(self): """Construct the mode creation URL.""" diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py index 44ac8dd899..5dbf2a94ed 100644 --- a/common/test/acceptance/pages/lms/instructor_dashboard.py +++ b/common/test/acceptance/pages/lms/instructor_dashboard.py @@ -92,6 +92,15 @@ class InstructorDashboardPage(CoursePage): email_section.wait_for_page() return email_section + def select_ecommerce_tab(self): + """ + Selects the E-commerce tab and returns an EcommercePage. + """ + self.q(css='[data-section="e-commerce"]').first.click() + ecommerce_section = EcommercePage(self.browser) + ecommerce_section.wait_for_page() + return ecommerce_section + @staticmethod def get_asset_path(file_name): """ @@ -1416,3 +1425,19 @@ class CertificatesPage(PageObject): Returns the message (error/success) in "Certificate Invalidation" section. """ return self.get_selector('.certificate-invalidation-container div.message') + + +class EcommercePage(PageObject): + """ + E-commerce section of the Instructor dashboard. + """ + url = None + + def is_browser_on_page(self): + return self.q(css='[data-section="e-commerce"].active-section').present + + def get_sections_header_values(self): + """ + Returns a list of the headings text under div. + """ + return self.q(css="div.wrap h3").text diff --git a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py index ab2cc8d06d..f28e7ac6cf 100644 --- a/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py +++ b/common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py @@ -1202,3 +1202,112 @@ class CertificateInvalidationTest(BaseInstructorDashboardTest): '.certificates-wrapper' ]) self.certificates_section.a11y_audit.check_for_accessibility_errors() + + +@attr(shard=10) +class EcommerceTest(BaseInstructorDashboardTest): + """ + Bok Choy tests for the "E-Commerce" tab. + """ + def setUp(self): + super(EcommerceTest, self).setUp() + + def setup_course(self, course_number): + """ + Sets up the course + """ + self.course_info['number'] = course_number + course_fixture = CourseFixture( + self.course_info["org"], + self.course_info["number"], + self.course_info["run"], + self.course_info["display_name"] + ) + course_fixture.install() + + def log_in_as_unique_user(self): + """ + Log in as a valid lms user. + """ + AutoAuthPage( + self.browser, + username="test_instructor", + email="test_instructor@example.com", + password="password", + course_id=self.course_id + ).visit() + + def visit_ecommerce_section(self): + """ + Log in to visit Instructor dashboard and click E-commerce tab + """ + self.log_in_as_unique_user() + instructor_dashboard_page = self.visit_instructor_dashboard() + return instructor_dashboard_page.select_ecommerce_tab() + + def add_course_mode(self, sku_value=None): + """ + Add an honor mode to the course + """ + ModeCreationPage(browser=self.browser, course_id=self.course_id, mode_slug=u'honor', min_price=10, + sku=sku_value).visit() + + def test_enrollment_codes_section_visible_for_non_ecommerce_course(self): + """ + Test Enrollment Codes UI, under E-commerce Tab, should be visible in the Instructor Dashboard with non + e-commerce course + """ + # Setup course + non_ecommerce_course_number = "34039497242734583224814321005482849780" + self.setup_course(non_ecommerce_course_number) + + # Add an honor mode to the course + self.add_course_mode() + + # Log in and visit E-commerce section under Instructor dashboard + self.assertIn(u'Enrollment Codes', self.visit_ecommerce_section().get_sections_header_values()) + + def test_coupon_codes_section_visible_for_non_ecommerce_course(self): + """ + Test Coupon Codes UI, under E-commerce Tab, should be visible in the Instructor Dashboard with non + e-commerce course + """ + # Setup course + non_ecommerce_course_number = "34039497242734583224814321005482849781" + self.setup_course(non_ecommerce_course_number) + + # Add an honor mode to the course + self.add_course_mode() + + # Log in and visit E-commerce section under Instructor dashboard + self.assertIn(u'Coupon Code List', self.visit_ecommerce_section().get_sections_header_values()) + + def test_enrollment_codes_section_not_visible_for_ecommerce_course(self): + """ + Test Enrollment Codes UI, under E-commerce Tab, should not be visible in the Instructor Dashboard with + e-commerce course + """ + # Setup course + ecommerce_course_number = "34039497242734583224814321005482849782" + self.setup_course(ecommerce_course_number) + + # Add an honor mode to the course with sku value + self.add_course_mode('test_sku') + + # Log in and visit E-commerce section under Instructor dashboard + self.assertNotIn(u'Enrollment Codes', self.visit_ecommerce_section().get_sections_header_values()) + + def test_coupon_codes_section_not_visible_for_ecommerce_course(self): + """ + Test Coupon Codes UI, under E-commerce Tab, should not be visible in the Instructor Dashboard with + e-commerce course + """ + # Setup course + ecommerce_course_number = "34039497242734583224814321005482849783" + self.setup_course(ecommerce_course_number) + + # Add an honor mode to the course with sku value + self.add_course_mode('test_sku') + + # Log in and visit E-commerce section under Instructor dashboard + self.assertNotIn(u'Coupon Code List', self.visit_ecommerce_section().get_sections_header_values()) diff --git a/common/test/db_fixtures/course_access_role.json b/common/test/db_fixtures/course_access_role.json new file mode 100644 index 0000000000..d7de251d74 --- /dev/null +++ b/common/test/db_fixtures/course_access_role.json @@ -0,0 +1,74 @@ +[ + { + "pk": 1, + "model": "auth.user", + "fields": { + "username": "test_instructor", + "email":"test_instructor@example.com", + "password": "password", + "is_staff": true, + "is_active": true + } + }, + { + "pk": 1, + "model": "student.userprofile", + "fields": { + "user": 1, + "name": "test instructor", + "courseware": "course.xml" + } + }, + { + "pk": 1, + "model": "student.registration", + "fields": { + "user": 1, + "activation_key": "52bfac10384d49219385dcd4cc17177q" + } + }, + { + "pk": 1, + "model": "student.courseaccessrole", + "fields": { + "id": "1", + "org": "test_org", + "course_id": "course-v1:test_org+34039497242734583224814321005482849780+test_run", + "role": "finance_admin", + "user_id": "1" + } + }, + { + "pk": 2, + "model": "student.courseaccessrole", + "fields": { + "id": "2", + "org": "test_org", + "course_id": "course-v1:test_org+34039497242734583224814321005482849781+test_run", + "role": "finance_admin", + "user_id": "1" + } + }, + { + "pk": 3, + "model": "student.courseaccessrole", + "fields": { + "id": "3", + "org": "test_org", + "course_id": "course-v1:test_org+34039497242734583224814321005482849782+test_run", + "role": "finance_admin", + "user_id": "1" + } + }, + { + "pk": 4, + "model": "student.courseaccessrole", + "fields": { + "id": "4", + "org": "test_org", + "course_id": "course-v1:test_org+34039497242734583224814321005482849783+test_run", + "role": "finance_admin", + "user_id": "1" + } + } +] diff --git a/lms/djangoapps/instructor/tests/test_ecommerce.py b/lms/djangoapps/instructor/tests/test_ecommerce.py index 605e407468..feacf20401 100644 --- a/lms/djangoapps/instructor/tests/test_ecommerce.py +++ b/lms/djangoapps/instructor/tests/test_ecommerce.py @@ -30,7 +30,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): # URL for instructor dash cls.url = reverse('instructor_dashboard', kwargs={'course_id': cls.course.id.to_deprecated_string()}) - cls.e_commerce_link = '' + cls.ecommerce_link = '' def setUp(self): super(TestECommerceDashboardViews, self).setUp() @@ -50,7 +50,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): Test Pass E-commerce Tab is in the Instructor Dashboard """ response = self.client.get(self.url) - self.assertIn(self.e_commerce_link, response.content) + self.assertIn(self.ecommerce_link, response.content) # Coupons should show up for White Label sites with priced honor modes. self.assertIn('Coupon Code List', response.content) @@ -61,7 +61,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): self.use_site(site=self.site_other) self.client.login(username=self.instructor.username, password="test") response = self.client.get(self.url) - self.assertIn(self.e_commerce_link, response.content) + self.assertIn(self.ecommerce_link, response.content) self.assertIn('Create Enrollment Report', response.content) def test_reports_section_not_under_e_commerce_tab(self): @@ -70,12 +70,12 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): value """ response = self.client.get(self.url) - self.assertIn(self.e_commerce_link, response.content) + self.assertIn(self.ecommerce_link, response.content) self.assertNotIn('Create Enrollment Report', response.content) def test_user_has_finance_admin_rights_in_e_commerce_tab(self): response = self.client.get(self.url) - self.assertIn(self.e_commerce_link, response.content) + self.assertIn(self.ecommerce_link, response.content) # Order/Invoice sales csv button text should render in e-commerce page self.assertIn('Total Credit Card Purchases', response.content) @@ -96,7 +96,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): the instructor dashboard """ response = self.client.get(self.url) - self.assertIn(self.e_commerce_link, response.content) + self.assertIn(self.ecommerce_link, response.content) # Total amount html should render in e-commerce page, total amount will be 0 course_honor_mode = CourseMode.mode_for_course(self.course.id, 'honor') @@ -351,6 +351,39 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase): # Get the response value, ensure the Coupon section is not included. response = self.client.get(self.url) - self.assertIn(self.e_commerce_link, response.content) + self.assertIn(self.ecommerce_link, response.content) # Coupons should show up for White Label sites with priced honor modes. self.assertNotIn('Coupons List', response.content) + + def test_coupon_code_section_not_under_e_commerce_tab(self): + """ + Test Coupon Creation UI, under E-commerce Tab, should not be available in the Instructor Dashboard with + e-commerce course + """ + # Setup e-commerce course + CourseMode.objects.filter(course_id=self.course.id).update(sku='test_sku') + + response = self.client.get(self.url) + self.assertIn(self.ecommerce_link, response.content) + self.assertNotIn('Coupon Code List', response.content) + + def test_enrollment_codes_section_not_under_e_commerce_tab(self): + """ + Test Enrollment Codes UI, under E-commerce Tab, should not be available in the Instructor Dashboard with + e-commerce course + """ + # Setup e-commerce course + CourseMode.objects.filter(course_id=self.course.id).update(sku='test_sku') + + response = self.client.get(self.url) + self.assertIn(self.ecommerce_link, response.content) + self.assertNotIn('

Enrollment Codes

', response.content) + + def test_enrollment_codes_section_visible_for_non_ecommerce_course(self): + """ + Test Enrollment Codes UI, under E-commerce Tab, should be available in the Instructor Dashboard with non + e-commerce course + """ + response = self.client.get(self.url) + self.assertIn(self.ecommerce_link, response.content) + self.assertIn('

Enrollment Codes

', response.content) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index de87be7c1b..7d07520a5e 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -285,7 +285,8 @@ def _section_e_commerce(course, access, paid_mode, coupons_enabled, reports_enab 'coupons_enabled': coupons_enabled, 'reports_enabled': reports_enabled, 'course_price': course_price, - 'total_amount': total_amount + 'total_amount': total_amount, + 'is_ecommerce_course': is_ecommerce_course(course_key) } return section_data @@ -695,3 +696,12 @@ def _section_metrics(course, access): 'post_metrics_data_csv_url': reverse('post_metrics_data_csv'), } return section_data + + +def is_ecommerce_course(course_key): + """ + Checks if the given course is an e-commerce course or not, by checking its SKU value from + CourseMode records for the course + """ + sku_count = len([mode.sku for mode in CourseMode.modes_for_course(course_key) if mode.sku]) + return sku_count > 0 diff --git a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html index e28cfa6edd..c75ae595ef 100644 --- a/lms/templates/instructor/instructor_dashboard_2/e-commerce.html +++ b/lms/templates/instructor/instructor_dashboard_2/e-commerce.html @@ -14,7 +14,8 @@ import pytz
-
+ %if not section_data['is_ecommerce_course']: +

${_('Enrollment Codes')}

%if section_data['sales_admin']: @@ -53,6 +54,7 @@ import pytz
+ %endif %if section_data['coupons_enabled']:
@@ -145,7 +147,7 @@ import pytz
%endif - %if section_data['coupons_enabled']: + %if section_data['coupons_enabled'] and not section_data['is_ecommerce_course']:

${_("Coupon Code List")}