New pre-requisite course feature via milestones app
This commit is contained in:
@@ -25,6 +25,8 @@ from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
|
||||
import ddt
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from util.milestones_helpers import seed_milestone_relationship_types
|
||||
|
||||
|
||||
def get_url(course_id, handler_name='settings_handler'):
|
||||
return reverse_course_url(handler_name, course_id)
|
||||
@@ -171,6 +173,9 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
"""
|
||||
Tests for modifying content on the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
def setUp(self):
|
||||
super(CourseDetailsViewTest, self).setUp()
|
||||
|
||||
def alter_field(self, url, details, field, val):
|
||||
"""
|
||||
Change the one field to the given value and then invoke the update post to see if it worked.
|
||||
@@ -243,6 +248,55 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
elif field in encoded and encoded[field] is not None:
|
||||
self.fail(field + " included in encoding but missing from details at " + context)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_pre_requisite_course_list_present(self):
|
||||
seed_milestone_relationship_types()
|
||||
settings_details_url = get_url(self.course.id)
|
||||
response = self.client.get_html(settings_details_url)
|
||||
self.assertContains(response, "Prerequisite Course")
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_pre_requisite_course_update_and_fetch(self):
|
||||
seed_milestone_relationship_types()
|
||||
url = get_url(self.course.id)
|
||||
resp = self.client.get_json(url)
|
||||
course_detail_json = json.loads(resp.content)
|
||||
# assert pre_requisite_courses is initialized
|
||||
self.assertEqual([], course_detail_json['pre_requisite_courses'])
|
||||
|
||||
# update pre requisite courses with a new course keys
|
||||
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
|
||||
pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run')
|
||||
pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)]
|
||||
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
|
||||
self.client.ajax_post(url, course_detail_json)
|
||||
|
||||
# fetch updated course to assert pre_requisite_courses has new values
|
||||
resp = self.client.get_json(url)
|
||||
course_detail_json = json.loads(resp.content)
|
||||
self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_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'])
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_invalid_pre_requisite_course(self):
|
||||
seed_milestone_relationship_types()
|
||||
url = get_url(self.course.id)
|
||||
resp = self.client.get_json(url)
|
||||
course_detail_json = json.loads(resp.content)
|
||||
|
||||
# update pre requisite courses one valid and one invalid key
|
||||
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
|
||||
pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key']
|
||||
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
|
||||
response = self.client.ajax_post(url, course_detail_json)
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseGradingTest(CourseTestCase):
|
||||
|
||||
@@ -74,6 +74,11 @@ from microsite_configuration import microsite
|
||||
from xmodule.course_module import CourseFields
|
||||
from xmodule.split_test_module import get_split_user_partitions
|
||||
|
||||
from util.milestones_helpers import (
|
||||
set_prerequisite_courses,
|
||||
is_valid_course_key
|
||||
)
|
||||
|
||||
MINIMUM_GROUP_ID = 100
|
||||
|
||||
# Note: the following content group configuration strings are not
|
||||
@@ -368,37 +373,10 @@ def _accessible_libraries_list(user):
|
||||
def course_listing(request):
|
||||
"""
|
||||
List all courses available to the logged in user
|
||||
Try to get all courses by first reversing django groups and fallback to old method if it fails
|
||||
Note: overhead of pymongo reads will increase if getting courses from django groups fails
|
||||
"""
|
||||
if GlobalStaff().has_user(request.user):
|
||||
# user has global access so no need to get courses from django groups
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
else:
|
||||
try:
|
||||
courses, in_process_course_actions = _accessible_courses_list_from_groups(request)
|
||||
except AccessListFallback:
|
||||
# user have some old groups or there was some error getting courses from django groups
|
||||
# so fallback to iterating through all courses
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
|
||||
courses, in_process_course_actions = get_courses_accessible_to_user(request)
|
||||
libraries = _accessible_libraries_list(request.user) if LIBRARIES_ENABLED else []
|
||||
|
||||
def format_course_for_view(course):
|
||||
"""
|
||||
Return a dict of the data which the view requires for each course
|
||||
"""
|
||||
return {
|
||||
'display_name': course.display_name,
|
||||
'course_key': unicode(course.location.course_key),
|
||||
'url': reverse_course_url('course_handler', course.id),
|
||||
'lms_link': get_lms_link_for_item(course.location),
|
||||
'rerun_link': _get_rerun_link_for_item(course.id),
|
||||
'org': course.display_org_with_default,
|
||||
'number': course.display_number_with_default,
|
||||
'run': course.location.run
|
||||
}
|
||||
|
||||
def format_in_process_course_view(uca):
|
||||
"""
|
||||
Return a dict of the data which the view requires for each unsucceeded course
|
||||
@@ -433,14 +411,7 @@ def course_listing(request):
|
||||
'can_edit': has_studio_write_access(request.user, library.location.library_key),
|
||||
}
|
||||
|
||||
# remove any courses in courses that are also in the in_process_course_actions list
|
||||
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
|
||||
courses = [
|
||||
format_course_for_view(c)
|
||||
for c in courses
|
||||
if not isinstance(c, ErrorDescriptor) and (c.id not in in_process_action_course_keys)
|
||||
]
|
||||
|
||||
courses = _remove_in_process_courses(courses, in_process_course_actions)
|
||||
in_process_course_actions = [format_in_process_course_view(uca) for uca in in_process_course_actions]
|
||||
|
||||
return render_to_response('index.html', {
|
||||
@@ -508,6 +479,53 @@ def course_index(request, course_key):
|
||||
})
|
||||
|
||||
|
||||
def get_courses_accessible_to_user(request):
|
||||
"""
|
||||
Try to get all courses by first reversing django groups and fallback to old method if it fails
|
||||
Note: overhead of pymongo reads will increase if getting courses from django groups fails
|
||||
"""
|
||||
if GlobalStaff().has_user(request.user):
|
||||
# user has global access so no need to get courses from django groups
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
else:
|
||||
try:
|
||||
courses, in_process_course_actions = _accessible_courses_list_from_groups(request)
|
||||
except AccessListFallback:
|
||||
# user have some old groups or there was some error getting courses from django groups
|
||||
# so fallback to iterating through all courses
|
||||
courses, in_process_course_actions = _accessible_courses_list(request)
|
||||
return courses, in_process_course_actions
|
||||
|
||||
|
||||
def _remove_in_process_courses(courses, in_process_course_actions):
|
||||
"""
|
||||
removes any in-process courses in courses list. in-process actually refers to courses
|
||||
that are in the process of being generated for re-run
|
||||
"""
|
||||
def format_course_for_view(course):
|
||||
"""
|
||||
Return a dict of the data which the view requires for each course
|
||||
"""
|
||||
return {
|
||||
'display_name': course.display_name,
|
||||
'course_key': unicode(course.location.course_key),
|
||||
'url': reverse_course_url('course_handler', course.id),
|
||||
'lms_link': get_lms_link_for_item(course.location),
|
||||
'rerun_link': _get_rerun_link_for_item(course.id),
|
||||
'org': course.display_org_with_default,
|
||||
'number': course.display_number_with_default,
|
||||
'run': course.location.run
|
||||
}
|
||||
|
||||
in_process_action_course_keys = [uca.course_key for uca in in_process_course_actions]
|
||||
courses = [
|
||||
format_course_for_view(c)
|
||||
for c in courses
|
||||
if not isinstance(c, ErrorDescriptor) and (c.id not in in_process_action_course_keys)
|
||||
]
|
||||
return courses
|
||||
|
||||
|
||||
def course_outline_initial_state(locator_to_show, course_structure):
|
||||
"""
|
||||
Returns the desired initial state for the course outline view. If the 'show' request parameter
|
||||
@@ -783,6 +801,7 @@ def settings_handler(request, course_key_string):
|
||||
json: update the Course and About xblocks through the CourseDetails model
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
prerequisite_course_enabled = settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False)
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course_module = get_course_and_check_access(course_key, request.user)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
@@ -797,8 +816,7 @@ def settings_handler(request, course_key_string):
|
||||
)
|
||||
|
||||
short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True)
|
||||
|
||||
return render_to_response('settings.html', {
|
||||
settings_context = {
|
||||
'context_course': course_module,
|
||||
'course_locator': course_key,
|
||||
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
|
||||
@@ -807,15 +825,31 @@ def settings_handler(request, course_key_string):
|
||||
'about_page_editable': about_page_editable,
|
||||
'short_description_editable': short_description_editable,
|
||||
'upload_asset_url': upload_asset_url
|
||||
})
|
||||
}
|
||||
if prerequisite_course_enabled:
|
||||
courses, in_process_course_actions = get_courses_accessible_to_user(request)
|
||||
# exclude current course from the list of available courses
|
||||
courses = [course for course in courses if course.id != course_key]
|
||||
if courses:
|
||||
courses = _remove_in_process_courses(courses, in_process_course_actions)
|
||||
settings_context.update({'possible_pre_requisite_courses': courses})
|
||||
|
||||
return render_to_response('settings.html', settings_context)
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
course_details = CourseDetails.fetch(course_key)
|
||||
return JsonResponse(
|
||||
CourseDetails.fetch(course_key),
|
||||
course_details,
|
||||
# encoder serializes dates, old locations, and instances
|
||||
encoder=CourseSettingsEncoder
|
||||
)
|
||||
else: # post or put, doesn't matter.
|
||||
# if pre-requisite course feature is enabled set pre-requisite course
|
||||
if prerequisite_course_enabled:
|
||||
prerequisite_course_keys = request.json.get('pre_requisite_courses', [])
|
||||
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)
|
||||
return JsonResponse(
|
||||
CourseDetails.update_from_json(course_key, request.json, request.user),
|
||||
encoder=CourseSettingsEncoder
|
||||
|
||||
@@ -39,6 +39,7 @@ class CourseDetails(object):
|
||||
self.effort = None # int hours/week
|
||||
self.course_image_name = ""
|
||||
self.course_image_asset_path = "" # URL of the course image
|
||||
self.pre_requisite_courses = [] # pre-requisite courses
|
||||
|
||||
@classmethod
|
||||
def _fetch_about_attribute(cls, course_key, attribute):
|
||||
@@ -64,6 +65,7 @@ class CourseDetails(object):
|
||||
course_details.end_date = descriptor.end
|
||||
course_details.enrollment_start = descriptor.enrollment_start
|
||||
course_details.enrollment_end = descriptor.enrollment_end
|
||||
course_details.pre_requisite_courses = descriptor.pre_requisite_courses
|
||||
course_details.course_image_name = descriptor.course_image
|
||||
course_details.course_image_asset_path = course_image_url(descriptor)
|
||||
|
||||
@@ -155,6 +157,11 @@ class CourseDetails(object):
|
||||
descriptor.course_image = jsondict['course_image_name']
|
||||
dirty = True
|
||||
|
||||
if 'pre_requisite_courses' in jsondict \
|
||||
and sorted(jsondict['pre_requisite_courses']) != sorted(descriptor.pre_requisite_courses):
|
||||
descriptor.pre_requisite_courses = jsondict['pre_requisite_courses']
|
||||
dirty = True
|
||||
|
||||
if dirty:
|
||||
module_store.update_item(descriptor, user.id)
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class CourseMetadata(object):
|
||||
'tags', # from xblock
|
||||
'visible_to_staff_only',
|
||||
'group_access',
|
||||
'pre_requisite_courses'
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -54,6 +54,12 @@ for log_name, log_level in LOG_OVERRIDES:
|
||||
# Use the auto_auth workflow for creating users and logging them in
|
||||
FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
|
||||
|
||||
# Enable milestones app
|
||||
FEATURES['MILESTONES_APP'] = True
|
||||
|
||||
# Enable pre-requisite course
|
||||
FEATURES['ENABLE_PREREQUISITE_COURSES'] = True
|
||||
|
||||
# Unfortunately, we need to use debug mode to serve staticfiles
|
||||
DEBUG = True
|
||||
|
||||
|
||||
@@ -128,6 +128,12 @@ FEATURES = {
|
||||
# DEFAULT_STORE_FOR_NEW_COURSE to be 'split' to have future courses
|
||||
# and libraries created with split.
|
||||
'ENABLE_CONTENT_LIBRARIES': False,
|
||||
|
||||
# Milestones application flag
|
||||
'MILESTONES_APP': False,
|
||||
|
||||
# Prerequisite courses feature flag
|
||||
'ENABLE_PREREQUISITE_COURSES': False,
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
@@ -744,7 +750,8 @@ OPTIONAL_APPS = (
|
||||
'openassessment.xblock',
|
||||
|
||||
# edxval
|
||||
'edxval'
|
||||
'edxval',
|
||||
'milestones'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -155,6 +155,9 @@ CACHES = {
|
||||
# Add external_auth to Installed apps for testing
|
||||
INSTALLED_APPS += ('external_auth', )
|
||||
|
||||
# Add milestones to Installed apps for testing
|
||||
INSTALLED_APPS += ('milestones', )
|
||||
|
||||
# hide ratelimit warnings while running tests
|
||||
filterwarnings('ignore', message='No request passed to the backend, unable to rate-limit')
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ var CourseDetails = Backbone.Model.extend({
|
||||
intro_video: null,
|
||||
effort: null, // an int or null,
|
||||
course_image_name: '', // the filename
|
||||
course_image_asset_path: '' // the full URL (/c4x/org/course/num/asset/filename)
|
||||
course_image_asset_path: '', // the full URL (/c4x/org/course/num/asset/filename)
|
||||
pre_requisite_courses: []
|
||||
},
|
||||
|
||||
validate: function(newattrs) {
|
||||
|
||||
@@ -4,7 +4,7 @@ define([
|
||||
], function($, CourseDetailsModel, MainView, AjaxHelpers) {
|
||||
'use strict';
|
||||
describe('Settings/Main', function () {
|
||||
var urlRoot = '/course-details',
|
||||
var urlRoot = '/course/settings/org/DemoX/Demo_Course',
|
||||
modelData = {
|
||||
start_date: "2014-10-05T00:00:00Z",
|
||||
end_date: "2014-11-05T20:00:00Z",
|
||||
@@ -19,7 +19,8 @@ define([
|
||||
intro_video : null,
|
||||
effort : null,
|
||||
course_image_name : '',
|
||||
course_image_asset_path : ''
|
||||
course_image_asset_path : '',
|
||||
pre_requisite_courses : []
|
||||
},
|
||||
mockSettingsPage = readFixtures('mock/mock-settings-page.underscore');
|
||||
|
||||
@@ -47,7 +48,6 @@ define([
|
||||
// Expect to see changes just in `start_date` field.
|
||||
start_date: "2014-10-05T22:00:00.000Z"
|
||||
});
|
||||
|
||||
this.view.$el.find('#course-start-time')
|
||||
.val('22:00')
|
||||
.trigger('input');
|
||||
@@ -56,8 +56,25 @@ define([
|
||||
// It sends `POST` request, because the model doesn't have `id`. In
|
||||
// this case, it is considered to be new according to Backbone documentation.
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'POST', '/course-details', expectedJson
|
||||
requests, 'POST', urlRoot, expectedJson
|
||||
);
|
||||
});
|
||||
|
||||
it('Selecting a course in pre-requisite drop down should save it as part of course details', function () {
|
||||
var pre_requisite_courses = ['test/CSS101/2012_T1'];
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
expectedJson = $.extend(true, {}, modelData, {
|
||||
pre_requisite_courses: pre_requisite_courses
|
||||
});
|
||||
this.view.$el.find('#pre-requisite-course')
|
||||
.val(pre_requisite_courses[0])
|
||||
.trigger('change');
|
||||
|
||||
this.view.saveView();
|
||||
AjaxHelpers.expectJsonRequest(
|
||||
requests, 'POST', urlRoot, expectedJson
|
||||
);
|
||||
AjaxHelpers.respondWithJson(requests, expectedJson);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ var DetailsView = ValidatingView.extend({
|
||||
// Leaving change in as fallback for older browsers
|
||||
"change input" : "updateModel",
|
||||
"change textarea" : "updateModel",
|
||||
"change select" : "updateModel",
|
||||
'click .remove-course-introduction-video' : "removeVideo",
|
||||
'focus #course-overview' : "codeMirrorize",
|
||||
'mouseover .timezone' : "updateTime",
|
||||
@@ -63,6 +64,9 @@ var DetailsView = ValidatingView.extend({
|
||||
var imageURL = this.model.get('course_image_asset_path');
|
||||
this.$el.find('#course-image-url').val(imageURL);
|
||||
this.$el.find('#course-image').attr('src', imageURL);
|
||||
var pre_requisite_courses = this.model.get('pre_requisite_courses');
|
||||
pre_requisite_courses = pre_requisite_courses.length > 0 ? pre_requisite_courses : '';
|
||||
this.$el.find('#' + this.fieldToSelectorMap['pre_requisite_courses']).val(pre_requisite_courses);
|
||||
|
||||
return this;
|
||||
},
|
||||
@@ -75,7 +79,8 @@ var DetailsView = ValidatingView.extend({
|
||||
'short_description' : 'course-short-description',
|
||||
'intro_video' : 'course-introduction-video',
|
||||
'effort' : "course-effort",
|
||||
'course_image_asset_path': 'course-image-url'
|
||||
'course_image_asset_path': 'course-image-url',
|
||||
'pre_requisite_courses': 'pre-requisite-course'
|
||||
},
|
||||
|
||||
updateTime : function(e) {
|
||||
@@ -154,6 +159,11 @@ var DetailsView = ValidatingView.extend({
|
||||
case 'course-short-description':
|
||||
this.setField(event);
|
||||
break;
|
||||
case 'pre-requisite-course':
|
||||
var value = $(event.currentTarget).val();
|
||||
value = value == "" ? [] : [value];
|
||||
this.model.set('pre_requisite_courses', value);
|
||||
break;
|
||||
// Don't make the user reload the page to check the Youtube ID.
|
||||
// Wait for a second to load the video, avoiding egregious AJAX calls.
|
||||
case 'course-introduction-video':
|
||||
|
||||
@@ -62,6 +62,19 @@
|
||||
<span class="tip tip-stacked timezone">(UTC)</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<li class="field field-select" id="field-pre-requisite-course">
|
||||
<label for="pre-requisite-course" class="">Prerequisite Course</label>
|
||||
<select class="input" id="pre-requisite-course">
|
||||
<option value="">None</option>
|
||||
<option value="test/CSS101/2012_T1">[Test] Communicating for Impact</option>
|
||||
<option value="Test/3423/2014_T2">CohortAverageTesting</option>
|
||||
<option value="edX/Open_DemoX/edx_demo_course">edX Demonstration Course</option>
|
||||
</select>
|
||||
<span class="tip tip-inline">Course that students must complete before beginning this course</span>
|
||||
<button type="submit" class="sr" name="submit" value="submit">set pre-requisite course</button>
|
||||
</li>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
@@ -307,6 +307,21 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">${_("Time spent on all course work")}</span>
|
||||
</li>
|
||||
% if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES'):
|
||||
<li class="field field-select" id="field-pre-requisite-course">
|
||||
<form action="#" class="pre-requisite-course-form" method="post">
|
||||
<label for="pre-requisite-course">${_("Prerequisite Course")}</label>
|
||||
<select class="input" id="pre-requisite-course">
|
||||
<option value="">${_("None")}</option>
|
||||
% for course_info in sorted(possible_pre_requisite_courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
|
||||
<option value="${course_info['course_key']}">${course_info['display_name']}</option>
|
||||
% endfor
|
||||
</select>
|
||||
<span class="tip tip-inline">${_("Course that students must complete before beginning this course")}</span>
|
||||
<button type="submit" class="sr" name="submit" value="submit">${_("set pre-requisite course")}</button>
|
||||
</form>
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Unit tests for getting the list of courses for a user through iterating all courses and
|
||||
by reversing group name formats.
|
||||
"""
|
||||
import mock
|
||||
from mock import patch, Mock
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -15,6 +16,12 @@ from xmodule.error_module import ErrorDescriptor
|
||||
from django.test.client import Client
|
||||
from student.models import CourseEnrollment
|
||||
from student.views import get_course_enrollment_pairs
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from util.milestones_helpers import (
|
||||
get_pre_requisite_courses_not_completed,
|
||||
set_prerequisite_courses,
|
||||
seed_milestone_relationship_types
|
||||
)
|
||||
import unittest
|
||||
from django.conf import settings
|
||||
|
||||
@@ -35,14 +42,16 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username=self.teacher.username, password='test')
|
||||
|
||||
def _create_course_with_access_groups(self, course_location):
|
||||
def _create_course_with_access_groups(self, course_location, metadata=None):
|
||||
"""
|
||||
Create dummy course with 'CourseFactory' and enroll the student
|
||||
"""
|
||||
metadata = {} if not metadata else metadata
|
||||
course = CourseFactory.create(
|
||||
org=course_location.org,
|
||||
number=course_location.course,
|
||||
run=course_location.run
|
||||
run=course_location.run,
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
CourseEnrollment.enroll(self.student, course.id)
|
||||
@@ -119,3 +128,38 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
courses_list = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
self.assertEqual(len(courses_list), 1, courses_list)
|
||||
self.assertEqual(courses_list[0][0].id, good_location)
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_course_listing_has_pre_requisite_courses(self):
|
||||
"""
|
||||
Creates four courses. Enroll test user in all courses
|
||||
Sets two of them as pre-requisites of another course.
|
||||
Checks course where pre-requisite course is set has appropriate info.
|
||||
"""
|
||||
seed_milestone_relationship_types()
|
||||
course_location2 = CourseKey.from_string('Org1/Course2/Run2')
|
||||
self._create_course_with_access_groups(course_location2)
|
||||
pre_requisite_course_location = CourseKey.from_string('Org1/Course3/Run3')
|
||||
self._create_course_with_access_groups(pre_requisite_course_location)
|
||||
pre_requisite_course_location2 = CourseKey.from_string('Org1/Course4/Run4')
|
||||
self._create_course_with_access_groups(pre_requisite_course_location2)
|
||||
# create a course with pre_requisite_courses
|
||||
pre_requisite_courses = [
|
||||
unicode(pre_requisite_course_location),
|
||||
unicode(pre_requisite_course_location2),
|
||||
]
|
||||
course_location = CourseKey.from_string('Org1/Course1/Run1')
|
||||
self._create_course_with_access_groups(course_location, {
|
||||
'pre_requisite_courses': pre_requisite_courses
|
||||
})
|
||||
|
||||
set_prerequisite_courses(course_location, pre_requisite_courses)
|
||||
# get dashboard
|
||||
course_enrollment_pairs = list(get_course_enrollment_pairs(self.student, None, []))
|
||||
courses_having_prerequisites = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if course.pre_requisite_courses)
|
||||
courses_requirements_not_met = get_pre_requisite_courses_not_completed(
|
||||
self.student,
|
||||
courses_having_prerequisites
|
||||
)
|
||||
self.assertEqual(len(courses_requirements_not_met[course_location]['courses']), len(pre_requisite_courses))
|
||||
|
||||
@@ -89,7 +89,9 @@ import dogstats_wrapper as dog_stats_api
|
||||
from util.db import commit_on_success_with_read_committed
|
||||
from util.json_request import JsonResponse
|
||||
from util.bad_request_rate_limiter import BadRequestRateLimiter
|
||||
|
||||
from util.milestones_helpers import (
|
||||
get_pre_requisite_courses_not_completed,
|
||||
)
|
||||
from microsite_configuration import microsite
|
||||
|
||||
from util.password_policy_validators import (
|
||||
@@ -540,8 +542,11 @@ def dashboard(request):
|
||||
staff_access = True
|
||||
errored_courses = modulestore().get_errored_courses()
|
||||
|
||||
show_courseware_links_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if has_access(request.user, 'load', course))
|
||||
show_courseware_links_for = frozenset(
|
||||
course.id for course, _enrollment in course_enrollment_pairs
|
||||
if has_access(request.user, 'load', course)
|
||||
and has_access(request.user, 'view_courseware_with_prerequisites', course)
|
||||
)
|
||||
|
||||
# Construct a dictionary of course mode information
|
||||
# used to render the course list. We re-use the course modes dict
|
||||
@@ -652,6 +657,11 @@ def dashboard(request):
|
||||
# Populate the Order History for the side-bar.
|
||||
order_history_list = order_history(user, course_org_filter=course_org_filter, org_filter_out_set=org_filter_out_set)
|
||||
|
||||
# get list of courses having pre-requisites yet to be completed
|
||||
courses_having_prerequisites = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if course.pre_requisite_courses)
|
||||
courses_requirements_not_met = get_pre_requisite_courses_not_completed(user, courses_having_prerequisites)
|
||||
|
||||
context = {
|
||||
'enrollment_message': enrollment_message,
|
||||
'course_enrollment_pairs': course_enrollment_pairs,
|
||||
@@ -681,7 +691,8 @@ def dashboard(request):
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
'enrolled_courses_either_paid': enrolled_courses_either_paid,
|
||||
'provider_states': [],
|
||||
'order_history_list': order_history_list
|
||||
'order_history_list': order_history_list,
|
||||
'courses_requirements_not_met': courses_requirements_not_met,
|
||||
}
|
||||
|
||||
if third_party_auth.is_enabled():
|
||||
|
||||
167
common/djangoapps/util/milestones_helpers.py
Normal file
167
common/djangoapps/util/milestones_helpers.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# pylint: disable=invalid-name
|
||||
"""
|
||||
Helper methods for milestones api calls.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from milestones.api import (
|
||||
get_course_milestones,
|
||||
add_milestone,
|
||||
add_course_milestone,
|
||||
remove_course_milestone,
|
||||
get_course_milestones_fulfillment_paths,
|
||||
add_user_milestone,
|
||||
get_user_milestones,
|
||||
)
|
||||
from milestones.models import MilestoneRelationshipType
|
||||
|
||||
|
||||
def add_prerequisite_course(course_key, prerequisite_course_key):
|
||||
"""
|
||||
It would create a milestone, then it would set newly created
|
||||
milestones as requirement for course referred by `course_key`
|
||||
and it would set newly created milestone as fulfilment
|
||||
milestone for course referred by `prerequisite_course_key`.
|
||||
"""
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
# create a milestone
|
||||
milestone = add_milestone({
|
||||
'name': _('Course {} requires {}'.format(unicode(course_key), unicode(prerequisite_course_key))),
|
||||
'namespace': unicode(prerequisite_course_key),
|
||||
'description': _('System defined milestone'),
|
||||
})
|
||||
# add requirement course milestone
|
||||
add_course_milestone(course_key, 'requires', milestone)
|
||||
|
||||
# add fulfillment course milestone
|
||||
add_course_milestone(prerequisite_course_key, 'fulfills', milestone)
|
||||
|
||||
|
||||
def remove_prerequisite_course(course_key, milestone):
|
||||
"""
|
||||
It would remove pre-requisite course milestone for course
|
||||
referred by `course_key`.
|
||||
"""
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
remove_course_milestone(
|
||||
course_key,
|
||||
milestone,
|
||||
)
|
||||
|
||||
|
||||
def set_prerequisite_courses(course_key, prerequisite_course_keys):
|
||||
"""
|
||||
It would remove any existing requirement milestones for the given `course_key`
|
||||
and create new milestones for each pre-requisite course in `prerequisite_course_keys`.
|
||||
To only remove course milestones pass `course_key` and empty list or
|
||||
None as `prerequisite_course_keys` .
|
||||
"""
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
#remove any existing requirement milestones with this pre-requisite course as requirement
|
||||
course_milestones = get_course_milestones(course_key=course_key, relationship="requires")
|
||||
if course_milestones:
|
||||
for milestone in course_milestones:
|
||||
remove_prerequisite_course(course_key, milestone)
|
||||
|
||||
# add milestones if pre-requisite course is selected
|
||||
if prerequisite_course_keys:
|
||||
for prerequisite_course_key_string in prerequisite_course_keys:
|
||||
prerequisite_course_key = CourseKey.from_string(prerequisite_course_key_string)
|
||||
add_prerequisite_course(course_key, prerequisite_course_key)
|
||||
|
||||
|
||||
def get_pre_requisite_courses_not_completed(user, enrolled_courses):
|
||||
"""
|
||||
It would make dict of prerequisite courses not completed by user among courses
|
||||
user has enrolled in. It calls the fulfilment api of milestones app and
|
||||
iterates over all fulfilment milestones not achieved to make dict of
|
||||
prerequisite courses yet to be completed.
|
||||
"""
|
||||
pre_requisite_courses = {}
|
||||
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES'):
|
||||
for course_key in enrolled_courses:
|
||||
required_courses = []
|
||||
fulfilment_paths = get_course_milestones_fulfillment_paths(course_key, {'id': user.id})
|
||||
for milestone_key, milestone_value in fulfilment_paths.items(): # pylint: disable=unused-variable
|
||||
for key, value in milestone_value.items():
|
||||
if key == 'courses' and value:
|
||||
for required_course in value:
|
||||
required_course_key = CourseKey.from_string(required_course)
|
||||
required_course_descriptor = modulestore().get_course(required_course_key)
|
||||
required_courses.append({
|
||||
'key': required_course_key,
|
||||
'display': get_course_display_name(required_course_descriptor)
|
||||
})
|
||||
|
||||
# if there are required courses add to dict
|
||||
if required_courses:
|
||||
pre_requisite_courses[course_key] = {'courses': required_courses}
|
||||
return pre_requisite_courses
|
||||
|
||||
|
||||
def get_prerequisite_courses_display(course_descriptor):
|
||||
"""
|
||||
It would retrieve pre-requisite courses, make display strings
|
||||
and return them as list
|
||||
"""
|
||||
pre_requisite_courses = []
|
||||
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False) and course_descriptor.pre_requisite_courses:
|
||||
for course_id in course_descriptor.pre_requisite_courses:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
required_course_descriptor = modulestore().get_course(course_key)
|
||||
pre_requisite_courses.append(get_course_display_name(required_course_descriptor))
|
||||
return pre_requisite_courses
|
||||
|
||||
|
||||
def get_course_display_name(descriptor):
|
||||
"""
|
||||
It would return display name from given course descriptor
|
||||
"""
|
||||
return ' '.join([
|
||||
descriptor.display_org_with_default,
|
||||
descriptor.display_number_with_default
|
||||
])
|
||||
|
||||
|
||||
def fulfill_course_milestone(course_key, user):
|
||||
"""
|
||||
Marks the course specified by the given course_key as complete for the given user.
|
||||
If any other courses require this course as a prerequisite, their milestones will be appropriately updated.
|
||||
"""
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
course_milestones = get_course_milestones(course_key=course_key, relationship="fulfills")
|
||||
for milestone in course_milestones:
|
||||
add_user_milestone({'id': user.id}, milestone)
|
||||
|
||||
|
||||
def milestones_achieved_by_user(user, namespace):
|
||||
"""
|
||||
It would fetch list of milestones completed by user
|
||||
"""
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
return get_user_milestones({'id': user.id}, namespace)
|
||||
|
||||
|
||||
def is_valid_course_key(key):
|
||||
"""
|
||||
validates course key. returns True if valid else False.
|
||||
"""
|
||||
try:
|
||||
course_key = CourseKey.from_string(key)
|
||||
except InvalidKeyError:
|
||||
course_key = key
|
||||
return isinstance(course_key, CourseKey)
|
||||
|
||||
|
||||
def seed_milestone_relationship_types():
|
||||
"""
|
||||
Helper method to pre-populate MRTs so the tests can run
|
||||
"""
|
||||
if settings.FEATURES.get('MILESTONES_APP', False):
|
||||
MilestoneRelationshipType.objects.create(name='requires')
|
||||
MilestoneRelationshipType.objects.create(name='fulfills')
|
||||
@@ -184,6 +184,11 @@ class CourseFields(object):
|
||||
help=_("Enter the date you want to advertise as the course start date, if this date is different from the set start date. To advertise the set start date, enter null."),
|
||||
scope=Scope.settings
|
||||
)
|
||||
pre_requisite_courses = List(
|
||||
display_name=_("Pre-Requisite Courses"),
|
||||
help=_("Pre-Requisite Course key if this course has a pre-requisite course"),
|
||||
scope=Scope.settings
|
||||
)
|
||||
grading_policy = Dict(
|
||||
help="Grading policy definition for this class",
|
||||
default={
|
||||
|
||||
@@ -11,8 +11,8 @@ data: |
|
||||
</section>
|
||||
|
||||
<section class="prerequisites">
|
||||
<h2>Prerequisites</h2>
|
||||
<p>Add information about course prerequisites here.</p>
|
||||
<h2>Requirements</h2>
|
||||
<p>Add information about the skills and knowledge students need to take this course.</p>
|
||||
</section>
|
||||
|
||||
<section class="course-staff">
|
||||
|
||||
@@ -95,3 +95,9 @@ class DashboardPage(PageObject):
|
||||
modal_is_visible = self.q(css='section#change_language.modal').visible
|
||||
return (language_is_selected and not modal_is_visible)
|
||||
return EmptyPromise(_check_func, "language changed and modal hidden")
|
||||
|
||||
def pre_requisite_message_displayed(self):
|
||||
"""
|
||||
Verify if pre-requisite course messages are being displayed.
|
||||
"""
|
||||
return self.q(css='section.prerequisites > .tip').visible
|
||||
|
||||
@@ -3,6 +3,7 @@ Course Schedule and Details Settings page.
|
||||
"""
|
||||
|
||||
from .course_page import CoursePage
|
||||
from .utils import press_the_notification_button
|
||||
|
||||
|
||||
class SettingsPage(CoursePage):
|
||||
@@ -14,3 +15,22 @@ class SettingsPage(CoursePage):
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='body.view-settings').present
|
||||
|
||||
@property
|
||||
def pre_requisite_course(self):
|
||||
"""
|
||||
Returns the pre-requisite course drop down field.
|
||||
"""
|
||||
return self.q(css='#pre-requisite-course')
|
||||
|
||||
def save_changes(self):
|
||||
"""
|
||||
Clicks save button.
|
||||
"""
|
||||
press_the_notification_button(self, "save")
|
||||
|
||||
def refresh_page(self):
|
||||
"""
|
||||
Reload the page.
|
||||
"""
|
||||
self.browser.refresh()
|
||||
|
||||
@@ -196,6 +196,31 @@ def get_options(select_browser_query):
|
||||
return Select(select_browser_query.first.results[0]).options
|
||||
|
||||
|
||||
def generate_course_key(org, number, run):
|
||||
"""
|
||||
Makes a CourseLocator from org, number and run
|
||||
"""
|
||||
default_store = os.environ.get('DEFAULT_STORE', 'draft')
|
||||
return CourseLocator(org, number, run, deprecated=(default_store == 'draft'))
|
||||
|
||||
|
||||
def select_option_by_value(browser_query, value):
|
||||
"""
|
||||
Selects a html select element by matching value attribute
|
||||
"""
|
||||
select = Select(browser_query.first.results[0])
|
||||
select.select_by_value(value)
|
||||
|
||||
|
||||
def is_option_value_selected(browser_query, value):
|
||||
"""
|
||||
return true if given value is selected in html select element, else return false.
|
||||
"""
|
||||
select = Select(browser_query.first.results[0])
|
||||
ddl_selected_value = select.first_selected_option.get_attribute('value')
|
||||
return ddl_selected_value == value
|
||||
|
||||
|
||||
class UniqueCourseTest(WebAppTest):
|
||||
"""
|
||||
Test that provides a unique course ID.
|
||||
|
||||
@@ -8,7 +8,13 @@ from unittest import skip
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
from ..helpers import UniqueCourseTest, load_data_str
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from ..helpers import (
|
||||
UniqueCourseTest,
|
||||
load_data_str,
|
||||
generate_course_key,
|
||||
select_option_by_value,
|
||||
)
|
||||
from ...pages.lms.auto_auth import AutoAuthPage
|
||||
from ...pages.common.logout import LogoutPage
|
||||
from ...pages.lms.find_courses import FindCoursesPage
|
||||
@@ -22,6 +28,7 @@ from ...pages.lms.problem import ProblemPage
|
||||
from ...pages.lms.video.video import VideoPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.login_and_register import CombinedLoginAndRegisterPage
|
||||
from ...pages.studio.settings import SettingsPage
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
|
||||
|
||||
|
||||
@@ -604,6 +611,90 @@ class TooltipTest(UniqueCourseTest):
|
||||
self.assertTrue(self.courseware_page.tooltips_displayed())
|
||||
|
||||
|
||||
class PreRequisiteCourseTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that pre-requisite course messages are displayed
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initialize pages and install a course fixture.
|
||||
"""
|
||||
super(PreRequisiteCourseTest, self).setUp()
|
||||
|
||||
CourseFixture(
|
||||
self.course_info['org'], self.course_info['number'],
|
||||
self.course_info['run'], self.course_info['display_name']
|
||||
).install()
|
||||
|
||||
self.prc_info = {
|
||||
'org': 'test_org',
|
||||
'number': self.unique_id,
|
||||
'run': 'prc_test_run',
|
||||
'display_name': 'PR Test Course' + self.unique_id
|
||||
}
|
||||
|
||||
CourseFixture(
|
||||
self.prc_info['org'], self.prc_info['number'],
|
||||
self.prc_info['run'], self.prc_info['display_name']
|
||||
).install()
|
||||
|
||||
pre_requisite_course_key = generate_course_key(
|
||||
self.prc_info['org'],
|
||||
self.prc_info['number'],
|
||||
self.prc_info['run']
|
||||
)
|
||||
self.pre_requisite_course_id = unicode(pre_requisite_course_key)
|
||||
|
||||
self.dashboard_page = DashboardPage(self.browser)
|
||||
self.settings_page = SettingsPage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run']
|
||||
|
||||
)
|
||||
# Auto-auth register for the course
|
||||
AutoAuthPage(self.browser, course_id=self.course_id).visit()
|
||||
|
||||
def test_dashboard_message(self):
|
||||
"""
|
||||
Scenario: Any course where there is a Pre-Requisite course Student dashboard should have
|
||||
appropriate messaging.
|
||||
Given that I am on the Student dashboard
|
||||
When I view a course with a pre-requisite course set
|
||||
Then At the bottom of course I should see course requirements message.'
|
||||
"""
|
||||
|
||||
# visit dashboard page and make sure there is not pre-requisite course message
|
||||
self.dashboard_page.visit()
|
||||
self.assertFalse(self.dashboard_page.pre_requisite_message_displayed())
|
||||
|
||||
# Logout and login as a staff.
|
||||
LogoutPage(self.browser).visit()
|
||||
AutoAuthPage(self.browser, course_id=self.course_id, staff=True).visit()
|
||||
|
||||
# visit course settings page and set pre-requisite course
|
||||
self.settings_page.visit()
|
||||
self._set_pre_requisite_course()
|
||||
|
||||
# Logout and login as a student.
|
||||
LogoutPage(self.browser).visit()
|
||||
AutoAuthPage(self.browser, course_id=self.course_id, staff=False).visit()
|
||||
|
||||
# visit dashboard page again now it should have pre-requisite course message
|
||||
self.dashboard_page.visit()
|
||||
EmptyPromise(lambda: self.dashboard_page.available_courses > 0, 'Dashboard page loaded').fulfill()
|
||||
self.assertTrue(self.dashboard_page.pre_requisite_message_displayed())
|
||||
|
||||
def _set_pre_requisite_course(self):
|
||||
"""
|
||||
set pre-requisite course
|
||||
"""
|
||||
select_option_by_value(self.settings_page.pre_requisite_course, self.pre_requisite_course_id)
|
||||
self.settings_page.save_changes()
|
||||
|
||||
|
||||
class ProblemExecutionTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests of problems.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Acceptance tests for Studio's Settings Details pages
|
||||
"""
|
||||
from acceptance.tests.studio.base_studio_test import StudioCourseTest
|
||||
from ...fixtures.course import CourseFixture
|
||||
from ..helpers import (
|
||||
generate_course_key,
|
||||
select_option_by_value,
|
||||
is_option_value_selected
|
||||
)
|
||||
|
||||
from ...pages.studio.settings import SettingsPage
|
||||
|
||||
|
||||
class SettingsMilestonesTest(StudioCourseTest):
|
||||
"""
|
||||
Tests for milestones feature in Studio's settings tab
|
||||
"""
|
||||
def setUp(self, is_staff=True):
|
||||
super(SettingsMilestonesTest, self).setUp(is_staff=is_staff)
|
||||
self.settings_detail = SettingsPage(
|
||||
self.browser,
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
# Before every test, make sure to visit the page first
|
||||
self.settings_detail.visit()
|
||||
self.assertTrue(self.settings_detail.is_browser_on_page())
|
||||
|
||||
def test_page_has_prerequisite_field(self):
|
||||
"""
|
||||
Test to make sure page has pre-requisite course field if milestones app is enabled.
|
||||
"""
|
||||
|
||||
self.assertTrue(self.settings_detail.pre_requisite_course.present)
|
||||
|
||||
def test_prerequisite_course_save_successfully(self):
|
||||
"""
|
||||
Scenario: Selecting course from Pre-Requisite course drop down save the selected course as pre-requisite
|
||||
course.
|
||||
Given that I am on the Schedule & Details page on studio
|
||||
When I select an item in pre-requisite course drop down and click Save Changes button
|
||||
Then My selected item should be saved as pre-requisite course
|
||||
And My selected item should be selected after refreshing the page.'
|
||||
"""
|
||||
course_number = self.unique_id
|
||||
CourseFixture(
|
||||
org='test_org',
|
||||
number=course_number,
|
||||
run='test_run',
|
||||
display_name='Test Course' + course_number
|
||||
).install()
|
||||
|
||||
pre_requisite_course_key = generate_course_key(
|
||||
org='test_org',
|
||||
number=course_number,
|
||||
run='test_run'
|
||||
)
|
||||
pre_requisite_course_id = unicode(pre_requisite_course_key)
|
||||
|
||||
# refreshing the page after creating a course fixture, in order reload the pre requisite course drop down.
|
||||
self.settings_detail.refresh_page()
|
||||
select_option_by_value(
|
||||
browser_query=self.settings_detail.pre_requisite_course,
|
||||
value=pre_requisite_course_id
|
||||
)
|
||||
|
||||
# trigger the save changes button.
|
||||
self.settings_detail.save_changes()
|
||||
|
||||
self.assertTrue('Your changes have been saved.' in self.settings_detail.browser.page_source)
|
||||
self.settings_detail.refresh_page()
|
||||
self.assertTrue(is_option_value_selected(browser_query=self.settings_detail.pre_requisite_course,
|
||||
value=pre_requisite_course_id))
|
||||
|
||||
# now reset/update the pre requisite course to none
|
||||
select_option_by_value(browser_query=self.settings_detail.pre_requisite_course, value='')
|
||||
|
||||
# trigger the save changes button.
|
||||
self.settings_detail.save_changes()
|
||||
self.assertTrue('Your changes have been saved.' in self.settings_detail.browser.page_source)
|
||||
self.assertTrue(is_option_value_selected(browser_query=self.settings_detail.pre_requisite_course, value=''))
|
||||
File diff suppressed because one or more lines are too long
@@ -200,9 +200,9 @@ CREATE TABLE `assessment_assessmentpart` (
|
||||
KEY `assessment_assessmentpart_c168f2dc` (`assessment_id`),
|
||||
KEY `assessment_assessmentpart_2f3b0dc9` (`option_id`),
|
||||
KEY `assessment_assessmentpart_a36470e4` (`criterion_id`),
|
||||
CONSTRAINT `option_id_refs_id_5353f6b204439dd5` FOREIGN KEY (`option_id`) REFERENCES `assessment_criterionoption` (`id`),
|
||||
CONSTRAINT `criterion_id_refs_id_507d3ff2eeb3dc44` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterion` (`id`),
|
||||
CONSTRAINT `assessment_id_refs_id_5cb07795bff26444` FOREIGN KEY (`assessment_id`) REFERENCES `assessment_assessment` (`id`),
|
||||
CONSTRAINT `criterion_id_refs_id_5353f6b204439dd5` FOREIGN KEY (`criterion_id`) REFERENCES `assessment_criterionoption` (`id`)
|
||||
CONSTRAINT `option_id_refs_id_5353f6b204439dd5` FOREIGN KEY (`option_id`) REFERENCES `assessment_criterionoption` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `assessment_criterion`;
|
||||
@@ -394,7 +394,7 @@ CREATE TABLE `auth_permission` (
|
||||
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
|
||||
KEY `auth_permission_e4470c6e` (`content_type_id`),
|
||||
CONSTRAINT `content_type_id_refs_id_728de91f` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=415 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=448 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -630,6 +630,21 @@ CREATE TABLE `circuit_servercircuit` (
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `contentstore_videouploadconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `contentstore_videouploadconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`profile_whitelist` longtext NOT NULL,
|
||||
`status_whitelist` longtext NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `contentstore_videouploadconfig_16905482` (`changed_by_id`),
|
||||
CONSTRAINT `changed_by_id_refs_id_31e0e2c8209c438f` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_action_state_coursererunstate`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -657,6 +672,20 @@ CREATE TABLE `course_action_state_coursererunstate` (
|
||||
CONSTRAINT `updated_user_id_refs_id_1334640c1744bdeb` FOREIGN KEY (`updated_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_creators_coursecreator`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `course_creators_coursecreator` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`state_changed` datetime NOT NULL,
|
||||
`state` varchar(24) NOT NULL,
|
||||
`note` varchar(512) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `user_id_refs_id_22dd4a06a0e6044` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `course_groups_courseusergroup`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -902,8 +931,8 @@ CREATE TABLE `django_admin_log` (
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `django_admin_log_fbfc09f1` (`user_id`),
|
||||
KEY `django_admin_log_e4470c6e` (`content_type_id`),
|
||||
CONSTRAINT `user_id_refs_id_c8665aa` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `content_type_id_refs_id_288599e6` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
CONSTRAINT `content_type_id_refs_id_288599e6` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`),
|
||||
CONSTRAINT `user_id_refs_id_c8665aa` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_comment_client_permission`;
|
||||
@@ -965,7 +994,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `app_label` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=138 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=149 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1195,12 +1224,15 @@ DROP TABLE IF EXISTS `edxval_video`;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `edxval_video` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`edx_video_id` varchar(50) NOT NULL,
|
||||
`edx_video_id` varchar(100) NOT NULL,
|
||||
`client_video_id` varchar(255) NOT NULL,
|
||||
`duration` double NOT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
`status` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `edx_video_id` (`edx_video_id`),
|
||||
KEY `edxval_video_de3f5709` (`client_video_id`)
|
||||
KEY `edxval_video_de3f5709` (`client_video_id`),
|
||||
KEY `edxval_video_c9ad71dd` (`status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `embargo_embargoedcourse`;
|
||||
@@ -1360,15 +1392,147 @@ CREATE TABLE `licenses_userlicense` (
|
||||
CONSTRAINT `software_id_refs_id_78738fcdf9e27be8` FOREIGN KEY (`software_id`) REFERENCES `licenses_coursesoftware` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `linkedin_linkedin`;
|
||||
DROP TABLE IF EXISTS `lms_xblock_xblockasidesconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `linkedin_linkedin` (
|
||||
CREATE TABLE `lms_xblock_xblockasidesconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`disabled_blocks` longtext NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `lms_xblock_xblockasidesconfig_16905482` (`changed_by_id`),
|
||||
CONSTRAINT `changed_by_id_refs_id_40c91094552627bc` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `mentoring_answer`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `mentoring_answer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`student_id` varchar(32) NOT NULL,
|
||||
`student_input` longtext NOT NULL,
|
||||
`created_on` datetime NOT NULL,
|
||||
`modified_on` datetime NOT NULL,
|
||||
`course_id` varchar(50) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mentoring_answer_course_id_7f581fd43d0d1f77_uniq` (`course_id`,`student_id`,`name`),
|
||||
KEY `mentoring_answer_52094d6e` (`name`),
|
||||
KEY `mentoring_answer_42ff452e` (`student_id`),
|
||||
KEY `mentoring_answer_ff48d8e5` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `mentoring_lightchild`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `mentoring_lightchild` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`student_id` varchar(32) NOT NULL,
|
||||
`course_id` varchar(50) NOT NULL,
|
||||
`student_data` longtext NOT NULL,
|
||||
`created_on` datetime NOT NULL,
|
||||
`modified_on` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mentoring_lightchild_student_id_2d3f2d211f8b8d41_uniq` (`student_id`,`course_id`,`name`),
|
||||
KEY `mentoring_lightchild_52094d6e` (`name`),
|
||||
KEY `mentoring_lightchild_42ff452e` (`student_id`),
|
||||
KEY `mentoring_lightchild_ff48d8e5` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `milestones_coursecontentmilestone`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `milestones_coursecontentmilestone` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
`content_id` varchar(255) NOT NULL,
|
||||
`milestone_id` int(11) NOT NULL,
|
||||
`milestone_relationship_type_id` int(11) NOT NULL,
|
||||
`active` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `milestones_coursecontentmilesto_course_id_68d1457cd52d6dff_uniq` (`course_id`,`content_id`,`milestone_id`),
|
||||
KEY `milestones_coursecontentmilestone_ff48d8e5` (`course_id`),
|
||||
KEY `milestones_coursecontentmilestone_cc8ff3c` (`content_id`),
|
||||
KEY `milestones_coursecontentmilestone_9cfa291f` (`milestone_id`),
|
||||
KEY `milestones_coursecontentmilestone_595c57ff` (`milestone_relationship_type_id`),
|
||||
CONSTRAINT `milestone_relationship_type_id_refs_id_57f7e3570d7ab186` FOREIGN KEY (`milestone_relationship_type_id`) REFERENCES `milestones_milestonerelationshiptype` (`id`),
|
||||
CONSTRAINT `milestone_id_refs_id_5c1b1e8cd7fabedc` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `milestones_coursemilestone`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `milestones_coursemilestone` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
`milestone_id` int(11) NOT NULL,
|
||||
`milestone_relationship_type_id` int(11) NOT NULL,
|
||||
`active` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `milestones_coursemilestone_course_id_5a06e10579eab3b7_uniq` (`course_id`,`milestone_id`),
|
||||
KEY `milestones_coursemilestone_ff48d8e5` (`course_id`),
|
||||
KEY `milestones_coursemilestone_9cfa291f` (`milestone_id`),
|
||||
KEY `milestones_coursemilestone_595c57ff` (`milestone_relationship_type_id`),
|
||||
CONSTRAINT `milestone_relationship_type_id_refs_id_2c1c593f874a03b6` FOREIGN KEY (`milestone_relationship_type_id`) REFERENCES `milestones_milestonerelationshiptype` (`id`),
|
||||
CONSTRAINT `milestone_id_refs_id_540108c1cd764354` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `milestones_milestone`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `milestones_milestone` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`namespace` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`display_name` varchar(255) NOT NULL,
|
||||
`description` longtext NOT NULL,
|
||||
`active` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `milestones_milestone_namespace_460a2f6943016c0b_uniq` (`namespace`,`name`),
|
||||
KEY `milestones_milestone_eb040977` (`namespace`),
|
||||
KEY `milestones_milestone_52094d6e` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `milestones_milestonerelationshiptype`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `milestones_milestonerelationshiptype` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`name` varchar(25) NOT NULL,
|
||||
`description` longtext NOT NULL,
|
||||
`active` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `milestones_usermilestone`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `milestones_usermilestone` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`has_linkedin_account` tinyint(1) DEFAULT NULL,
|
||||
`emailed_courses` longtext NOT NULL,
|
||||
PRIMARY KEY (`user_id`),
|
||||
CONSTRAINT `user_id_refs_id_7b29e97d72e31bb2` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
`milestone_id` int(11) NOT NULL,
|
||||
`source` longtext NOT NULL,
|
||||
`collected` datetime DEFAULT NULL,
|
||||
`active` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `milestones_usermilestone_user_id_10206aa452468351_uniq` (`user_id`,`milestone_id`),
|
||||
KEY `milestones_usermilestone_fbfc09f1` (`user_id`),
|
||||
KEY `milestones_usermilestone_9cfa291f` (`milestone_id`),
|
||||
CONSTRAINT `milestone_id_refs_id_1a7fbf83af7fa460` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `notes_note`;
|
||||
@@ -1601,6 +1765,7 @@ CREATE TABLE `shoppingcart_coupon` (
|
||||
`created_by_id` int(11) NOT NULL,
|
||||
`created_at` datetime NOT NULL,
|
||||
`is_active` tinyint(1) NOT NULL,
|
||||
`expiration_date` datetime,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `shoppingcart_coupon_65da3d2c` (`code`),
|
||||
KEY `shoppingcart_coupon_b5de30be` (`created_by_id`),
|
||||
@@ -1659,6 +1824,7 @@ CREATE TABLE `shoppingcart_courseregistrationcode` (
|
||||
`created_at` datetime NOT NULL,
|
||||
`invoice_id` int(11),
|
||||
`order_id` int(11),
|
||||
`mode_slug` varchar(100),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `shoppingcart_courseregistrationcode_code_6614bad3cae62199_uniq` (`code`),
|
||||
KEY `shoppingcart_courseregistrationcode_65da3d2c` (`code`),
|
||||
@@ -1666,9 +1832,9 @@ CREATE TABLE `shoppingcart_courseregistrationcode` (
|
||||
KEY `shoppingcart_courseregistrationcode_b5de30be` (`created_by_id`),
|
||||
KEY `shoppingcart_courseregistrationcode_59f72b12` (`invoice_id`),
|
||||
KEY `shoppingcart_courseregistrationcode_8337030b` (`order_id`),
|
||||
CONSTRAINT `order_id_refs_id_6378d414be36d837` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`),
|
||||
CONSTRAINT `created_by_id_refs_id_7eaaed0838397037` FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `invoice_id_refs_id_6e8c54da995f0ae8` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`)
|
||||
CONSTRAINT `invoice_id_refs_id_6e8c54da995f0ae8` FOREIGN KEY (`invoice_id`) REFERENCES `shoppingcart_invoice` (`id`),
|
||||
CONSTRAINT `order_id_refs_id_6378d414be36d837` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `shoppingcart_donation`;
|
||||
@@ -1792,9 +1958,12 @@ CREATE TABLE `shoppingcart_paidcourseregistration` (
|
||||
`orderitem_ptr_id` int(11) NOT NULL,
|
||||
`course_id` varchar(128) NOT NULL,
|
||||
`mode` varchar(50) NOT NULL,
|
||||
`course_enrollment_id` int(11),
|
||||
PRIMARY KEY (`orderitem_ptr_id`),
|
||||
KEY `shoppingcart_paidcourseregistration_ff48d8e5` (`course_id`),
|
||||
KEY `shoppingcart_paidcourseregistration_4160619e` (`mode`),
|
||||
KEY `shoppingcart_paidcourseregistration_9e513f0b` (`course_enrollment_id`),
|
||||
CONSTRAINT `course_enrollment_id_refs_id_50077099dc061be6` FOREIGN KEY (`course_enrollment_id`) REFERENCES `student_courseenrollment` (`id`),
|
||||
CONSTRAINT `orderitem_ptr_id_refs_id_c5c6141d8709d99` FOREIGN KEY (`orderitem_ptr_id`) REFERENCES `shoppingcart_orderitem` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
@@ -1818,10 +1987,13 @@ CREATE TABLE `shoppingcart_registrationcoderedemption` (
|
||||
`registration_code_id` int(11) NOT NULL,
|
||||
`redeemed_by_id` int(11) NOT NULL,
|
||||
`redeemed_at` datetime DEFAULT NULL,
|
||||
`course_enrollment_id` int(11),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `shoppingcart_registrationcoderedemption_8337030b` (`order_id`),
|
||||
KEY `shoppingcart_registrationcoderedemption_d25b37dc` (`registration_code_id`),
|
||||
KEY `shoppingcart_registrationcoderedemption_e151467a` (`redeemed_by_id`),
|
||||
KEY `shoppingcart_registrationcoderedemption_9e513f0b` (`course_enrollment_id`),
|
||||
CONSTRAINT `course_enrollment_id_refs_id_6d4e7d1dc9486127` FOREIGN KEY (`course_enrollment_id`) REFERENCES `student_courseenrollment` (`id`),
|
||||
CONSTRAINT `order_id_refs_id_3e4c388753a8a5c9` FOREIGN KEY (`order_id`) REFERENCES `shoppingcart_order` (`id`),
|
||||
CONSTRAINT `redeemed_by_id_refs_id_2c29fd0d4e320dc9` FOREIGN KEY (`redeemed_by_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `registration_code_id_refs_id_2b7812ae4d01e47b` FOREIGN KEY (`registration_code_id`) REFERENCES `shoppingcart_courseregistrationcode` (`id`)
|
||||
@@ -1889,7 +2061,7 @@ CREATE TABLE `south_migrationhistory` (
|
||||
`migration` varchar(255) NOT NULL,
|
||||
`applied` datetime NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=188 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=205 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `splash_splashconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2237,6 +2409,26 @@ CREATE TABLE `user_api_usercoursetag` (
|
||||
CONSTRAINT `user_id_refs_id_1d26ef6c47a9a367` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `user_api_userorgtag`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `user_api_userorgtag` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`key` varchar(255) NOT NULL,
|
||||
`org` varchar(255) NOT NULL,
|
||||
`value` longtext NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `user_api_userorgtag_user_id_694f9e3322120c6f_uniq` (`user_id`,`org`,`key`),
|
||||
KEY `user_api_userorgtag_user_id_694f9e3322120c6f` (`user_id`,`org`,`key`),
|
||||
KEY `user_api_userorgtag_fbfc09f1` (`user_id`),
|
||||
KEY `user_api_userorgtag_45544485` (`key`),
|
||||
KEY `user_api_userorgtag_4f5f82e2` (`org`),
|
||||
CONSTRAINT `user_id_refs_id_4fedbcc0e54b717f` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `user_api_userpreference`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2579,6 +2771,20 @@ CREATE TABLE `workflow_assessmentworkflowstep` (
|
||||
CONSTRAINT `workflow_id_refs_id_4e31588b69d0b483` FOREIGN KEY (`workflow_id`) REFERENCES `workflow_assessmentworkflow` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `xblock_config_studioconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `xblock_config_studioconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`disabled_blocks` longtext NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `xblock_config_studioconfig_16905482` (`changed_by_id`),
|
||||
CONSTRAINT `changed_by_id_refs_id_3d4ae52c6ef7f7d7` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
|
||||
Binary file not shown.
@@ -138,8 +138,8 @@ Get the HTML for the course about page.
|
||||
<p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>
|
||||
</section>\n\n
|
||||
<section class=\"prerequisites\">\n
|
||||
<h2>Prerequisites</h2>\n
|
||||
<p>Add information about course prerequisites here.</p>\n </section>\n\n
|
||||
<h2>Requirements</h2>\n
|
||||
<p>Add information about the skills and knowledge students need to take this course.</p>\n </section>\n\n
|
||||
<section class=\"course-staff\">\n
|
||||
<h2>Course Staff</h2>\n
|
||||
<article class=\"teacher\">\n
|
||||
|
||||
@@ -19,6 +19,12 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
|
||||
from util.milestones_helpers import (
|
||||
seed_milestone_relationship_types,
|
||||
set_prerequisite_courses,
|
||||
)
|
||||
|
||||
FEATURES_WITH_STARTDATE = settings.FEATURES.copy()
|
||||
FEATURES_WITH_STARTDATE['DISABLE_START_DATES'] = False
|
||||
@@ -109,6 +115,52 @@ class AnonymousIndexPageTest(ModuleStoreTestCase):
|
||||
self.assertEqual(response._headers.get("location")[1], "/login")
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class PreRequisiteCourseCatalog(ModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
"""
|
||||
Test to simulate and verify fix for disappearing courses in
|
||||
course catalog when using pre-requisite courses
|
||||
"""
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def setUp(self):
|
||||
seed_milestone_relationship_types()
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_course_with_prereq(self):
|
||||
"""
|
||||
Simulate having a course which has closed enrollments that has
|
||||
a pre-req course
|
||||
"""
|
||||
pre_requisite_course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='900',
|
||||
display_name='pre requisite course',
|
||||
)
|
||||
|
||||
pre_requisite_courses = [unicode(pre_requisite_course.id)]
|
||||
|
||||
# for this failure to occur, the enrollment window needs to be in the past
|
||||
course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='1000',
|
||||
display_name='course that has pre requisite',
|
||||
# closed enrollment
|
||||
enrollment_start=datetime.datetime(2013, 1, 1),
|
||||
enrollment_end=datetime.datetime(2014, 1, 1),
|
||||
start=datetime.datetime(2013, 1, 1),
|
||||
end=datetime.datetime(2030, 1, 1),
|
||||
pre_requisite_courses=pre_requisite_courses,
|
||||
)
|
||||
set_prerequisite_courses(course.id, pre_requisite_courses)
|
||||
|
||||
resp = self.client.get('/')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# make sure both courses are visible in the catalog
|
||||
self.assertIn('pre requisite course', resp.content)
|
||||
self.assertIn('course that has pre requisite', resp.content)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MOCK_MODULESTORE)
|
||||
class IndexPageCourseCardsSortingTests(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from datetime import datetime
|
||||
from model_utils import Choices
|
||||
from xmodule_django.models import CourseKeyField, NoneToEmptyManager
|
||||
from util.milestones_helpers import fulfill_course_milestone
|
||||
|
||||
"""
|
||||
Certificates are created for a student and an offering of a course.
|
||||
@@ -118,6 +122,17 @@ class GeneratedCertificate(models.Model):
|
||||
return None
|
||||
|
||||
|
||||
@receiver(post_save, sender=GeneratedCertificate)
|
||||
def handle_post_cert_generated(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument
|
||||
"""
|
||||
Handles post_save signal of GeneratedCertificate, and mark user collected
|
||||
course milestone entry if user has passed the course
|
||||
or certificate status is 'generating'.
|
||||
"""
|
||||
if settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES') and instance.status == CertificateStatuses.generating:
|
||||
fulfill_course_milestone(instance.course_id, instance.user)
|
||||
|
||||
|
||||
def certificate_status_for_student(student, course_id):
|
||||
'''
|
||||
This returns a dictionary with a key for status, and other information.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Tests for the certificates models.
|
||||
"""
|
||||
|
||||
from mock import patch
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -9,6 +11,13 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from certificates.models import CertificateStatuses, GeneratedCertificate, certificate_status_for_student
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
|
||||
from util.milestones_helpers import (
|
||||
set_prerequisite_courses,
|
||||
milestones_achieved_by_user,
|
||||
seed_milestone_relationship_types,
|
||||
)
|
||||
|
||||
|
||||
class CertificatesModelTest(ModuleStoreTestCase):
|
||||
@@ -23,3 +32,26 @@ class CertificatesModelTest(ModuleStoreTestCase):
|
||||
certificate_status = certificate_status_for_student(student, course.id)
|
||||
self.assertEqual(certificate_status['status'], CertificateStatuses.unavailable)
|
||||
self.assertEqual(certificate_status['mode'], GeneratedCertificate.MODES.honor)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_course_milestone_collected(self):
|
||||
seed_milestone_relationship_types()
|
||||
student = UserFactory()
|
||||
course = CourseFactory.create(org='edx', number='998', display_name='Test Course')
|
||||
pre_requisite_course = CourseFactory.create(org='edx', number='999', display_name='Pre requisite Course')
|
||||
# set pre-requisite course
|
||||
set_prerequisite_courses(course.id, [unicode(pre_requisite_course.id)])
|
||||
# get milestones collected by user before completing the pre-requisite course
|
||||
completed_milestones = milestones_achieved_by_user(student, unicode(pre_requisite_course.id))
|
||||
self.assertEqual(len(completed_milestones), 0)
|
||||
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=pre_requisite_course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='verified'
|
||||
)
|
||||
# get milestones collected by user after user has completed the pre-requisite course
|
||||
completed_milestones = milestones_achieved_by_user(student, unicode(pre_requisite_course.id))
|
||||
self.assertEqual(len(completed_milestones), 1)
|
||||
self.assertEqual(completed_milestones[0]['namespace'], unicode(pre_requisite_course.id))
|
||||
|
||||
@@ -28,6 +28,7 @@ from student.roles import (
|
||||
)
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from util.milestones_helpers import get_pre_requisite_courses_not_completed
|
||||
DEBUG_ACCESS = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -267,8 +268,24 @@ def _has_access_course_desc(user, action, course):
|
||||
_has_staff_access_to_descriptor(user, course, course.id)
|
||||
)
|
||||
|
||||
def can_view_courseware_with_prerequisites(): # pylint: disable=invalid-name
|
||||
"""
|
||||
Checks if prerequisite courses feature is enabled and course has prerequisites
|
||||
and user is neither staff nor anonymous then it returns False if user has not
|
||||
passed prerequisite courses otherwise return True.
|
||||
"""
|
||||
if settings.FEATURES['ENABLE_PREREQUISITE_COURSES'] \
|
||||
and not _has_staff_access_to_descriptor(user, course, course.id) \
|
||||
and course.pre_requisite_courses \
|
||||
and not user.is_anonymous() \
|
||||
and get_pre_requisite_courses_not_completed(user, [course.id]):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
'view_courseware_with_prerequisites': can_view_courseware_with_prerequisites,
|
||||
'load_forum': can_load_forum,
|
||||
'load_mobile': can_load_mobile,
|
||||
'load_mobile_no_enrollment_check': can_load_mobile_no_enroll_check,
|
||||
|
||||
@@ -20,6 +20,10 @@ from shoppingcart.models import Order, PaidCourseRegistration
|
||||
from xmodule.course_module import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from util.milestones_helpers import (
|
||||
set_prerequisite_courses,
|
||||
seed_milestone_relationship_types,
|
||||
)
|
||||
|
||||
from .helpers import LoginEnrollmentTestCase
|
||||
|
||||
@@ -33,7 +37,6 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
"""
|
||||
Tests about xblock.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.course = CourseFactory.create()
|
||||
self.about = ItemFactory.create(
|
||||
@@ -120,6 +123,60 @@ class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
info_url = reverse('info', args=[self.course.id.to_deprecated_string()])
|
||||
self.assertTrue(target_url.endswith(info_url))
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_pre_requisite_course(self):
|
||||
seed_milestone_relationship_types()
|
||||
pre_requisite_course = CourseFactory.create(org='edX', course='900', display_name='pre requisite course')
|
||||
course = CourseFactory.create(pre_requisite_courses=[unicode(pre_requisite_course.id)])
|
||||
self.setup_user()
|
||||
url = reverse('about_course', args=[unicode(course.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn("<span class=\"important-dates-item-text pre-requisite\">{} {}</span>"
|
||||
.format(pre_requisite_course.display_org_with_default,
|
||||
pre_requisite_course.display_number_with_default),
|
||||
resp.content.strip('\n'))
|
||||
|
||||
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_about_page_unfulfilled_prereqs(self):
|
||||
seed_milestone_relationship_types()
|
||||
pre_requisite_course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='900',
|
||||
display_name='pre requisite course',
|
||||
)
|
||||
|
||||
pre_requisite_courses = [unicode(pre_requisite_course.id)]
|
||||
|
||||
# for this failure to occur, the enrollment window needs to be in the past
|
||||
course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='1000',
|
||||
# closed enrollment
|
||||
enrollment_start=datetime.datetime(2013, 1, 1),
|
||||
enrollment_end=datetime.datetime(2014, 1, 1),
|
||||
start=datetime.datetime(2013, 1, 1),
|
||||
end=datetime.datetime(2030, 1, 1),
|
||||
pre_requisite_courses=pre_requisite_courses,
|
||||
)
|
||||
set_prerequisite_courses(course.id, pre_requisite_courses)
|
||||
|
||||
self.setup_user()
|
||||
self.enroll(self.course, True)
|
||||
self.enroll(pre_requisite_course, True)
|
||||
|
||||
url = reverse('about_course', args=[unicode(course.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn("<span class=\"important-dates-item-text pre-requisite\">{} {}</span>"
|
||||
.format(pre_requisite_course.display_org_with_default,
|
||||
pre_requisite_course.display_number_with_default),
|
||||
resp.content.strip('\n'))
|
||||
|
||||
url = reverse('about_course', args=[unicode(pre_requisite_course.id)])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_CLOSED_MODULESTORE)
|
||||
class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
|
||||
|
||||
@@ -2,27 +2,35 @@ import datetime
|
||||
import pytz
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from mock import Mock, patch
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
import courseware.access as access
|
||||
from courseware.masquerade import CourseMasquerade
|
||||
from courseware.tests.factories import UserFactory, StaffFactory, InstructorFactory
|
||||
from student.tests.factories import AnonymousUserFactory, CourseEnrollmentAllowedFactory
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from student.tests.factories import AnonymousUserFactory, CourseEnrollmentAllowedFactory, CourseEnrollmentFactory
|
||||
from xmodule.course_module import (
|
||||
CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CATALOG_VISIBILITY_ABOUT,
|
||||
CATALOG_VISIBILITY_NONE
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from util.milestones_helpers import (
|
||||
set_prerequisite_courses,
|
||||
fulfill_course_milestone,
|
||||
seed_milestone_relationship_types,
|
||||
)
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
class AccessTestCase(TestCase):
|
||||
class AccessTestCase(LoginEnrollmentTestCase):
|
||||
"""
|
||||
Tests for the various access controls on the student dashboard
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
|
||||
self.course = course_key.make_usage_key('course', course_key.run)
|
||||
@@ -243,6 +251,78 @@ class AccessTestCase(TestCase):
|
||||
self.assertTrue(access._has_access_course_desc(staff, 'see_in_catalog', course))
|
||||
self.assertTrue(access._has_access_course_desc(staff, 'see_about_page', course))
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_access_on_course_with_pre_requisites(self):
|
||||
"""
|
||||
Test course access when a course has pre-requisite course yet to be completed
|
||||
"""
|
||||
seed_milestone_relationship_types()
|
||||
user = UserFactory.create()
|
||||
|
||||
pre_requisite_course = CourseFactory.create(
|
||||
org='test_org', number='788', run='test_run'
|
||||
)
|
||||
|
||||
pre_requisite_courses = [unicode(pre_requisite_course.id)]
|
||||
course = CourseFactory.create(
|
||||
org='test_org', number='786', run='test_run', pre_requisite_courses=pre_requisite_courses
|
||||
)
|
||||
set_prerequisite_courses(course.id, pre_requisite_courses)
|
||||
|
||||
#user should not be able to load course even if enrolled
|
||||
CourseEnrollmentFactory(user=user, course_id=course.id)
|
||||
self.assertFalse(access._has_access_course_desc(user, 'view_courseware_with_prerequisites', course))
|
||||
|
||||
# Staff can always access course
|
||||
staff = StaffFactory.create(course_key=course.id)
|
||||
self.assertTrue(access._has_access_course_desc(staff, 'view_courseware_with_prerequisites', course))
|
||||
|
||||
# User should be able access after completing required course
|
||||
fulfill_course_milestone(pre_requisite_course.id, user)
|
||||
self.assertTrue(access._has_access_course_desc(user, 'view_courseware_with_prerequisites', course))
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
|
||||
def test_courseware_page_unfulfilled_prereqs(self):
|
||||
"""
|
||||
Test courseware access when a course has pre-requisite course yet to be completed
|
||||
"""
|
||||
seed_milestone_relationship_types()
|
||||
pre_requisite_course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='900',
|
||||
run='test_run',
|
||||
)
|
||||
|
||||
pre_requisite_courses = [unicode(pre_requisite_course.id)]
|
||||
course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='1000',
|
||||
run='test_run',
|
||||
pre_requisite_courses=pre_requisite_courses,
|
||||
)
|
||||
set_prerequisite_courses(course.id, pre_requisite_courses)
|
||||
|
||||
test_password = 't3stp4ss.!'
|
||||
user = UserFactory.create()
|
||||
user.set_password(test_password)
|
||||
user.save()
|
||||
self.login(user.email, test_password)
|
||||
CourseEnrollmentFactory(user=user, course_id=course.id)
|
||||
|
||||
url = reverse('courseware', args=[unicode(course.id)])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(
|
||||
response,
|
||||
reverse(
|
||||
'dashboard'
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
fulfill_course_milestone(pre_requisite_course.id, user)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class UserRoleTestCase(TestCase):
|
||||
"""
|
||||
|
||||
@@ -56,6 +56,7 @@ import shoppingcart
|
||||
from shoppingcart.models import CourseRegistrationCode
|
||||
from shoppingcart.utils import is_shopping_cart_enabled
|
||||
from opaque_keys import InvalidKeyError
|
||||
from util.milestones_helpers import get_prerequisite_courses_display
|
||||
|
||||
from microsite_configuration import microsite
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
@@ -349,6 +350,17 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
log.debug(u'User %s tried to view course %s but is not enrolled', user, course.location.to_deprecated_string())
|
||||
return redirect(reverse('about_course', args=[course_key.to_deprecated_string()]))
|
||||
|
||||
# see if all pre-requisites (as per the milestones app feature) have been fulfilled
|
||||
# Note that if the pre-requisite feature flag has been turned off (default) then this check will
|
||||
# always pass
|
||||
if not has_access(user, 'view_courseware_with_prerequisites', course):
|
||||
# prerequisites have not been fulfilled therefore redirect to the Dashboard
|
||||
log.info(
|
||||
u'User %d tried to view course %s '
|
||||
u'without fulfilling prerequisites',
|
||||
user.id, unicode(course.id))
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
# check to see if there is a required survey that must be taken before
|
||||
# the user can access the course.
|
||||
if survey.utils.must_answer_survey(course, user):
|
||||
@@ -757,8 +769,13 @@ def course_about(request, course_id):
|
||||
else:
|
||||
course_target = reverse('about_course', args=[course.id.to_deprecated_string()])
|
||||
|
||||
show_courseware_link = (has_access(request.user, 'load', course) or
|
||||
settings.FEATURES.get('ENABLE_LMS_MIGRATION'))
|
||||
show_courseware_link = (
|
||||
(
|
||||
has_access(request.user, 'load', course)
|
||||
and has_access(request.user, 'view_courseware_with_prerequisites', course)
|
||||
)
|
||||
or settings.FEATURES.get('ENABLE_LMS_MIGRATION')
|
||||
)
|
||||
|
||||
# Note: this is a flow for payment for course registration, not the Verified Certificate flow.
|
||||
registration_price = 0
|
||||
@@ -790,6 +807,9 @@ def course_about(request, course_id):
|
||||
|
||||
is_shib_course = uses_shib(course)
|
||||
|
||||
# get prerequisite courses display names
|
||||
pre_requisite_courses = get_prerequisite_courses_display(course)
|
||||
|
||||
return render_to_response('courseware/course_about.html', {
|
||||
'course': course,
|
||||
'staff_access': staff_access,
|
||||
@@ -811,6 +831,7 @@ def course_about(request, course_id):
|
||||
'disable_courseware_header': True,
|
||||
'is_shopping_cart_enabled': _is_shopping_cart_enabled,
|
||||
'cart_link': reverse('shoppingcart.views.show_cart'),
|
||||
'pre_requisite_courses': pre_requisite_courses
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,12 @@ LOG_OVERRIDES = [
|
||||
for log_name, log_level in LOG_OVERRIDES:
|
||||
logging.getLogger(log_name).setLevel(log_level)
|
||||
|
||||
# Enable milestones app
|
||||
FEATURES['MILESTONES_APP'] = True
|
||||
|
||||
# Enable pre-requisite course
|
||||
FEATURES['ENABLE_PREREQUISITE_COURSES'] = True
|
||||
|
||||
# Unfortunately, we need to use debug mode to serve staticfiles
|
||||
DEBUG = True
|
||||
|
||||
|
||||
@@ -314,6 +314,12 @@ FEATURES = {
|
||||
|
||||
# let students save and manage their annotations
|
||||
'ENABLE_EDXNOTES': False,
|
||||
|
||||
# Milestones application flag
|
||||
'MILESTONES_APP': False,
|
||||
|
||||
# Prerequisite courses feature flag
|
||||
'ENABLE_PREREQUISITE_COURSES': False,
|
||||
}
|
||||
|
||||
# Ignore static asset files on import which match this pattern
|
||||
@@ -1643,6 +1649,7 @@ if FEATURES.get('AUTH_USE_CAS'):
|
||||
INSTALLED_APPS += ('django_cas',)
|
||||
MIDDLEWARE_CLASSES += ('django_cas.middleware.CASMiddleware',)
|
||||
|
||||
|
||||
###################### Registration ##################################
|
||||
|
||||
# For each of the fields, give one of the following values:
|
||||
@@ -1912,7 +1919,8 @@ OPTIONAL_APPS = (
|
||||
'openassessment.xblock',
|
||||
|
||||
# edxval
|
||||
'edxval'
|
||||
'edxval',
|
||||
'milestones'
|
||||
)
|
||||
|
||||
for app_name in OPTIONAL_APPS:
|
||||
|
||||
@@ -437,5 +437,9 @@ MONGODB_LOG = {
|
||||
'db': 'xlog',
|
||||
}
|
||||
|
||||
|
||||
# Enable EdxNotes for tests.
|
||||
FEATURES['ENABLE_EDXNOTES'] = True
|
||||
|
||||
# Add milestones to Installed apps for testing
|
||||
INSTALLED_APPS += ('milestones', )
|
||||
|
||||
@@ -564,6 +564,20 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.prerequisite-course {
|
||||
.pre-requisite {
|
||||
max-width: 39%;
|
||||
@extend %text-truncated;
|
||||
}
|
||||
.tip {
|
||||
float: left;
|
||||
margin: $baseline 0 ($baseline/2);
|
||||
font-size: 0.8em;
|
||||
color: $lighter-base-font-color;
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +482,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.prerequisites {
|
||||
@include clearfix;
|
||||
|
||||
.tip {
|
||||
font-family: $sans-serif;
|
||||
font-size: 1em;
|
||||
color: $lighter-base-font-color;
|
||||
margin-top: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// "enrolled as" status
|
||||
.sts-enrollment {
|
||||
position: absolute;
|
||||
|
||||
@@ -319,8 +319,17 @@
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if pre_requisite_courses:
|
||||
<li class="prerequisite-course important-dates-item">
|
||||
<i class="icon fa fa-list-ul"></i>
|
||||
<p class="important-dates-item-title">${_("Prerequisites")}</p>
|
||||
## Multiple pre-requisite courses are not supported on frontend that's why we are pulling first element
|
||||
<span class="important-dates-item-text pre-requisite">${pre_requisite_courses[0]}</span>
|
||||
<p class="tip">${_("You must successfully complete {course} before you begin this course").format(course=pre_requisite_courses[0])}.</p>
|
||||
</li>
|
||||
% endif
|
||||
% if get_course_about_section(course, "prerequisites"):
|
||||
<li class="important-dates-item"><i class="icon fa fa-book"></i><p class="important-dates-item-title">${_("Prerequisites")}</p><span class="important-dates-item-text prerequisites">${get_course_about_section(course, "prerequisites")}</span></li>
|
||||
<li class="important-dates-item"><i class="icon fa fa-book"></i><p class="important-dates-item-title">${_("Requirements")}</p><span class="important-dates-item-text prerequisites">${get_course_about_section(course, "prerequisites")}</span></li>
|
||||
% endif
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
@@ -190,7 +190,8 @@
|
||||
<% is_paid_course = (course.id in enrolled_courses_either_paid) %>
|
||||
<% is_course_blocked = (course.id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(course.id, {}) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status" />
|
||||
<% course_requirements = courses_requirements_not_met.get(course.id) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements" />
|
||||
% endfor
|
||||
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status" />
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements" />
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -324,6 +324,19 @@ from student.helpers import (
|
||||
|
||||
|
||||
</section>
|
||||
% if course_requirements:
|
||||
## Multiple pre-requisite courses are not supported on frontend that's why we are pulling first element
|
||||
<% prc_target = reverse('about_course', args=[unicode(course_requirements['courses'][0]['key'])]) %>
|
||||
<section class="prerequisites">
|
||||
<p class="tip">
|
||||
${_("You must successfully complete {link_start}{prc_display}{link_end} before you begin this course.").format(
|
||||
link_start='<a href="{}">'.format(prc_target),
|
||||
link_end='</a>',
|
||||
prc_display=course_requirements['courses'][0]['display'],
|
||||
)}
|
||||
</p>
|
||||
</section>
|
||||
% endif
|
||||
</article>
|
||||
</article>
|
||||
</li>
|
||||
|
||||
@@ -36,3 +36,4 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a
|
||||
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.0#egg=oauth2-provider
|
||||
-e git+https://github.com/edx/edx-val.git@ba00a5f2e0571e9a3f37d293a98efe4cbca850d5#egg=edx-val
|
||||
-e git+https://github.com/pmitros/RecommenderXBlock.git@b41ba8778b98da0ea680ffb8bbc59492d669df2d#egg=recommender-xblock
|
||||
-e git+https://github.com/edx/edx-milestones.git@4dfe78a2aae9559ccc979746d13a9b67f0ec311e#egg=edx-milestones
|
||||
|
||||
Reference in New Issue
Block a user