def test_models_course_run_mark_dirty_parler(self): """ Updating the value of a field translatable via parler should mark the related course page dirty (waiting to be published) only in the impacted language. """ course = CourseFactory(page_languages=["en", "fr"]) course_run = CourseRunFactory(direct_course=course) CourseRunTranslation.objects.create(master=course_run, language_code="fr", title="mon titre") self.assertTrue(course_run.direct_course.extended_object.publish("en")) self.assertTrue(course_run.direct_course.extended_object.publish("fr")) course_run.refresh_from_db() self.assertIsNotNone(course_run.public_course_run) title_query = course_run.direct_course.extended_object.title_set title_obj_en = title_query.get(language="en") title_obj_fr = title_query.get(language="fr") self.assertEqual(title_query.count(), 2) with switch_language(course_run, "fr"): course_run.title = "nouveau titre" course_run.save() self.assertEqual(title_obj_en.publisher_state, PUBLISHER_STATE_DEFAULT) self.assertEqual(title_obj_fr.publisher_state, PUBLISHER_STATE_DEFAULT) course_run.mark_course_dirty() title_obj_en.refresh_from_db() title_obj_fr.refresh_from_db() self.assertEqual(title_obj_en.publisher_state, PUBLISHER_STATE_DEFAULT) self.assertEqual(title_obj_fr.publisher_state, PUBLISHER_STATE_DIRTY)
def test_api_course_run_sync_catalog_visibility_course_and_search( self, mock_signal): """ Verify that the catalog visibility is updated with `course_and_search` """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "catalog_visibility": "course_and_search", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 84583c2b96759c26ed1ecb3fc7d6d4805853cbbcd5ac40e777c0aa1c319fd490" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.catalog_visibility, CourseRunCatalogVisibility.COURSE_AND_SEARCH) self.assertFalse(mock_signal.called)
def test_api_course_run_sync_catalog_visibility_hidden(self, mock_signal): """ Verify that the catalog visibility is updated with `hidden` """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "catalog_visibility": "hidden", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 b18aaa037ac3c4630fb3d88597cf5138e8c145a10b912688de82ee2c963a7ed6" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.catalog_visibility, CourseRunCatalogVisibility.HIDDEN) self.assertFalse(mock_signal.called)
def test_models_course_run_copy_translations_one_language(self): """ The "copy_translations" method called for a specific language should only port parler's translations for this language from one course run to the other. """ course_run = CourseRunFactory(title="my title") CourseRunTranslation.objects.create(master=course_run, language_code="fr", title="mon titre") old_course_run = CourseRun.objects.get(pk=course_run.pk) self.assertEqual(CourseRunTranslation.objects.count(), 2) with switch_language(old_course_run, "fr"): self.assertEqual(old_course_run.title, "mon titre") course_run.pk = None course_run.save() self.assertEqual(CourseRunTranslation.objects.count(), 2) course_run.copy_translations(old_course_run, language="fr") self.assertEqual(CourseRun.objects.count(), 2) self.assertEqual(CourseRunTranslation.objects.count(), 3) self.assertEqual( CourseRunTranslation.objects.filter(master=course_run).count(), 1) course_run.refresh_from_db() self.assertEqual(course_run.title, "mon titre") # Fallback to french with switch_language(course_run, "fr"): self.assertEqual(course_run.title, "mon titre")
def test_api_course_run_sync_catalog_visibility_none(self, mock_signal): """ Test the adaptation of the Open edX catalog visibility of the `none` value to the Richie `hidden`. """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "catalog_visibility": "none", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 e9f23ed36de0865dbfd543ce1c83e0ec1700c8821cf6ff960ec549f2e6c4a6db" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.catalog_visibility, CourseRunCatalogVisibility.HIDDEN) self.assertFalse(mock_signal.called)
def test_api_course_run_sync_catalog_visibility_both(self, mock_signal): """ Test the adaptation of the Open edX catalog visibility of the `both` value to the Richie `course_and_search`. """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "catalog_visibility": "both", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 e93c38b636ec55fa4c1393f59e1758d53f55b0753737f3c451e4084ec10dd1ca" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.catalog_visibility, CourseRunCatalogVisibility.COURSE_AND_SEARCH) self.assertFalse(mock_signal.called)
def test_models_course_run_copy_translations_all_languages(self): """ The "copy_translations" method should port parler's translations for all languages from one course run to the other. """ course_run = CourseRunFactory(title="my title") CourseRunTranslation.objects.create(master=course_run, language_code="fr", title="mon titre") old_course_run = CourseRun.objects.get(pk=course_run.pk) self.assertEqual(CourseRunTranslation.objects.count(), 2) with switch_language(old_course_run, "fr"): self.assertEqual(old_course_run.title, "mon titre") course_run.pk = None course_run.save() self.assertEqual(CourseRunTranslation.objects.count(), 2) course_run.copy_translations(old_course_run) self.assertEqual(CourseRun.objects.count(), 2) self.assertEqual(CourseRunTranslation.objects.count(), 4) self.assertEqual( CourseRunTranslation.objects.filter(master=course_run).count(), 2) course_run.refresh_from_db() self.assertEqual(course_run.title, "my title") with switch_language(course_run, "fr"): self.assertEqual(course_run.title, "mon titre")
def test_api_course_run_sync_catalog_visibility_about(self, mock_signal): """ Test the adaptation of the Open edX catalog visibility of the `about` value to the Richie `course_only`. """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "catalog_visibility": "about", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 58f845704d268db96dfdcbd88a7f8a6f0124611fd741ec7a7eb6a12cd0af79f1" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.catalog_visibility, CourseRunCatalogVisibility.COURSE_ONLY) self.assertFalse(mock_signal.called)
def test_api_course_run_sync_enrollment_count(self, mock_signal): """ Check if the enrollment count of a course is updated """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "enrollment_count": 865, } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 f510d35ba7492eb5c5133fab6bd2fa18328307089735a20f4a7f4a828e0598b5" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.enrollment_count, 865) self.assertFalse(mock_signal.called)
def test_api_course_run_sync_catalog_visibility_course_only( self, mock_signal): """ Verify that the catalog visibility is updated with `course_only` """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link) mock_signal.reset_mock() data = { "resource_link": link, "catalog_visibility": "course_only", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 c0c06e769ebb2d84ff14149f50aea5503ae192a4074f9c9b2478732a02936601" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) course_run.refresh_from_db() self.assertEqual(course_run.catalog_visibility, CourseRunCatalogVisibility.COURSE_ONLY) self.assertFalse(mock_signal.called)
def test_admin_course_run_delete_superuser_public(self): """ Validate that the public course run can not be deleted via the admin. """ course_run = CourseRunFactory() course_run.direct_course.extended_object.publish("en") course_run.refresh_from_db() user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") self._prepare_delete(course_run.public_course_run, 200, self.assertFalse)
def test_models_course_run_mark_dirty_delete_course_run_to_be_scheduled(self): """ Deleting a course run yet to be scheduled should not mark the related course page dirty. """ field = random.choice(["start", "enrollment_start"]) course_run = CourseRunFactory(**{field: None}) self.assertTrue(course_run.direct_course.extended_object.publish("en")) title_obj = course_run.direct_course.extended_object.title_set.first() course_run.refresh_from_db() course_run.delete() title_obj.refresh_from_db() self.assertEqual(title_obj.publisher_state, PUBLISHER_STATE_DEFAULT)
def test_admin_course_run_change_view_post_superuser_public(self): """ Validate that the public course run can not be updated via the admin. """ course_run = CourseRunFactory() snapshot = CourseFactory( page_parent=course_run.direct_course.extended_object) course_run.direct_course.extended_object.publish("en") course_run.refresh_from_db() user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") self._prepare_change_view_post(course_run.public_course_run, snapshot, 403, self.assertNotEqual)
def test_models_course_run_mark_dirty_delete_course_run(self): """ Deleting a scheduled course run should mark the related course page dirty. """ course_run = CourseRunFactory() self.assertTrue(course_run.direct_course.extended_object.publish("en")) title_obj = course_run.direct_course.extended_object.title_set.first() course_run.refresh_from_db() self.assertEqual(title_obj.publisher_state, PUBLISHER_STATE_DEFAULT) course_run.delete() title_obj.refresh_from_db() self.assertEqual(title_obj.publisher_state, PUBLISHER_STATE_DIRTY)
def test_admin_course_run_delete_superuser_draft(self): """ Validate that the draft course run can be deleted via the admin and that it deletes the associated public course run. """ course_run = CourseRunFactory() course_run.direct_course.extended_object.publish("en") course_run.refresh_from_db() self.assertEqual(CourseRun.objects.count(), 2) user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") response = self._prepare_delete(course_run, 200, self.assertFalse) self.assertEqual(Course.objects.count(), 2) self.assertContains(response, "was deleted successfully")
def test_admin_course_run_change_view_post(self): """ Validate that the course run can be updated via the admin. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") # Create a course run course_run = CourseRunFactory() # Get the admin change view url = reverse("admin:courses_courserun_change", args=[course_run.id]) data = { "languages": ["fr", "en"], "resource_link": "https://example.com/my-resource-link", "start_0": "2015-01-15", "start_1": "07:06:15", "end_0": "2015-01-30", "end_1": "23:52:34", "enrollment_start_0": "2015-01-02", "enrollment_start_1": "13:13:07", "enrollment_end_0": "2015-01-23", "enrollment_end_1": "09:07:11", } with timezone.override(pytz.utc): response = self.client.post(url, data) self.assertEqual(response.status_code, 302) # Check that the course run was updated as expected course_run.refresh_from_db() self.assertEqual(course_run.languages, ["fr", "en"]) self.assertEqual( course_run.resource_link, "https://example.com/my-resource-link" ) self.assertEqual( course_run.start, datetime(2015, 1, 15, 7, 6, 15, tzinfo=pytz.utc) ) self.assertEqual( course_run.end, datetime(2015, 1, 30, 23, 52, 34, tzinfo=pytz.utc) ) self.assertEqual( course_run.enrollment_start, datetime(2015, 1, 2, 13, 13, 7, tzinfo=pytz.utc), ) self.assertEqual( course_run.enrollment_end, datetime(2015, 1, 23, 9, 7, 11, tzinfo=pytz.utc) )
def test_models_course_run_get_course_child_of_snapshot(self): """ We should be able to retrieve the course from a course run that is a child of one of its snapshots. """ course = CourseFactory(should_publish=True) snapshot = CourseFactory(page_parent=course.extended_object) course_run = CourseRunFactory(direct_course=snapshot) self.assertTrue(snapshot.extended_object.publish("en")) course_run.refresh_from_db() # Add a sibling course to make sure it is not returned CourseFactory(should_publish=True) self.assertEqual(course_run.get_course(), course) self.assertEqual(course_run.public_course_run.get_course(), course.public_extension)
def test_models_course_copy_relations_publish(self): """ When publishing a draft course, the draft course run should be copied to a newly created course run with its parler translations. In a second part of the test, we check that when publishing a course that was already published, the draft course run should be copied to the existing public course run with its parler translations. """ # 1- Publishing a draft course course = CourseFactory(page_title="my course title") TitleFactory(page=course.extended_object, language="fr", title="mon titre de cours") course_run = CourseRunFactory(direct_course=course, title="my run") CourseRunTranslation.objects.create(master=course_run, language_code="fr", title="ma session") self.assertEqual(Course.objects.count(), 1) self.assertEqual(CourseRun.objects.count(), 1) self.assertEqual(CourseRunTranslation.objects.count(), 2) self.assertEqual(Title.objects.count(), 2) self.assertTrue(course.extended_object.publish("fr")) self.assertEqual(Course.objects.count(), 2) self.assertEqual(CourseRun.objects.count(), 2) self.assertEqual(CourseRunTranslation.objects.count(), 3) self.assertEqual(Title.objects.count(), 3) # 2- Publishing a course that was already published self.assertTrue(course.extended_object.publish("en")) self.assertEqual(CourseRunTranslation.objects.count(), 4) self.assertEqual(Title.objects.count(), 4) course_run.refresh_from_db() public_course_run = course_run.public_course_run self.assertEqual(public_course_run.title, "my run") with switch_language(public_course_run, "fr"): self.assertEqual(public_course_run.title, "ma session")
def test_models_course_run_get_course_direct_child(self): """ We should be able to retrieve the course from a course run that is its direct child. """ course = CourseFactory() course_run = CourseRunFactory(direct_course=course) self.assertTrue(course.extended_object.publish("en")) course.refresh_from_db() course_run.refresh_from_db() # Add a sibling course to make sure it is not returned CourseFactory(should_publish=True) # Add a snapshot to make sure it does not interfere CourseFactory(page_parent=course.extended_object, should_publish=True) self.assertEqual(course_run.get_course(), course) self.assertEqual(course_run.public_course_run.get_course(), course.public_extension)
def test_update_course_run_by_anonymous_user(self): """ The course run API does not support course run update. """ course_run = CourseRunFactory( resource_link="https://example.com/old-course-run/") response = self.client.put( f"/api/v1.0/course-runs/{course_run.id}/", { **CourseRunSerializer(course_run).data, "resource_link": "https://example.com/new-course-run/", }, ) self.assertEqual(response.status_code, 403) course_run.refresh_from_db() self.assertEqual(course_run.resource_link, "https://example.com/old-course-run/")
def test_admin_course_run_delete_staff_user_page_permission(self): """Staff users with all necessary permissions can delete a course run via the admin.""" course_run = CourseRunFactory() course_run.direct_course.extended_object.publish("en") course_run.refresh_from_db() user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") # Add all necessary model and object level permissions self.add_permission(user, "delete_courserun") self.add_permission(user, "change_page") PagePermission.objects.create( page=course_run.direct_course.extended_object, user=user, can_add=False, can_change=True, can_delete=False, can_publish=False, can_move_page=False, ) self._prepare_delete(course_run, 200, self.assertFalse)
def test_models_course_run_get_course_direct_child_with_parent(self): """ We should be able to retrieve the course from a course run that is its direct child when the course is below a root page (this is creating a difficulty because the query we build in `get_course` can create duplicates if we don't add the right clauses). """ page = create_i18n_page("A page", published=True) course = CourseFactory(page_parent=page) course_run = CourseRunFactory(direct_course=course) self.assertTrue(course.extended_object.publish("en")) course.refresh_from_db() course_run.refresh_from_db() # Add a sibling course to make sure it is not returned CourseFactory(should_publish=True) # Add a snapshot to make sure it does not interfere CourseFactory(page_parent=course.extended_object, should_publish=True) self.assertEqual(course_run.get_course(), course) self.assertEqual(course_run.public_course_run.get_course(), course.public_extension)
def test_admin_page_snapshot_with_cms_permissions(self): """ Confirm the creation of a snapshot works as expected: - snapshot title and slug are set to a timestamp, - publication status of the course is respected on the snapshot, - course runs are moved below the snapshot, - publication status of course runs is respected, """ user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") # Create a course page (not published in german) course = CourseFactory( page_title={"en": "a course", "fr": "un cours", "de": "ein Kurs"} ) # Create a course run for this course course_run = CourseRunFactory(direct_course=course) self.assertTrue(course.extended_object.publish("en")) self.assertTrue(course.extended_object.publish("fr")) # It should have copied the course run to the published page self.assertEqual(CourseRun.objects.count(), 2) # Add the necessary permissions (global and per page) self.add_permission(user, "add_page") self.add_permission(user, "change_page") self.add_page_permission( user, course.extended_object, can_change=True, can_add=True ) # Trigger the creation of a snapshot for the course url = f"/en/admin/courses/course/{course.id:d}/snapshot/" now = datetime(2010, 1, 1, tzinfo=timezone.utc) with mock.patch.object(timezone, "now", return_value=now): response = self.client.post(url, follow=True) self.assertEqual(response.status_code, 200) content = json.loads(response.content) self.assertEqual(Course.objects.count(), 4) self.assertEqual(CourseRun.objects.count(), 2) snapshot = ( Course.objects.exclude(id=course.id) .exclude(public_extension__isnull=True) .get() ) self.assertEqual(content, {"id": snapshot.id}) # The snapshot title and slug should include the version with datetime of snapshot expected_titles = { "en": "a course (Archived on 2010-01-01 00:00:00)", "fr": "un cours (Archived on 2010-01-01 00:00:00)", "de": "ein Kurs (Archived on 2010-01-01 00:00:00)", } for language in ["en", "fr", "de"]: self.assertEqual( snapshot.extended_object.get_title(language), expected_titles[language] ) self.assertEqual( snapshot.extended_object.get_slug(language), "archived-on-2010-01-01-000000", ) # The publication status of the course should be respected on the snapshot self.assertTrue(snapshot.check_publication("en")) self.assertTrue(snapshot.check_publication("fr")) self.assertFalse(snapshot.check_publication("de")) # The course run should have moved below the snapshot self.assertEqual(CourseRun.objects.count(), 2) course_run.refresh_from_db() self.assertEqual(course_run.direct_course, snapshot)