Commit af087291 by George Song Committed by GitHub

Merge pull request #15623 from edx/george/enrollments-prereq

Fixed milestone issues
parents abc503ba fc5716d9
...@@ -259,6 +259,7 @@ Muhammad Rehan <muhammadrehan69@gmail.com> ...@@ -259,6 +259,7 @@ Muhammad Rehan <muhammadrehan69@gmail.com>
Shawn Milochik <shawn@milochik.com> Shawn Milochik <shawn@milochik.com>
Afeef Janjua <janjua.afeef@gmail.com> Afeef Janjua <janjua.afeef@gmail.com>
Jacek Bzdak <jbzdak@gmail.com> Jacek Bzdak <jbzdak@gmail.com>
Ahmed Jazzar <ajazzar@qrf.org>
Jillian Vogel <pomegranited@gmail.com> Jillian Vogel <pomegranited@gmail.com>
Dan Powell <dan@abakas.com> Dan Powell <dan@abakas.com>
Mariana Araújo <simbelm.ne@gmail.com> Mariana Araújo <simbelm.ne@gmail.com>
......
...@@ -15,6 +15,7 @@ from milestones.tests.utils import MilestonesTestCaseMixin ...@@ -15,6 +15,7 @@ from milestones.tests.utils import MilestonesTestCaseMixin
from mock import Mock, patch from mock import Mock, patch
from contentstore.utils import reverse_course_url, reverse_usage_url from contentstore.utils import reverse_course_url, reverse_usage_url
from milestones.models import MilestoneRelationshipType
from models.settings.course_grading import CourseGradingModel, GRADING_POLICY_CHANGED_EVENT_TYPE, hash_grading_policy from models.settings.course_grading import CourseGradingModel, GRADING_POLICY_CHANGED_EVENT_TYPE, hash_grading_policy
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder from models.settings.encoder import CourseSettingsEncoder
...@@ -22,6 +23,7 @@ from openedx.core.djangoapps.models.course_details import CourseDetails ...@@ -22,6 +23,7 @@ from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from student.roles import CourseInstructorRole, CourseStaffRole from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from util import milestones_helpers
from xblock_django.models import XBlockStudioConfigurationFlag from xblock_django.models import XBlockStudioConfigurationFlag
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -100,6 +102,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -100,6 +102,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
resp = self.client.ajax_post(url, payload) resp = self.client.ajax_post(url, payload)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val)) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
MilestoneRelationshipType.objects.get_or_create(name='requires')
MilestoneRelationshipType.objects.get_or_create(name='fulfills')
@staticmethod @staticmethod
def convert_datetime_to_iso(datetime_obj): def convert_datetime_to_iso(datetime_obj):
""" """
...@@ -172,6 +177,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -172,6 +177,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True}) @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
def test_pre_requisite_course_update_and_fetch(self): def test_pre_requisite_course_update_and_fetch(self):
self.assertFalse(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id),
msg='The initial empty state should be: no prerequisite courses')
url = get_url(self.course.id) url = get_url(self.course.id)
resp = self.client.get_json(url) resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content) course_detail_json = json.loads(resp.content)
...@@ -190,6 +198,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -190,6 +198,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
course_detail_json = json.loads(resp.content) course_detail_json = json.loads(resp.content)
self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses']) self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses'])
self.assertTrue(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id),
msg='Should have prerequisite courses')
# remove pre requisite course # remove pre requisite course
course_detail_json['pre_requisite_courses'] = [] course_detail_json['pre_requisite_courses'] = []
self.client.ajax_post(url, course_detail_json) self.client.ajax_post(url, course_detail_json)
...@@ -197,6 +208,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -197,6 +208,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
course_detail_json = json.loads(resp.content) course_detail_json = json.loads(resp.content)
self.assertEqual([], course_detail_json['pre_requisite_courses']) self.assertEqual([], course_detail_json['pre_requisite_courses'])
self.assertFalse(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id),
msg='Should not have prerequisite courses anymore')
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True}) @mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
def test_invalid_pre_requisite_course(self): def test_invalid_pre_requisite_course(self):
url = get_url(self.course.id) url = get_url(self.course.id)
...@@ -268,6 +282,14 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -268,6 +282,14 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True) @unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
def test_entrance_exam_created_updated_and_deleted_successfully(self): def test_entrance_exam_created_updated_and_deleted_successfully(self):
"""
This tests both of the entrance exam settings and the `any_unfulfilled_milestones` helper.
Splitting the test requires significant refactoring `settings_handler()` view.
"""
self.assertFalse(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id),
msg='The initial empty state should be: no entrance exam')
settings_details_url = get_url(self.course.id) settings_details_url = get_url(self.course.id)
data = { data = {
'entrance_exam_enabled': 'true', 'entrance_exam_enabled': 'true',
...@@ -299,6 +321,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -299,6 +321,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertTrue(course.entrance_exam_enabled) self.assertTrue(course.entrance_exam_enabled)
self.assertEquals(course.entrance_exam_minimum_score_pct, .80) self.assertEquals(course.entrance_exam_minimum_score_pct, .80)
self.assertTrue(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id),
msg='The entrance exam should be required.')
# Delete the entrance exam # Delete the entrance exam
data['entrance_exam_enabled'] = "false" data['entrance_exam_enabled'] = "false"
response = self.client.post( response = self.client.post(
...@@ -312,6 +337,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin): ...@@ -312,6 +337,9 @@ class CourseDetailsViewTest(CourseTestCase, MilestonesTestCaseMixin):
self.assertFalse(course.entrance_exam_enabled) self.assertFalse(course.entrance_exam_enabled)
self.assertEquals(course.entrance_exam_minimum_score_pct, None) self.assertEquals(course.entrance_exam_minimum_score_pct, None)
self.assertFalse(milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user.id),
msg='The entrance exam should not be required anymore')
@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True) @unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
def test_entrance_exam_store_default_min_score(self): def test_entrance_exam_store_default_min_score(self):
""" """
......
...@@ -51,6 +51,7 @@ from course_action_state.managers import CourseActionStateItemNotFoundError ...@@ -51,6 +51,7 @@ from course_action_state.managers import CourseActionStateItemNotFoundError
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from course_creators.views import add_user_with_status_unrequested, get_course_creator_status from course_creators.views import add_user_with_status_unrequested, get_course_creator_status
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from milestones import api as milestones_api
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder from models.settings.encoder import CourseSettingsEncoder
...@@ -73,6 +74,7 @@ from util.milestones_helpers import ( ...@@ -73,6 +74,7 @@ from util.milestones_helpers import (
is_entrance_exams_enabled, is_entrance_exams_enabled,
is_prerequisite_courses_enabled, is_prerequisite_courses_enabled,
is_valid_course_key, is_valid_course_key,
remove_prerequisite_course,
set_prerequisite_courses set_prerequisite_courses
) )
from util.organizations_helpers import add_organization_course, get_organization_by_short_name, organizations_enabled from util.organizations_helpers import add_organization_course, get_organization_by_short_name, organizations_enabled
...@@ -1075,6 +1077,11 @@ def settings_handler(request, course_key_string): ...@@ -1075,6 +1077,11 @@ def settings_handler(request, course_key_string):
if not all(is_valid_course_key(course_key) for course_key in prerequisite_course_keys): if not all(is_valid_course_key(course_key) for course_key in prerequisite_course_keys):
return JsonResponseBadRequest({"error": _("Invalid prerequisite course key")}) return JsonResponseBadRequest({"error": _("Invalid prerequisite course key")})
set_prerequisite_courses(course_key, prerequisite_course_keys) set_prerequisite_courses(course_key, prerequisite_course_keys)
else:
# None is chosen, so remove the course prerequisites
course_milestones = milestones_api.get_course_milestones(course_key=course_key, relationship="requires")
for milestone in course_milestones:
remove_prerequisite_course(course_key, milestone)
# If the entrance exams feature has been enabled, we'll need to check for some # If the entrance exams feature has been enabled, we'll need to check for some
# feature-specific settings and handle them accordingly # feature-specific settings and handle them accordingly
......
...@@ -384,9 +384,12 @@ def any_unfulfilled_milestones(course_id, user_id): ...@@ -384,9 +384,12 @@ def any_unfulfilled_milestones(course_id, user_id):
""" Returns a boolean if user has any unfulfilled milestones """ """ Returns a boolean if user has any unfulfilled milestones """
if not settings.FEATURES.get('MILESTONES_APP'): if not settings.FEATURES.get('MILESTONES_APP'):
return False return False
return bool(
get_course_milestones_fulfillment_paths(course_id, {"id": user_id}) fulfillment_paths = milestones_api.get_course_milestones_fulfillment_paths(course_id, {'id': user_id})
)
# Returns True if any of the milestones is unfulfilled. False if
# values is empty or all values are.
return any(fulfillment_paths.values())
def get_course_milestones_fulfillment_paths(course_id, user_id): def get_course_milestones_fulfillment_paths(course_id, user_id):
......
...@@ -3,15 +3,18 @@ Tests for the milestones helpers library, which is the integration point for the ...@@ -3,15 +3,18 @@ Tests for the milestones helpers library, which is the integration point for the
""" """
import ddt import ddt
from django.conf import settings
from milestones.exceptions import InvalidCourseKeyException, InvalidUserException from milestones.exceptions import InvalidCourseKeyException, InvalidUserException
from mock import patch from mock import patch
from util import milestones_helpers from milestones import api as milestones_api
from milestones.models import MilestoneRelationshipType
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from util import milestones_helpers
@patch.dict('django.conf.settings.FEATURES', {'MILESTONES_APP': False}) @patch.dict(settings.FEATURES, {'MILESTONES_APP': False})
@ddt.ddt @ddt.ddt
class MilestonesHelpersTestCase(ModuleStoreTestCase): class MilestonesHelpersTestCase(ModuleStoreTestCase):
""" """
...@@ -33,11 +36,14 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase): ...@@ -33,11 +36,14 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
self.user = {'id': '123'} self.user = {'id': '123'}
self.milestone = { self.milestone = milestones_api.add_milestone({
'name': 'Test Milestone', 'name': 'Test Milestone',
'namespace': 'doesnt.matter', 'namespace': 'doesnt.matter',
'description': 'Testing Milestones Helpers Library', 'description': 'Testing Milestones Helpers Library',
} })
MilestoneRelationshipType.objects.get_or_create(name='requires')
MilestoneRelationshipType.objects.get_or_create(name='fulfills')
@ddt.data( @ddt.data(
(False, False, False), (False, False, False),
...@@ -115,13 +121,16 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase): ...@@ -115,13 +121,16 @@ class MilestonesHelpersTestCase(ModuleStoreTestCase):
response = milestones_helpers.get_service() response = milestones_helpers.get_service()
self.assertIsNone(response) self.assertIsNone(response)
@patch.dict('django.conf.settings.FEATURES', {'MILESTONES_APP': True}) @patch.dict(settings.FEATURES, {'MILESTONES_APP': True})
def test_any_unfulfilled_milestones(self): def test_any_unfulfilled_milestones(self):
""" """
Tests any_unfulfilled_milestones for invalid arguments with Tests any_unfulfilled_milestones for invalid arguments with the app enabled.
the app enabled """
"""
# Should not raise any exceptions
milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user['id'])
with self.assertRaises(InvalidCourseKeyException): with self.assertRaises(InvalidCourseKeyException):
milestones_helpers.any_unfulfilled_milestones(None, self.user) milestones_helpers.any_unfulfilled_milestones(None, self.user['id'])
with self.assertRaises(InvalidUserException): with self.assertRaises(InvalidUserException):
milestones_helpers.any_unfulfilled_milestones(self.course.id, None) milestones_helpers.any_unfulfilled_milestones(self.course.id, None)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment