Commit 788cece4 by Muhammad Shoaib Committed by Douglas Hall

PHX-161

- added the new field review_rules for software secure
- added a new tab name "Additional Settings" for the proctored/timed exams
parent 9e5eda0a
......@@ -15,9 +15,13 @@ from edx_proctoring.api import (
update_exam,
create_exam,
get_all_exams_for_course,
update_review_policy,
create_exam_review_policy,
remove_review_policy,
)
from edx_proctoring.exceptions import (
ProctoredExamNotFoundException
ProctoredExamNotFoundException,
ProctoredExamReviewPolicyNotFoundException
)
log = logging.getLogger(__name__)
......@@ -72,7 +76,7 @@ def register_special_exams(course_key):
try:
exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location))
# update case, make sure everything is synced
update_exam(
exam_id = update_exam(
exam_id=exam['id'],
exam_name=timed_exam.display_name,
time_limit_mins=timed_exam.default_time_limit_minutes,
......@@ -83,6 +87,7 @@ def register_special_exams(course_key):
)
msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id'])
log.info(msg)
except ProctoredExamNotFoundException:
exam_id = create_exam(
course_id=unicode(course_key),
......@@ -97,6 +102,30 @@ def register_special_exams(course_key):
msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id)
log.info(msg)
# only create/update exam policy for the proctored exams
if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam:
try:
update_review_policy(
exam_id=exam_id,
set_by_user_id=timed_exam.edited_by,
review_policy=timed_exam.exam_review_rules
)
except ProctoredExamReviewPolicyNotFoundException:
if timed_exam.exam_review_rules: # won't save an empty rule.
create_exam_review_policy(
exam_id=exam_id,
set_by_user_id=timed_exam.edited_by,
review_policy=timed_exam.exam_review_rules
)
msg = 'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id)
log.info(msg)
else:
try:
# remove any associated review policy
remove_review_policy(exam_id=exam_id)
except ProctoredExamReviewPolicyNotFoundException:
pass
# then see which exams we have in edx-proctoring that are not in
# our current list. That means the the user has disabled it
exams = get_all_exams_for_course(course_key)
......
......@@ -11,7 +11,10 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from contentstore.signals import listen_for_course_publish
from edx_proctoring.api import get_all_exams_for_course
from edx_proctoring.api import (
get_all_exams_for_course,
get_review_policy_by_exam_id
)
@ddt.ddt
......@@ -44,21 +47,28 @@ class TestProctoredExams(ModuleStoreTestCase):
self.assertEqual(len(exams), 1)
exam = exams[0]
if exam['is_proctored'] and not exam['is_practice_exam']:
# get the review policy object
exam_review_policy = get_review_policy_by_exam_id(exam['id'])
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
self.assertEqual(exam['course_id'], unicode(self.course.id))
self.assertEqual(exam['content_id'], unicode(sequence.location))
self.assertEqual(exam['exam_name'], sequence.display_name)
self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes)
self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam)
self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam)
self.assertEqual(exam['is_active'], expected_active)
@ddt.data(
(True, 10, True, True, False),
(True, 10, False, True, False),
(True, 10, True, True, True),
(True, 10, True, False, True, False),
(True, 10, False, False, True, False),
(True, 10, True, True, True, True),
)
@ddt.unpack
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes,
is_proctored_exam, expected_active, republish):
is_proctored_exam, is_practice_exam, expected_active, republish):
"""
Happy path testing to see that when a course is published which contains
a proctored exam, it will also put an entry into the exam tables
......@@ -73,7 +83,9 @@ class TestProctoredExams(ModuleStoreTestCase):
is_time_limited=is_time_limited,
default_time_limit_minutes=default_time_limit_minutes,
is_proctored_exam=is_proctored_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1)
is_practice_exam=is_practice_exam,
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
exam_review_rules="allow_use_of_paper"
)
listen_for_course_publish(self, self.course.id)
......@@ -205,7 +217,8 @@ class TestProctoredExams(ModuleStoreTestCase):
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_exam=True
is_proctored_exam=True,
exam_review_rules="allow_use_of_paper"
)
listen_for_course_publish(self, self.course.id)
......
......@@ -869,6 +869,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"is_proctored_exam": xblock.is_proctored_exam,
"is_practice_exam": xblock.is_practice_exam,
"is_time_limited": xblock.is_time_limited,
"exam_review_rules": xblock.exam_review_rules,
"default_time_limit_minutes": xblock.default_time_limit_minutes
})
......
......@@ -49,6 +49,7 @@ class CourseMetadata(object):
'is_proctored_enabled',
'is_time_limited',
'is_practice_exam',
'exam_review_rules',
'self_paced'
]
......
......@@ -216,7 +216,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor', 'timed-examination-preference-editor'
'staff-lock-editor', 'settings-tab-section', 'timed-examination-preference-editor'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON({}, [
......@@ -580,7 +580,8 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
describe("Subsection", function() {
var getDisplayNameWrapper, setEditModalValues, mockServerValuesJson,
selectDisableSpecialExams, selectTimedExam, selectProctoredExam, selectPracticeExam;
selectDisableSpecialExams, selectGeneralSettings, selectAdvancedSettings,
selectTimedExam, selectProctoredExam, selectPracticeExam;
getDisplayNameWrapper = function() {
return getItemHeaders('subsection').find('.wrapper-xblock-field');
......@@ -597,6 +598,14 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
this.$("#id_not_timed").prop('checked', true).trigger('change');
};
selectGeneralSettings = function() {
this.$(".modal-section .general-settings-button").click();
};
selectAdvancedSettings = function() {
this.$(".modal-section .advanced-settings-button").click();
};
selectTimedExam = function(time_limit) {
this.$("#id_timed_exam").prop('checked', true).trigger('change');
this.$("#id_time_limit").val(time_limit);
......@@ -701,6 +710,22 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
collapseItemsAndVerifyState('subsection');
expandItemsAndVerifyState('subsection');
});
it('can show general settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectGeneralSettings();
expect($('.modal-section .general-settings-button')).toHaveClass('active');
expect($('.modal-section .advanced-settings-button')).not.toHaveClass('active');
});
it('can show advanced settings', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
selectAdvancedSettings();
expect($('.modal-section .general-settings-button')).not.toHaveClass('active');
expect($('.modal-section .advanced-settings-button')).toHaveClass('active');
});
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
......@@ -715,6 +740,7 @@ define(["jquery", "common/js/spec_helpers/ajax_helpers", "common/js/components/u
"visible_to_staff_only": true,
"start":"2014-07-09T00:00:00.000Z",
"due":"2014-07-10T00:00:00.000Z",
"exam_review_rules": "",
"is_time_limited": true,
"is_practice_exam": false,
"is_proctored_enabled": true,
......
......@@ -84,7 +84,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
getContext: function () {
return $.extend({
xblockInfo: this.model,
introductionMessage: this.getIntroductionMessage()
introductionMessage: this.getIntroductionMessage(),
enable_proctored_exams: this.options.enable_proctored_exams,
enable_timed_exams: this.options.enable_timed_exams
});
},
......@@ -114,6 +116,78 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
gettext('Change the settings for %(display_name)s'),
{ display_name: this.model.get('display_name') }, true
);
},
initializeEditors: function () {
var special_exams_editors = this.options.special_exam_editors;
if (typeof special_exams_editors !== 'undefined' && special_exams_editors.length > 0) {
var tabs_html = this.loadTemplate('settings-tab-section');
this.$('.modal-section').html(tabs_html);
this.options.editors = _.map(this.options.editors, function (Editor) {
return new Editor({
parentElement: this.$('.modal-section .general-settings'),
model: this.model,
xblockType: this.options.xblockType,
enable_proctored_exams: this.options.enable_proctored_exams,
enable_timed_exams: this.options.enable_timed_exams
});
}, this);
this.options.special_exam_editors = _.map(special_exams_editors, function (Editor) {
return new Editor({
parentElement: this.$('.modal-section .advanced-settings'),
model: this.model,
xblockType: this.options.xblockType,
enable_proctored_exams: this.options.enable_proctored_exams,
enable_timed_exams: this.options.enable_timed_exams
});
}, this);
this.hideAdvancedSettings();
} else {
CourseOutlineXBlockModal.prototype.initializeEditors.call(this);
}
},
events: {
'click .action-save': 'save',
'click .general-settings-button': 'showGeneralSettings',
'click .advanced-settings-button': 'showAdvancedSettings'
},
/**
* Return request data.
* @return {Object}
*/
getRequestData: function () {
var combined_editors = this.options.editors.concat(this.options.special_exam_editors);
var requestData = _.map(combined_editors, function (editor) {
return editor.getRequestData();
});
return $.extend.apply(this, [true, {}].concat(requestData));
},
hideAdvancedSettings: function() {
this.$('.modal-section .general-settings-button').addClass('active');
this.$('.modal-section .advanced-settings-button').removeClass('active');
this.$('.modal-section .general-settings').show();
this.$('.modal-section .advanced-settings').hide();
},
hideGeneralSettings: function() {
this.$('.modal-section .general-settings-button').removeClass('active');
this.$('.modal-section .advanced-settings-button').addClass('active');
this.$('.modal-section .general-settings').hide();
this.$('.modal-section .advanced-settings').show();
},
showGeneralSettings: function (event) {
event.preventDefault();
this.hideAdvancedSettings();
},
showAdvancedSettings: function (event) {
event.preventDefault();
this.hideGeneralSettings();
}
});
......@@ -267,20 +341,40 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
className: 'edit-settings-timed-examination',
events : {
'change #id_not_timed': 'notTimedExam',
'change #id_timed_exam': 'showTimeLimit',
'change #id_practice_exam': 'showTimeLimit',
'change #id_proctored_exam': 'showTimeLimit',
'change #id_timed_exam': 'setTimedExam',
'change #id_practice_exam': 'setPracticeExam',
'change #id_proctored_exam': 'setProctoredExam',
'focusout #id_time_limit': 'timeLimitFocusout'
},
notTimedExam: function (event) {
event.preventDefault();
this.$('#id_time_limit_div').hide();
this.$('.exam-review-rules-list-fields').hide();
this.$('#id_time_limit').val('00:00');
},
showTimeLimit: function (event) {
event.preventDefault();
selectSpecialExam: function (showRulesField) {
this.$('#id_time_limit_div').show();
this.$('#id_time_limit').val("00:30");
if (!this.isValidTimeLimit(this.$('#id_time_limit').val())) {
this.$('#id_time_limit').val('00:30');
}
if (showRulesField) {
this.$('.exam-review-rules-list-fields').show();
}
else {
this.$('.exam-review-rules-list-fields').hide();
}
},
setTimedExam: function (event) {
event.preventDefault();
this.selectSpecialExam(false);
},
setPracticeExam: function (event) {
event.preventDefault();
this.selectSpecialExam(false);
},
setProctoredExam: function (event) {
event.preventDefault();
this.selectSpecialExam(true);
},
timeLimitFocusout: function(event) {
event.preventDefault();
......@@ -301,6 +395,8 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
this.setExamType(this.model.get('is_time_limited'), this.model.get('is_proctored_exam'),
this.model.get('is_practice_exam'));
this.setExamTime(this.model.get('default_time_limit_minutes'));
this.setReviewRules(this.model.get('exam_review_rules'));
},
setExamType: function(is_time_limited, is_proctored_exam, is_practice_exam) {
if (!is_time_limited) {
......@@ -309,12 +405,14 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
this.$('#id_time_limit_div').show();
this.$('.exam-review-rules-list-fields').hide();
if (this.options.enable_proctored_exams && is_proctored_exam) {
if (is_practice_exam) {
this.$('#id_practice_exam').prop('checked', true);
} else {
this.$('#id_proctored_exam').prop('checked', true);
this.$('.exam-review-rules-list-fields').show();
}
} else {
// Since we have an early exit at the top of the method
......@@ -327,6 +425,9 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var time = this.convertTimeLimitMinutesToString(value);
this.$('#id_time_limit').val(time);
},
setReviewRules: function (value) {
this.$('#id_exam_review_rules').val(value);
},
isValidTimeLimit: function(time_limit) {
var pattern = new RegExp('^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$');
return pattern.test(time_limit) && time_limit !== "00:00";
......@@ -351,6 +452,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var is_practice_exam;
var is_proctored_exam;
var time_limit = this.getExamTimeLimit();
var exam_review_rules = this.$('#id_exam_review_rules').val();
if (this.$("#id_not_timed").is(':checked')){
is_time_limited = false;
......@@ -374,6 +476,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
metadata: {
'is_practice_exam': is_practice_exam,
'is_time_limited': is_time_limited,
'exam_review_rules': exam_review_rules,
// We have to use the legacy field name
// as the Ajax handler directly populates
// the xBlocks fields. We will have to
......@@ -584,6 +687,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
getEditModal: function (xblockInfo, options) {
var editors = [];
var special_exam_editors = [];
if (xblockInfo.isChapter()) {
editors = [ReleaseDateEditor, StaffLockEditor];
......@@ -592,7 +696,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
var enable_special_exams = (options.enable_proctored_exams || options.enable_timed_exams);
if (enable_special_exams) {
editors.push(TimedExaminationPreferenceEditor);
special_exam_editors.push(TimedExaminationPreferenceEditor);
}
editors.push(StaffLockEditor);
......@@ -610,6 +714,7 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
}
return new SettingsXBlockModal($.extend({
editors: editors,
special_exam_editors: special_exam_editors,
model: xblockInfo
}, options));
},
......
......@@ -99,6 +99,37 @@
&:last-child {
margin-bottom: 0;
}
.settings-tab {
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l3;
li.settings-section {
display: inline-block;
margin-right: $baseline;
.general-settings-button,
.advanced-settings-button {
@extend %t-copy-sub1;
@extend %t-regular;
background-image: none;
background-color: $white;
color: $mediumGrey;
border-radius: 0;
box-shadow: none;
border: 0;
padding: ($baseline/4) ($baseline/2);
text-transform: uppercase;
&:hover {
background-color: $white;
color: $blue;
}
&.active {
border-bottom: 4px solid $blue-d2;
color: $offBlack;
}
}
}
}
}
.modal-section-title {
......@@ -528,7 +559,8 @@
.wrapper-modal-window-bulkpublish-subsection,
.wrapper-modal-window-bulkpublish-unit,
.course-outline-modal {
.exam-time-list-fields {
.exam-time-list-fields,
.exam-review-rules-list-fields {
margin: 0 0 ($baseline/2) ($baseline/2);
}
.list-fields {
......
......@@ -22,7 +22,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor']:
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'settings-tab-section']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
......
<%
var enable_proctored_exams = enable_proctored_exams;
var enable_timed_exams = enable_timed_exams;
%>
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
<div class="message modal-introduction">
<p><%= introductionMessage %></p>
</div>
<% if (!( enable_proctored_exams || enable_timed_exams )) { %>
<div class="message modal-introduction">
<p><%- introductionMessage %></p>
</div>
<% } %>
<div class="modal-section"></div>
</div>
<ul class="settings-tab">
<li class="settings-section">
<button class="general-settings-button" href="#"><%- gettext('General Settings') %></button>
</li>
<li class="settings-section">
<button class="advanced-settings-button" href="#"><%- gettext('Advanced') %></button>
</li>
</ul>
<div class='general-settings'></div>
<div class='advanced-settings'></div>
<form>
<h3 class="modal-section-title"><%- gettext('Additional Options:') %></h3>
<div class="modal-section-content has-actions">
<div class='exam-time-list-fields'>
<ul class="list-fields list-input">
<li class="field-radio">
......@@ -22,7 +19,7 @@
<%- gettext('Timed') %>
</label>
</li>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time on per learner basis through the Instructor Dashboard.') %> </p>
<p class='field-message' id='timed-exam-description'> <%- gettext('Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the Instructor Dashboard.') %> </p>
</ul>
</div>
......@@ -35,7 +32,7 @@
<%- gettext('Proctored') %>
</label>
</li>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed, and software records video of each learner taking the exam. These videos are then reviewed by a third party.') %> </p>
<p class='field-message' id='proctored-exam-description'> <%- gettext('Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules.') %> </p>
</ul>
</div>
<div class='exam-time-list-fields'>
......@@ -46,7 +43,7 @@
<%- gettext('Practice Proctored') %>
</label>
</li>
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of the practice exam do not count towards the learner's grade.") %> </p>
<p class='field-message' id='practice-exam-description'> <%- gettext("Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner's grade.") %> </p>
</ul>
</div>
<% } %>
......@@ -61,5 +58,15 @@
<p class='field-message' id='time-limit-description'><%- gettext('Learners see warnings when 20% and 5% of the allotted time remains. You can grant learners extra time to complete the exam through the Instructor Dashboard.') %></p>
</ul>
</div>
<div class='exam-review-rules-list-fields is-hidden'>
<ul class="list-fields list-input exam-review-rules">
<li class="field field-text field-exam-review-rules">
<label for="id_exam_review_rules" class="label"><%- gettext('Review Rules') %> </label>
<textarea id="id_exam_review_rules" cols="50" maxlength="255" name="review_rules" aria-describedby="review-rules-description"
class="review-rules input input-text" autocomplete="off" />
</li>
<p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p>
</ul>
</div>
</div>
</form>
......@@ -11,7 +11,7 @@ import warnings
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Integer, Scope, Boolean
from xblock.fields import Integer, Scope, Boolean, String
from xblock.fragment import Fragment
from .exceptions import NotFoundError
......@@ -88,6 +88,15 @@ class ProctoringFields(object):
scope=Scope.settings,
)
exam_review_rules = String(
display_name=_("Software Secure Review Rules"),
help=_(
"This setting indicates what rules the proctoring team should follow when viewing the videos."
),
default='',
scope=Scope.settings,
)
is_practice_exam = Boolean(
display_name=_("Is Practice Exam"),
help=_(
......
......@@ -530,6 +530,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
self.q(css=".action-save").first.click()
self.wait_for_ajax()
def select_advanced_settings_tab(self):
"""
Select the advanced settings tab
"""
self.q(css=".advanced-settings-button").first.click()
self.wait_for_element_presence('#id_not_timed', 'Advanced settings fields not present.')
def make_exam_proctored(self):
"""
Makes a Proctored exam.
......@@ -576,6 +583,12 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
"""
return self.q(css="#id_time_limit_div").visible
def exam_review_rules_field_visible(self):
"""
Returns whether the review rules field is visible
"""
return self.q(css=".exam-review-rules-list-fields").visible
def proctoring_items_are_displayed(self):
"""
Returns True if all the items are found.
......
......@@ -211,6 +211,10 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# open the exam settings to make it a proctored exam.
self.course_outline.open_exam_settings_dialog()
# select advanced settings tab
self.course_outline.select_advanced_settings_tab()
self.course_outline.make_exam_proctored()
# login as a verified student and visit the courseware.
......@@ -233,6 +237,10 @@ class ProctoredExamsTest(BaseInstructorDashboardTest):
# open the exam settings to make it a proctored exam.
self.course_outline.open_exam_settings_dialog()
# select advanced settings tab
self.course_outline.select_advanced_settings_tab()
self.course_outline.make_exam_timed()
# login as a verified student and visit the courseware.
......
......@@ -94,7 +94,7 @@ git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils==v1.0.0
-e git+https://github.com/edx/edx-reverification-block.git@0.0.5#egg=edx-reverification-block==0.0.5
-e git+https://github.com/edx/edx-user-state-client.git@30c0ad4b9f57f8d48d6943eb585ec8a9205f4469#egg=edx-user-state-client
git+https://github.com/edx/edx-organizations.git@release-2015-12-08#egg=edx-organizations==0.2.0
git+https://github.com/edx/edx-proctoring.git@0.11.6#egg=edx-proctoring==0.11.6
git+https://github.com/edx/edx-proctoring.git@0.12.1#egg=edx-proctoring==0.12.1
git+https://github.com/edx/xblock-lti-consumer.git@v1.0.0#egg=xblock-lti-consumer==v1.0.0
# Third Party XBlocks
......
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