diff --git a/common/djangoapps/student/tests/test_certificates.py b/common/djangoapps/student/tests/test_certificates.py new file mode 100644 index 0000000000..4a440f7ed9 --- /dev/null +++ b/common/djangoapps/student/tests/test_certificates.py @@ -0,0 +1,54 @@ +"""Tests for display of certificates on the student dashboard. """ + +import unittest +import ddt + +from django.conf import settings +from django.core.urlresolvers import reverse + +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from student.tests.factories import UserFactory, CourseEnrollmentFactory +from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error + + +@ddt.ddt +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class CertificateDisplayTest(ModuleStoreTestCase): + """Tests display of certificates on the student dashboard. """ + + USERNAME = "test_user" + PASSWORD = "password" + DOWNLOAD_URL = "http://www.example.com/certificate.pdf" + + def setUp(self): + super(CertificateDisplayTest, self).setUp() + self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) + result = self.client.login(username=self.USERNAME, password=self.PASSWORD) + self.assertTrue(result, msg="Could not log in") + + self.course = CourseFactory() + self.course.certificates_display_behavior = "early_with_info" + self.update_course(self.course, self.user.username) + + @ddt.data('verified', 'professional') + def test_display_verified_certificate(self, enrollment_mode): + self._create_certificate(enrollment_mode) + self._check_can_download_certificate() + + def _create_certificate(self, enrollment_mode): + """Simulate that the user has a generated certificate. """ + CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, mode=enrollment_mode) + GeneratedCertificateFactory( + user=self.user, + course_id=self.course.id, + mode=enrollment_mode, + download_url=self.DOWNLOAD_URL, + status="downloadable", + grade=0.98, + ) + + def _check_can_download_certificate(self): + response = self.client.get(reverse('dashboard')) + self.assertContains(response, u'Download Your ID Verified') + self.assertContains(response, self.DOWNLOAD_URL) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py b/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py index 554e3c5fc2..fe9056b09a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_cross_modulestore_import_export.py @@ -13,6 +13,7 @@ and then for each combination of modulestores, performing the sequence: """ from contextlib import contextmanager, nested import itertools +import os from path import path import random from shutil import rmtree @@ -314,14 +315,15 @@ class MixedModulestoreBuilder(StoreBuilderBase): # Split stores all asset metadata in the structure collection. return store.db_connection.structures - MIXED_MODULESTORE_BOTH_SETUP = MixedModulestoreBuilder([ ('draft', MongoModulestoreBuilder()), ('split', VersioningModulestoreBuilder()) ]) +DRAFT_MODULESTORE_SETUP = MixedModulestoreBuilder([('draft', MongoModulestoreBuilder())]) +SPLIT_MODULESTORE_SETUP = MixedModulestoreBuilder([('split', VersioningModulestoreBuilder())]) MIXED_MODULESTORE_SETUPS = ( - MixedModulestoreBuilder([('draft', MongoModulestoreBuilder())]), - MixedModulestoreBuilder([('split', VersioningModulestoreBuilder())]), + DRAFT_MODULESTORE_SETUP, + SPLIT_MODULESTORE_SETUP, ) MIXED_MS_SETUPS_SHORT = ( 'mixed_mongo', @@ -347,6 +349,8 @@ COURSE_DATA_NAMES = ( 'split_test_module_draft', ) +EXPORTED_COURSE_DIR_NAME = 'exported_source_course' + @ddt.ddt @attr('mongo') @@ -397,14 +401,14 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase): source_content, source_course_key, self.export_dir, - 'exported_source_course', + EXPORTED_COURSE_DIR_NAME, ) import_course_from_xml( dest_store, 'test_user', self.export_dir, - source_dirs=['exported_source_course'], + source_dirs=[EXPORTED_COURSE_DIR_NAME], static_content_store=dest_content, target_id=dest_course_key, raise_on_failure=True, @@ -448,3 +452,58 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase): dest_store, dest_course_key, ) + + def test_split_course_export_import(self): + # Construct the contentstore for storing the first import + with MongoContentstoreBuilder().build() as source_content: + # Construct the modulestore for storing the first import (using the previously created contentstore) + with SPLIT_MODULESTORE_SETUP.build(contentstore=source_content) as source_store: + # Construct the contentstore for storing the second import + with MongoContentstoreBuilder().build() as dest_content: + # Construct the modulestore for storing the second import (using the second contentstore) + with SPLIT_MODULESTORE_SETUP.build(contentstore=dest_content) as dest_store: + source_course_key = source_store.make_course_key('a', 'source', '2015_Fall') + dest_course_key = dest_store.make_course_key('a', 'dest', '2015_Fall') + + import_course_from_xml( + source_store, + 'test_user', + TEST_DATA_DIR, + source_dirs=['split_course_with_static_tabs'], + static_content_store=source_content, + target_id=source_course_key, + raise_on_failure=True, + create_if_not_present=True, + ) + + export_course_to_xml( + source_store, + source_content, + source_course_key, + self.export_dir, + EXPORTED_COURSE_DIR_NAME, + ) + + source_course = source_store.get_course(source_course_key, depth=None, lazy=False) + + self.assertEqual(source_course.url_name, 'course') + + export_dir_path = path(self.export_dir) + policy_dir = export_dir_path / 'exported_source_course' / 'policies' / source_course.url_name + policy_path = policy_dir / 'policy.json' + self.assertTrue(os.path.exists(policy_path)) + + import_course_from_xml( + dest_store, + 'test_user', + self.export_dir, + source_dirs=[EXPORTED_COURSE_DIR_NAME], + static_content_store=dest_content, + target_id=dest_course_key, + raise_on_failure=True, + create_if_not_present=True, + ) + + dest_course = dest_store.get_course(dest_course_key, depth=None, lazy=False) + + self.assertEqual(dest_course.url_name, 'course') diff --git a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py index f62d0e3075..a3d95a9b78 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_exporter.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_exporter.py @@ -264,8 +264,14 @@ class CourseExportManager(ExportManager): 'about', 'about', '.html' ) + course_policy_dir_name = courselike.location.run + if courselike.url_name != courselike.location.run and courselike.url_name == 'course': + # Use url_name for split mongo because course_run is not used when loading policies. + course_policy_dir_name = courselike.url_name + + course_run_policy_dir = policies_dir.makeopendir(course_policy_dir_name) + # export the grading policy - course_run_policy_dir = policies_dir.makeopendir(courselike.location.run) with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: grading_policy.write(dumps(courselike.grading_policy, cls=EdxJSONEncoder, sort_keys=True, indent=4)) diff --git a/common/test/data/split_course_with_static_tabs/course.xml b/common/test/data/split_course_with_static_tabs/course.xml new file mode 100644 index 0000000000..86b14ddc4e --- /dev/null +++ b/common/test/data/split_course_with_static_tabs/course.xml @@ -0,0 +1,3 @@ + + + diff --git a/common/test/data/split_course_with_static_tabs/course/course.xml b/common/test/data/split_course_with_static_tabs/course/course.xml new file mode 100644 index 0000000000..c92973175c --- /dev/null +++ b/common/test/data/split_course_with_static_tabs/course/course.xml @@ -0,0 +1,2 @@ + + diff --git a/common/test/data/split_course_with_static_tabs/policies/assets.json b/common/test/data/split_course_with_static_tabs/policies/assets.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/common/test/data/split_course_with_static_tabs/policies/assets.json @@ -0,0 +1 @@ +{} diff --git a/common/test/data/split_course_with_static_tabs/policies/course/grading_policy.json b/common/test/data/split_course_with_static_tabs/policies/course/grading_policy.json new file mode 100644 index 0000000000..2095bb70f7 --- /dev/null +++ b/common/test/data/split_course_with_static_tabs/policies/course/grading_policy.json @@ -0,0 +1 @@ +{"GRADER": [{"short_label": "HW", "min_count": 12, "type": "Homework", "drop_count": 2, "weight": 0.15}, {"min_count": 12, "type": "Lab", "drop_count": 2, "weight": 0.15}, {"short_label": "Midterm", "min_count": 1, "type": "Midterm Exam", "drop_count": 0, "weight": 0.3}, {"short_label": "Final", "min_count": 1, "type": "Final Exam", "drop_count": 0, "weight": 0.4}], "GRADE_CUTOFFS": {"Pass": 0.5}} diff --git a/common/test/data/split_course_with_static_tabs/policies/course/policy.json b/common/test/data/split_course_with_static_tabs/policies/course/policy.json new file mode 100644 index 0000000000..c0f93501ce --- /dev/null +++ b/common/test/data/split_course_with_static_tabs/policies/course/policy.json @@ -0,0 +1 @@ +{"course/3111": {"tabs": [{"type": "courseware", "name": "Courseware"}, {"type": "course_info", "name": "Course Info"}, {"type": "textbooks", "name": "Textbooks"}, {"type": "discussion", "name": "Discussion"}, {"type": "wiki", "name": "Wiki"}, {"type": "progress", "name": "Progress"}, {"name": "Test Page", "type": "static_tab", "url_slug": "test_page.html"}], "advanced_modules": ["split_test"], "display_name": "split export test", "user_partitions": [{"description": "Experiment 1", "version": 1, "id": 0, "groups": [{"version": 1, "id": 0, "name": "group 0"}, {"version": 1, "id": 1, "name": "group 1"}], "name": "Experiment 0,1"}, {"description": "Experiment 2", "version": 1, "id": 1, "groups": [{"version": 1, "id": 0, "name": "group A"}, {"version": 1, "id": 1, "name": "group B"}, {"version": 1, "id": 2, "name": "group C"}], "name": "Experiment A,B,C"}], "discussion_topics": {"General": {"id": "i4x-foo-1111-course-3111"}}}} diff --git a/common/test/data/split_course_with_static_tabs/tabs/test_page.html b/common/test/data/split_course_with_static_tabs/tabs/test_page.html new file mode 100644 index 0000000000..6cb87b8787 --- /dev/null +++ b/common/test/data/split_course_with_static_tabs/tabs/test_page.html @@ -0,0 +1 @@ +

Add the content you want students to see on this page.

diff --git a/lms/djangoapps/courseware/management/commands/tests/test_dump_course.py b/lms/djangoapps/courseware/management/commands/tests/test_dump_course.py index 2f2e986c8a..ef97b66e0b 100644 --- a/lms/djangoapps/courseware/management/commands/tests/test_dump_course.py +++ b/lms/djangoapps/courseware/management/commands/tests/test_dump_course.py @@ -44,6 +44,7 @@ class CommandsTestBase(ModuleStoreTestCase): """ __test__ = False + url_name = '2012_Fall' def setUp(self): super(CommandsTestBase, self).setUp() @@ -201,7 +202,7 @@ class CommandsTestBase(ModuleStoreTestCase): assert_in = self.assertIn assert_in('edX-simple-2012_Fall', names) - assert_in('edX-simple-2012_Fall/policies/2012_Fall/policy.json', names) + assert_in('edX-simple-2012_Fall/policies/{}/policy.json'.format(self.url_name), names) assert_in('edX-simple-2012_Fall/html/toylab.html', names) assert_in('edX-simple-2012_Fall/videosequence/A_simple_sequence.xml', names) assert_in('edX-simple-2012_Fall/sequential/Lecture_2.xml', names) @@ -232,3 +233,4 @@ class CommandSplitMongoTestCase(CommandsTestBase): """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE __test__ = True + url_name = 'course' diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html index 676e4b91fd..798c12b5db 100644 --- a/lms/templates/dashboard/_dashboard_certificate_information.html +++ b/lms/templates/dashboard/_dashboard_certificate_information.html @@ -1,6 +1,9 @@ <%page args="cert_status, course, enrollment" /> -<%! from django.utils.translation import ugettext as _ %> +<%! +from django.utils.translation import ugettext as _ +from course_modes.models import CourseMode +%> <%namespace name='static' file='../static_content.html'/> <% @@ -61,7 +64,7 @@ else: ${_("Download Your {cert_name_short} (PDF)").format(cert_name_short=cert_name_short)} - % elif cert_status['show_download_url'] and enrollment.mode == 'verified': + % elif cert_status['show_download_url'] and enrollment.mode in CourseMode.VERIFIED_MODES: