def test_models_course_run_state_coming(self): """ A course run that is future and not yet open for enrollment should return a state with a CTA to see details with the start date. """ course_run = CourseRunFactory( enrollment_start=self.now + timedelta(hours=1), enrollment_end=self.now + timedelta(hours=2), start=self.now + timedelta(hours=3), end=self.now + timedelta(hours=4), ) self.assertEqual( dict(course_run.state), { "priority": 2, "text": "starting on", "call_to_action": None, "datetime": self.now + timedelta(hours=3), }, )
def test_enrollment_create_failure(self, lms_mock): """ What we do when the enrollment fails. """ user = UserFactory() course_run = CourseRunFactory( start=arrow.utcnow().shift(days=-5).datetime, end=arrow.utcnow().shift(days=+90).datetime, enrollment_start=arrow.utcnow().shift(days=-35).datetime, enrollment_end=arrow.utcnow().shift(days=+10).datetime, ) self.client.force_login(user) lms_mock.set_enrollment.return_value = False response = self.client.post("/api/v1.0/enrollments/", data={"course_run": course_run.id}) self.assertEqual(response.status_code, 400) lms_mock.set_enrollment.assert_called_once_with( user, course_run.resource_link)
def test_admin_course_run_change_view_get_superuser_public(self): """Public course runs should not render a change view.""" user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") # Create a public course run course = CourseFactory() CourseRunFactory(direct_course=course) course.extended_object.publish("en") self.assertEqual(CourseRun.objects.count(), 2) public_course_run = CourseRun.objects.get( draft_course_run__isnull=False) # Get the admin change view url = reverse("admin:courses_courserun_change", args=[public_course_run.id]) response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 403)
def test_models_course_run_state_ongoing_closed(self): """ A course run that is on-going but closed for enrollment should return a state with "on-going" as text and no CTA. """ course_run = CourseRunFactory( enrollment_start=self.now - timedelta(hours=3), start=self.now - timedelta(hours=2), enrollment_end=self.now - timedelta(hours=1), end=self.now + timedelta(hours=1), ) self.assertEqual( dict(course_run.state), { "priority": 4, "text": "on-going", "call_to_action": None, "datetime": None, }, )
def test_models_course_copy_relations_publish_recursive_loop(self): """ In a previous version of the the CourseRun method "copy_translations" in which we used instances instead of update queries, this test was generating an infinite recursive loop. """ 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") course_run_translation_fr = CourseRunTranslation.objects.create( master=course_run, language_code="fr", title="ma session" ) self.assertTrue(course.extended_object.publish("fr")) course_run_translation_fr.title = "ma session modifiée" course_run_translation_fr.save() self.assertTrue(course.extended_object.publish("fr"))
def test_models_course_run_state_future_open(self): """ A course run that is future and open for enrollment should return a state with a CTA to enroll and the start date. """ course_run = CourseRunFactory( enrollment_start=self.now - timedelta(hours=1), enrollment_end=self.now + timedelta(hours=1), start=self.now + timedelta(hours=2), end=self.now + timedelta(hours=3), ) self.assertEqual( dict(course_run.state), { "priority": 1, "text": "starting on", "call_to_action": "enroll now", "datetime": self.now + timedelta(hours=2), }, )
def prepare_to_test_state(self, state): """ Not a test. Create objects and mock to help testing the impact of the state on template rendering. """ course = CourseFactory(page_title="my course", should_publish=True) CourseRunFactory( page_parent=course.extended_object, page_title="my course run", should_publish=True, ) url = course.extended_object.get_absolute_url() with mock.patch.object( CourseRun, "state", new_callable=mock.PropertyMock, return_value=state ): response = self.client.get(url) self.assertEqual(response.status_code, 200) return response
def test_enrollment_create(self, lms_mock): """ Course is open for enrollment and user can join it. Enrollment is successful. """ user = UserFactory() course_run = CourseRunFactory( start=arrow.utcnow().shift(days=-5).datetime, end=arrow.utcnow().shift(days=+90).datetime, enrollment_start=arrow.utcnow().shift(days=-35).datetime, enrollment_end=arrow.utcnow().shift(days=+10).datetime, ) self.client.force_login(user) lms_mock.set_enrollment.return_value = True response = self.client.post("/api/v1.0/enrollments/", data={"course_run": course_run.id}) self.assertEqual(response.status_code, 201) lms_mock.set_enrollment.assert_called_once_with( user, course_run.resource_link)
def test_templates_course_run_detail_breadcrumb_below_course(self): """ Validate the format of the breadcrumb on a course run directly placed below the course. """ home_page = PageFactory(title__title="home", title__language="en", should_publish=True) search_page = PageFactory( title__title="courses", title__language="en", parent=home_page, should_publish=True, ) course = CourseFactory( page_title="course name", page_parent=search_page, page_in_navigation=True, should_publish=True, ) course_run = CourseRunFactory( page_title="session 42", page_parent=course.extended_object, should_publish=True, ) response = self.client.get( course_run.extended_object.get_absolute_url()) self.assertEqual(response.status_code, 200) self.assertContains( response, ('<ul class="breadcrumbs__list">' ' <li class="breadcrumbs__item">You are here:</li>' ' <li class="breadcrumbs__item"><a href="/en/home/">home</a></li>' ' <li class="breadcrumbs__item"><a href="/en/home/courses/">courses</a></li>' ' <li class="breadcrumbs__item">' ' <a href="/en/home/courses/course-name/">course name</a>' " </li>" ' <li class="breadcrumbs__item"><span class="active">session 42</span></li>' "</ul>"), html=True, )
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_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_indexers_courses_get_es_documents_no_end_no_enrollment_end(self): """ Course runs with no end date and no date of end of enrollment should be open for ever. """ course = CourseFactory(should_publish=True) CourseRunFactory( page_parent=course.extended_object, end=None, enrollment_end=None, should_publish=True, ) indexed_courses = list( CoursesIndexer.get_es_documents(index="some_index", action="some_action") ) self.assertEqual(len(indexed_courses), 1) self.assertEqual(len(indexed_courses[0]["course_runs"]), 1) self.assertEqual(indexed_courses[0]["course_runs"][0]["end"].year, 9999) self.assertEqual( indexed_courses[0]["course_runs"][0]["enrollment_end"].year, 9999 )
def test_admin_course_run_change_view_get_staff_missing_object_permission( self): """ Staff users missing permissions should not be allowed to view a course run's change view. """ user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") course_run = CourseRunFactory() # Create the required model permissions but not the page permission self.add_permission(user, "change_courserun") self.add_permission(user, "change_page") # Get the admin change view url = reverse("admin:courses_courserun_change", args=[course_run.id]) response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) self.assertNotContains(response, "id_title") self.assertContains(response, "Perhaps it was deleted?")
def test_course_enrollment_widget_props_tag(self): """ CourseEnrollment required id, resource_link and state.priority course run's properties. course_enrollment_widget_props should return these properties wrapped into a courseRun object as a stringified json. """ course_run = CourseRunFactory( resource_link= "http://example.edx:8073/courses/course-v1:edX+DemoX+Demo_Course/course/" ) context = {"run": course_run} self.assertEqual( course_enrollment_widget_props(context), json.dumps({ "courseRun": { "id": course_run.id, "resource_link": course_run.resource_link, "priority": course_run.direct_course.state["priority"], } }), )
def test_models_course_run_mark_dirty_any_field(self): """ Updating the value of any editable field on the course run should mark the related course page dirty (waiting to be published). """ fields = map( lambda f: f.name, filter( lambda f: f.editable and not f.auto_created and not f.name == "direct_course", CourseRun._meta.fields, ), ) stub = CourseRunFactory( sync_mode="manual", catalog_visibility=CourseRunCatalogVisibility.COURSE_ONLY, ) # New random values to update our course run for field in fields: course_run = CourseRunFactory() self.assertTrue( course_run.direct_course.extended_object.publish("en")) title_obj = course_run.direct_course.extended_object.title_set.first( ) setattr(course_run, field, getattr(stub, field)) course_run.save() self.assertEqual( title_obj.publisher_state, PUBLISHER_STATE_DEFAULT, msg=f"Before refreshing from db {field:s}", ) course_run.mark_course_dirty() title_obj.refresh_from_db() self.assertEqual( title_obj.publisher_state, PUBLISHER_STATE_DIRTY, msg=f"After refreshing from db {field:s}", )
def test_admin_course_run_change_view_post_staff_user_missing_permission(self): """ Staff users with missing page permissions can not update a course run via the admin unless CMS permissions are not activated. """ course_run = CourseRunFactory() snapshot = CourseFactory(page_parent=course_run.direct_course.extended_object) user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") # Add only model permissions, not page permission on the course page self.add_permission(user, "add_courserun") self.add_permission(user, "change_courserun") self.add_permission(user, "change_page") self._prepare_change_view_post(course_run, snapshot, 403, self.assertNotEqual) # But it should work if CMS permissions are not activated with override_settings(CMS_PERMISSION=False): self._prepare_change_view_post(course_run, snapshot, 200, self.assertEqual)
def test_admin_course_run_delete_staff_user_missing_permission(self): """ Staff users with missing page permissions can not delete a course run via the admin unless CMS permissions are not activated. """ course_run = CourseRunFactory() user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") # Add only model permissions, not page permission on the course page self.add_permission(user, "delete_courserun") self.add_permission(user, "change_page") self._prepare_delete(course_run, 200, self.assertTrue) # But it should work if CMS permissions are not activated with override_settings(CMS_PERMISSION=False): self._prepare_delete(course_run, 200, self.assertFalse) # The course object should not be deleted self.assertEqual(Course.objects.count(), 1)
def test_models_course_run_state_no_end_date(self): """ A course run with no end date is deemed to be forever on-going. """ course_run = CourseRunFactory(end=None) # The course run should be open during its enrollment period now = datetime.utcfromtimestamp( random.randrange( int(course_run.enrollment_start.timestamp()) + 1, int(course_run.enrollment_end.timestamp()) - 1, ) ).replace(tzinfo=pytz.utc) with mock.patch.object(timezone, "now", return_value=now): state = course_run.state self.assertIn(dict(state)["priority"], [0, 1]) # The course run should be on-going at any date after its end of enrollment now = datetime.utcfromtimestamp( random.randrange( int(course_run.enrollment_end.timestamp()), int(datetime(9999, 12, 31).timestamp()), ) ).replace(tzinfo=pytz.utc) with mock.patch.object(timezone, "now", return_value=now): state = course_run.state self.assertEqual( dict(state), { "priority": 5, "text": "on-going", "call_to_action": None, "datetime": None, }, )
def test_indexers_courses_related_objects_consistency(self): """ The organization and category ids in the Elasticsearch course document should be the same as the ids with which the corresponding organization and category objects are indexed. """ # Create a course with a page in both english and french organization = OrganizationFactory(should_publish=True) category = CategoryFactory(should_publish=True) course = CourseFactory( fill_organizations=[organization], fill_categories=[category], should_publish=True, ) CourseRunFactory(page_parent=course.extended_object, should_publish=True) course_document = list( CoursesIndexer.get_es_documents(index="some_index", action="some_action") )[0] self.assertEqual( course_document["organizations"], [ next( OrganizationsIndexer.get_es_documents( index="some_index", action="some_action" ) )["_id"] ], ) self.assertEqual( course_document["categories"], [ next( CategoriesIndexer.get_es_documents( index="some_index", action="some_action" ) )["_id"] ], )
def test_enrollment_create_closed(self, lms_mock): """ Attempting to enroll in a course that is not open for enrollment anymore results in an error. """ user = UserFactory() course_run = CourseRunFactory( start=arrow.utcnow().shift(days=-35).datetime, end=arrow.utcnow().shift(days=+60).datetime, enrollment_start=arrow.utcnow().shift(days=-65).datetime, enrollment_end=arrow.utcnow().shift(days=-20).datetime, ) self.client.force_login(user) response = self.client.post("/api/v1.0/enrollments/", data={"course_run": course_run.id}) self.assertEqual(response.status_code, 400) self.assertEqual( response.json(), {"errors": ["Course run is not open for enrollments."]}) lms_mock.set_enrollment.assert_not_called()
def test_admin_course_run_change_view_get_staff_all_permissions(self): """ Staff users with all permissions should allowed to view a course run's change view. """ user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") course_run = CourseRunFactory() # Create the required permissions self.add_permission(user, "change_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, ) # Get the admin change view url = reverse("admin:courses_courserun_change", args=[course_run.id]) response = self.client.get(url, follow=True) # Check that the page includes the page title self.assertContains(response, course_run.title, status_code=200) # Check that the page includes all our fields self.assertContains(response, "id_title", count=2) self.assertContains(response, "id_resource_link", count=2) self.assertContains(response, "id_start_", count=3) self.assertContains(response, "id_end_", count=3) self.assertContains(response, "id_enrollment_start_", count=3) self.assertContains(response, "id_enrollment_end_", count=3) self.assertContains(response, "id_languages", count=2) self.assertContains(response, "id_enrollment_count", count=2)
def prepare_to_test_state(self, state, **kwargs): """ Not a test. Create objects and mock to help testing the impact of the state on template rendering. """ course = CourseFactory(should_publish=True) resource_link = kwargs.get( "resource_link") or "https://www.example.com/enroll" course_run = CourseRunFactory( page_parent=course.extended_object, resource_link=resource_link, should_publish=True, ) url = course_run.extended_object.get_absolute_url() with mock.patch.object(CourseRun, "state", new_callable=mock.PropertyMock, return_value=state): response = self.client.get(url) self.assertEqual(response.status_code, 200) return (response, course_run)
def test_templates_course_detail_course_run_title_empty(self): """ A course run title can be empty and in this case the "From ..." string should be capitalized. """ course = CourseFactory() page = course.extended_object CourseRunFactory( direct_course=course, title=None, start=pytz.utc.localize(datetime(2020, 12, 12)), end=pytz.utc.localize(datetime(2020, 12, 15)), ) self.assertTrue(page.publish("en")) url = course.extended_object.get_absolute_url() response = self.client.get(url) self.assertEqual(response.status_code, 200) # The description line should start with a capital letter self.assertContains(response, "<li>From Dec. 12, 2020 to Dec. 15, 2020</li>", html=True)
def test_models_course_state_ongoing_open(self): """ Confirm course state when there is an on-going course run open for enrollment. """ course = CourseFactory() course_run = CourseRunFactory( page_parent=course.extended_object, start=self.now - timedelta(hours=1), end=self.now + timedelta(hours=2), enrollment_end=self.now + timedelta(hours=1), ) with self.assertNumQueries(2): state = course.state expected_state = CourseState(0, course_run.enrollment_end) self.assertEqual(state, expected_state) # Adding courses in less priorietary states should not change the result self.create_run_ongoing_closed(course) self.create_run_future_closed(course) self.create_run_future_open(course) with self.assertNumQueries(2): state = course.state self.assertEqual(state, expected_state)
def test_admin_course_run_change_view_post_staff_user_page_permission(self): """Staff users with all necessary permissions can update a course run via the admin.""" course_run = CourseRunFactory() snapshot = CourseFactory(page_parent=course_run.direct_course.extended_object) user = UserFactory(is_staff=True) self.client.login(username=user.username, password="******") # Add all necessary model and object level permissions self.add_permission(user, "add_courserun") self.add_permission(user, "change_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_change_view_post(course_run, snapshot, 200, self.assertEqual)
def test_models_course_copy_relations_cloning(self): """When cloning a page, the course runs should not be copied.""" course = CourseFactory(page_title="my course title") page = course.extended_object 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" ) # Copy the course page as its own child copy_page = page.copy( page.node.site, parent_node=page.node, translations=True, extensions=True ) self.assertEqual(Course.objects.count(), 2) self.assertEqual(CourseRun.objects.count(), 1) self.assertEqual(CourseRunTranslation.objects.count(), 2) self.assertEqual(Title.objects.count(), 4) self.assertIsNone(copy_page.course.runs.first())
def test_admin_course_run_list_view_superuser(self): """ The admin list view of course runs should only show the id field. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") # Create a course run linked to a page course = CourseFactory() course_run = CourseRunFactory(direct_course=course) course.extended_object.publish("en") self.assertEqual(CourseRun.objects.count(), 2) # Get the admin list view url = reverse("admin:courses_courserun_changelist") response = self.client.get(url, follow=True) self.assertEqual(response.status_code, 200) # Check that only the id field is displayed self.assertContains(response, "field-id", 2) self.assertContains(response, "field-", 2) # Check that the page includes both course run self.assertContains( response, '<p class="paginator">2 course runs</p>', html=True ) change_url_draft = reverse( "admin:courses_courserun_change", args=[course_run.id] ) self.assertContains(response, change_url_draft) change_url_public = reverse( "admin:courses_courserun_change", args=[course_run.public_course_run.id] ) self.assertContains(response, change_url_public)
def test_models_course_run_mark_dirty_direct_course_field(self): """ Changing the course to which a course run is related should mark both the source and the target course pages dirty (waiting to be published). """ course_run = CourseRunFactory() course_source = course_run.direct_course course_target = CourseFactory(should_publish=True) self.assertTrue(course_source.extended_object.publish("en")) title_obj_source = course_source.extended_object.title_set.first() title_obj_target = course_target.extended_object.title_set.first() course_run.direct_course = course_target course_run.save() self.assertEqual(title_obj_source.publisher_state, PUBLISHER_STATE_DEFAULT) self.assertEqual(title_obj_target.publisher_state, PUBLISHER_STATE_DEFAULT) course_run.mark_course_dirty() title_obj_source.refresh_from_db() title_obj_target.refresh_from_db() self.assertEqual(title_obj_source.publisher_state, PUBLISHER_STATE_DIRTY) self.assertEqual(title_obj_target.publisher_state, PUBLISHER_STATE_DIRTY)
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)
def test_models_course_run_get_languages_display_one_language(self): """ With one language, it should return its readable version without any comma. """ course_run = CourseRunFactory(languages=["fr"]) self.assertEqual(course_run.get_languages_display(), "French")