diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 42865e2c9f..a0d2b05525 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -49,9 +49,10 @@ from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.views.index import render_accordion from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from milestones.tests.utils import MilestonesTestCaseMixin +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.djangoapps.crawlers.models import CrawlersConfig from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.lib.gating import api as gating_api -from openedx.core.djangoapps.crawlers.models import CrawlersConfig from student.models import CourseEnrollment from student.tests.factories import AdminFactory, UserFactory, CourseEnrollmentFactory from util.tests.mixins.enterprise import EnterpriseTestConsentRequired @@ -808,44 +809,64 @@ class ViewsTestCase(ModuleStoreTestCase): self.assertEqual(response.status_code, 200) self.assertIn('Financial Assistance Application', response.content) - def test_financial_assistance_form(self): - non_verified_course = CourseFactory.create().id - verified_course_verified_track = CourseFactory.create().id - verified_course_audit_track = CourseFactory.create().id - verified_course_deadline_passed = CourseFactory.create().id - unenrolled_course = CourseFactory.create().id + @ddt.data(([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, True, datetime.now(UTC) - timedelta(days=1)), + ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.VERIFIED, True, None), + ([CourseMode.AUDIT, CourseMode.VERIFIED], CourseMode.AUDIT, False, None), + ([CourseMode.AUDIT], CourseMode.AUDIT, False, None)) + @ddt.unpack + def test_financial_assistance_form_course_exclusion( + self, course_modes, enrollment_mode, eligible_for_aid, expiration): + """Verify that learner cannot get the financial aid for the courses having one of the + following attributes: + 1. User is enrolled in the verified mode. + 2. Course is expired. + 3. Course does not provide financial assistance. + 4. Course does not have verified mode. + """ + # Create course + course = CourseFactory.create() - enrollments = ( - (non_verified_course, CourseMode.AUDIT, None), - (verified_course_verified_track, CourseMode.VERIFIED, None), - (verified_course_audit_track, CourseMode.AUDIT, None), - (verified_course_deadline_passed, CourseMode.AUDIT, datetime.now(UTC) - timedelta(days=1)) - ) - for course, mode, expiration in enrollments: - CourseModeFactory.create(mode_slug=CourseMode.AUDIT, course_id=course) - if course != non_verified_course: - CourseModeFactory.create( - mode_slug=CourseMode.VERIFIED, - course_id=course, - expiration_datetime=expiration - ) - CourseEnrollmentFactory(course_id=course, user=self.user, mode=mode) + # Create Course Modes + for mode in course_modes: + CourseModeFactory.create(mode_slug=mode, course_id=course.id, expiration_datetime=expiration) + + # Enroll user in the course + CourseEnrollmentFactory(course_id=course.id, user=self.user, mode=enrollment_mode) + # load course into course overview + CourseOverview.get_from_id(course.id) + + # add whether course is eligible for financial aid or not + course_overview = CourseOverview.objects.get(id=course.id) + course_overview.eligible_for_financial_aid = eligible_for_aid + course_overview.save() url = reverse('financial_assistance_form') response = self.client.get(url) self.assertEqual(response.status_code, 200) - # Ensure that the user can only apply for assistance in - # courses which have a verified mode which hasn't expired yet, - # where the user is not already enrolled in verified mode - self.assertIn(str(verified_course_audit_track), response.content) - for course in ( - non_verified_course, - verified_course_verified_track, - verified_course_deadline_passed, - unenrolled_course - ): - self.assertNotIn(str(course), response.content) + self.assertNotIn(str(course.id), response.content) + + def test_financial_assistance_form(self): + """Verify that learner can get the financial aid for the course in which + he/she is enrolled in audit mode whereas the course provide verified mode. + """ + # Create course + course = CourseFactory.create().id + + # Create Course Modes + CourseModeFactory.create(mode_slug=CourseMode.AUDIT, course_id=course) + CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=course) + + # Enroll user in the course + CourseEnrollmentFactory(course_id=course, user=self.user, mode=CourseMode.AUDIT) + # load course into course overview + CourseOverview.get_from_id(course) + + url = reverse('financial_assistance_form') + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.assertIn(str(course), response.content) def _submit_financial_assistance_form(self, data): """Submit a financial assistance request.""" diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 03c1d0c5e3..947881853b 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -1536,16 +1536,7 @@ def financial_assistance_request(request): def financial_assistance_form(request): """Render the financial assistance application form page.""" user = request.user - enrolled_courses = [ - {'name': enrollment.course_overview.display_name, 'value': unicode(enrollment.course_id)} - for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created') - - if enrollment.mode != CourseMode.VERIFIED and CourseMode.objects.filter( - Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC())), - course_id=enrollment.course_id, - mode_slug=CourseMode.VERIFIED - ).exists() - ] + enrolled_courses = get_financial_aid_courses(user) incomes = ['Less than $5,000', '$5,000 - $10,000', '$10,000 - $15,000', '$15,000 - $20,000', '$20,000 - $25,000'] annual_incomes = [ {'name': _(income), 'value': income} for income in incomes # pylint: disable=translation-of-non-string @@ -1642,3 +1633,25 @@ def financial_assistance_form(request): } ], }) + + +def get_financial_aid_courses(user): + """ Retrieve the courses eligible for financial assistance. """ + financial_aid_courses = [] + for enrollment in CourseEnrollment.enrollments_for_user(user).order_by('-created'): + + if enrollment.mode != CourseMode.VERIFIED and \ + enrollment.course_overview.eligible_for_financial_aid and \ + CourseMode.objects.filter( + Q(_expiration_datetime__isnull=True) | Q(_expiration_datetime__gt=datetime.now(UTC())), + course_id=enrollment.course_id, + mode_slug=CourseMode.VERIFIED).exists(): + + financial_aid_courses.append( + { + 'name': enrollment.course_overview.display_name, + 'value': unicode(enrollment.course_id) + } + ) + + return financial_aid_courses diff --git a/openedx/core/djangoapps/catalog/management/commands/sync_course_runs.py b/openedx/core/djangoapps/catalog/management/commands/sync_course_runs.py index 1d588fb3ba..f053c3fe75 100644 --- a/openedx/core/djangoapps/catalog/management/commands/sync_course_runs.py +++ b/openedx/core/djangoapps/catalog/management/commands/sync_course_runs.py @@ -35,7 +35,9 @@ class Command(BaseCommand): course_metadata_updated = 0 for course_run in course_runs: + is_course_metadata_updated = False marketing_url = course_run['marketing_url'] + eligible_for_financial_aid = course_run['eligible_for_financial_aid'] course_key = CourseKey.from_string(course_run['key']) try: course_overview = CourseOverview.objects.get(id=course_key) @@ -50,6 +52,14 @@ class Command(BaseCommand): # Check whether course overview's marketing url is outdated - this saves a db hit. if course_overview.marketing_url != marketing_url: course_overview.marketing_url = marketing_url + is_course_metadata_updated = True + + # Check whether course overview's eligible for financial aid is outdated + if course_overview.eligible_for_financial_aid != eligible_for_financial_aid: + course_overview.eligible_for_financial_aid = eligible_for_financial_aid + is_course_metadata_updated = True + + if is_course_metadata_updated: course_overview.save() course_metadata_updated += 1 diff --git a/openedx/core/djangoapps/catalog/management/commands/tests/test_sync_course_runs.py b/openedx/core/djangoapps/catalog/management/commands/tests/test_sync_course_runs.py index 44af166dc2..bc5402ee95 100644 --- a/openedx/core/djangoapps/catalog/management/commands/tests/test_sync_course_runs.py +++ b/openedx/core/djangoapps/catalog/management/commands/tests/test_sync_course_runs.py @@ -29,7 +29,8 @@ class TestSyncCourseRunsCommand(ModuleStoreTestCase): # create a catalog course run with the same course id. self.catalog_course_run = CourseRunFactory( key=unicode(self.course.id), - marketing_url='test_marketing_url' + marketing_url='test_marketing_url', + eligible_for_financial_aid=False ) def get_course_overview_marketing_url(self, course_id): @@ -38,18 +39,25 @@ class TestSyncCourseRunsCommand(ModuleStoreTestCase): """ return CourseOverview.objects.get(id=course_id).marketing_url - def test_marketing_url_on_sync(self, mock_catalog_course_runs): + def test_course_run_sync(self, mock_catalog_course_runs): """ - Verify the updated marketing url on execution of the management command. + Verify on executing management command course overview data is updated + with course run data from course discovery. """ mock_catalog_course_runs.return_value = [self.catalog_course_run] earlier_marketing_url = self.get_course_overview_marketing_url(self.course.id) + course_overview = CourseOverview.objects.get(id=self.course.id) + earlier_eligible_for_financial_aid = course_overview.eligible_for_financial_aid call_command('sync_course_runs') + course_overview.refresh_from_db() updated_marketing_url = self.get_course_overview_marketing_url(self.course.id) + updated_eligible_for_financial_aid = course_overview.eligible_for_financial_aid # Assert that the Marketing URL has changed. self.assertNotEqual(earlier_marketing_url, updated_marketing_url) + self.assertNotEqual(earlier_eligible_for_financial_aid, updated_eligible_for_financial_aid) self.assertEqual(updated_marketing_url, 'test_marketing_url') + self.assertEqual(updated_eligible_for_financial_aid, False) @mock.patch(COMMAND_MODULE + '.log.info') def test_course_overview_does_not_exist(self, mock_log_info, mock_catalog_course_runs): diff --git a/openedx/core/djangoapps/catalog/tests/factories.py b/openedx/core/djangoapps/catalog/tests/factories.py index f365f1b9e2..32cf716b16 100644 --- a/openedx/core/djangoapps/catalog/tests/factories.py +++ b/openedx/core/djangoapps/catalog/tests/factories.py @@ -88,6 +88,7 @@ class CourseRunFactory(DictFactoryBase): image = ImageFactory() key = factory.LazyFunction(generate_course_run_key) marketing_url = factory.Faker('url') + eligible_for_financial_aid = True seats = factory.LazyFunction(partial(generate_instances, SeatFactory)) pacing_type = 'self_paced' short_description = factory.Faker('sentence') diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0012_courseoverview_eligible_for_financial_aid.py b/openedx/core/djangoapps/content/course_overviews/migrations/0012_courseoverview_eligible_for_financial_aid.py new file mode 100644 index 0000000000..6ebf62255f --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0012_courseoverview_eligible_for_financial_aid.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course_overviews', '0011_courseoverview_marketing_url'), + ] + + operations = [ + migrations.AddField( + model_name='courseoverview', + name='eligible_for_financial_aid', + field=models.BooleanField(default=True), + ), + ] diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index b3e2c2d634..e8eeac4008 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -98,6 +98,7 @@ class CourseOverview(TimeStampedModel): effort = TextField(null=True) self_paced = BooleanField(default=False) marketing_url = TextField(null=True) + eligible_for_financial_aid = BooleanField(default=True) @classmethod def _create_from_course(cls, course):