Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ Muhammad Rehan <muhammadrehan69@gmail.com>
Shawn Milochik <shawn@milochik.com>
Afeef Janjua <janjua.afeef@gmail.com>
Jacek Bzdak <jbzdak@gmail.com>
Ahmed Jazzar <ajazzar@qrf.org>
Jillian Vogel <pomegranited@gmail.com>
Dan Powell <dan@abakas.com>
Mariana Araújo <simbelm.ne@gmail.com>
Expand Down
28 changes: 28 additions & 0 deletions cms/djangoapps/contentstore/tests/test_course_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
from mock import Mock, patch

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_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import UserFactory
from util import milestones_helpers
from xblock_django.models import XBlockStudioConfigurationFlag
from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum
Expand Down Expand Up @@ -100,6 +102,9 @@ def alter_field(self, url, details, field, val):
resp = self.client.ajax_post(url, payload)
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
def convert_datetime_to_iso(datetime_obj):
"""
Expand Down Expand Up @@ -172,6 +177,9 @@ def test_pre_requisite_course_list_present(self):

@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True})
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)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
Expand All @@ -190,13 +198,19 @@ def test_pre_requisite_course_update_and_fetch(self):
course_detail_json = json.loads(resp.content)
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
course_detail_json['pre_requisite_courses'] = []
self.client.ajax_post(url, course_detail_json)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
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})
def test_invalid_pre_requisite_course(self):
url = get_url(self.course.id)
Expand Down Expand Up @@ -268,6 +282,14 @@ def test_marketing_site_fetch(self):

@unittest.skipUnless(settings.FEATURES.get('ENTRANCE_EXAMS', False), True)
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)
data = {
'entrance_exam_enabled': 'true',
Expand Down Expand Up @@ -299,6 +321,9 @@ def test_entrance_exam_created_updated_and_deleted_successfully(self):
self.assertTrue(course.entrance_exam_enabled)
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
data['entrance_exam_enabled'] = "false"
response = self.client.post(
Expand All @@ -312,6 +337,9 @@ def test_entrance_exam_created_updated_and_deleted_successfully(self):
self.assertFalse(course.entrance_exam_enabled)
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)
def test_entrance_exam_store_default_min_score(self):
"""
Expand Down
7 changes: 7 additions & 0 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
from course_creators.views import add_user_with_status_unrequested, get_course_creator_status
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_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder
Expand All @@ -73,6 +74,7 @@
is_entrance_exams_enabled,
is_prerequisite_courses_enabled,
is_valid_course_key,
remove_prerequisite_course,
set_prerequisite_courses
)
from util.organizations_helpers import add_organization_course, get_organization_by_short_name, organizations_enabled
Expand Down Expand Up @@ -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):
return JsonResponseBadRequest({"error": _("Invalid prerequisite course key")})
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
# feature-specific settings and handle them accordingly
Expand Down
9 changes: 6 additions & 3 deletions common/djangoapps/util/milestones_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,12 @@ def any_unfulfilled_milestones(course_id, user_id):
""" Returns a boolean if user has any unfulfilled milestones """
if not settings.FEATURES.get('MILESTONES_APP'):
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):
Expand Down
27 changes: 18 additions & 9 deletions common/djangoapps/util/tests/test_milestones_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
"""

import ddt
from django.conf import settings
from milestones.exceptions import InvalidCourseKeyException, InvalidUserException
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.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
class MilestonesHelpersTestCase(ModuleStoreTestCase):
"""
Expand All @@ -33,11 +36,14 @@ def setUp(self):

self.user = {'id': '123'}

self.milestone = {
self.milestone = milestones_api.add_milestone({
'name': 'Test Milestone',
'namespace': 'doesnt.matter',
'description': 'Testing Milestones Helpers Library',
}
})

MilestoneRelationshipType.objects.get_or_create(name='requires')
MilestoneRelationshipType.objects.get_or_create(name='fulfills')

@ddt.data(
(False, False, False),
Expand Down Expand Up @@ -115,13 +121,16 @@ def test_get_service_returns_none_when_app_disabled(self):
response = milestones_helpers.get_service()
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):
"""
Tests any_unfulfilled_milestones for invalid arguments with
the app enabled
"""
Tests any_unfulfilled_milestones for invalid arguments with the app enabled.
"""

# Should not raise any exceptions
milestones_helpers.any_unfulfilled_milestones(self.course.id, self.user['id'])

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):
milestones_helpers.any_unfulfilled_milestones(self.course.id, None)