Commit 1c839cc0 by Matt Drayer

Addressed several outstanding issues related to initial entrance exams feature delivery

parent 495583ac
......@@ -10,7 +10,8 @@ from django_future.csrf import ensure_csrf_cookie
from django.http import HttpResponse
from django.test import RequestFactory
from contentstore.views.item import create_item, delete_item
from contentstore.views.helpers import create_xblock
from contentstore.views.item import delete_item
from milestones import api as milestones_api
from models.settings.course_metadata import CourseMetadata
from opaque_keys.edx.keys import CourseKey, UsageKey
......@@ -108,10 +109,14 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
'is_entrance_exam': True,
'in_entrance_exam': True,
}
factory = RequestFactory()
internal_request = factory.post('/', json.dumps(payload), content_type="application/json")
internal_request.user = request.user
created_item = json.loads(create_item(internal_request).content)
parent_locator = unicode(course.location)
created_block = create_xblock(
parent_locator=parent_locator,
user=request.user,
category='chapter',
display_name='Entrance Exam',
is_entrance_exam=True
)
# Set the entrance exam metadata flags for this course
# Reload the course so we don't overwrite the new child reference
......@@ -119,7 +124,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
metadata = {
'entrance_exam_enabled': True,
'entrance_exam_minimum_score_pct': entrance_exam_minimum_score_pct / 100,
'entrance_exam_id': created_item['locator'],
'entrance_exam_id': unicode(created_block.location),
}
CourseMetadata.update_from_dict(metadata, course, request.user)
......@@ -146,7 +151,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
)
milestones_api.add_course_content_milestone(
unicode(course.id),
created_item['locator'],
unicode(created_block.location),
relationship_types['FULFILLS'],
milestone
)
......
......@@ -4,16 +4,22 @@ Helper methods for Studio views.
from __future__ import absolute_import
from uuid import uuid4
import urllib
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_string, render_to_response
from opaque_keys.edx.keys import UsageKey
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore
from xmodule.tabs import StaticTab
from contentstore.utils import reverse_course_url, reverse_library_url, reverse_usage_url
from models.settings.course_grading import CourseGradingModel
__all__ = ['edge', 'event', 'landing']
......@@ -154,3 +160,105 @@ def xblock_primary_child_category(xblock):
elif category == 'sequential':
return 'vertical'
return None
def usage_key_with_run(usage_key_string):
"""
Converts usage_key_string to a UsageKey, adding a course run if necessary
"""
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
return usage_key
def create_xblock(parent_locator, user, category, display_name, boilerplate=None, is_entrance_exam=False):
"""
Performs the actual grunt work of creating items/xblocks -- knows nothing about requests, views, etc.
"""
store = modulestore()
usage_key = usage_key_with_run(parent_locator)
with store.bulk_operations(usage_key.course_key):
parent = store.get_item(usage_key)
dest_usage_key = usage_key.replace(category=category, name=uuid4().hex)
# get the metadata, display_name, and definition from the caller
metadata = {}
data = None
template_id = boilerplate
if template_id:
clz = parent.runtime.load_block_type(category)
if clz is not None:
template = clz.get_template(template_id)
if template is not None:
metadata = template.get('metadata', {})
data = template.get('data')
if display_name is not None:
metadata['display_name'] = display_name
# We should use the 'fields' kwarg for newer module settings/values (vs. metadata or data)
fields = {}
# Entrance Exams: Chapter module positioning
child_position = None
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
if category == 'chapter' and is_entrance_exam:
fields['is_entrance_exam'] = is_entrance_exam
fields['in_entrance_exam'] = True # Inherited metadata, all children will have it
child_position = 0
# TODO need to fix components that are sending definition_data as strings, instead of as dicts
# For now, migrate them into dicts here.
if isinstance(data, basestring):
data = {'data': data}
created_block = store.create_child(
user.id,
usage_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
fields=fields,
definition_data=data,
metadata=metadata,
runtime=parent.runtime,
position=child_position,
)
# Entrance Exams: Grader assignment
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
course = store.get_course(usage_key.course_key)
if hasattr(course, 'entrance_exam_enabled') and course.entrance_exam_enabled:
if category == 'sequential' and parent_locator == course.entrance_exam_id:
grader = {
"type": "Entrance Exam",
"min_count": 0,
"drop_count": 0,
"short_label": "Entrance",
"weight": 0
}
grading_model = CourseGradingModel.update_grader_from_json(
course.id,
grader,
user
)
CourseGradingModel.update_section_grader_type(
created_block,
grading_model['type'],
user
)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs
if category == 'static_tab':
display_name = display_name or _("Empty") # Prevent name being None
course = store.get_course(dest_usage_key.course_key)
course.tabs.append(
StaticTab(
name=display_name,
url_slug=dest_usage_key.name,
)
)
store.update_item(course, user.id)
return created_block
......@@ -42,7 +42,7 @@ from student.auth import has_studio_write_access, has_studio_read_access
from contentstore.utils import find_release_date_source, find_staff_lock_source, is_currently_visible_to_students, \
ancestor_has_staff_lock, has_children_visible_to_specific_content_groups
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \
xblock_type_display_name, get_parent_xblock
xblock_type_display_name, get_parent_xblock, create_xblock, usage_key_with_run
from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel
......@@ -79,15 +79,6 @@ def hash_resource(resource):
return md5.hexdigest()
def usage_key_with_run(usage_key_string):
"""
Converts usage_key_string to a UsageKey, adding a course run if necessary
"""
usage_key = UsageKey.from_string(usage_key_string)
usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
return usage_key
def _filter_entrance_exam_grader(graders):
"""
If the entrance exams feature is enabled we need to hide away the grader from
......@@ -536,13 +527,12 @@ def create_item(request):
@expect_json
def _create_item(request):
"""View for create items."""
usage_key = usage_key_with_run(request.json['parent_locator'])
parent_locator = request.json['parent_locator']
usage_key = usage_key_with_run(parent_locator)
if not has_studio_write_access(request.user, usage_key.course_key):
raise PermissionDenied()
category = request.json['category']
display_name = request.json.get('display_name')
if isinstance(usage_key, LibraryUsageLocator):
# Only these categories are supported at this time.
if category not in ['html', 'problem', 'video']:
......@@ -550,91 +540,17 @@ def _create_item(request):
"Category '%s' not supported for Libraries" % category, content_type='text/plain'
)
store = modulestore()
with store.bulk_operations(usage_key.course_key):
parent = store.get_item(usage_key)
dest_usage_key = usage_key.replace(category=category, name=uuid4().hex)
# get the metadata, display_name, and definition from the request
metadata = {}
data = None
template_id = request.json.get('boilerplate')
if template_id:
clz = parent.runtime.load_block_type(category)
if clz is not None:
template = clz.get_template(template_id)
if template is not None:
metadata = template.get('metadata', {})
data = template.get('data')
if display_name is not None:
metadata['display_name'] = display_name
# Entrance Exams: Chapter module positioning
child_position = None
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
is_entrance_exam = request.json.get('is_entrance_exam', False)
if category == 'chapter' and is_entrance_exam:
metadata['is_entrance_exam'] = is_entrance_exam
metadata['in_entrance_exam'] = True # Inherited metadata, all children will have it
child_position = 0
# TODO need to fix components that are sending definition_data as strings, instead of as dicts
# For now, migrate them into dicts here.
if isinstance(data, basestring):
data = {'data': data}
created_block = store.create_child(
request.user.id,
usage_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
definition_data=data,
metadata=metadata,
runtime=parent.runtime,
position=child_position
)
# Entrance Exams: Grader assignment
if settings.FEATURES.get('ENTRANCE_EXAMS', False):
course = store.get_course(usage_key.course_key)
if hasattr(course, 'entrance_exam_enabled') and course.entrance_exam_enabled:
if category == 'sequential' and request.json.get('parent_locator') == course.entrance_exam_id:
grader = {
"type": "Entrance Exam",
"min_count": 0,
"drop_count": 0,
"short_label": "Entrance",
"weight": 0
}
grading_model = CourseGradingModel.update_grader_from_json(
course.id,
grader,
request.user
)
CourseGradingModel.update_section_grader_type(
created_block,
grading_model['type'],
request.user
)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs
if category == 'static_tab':
display_name = display_name or _("Empty") # Prevent name being None
course = store.get_course(dest_usage_key.course_key)
course.tabs.append(
StaticTab(
name=display_name,
url_slug=dest_usage_key.name,
)
)
store.update_item(course, request.user.id)
return JsonResponse(
{"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)}
)
created_block = create_xblock(
parent_locator=parent_locator,
user=request.user,
category=category,
display_name=request.json.get('display_name'),
boilerplate=request.json.get('boilerplate')
)
return JsonResponse(
{"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)}
)
def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_name=None):
......
......@@ -1378,6 +1378,20 @@ class TestXBlockInfo(ItemTest):
json_response = json.loads(resp.content)
self.validate_course_xblock_info(json_response, course_outline=True)
def test_chapter_entrance_exam_xblock_info(self):
chapter = ItemFactory.create(
parent_location=self.course.location, category='chapter', display_name="Entrance Exam",
user_id=self.user.id, is_entrance_exam=True
)
chapter = modulestore().get_item(chapter.location)
xblock_info = create_xblock_info(
chapter,
include_child_info=True,
include_children_predicate=ALWAYS,
)
self.assertEqual(xblock_info['override_type'], {'is_entrance_exam': True})
self.assertEqual(xblock_info['display_name'], 'Entrance Exam')
def test_chapter_xblock_info(self):
chapter = modulestore().get_item(self.chapter.location)
xblock_info = create_xblock_info(
......
......@@ -7,6 +7,16 @@ define(['backbone', 'js/models/xblock_info'],
expect(new XBlockInfo({'category': 'sequential'}).isEditableOnCourseOutline()).toBe(true);
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(true);
});
it('cannot delete an entrance exam', function(){
expect(new XBlockInfo({'category': 'chapter', 'override_type': {'is_entrance_exam':true}})
.canBeDeleted()).toBe(false);
});
it('can delete module rather then entrance exam', function(){
expect(new XBlockInfo({'category': 'chapter', 'override_type': {'is_entrance_exam':false}}).canBeDeleted()).toBe(true);
expect(new XBlockInfo({'category': 'chapter', 'override_type': {}}).canBeDeleted()).toBe(true);
});
});
}
);
......@@ -3,6 +3,13 @@ define([
'js/common_helpers/ajax_helpers'
], function($, CourseDetailsModel, MainView, AjaxHelpers) {
'use strict';
var SELECTORS = {
entrance_exam_min_score: '#entrance-exam-minimum-score-pct',
entrance_exam_enabled_field: '#entrance-exam-enabled',
grade_requirement_div: '.div-grade-requirements div'
};
describe('Settings/Main', function () {
var urlRoot = '/course/settings/org/DemoX/Demo_Course',
modelData = {
......@@ -79,12 +86,52 @@ define([
AjaxHelpers.respondWithJson(requests, expectedJson);
});
it('should save entrance exam course details information correctly', function () {
var entrance_exam_minimum_score_pct = '60';
var entrance_exam_enabled = 'true';
it('should disallow save with an invalid minimum score percentage', function(){
var entrance_exam_enabled_field = this.view.$(SELECTORS.entrance_exam_enabled_field),
entrance_exam_min_score = this.view.$(SELECTORS.entrance_exam_min_score);
//input some invalid values.
expect(entrance_exam_min_score.val('101').trigger('input')).toHaveClass("error");
expect(entrance_exam_min_score.val('invalidVal').trigger('input')).toHaveClass("error");
});
it('should provide a default value for the minimum score percentage', function(){
var entrance_exam_min_score = this.view.$(SELECTORS.entrance_exam_min_score);
//if input an empty value, model should be populated with the default value.
entrance_exam_min_score.val('').trigger('input');
expect(this.model.get('entrance_exam_minimum_score_pct'))
.toEqual(this.model.defaults.entrance_exam_minimum_score_pct);
});
it('show and hide the grade requirement section when the check box is selected and deselected respectively', function(){
var entrance_exam_enabled_field = this.view.$(SELECTORS.entrance_exam_enabled_field);
// select the entrance-exam-enabled checkbox. grade requirement section should be visible.
entrance_exam_enabled_field
.attr('checked', 'true')
.trigger('change');
this.view.render();
expect(this.view.$(SELECTORS.grade_requirement_div)).toBeVisible();
// deselect the entrance-exam-enabled checkbox. grade requirement section should be hidden.
entrance_exam_enabled_field
.removeAttr('checked')
.trigger('change');
expect(this.view.$(SELECTORS.grade_requirement_div)).toBeHidden();
var entrance_exam_min_score = this.view.$('#entrance-exam-minimum-score-pct');
var entrance_exam_enabled_field = this.view.$('#entrance-exam-enabled');
});
it('should save entrance exam course details information correctly', function () {
var entrance_exam_minimum_score_pct = '60',
entrance_exam_enabled = 'true',
entrance_exam_min_score = this.view.$(SELECTORS.entrance_exam_min_score),
entrance_exam_enabled_field = this.view.$(SELECTORS.entrance_exam_enabled_field);
var requests = AjaxHelpers.requests(this),
expectedJson = $.extend(true, {}, modelData, {
......@@ -92,22 +139,10 @@ define([
entrance_exam_minimum_score_pct: entrance_exam_minimum_score_pct
});
expect(this.view.$('.div-grade-requirements div')).toBeHidden();
// select the entrance-exam-enabled checkbox. grade requirement section should be visible
// select the entrance-exam-enabled checkbox.
entrance_exam_enabled_field
.attr('checked', entrance_exam_enabled)
.attr('checked', 'true')
.trigger('change');
expect(this.view.$('.div-grade-requirements div')).toBeVisible();
//input some invalid values.
expect(entrance_exam_min_score.val('101').trigger('input')).toHaveClass("error");
expect(entrance_exam_min_score.val('invalidVal').trigger('input')).toHaveClass("error");
//if input an empty value, model should be populated with the default value.
entrance_exam_min_score.val('').trigger('input');
expect(this.model.get('entrance_exam_minimum_score_pct'))
.toEqual(this.model.defaults.entrance_exam_minimum_score_pct);
// input a valid value for entrance exam minimum score.
entrance_exam_min_score.val(entrance_exam_minimum_score_pct).trigger('input');
......
......@@ -52,7 +52,7 @@ class SequenceFields(object):
"Tag this course module as an Entrance Exam. " +
"Note, you must enable Entrance Exams for this course setting to take effect."
),
scope=Scope.settings,
scope=Scope.content,
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment