From 53cf5638b6b48155aba80f5c5b589103d29b215b Mon Sep 17 00:00:00 2001
From: Zia Fazal <zia.fazal@arbisoft.com>
Date: Thu, 5 Apr 2018 17:59:26 +0500
Subject: [PATCH] added radio input fields to pre-requisite selection

Initial changes to gate section based on completion percentage

code refactoring and added evaluation of completion milestone

Fixed broken unit tests and added new tests

Fixed broken tests and quality violations

Fixed Pep8 violation

Fixed eslint quality violations

Test changes as suggested by reviewer

changes after feedbacy from reviewer

Update the docstring with suggested changes

excluding chapter from the blocks

Disallow empty values for min score and min completion

Changes afte feedback from UX/Accessibility

removed blank line
---
 .../contentstore/signals/handlers.py          |   2 +-
 .../contentstore/tests/test_gating.py         |  10 +-
 cms/djangoapps/contentstore/views/item.py     |  16 ++-
 .../contentstore/views/tests/test_gating.py   |  26 +++-
 .../spec/views/pages/course_outline_spec.js   |  95 +++++++++++--
 .../js/views/modals/course_outline_modals.js  |  52 ++++---
 cms/static/sass/elements/_modal-window.scss   |   8 ++
 cms/templates/js/access-editor.underscore     |  21 ++-
 .../test/acceptance/pages/studio/overview.py  |   3 +-
 .../acceptance/tests/lms/test_lms_gating.py   |   2 +-
 .../transformers/tests/test_milestones.py     |  24 ++--
 lms/djangoapps/gating/api.py                  |  13 +-
 lms/djangoapps/gating/signals.py              |  16 +++
 lms/djangoapps/gating/tasks.py                |  64 +++++++++
 lms/djangoapps/gating/tests/test_api.py       |  46 ++++---
 .../gating/tests/test_integration.py          |  42 +++---
 lms/templates/_gated_content.html             |   2 +-
 openedx/core/lib/gating/api.py                | 129 ++++++++++++++----
 openedx/core/lib/gating/tests/test_api.py     |  52 ++++---
 .../tests/views/test_course_outline.py        |   4 +-
 20 files changed, 471 insertions(+), 156 deletions(-)
 create mode 100644 lms/djangoapps/gating/tasks.py

diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py
index f32cf63fb25..7330305b3bb 100644
--- a/cms/djangoapps/contentstore/signals/handlers.py
+++ b/cms/djangoapps/contentstore/signals/handlers.py
@@ -102,7 +102,7 @@ def handle_item_deleted(**kwargs):
             # Remove prerequisite milestone data
             gating_api.remove_prerequisite(module.location)
             # Remove any 'requires' course content milestone relationships
-            gating_api.set_required_content(course_key, module.location, None, None)
+            gating_api.set_required_content(course_key, module.location, None, None, None)
 
 
 @receiver(GRADING_POLICY_CHANGED)
diff --git a/cms/djangoapps/contentstore/tests/test_gating.py b/cms/djangoapps/contentstore/tests/test_gating.py
index b488cd31d80..e61da3ace53 100644
--- a/cms/djangoapps/contentstore/tests/test_gating.py
+++ b/cms/djangoapps/contentstore/tests/test_gating.py
@@ -41,7 +41,7 @@ class TestHandleItemDeleted(ModuleStoreTestCase, MilestonesTestCaseMixin):
             display_name="Gated Sequential"
         )
         gating_api.add_prerequisite(self.course.id, self.open_seq.location)
-        gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100)
+        gating_api.set_required_content(self.course.id, self.gated_seq.location, self.open_seq.location, 100, 100)
 
     @patch('contentstore.signals.handlers.gating_api.set_required_content')
     @patch('contentstore.signals.handlers.gating_api.remove_prerequisite')
@@ -49,7 +49,9 @@ class TestHandleItemDeleted(ModuleStoreTestCase, MilestonesTestCaseMixin):
         """ Test gating milestone data is cleanup up when course content item is deleted """
         handle_item_deleted(usage_key=self.chapter.location, user_id=0)
         mock_remove_prereq.assert_called_with(self.open_seq.location)
-        mock_set_required.assert_called_with(self.open_seq.location.course_key, self.open_seq.location, None, None)
+        mock_set_required.assert_called_with(
+            self.open_seq.location.course_key, self.open_seq.location, None, None, None
+        )
 
     @patch('contentstore.signals.handlers.gating_api.set_required_content')
     @patch('contentstore.signals.handlers.gating_api.remove_prerequisite')
@@ -57,4 +59,6 @@ class TestHandleItemDeleted(ModuleStoreTestCase, MilestonesTestCaseMixin):
         """ Test gating milestone data is cleanup up when course content item is deleted """
         handle_item_deleted(usage_key=self.open_seq.location, user_id=0)
         mock_remove_prereq.assert_called_with(self.open_seq.location)
-        mock_set_required.assert_called_with(self.open_seq.location.course_key, self.open_seq.location, None, None)
+        mock_set_required.assert_called_with(
+            self.open_seq.location.course_key, self.open_seq.location, None, None, None
+        )
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 1f22a5b77b0..5baf93c53ad 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -135,7 +135,9 @@ def xblock_handler(request, usage_key_string):
                 :isPrereq: Set this xblock as a prerequisite which can be used to limit access to other xblocks
                 :prereqUsageKey: Use the xblock identified by this usage key to limit access to this xblock
                 :prereqMinScore: The minimum score that needs to be achieved on the prerequisite xblock
-                        identifed by prereqUsageKey
+                        identifed by prereqUsageKey. Ranging from 0 to 100.
+                :prereqMinCompletion: The minimum completion percentage that needs to be achieved on the
+                        prerequisite xblock identifed by prereqUsageKey. Ranging from 0 to 100.
                 :publish: can be:
                   'make_public': publish the content
                   'republish': publish this item *only* if it was previously published
@@ -199,6 +201,7 @@ def xblock_handler(request, usage_key_string):
                 is_prereq=request.json.get('isPrereq'),
                 prereq_usage_key=request.json.get('prereqUsageKey'),
                 prereq_min_score=request.json.get('prereqMinScore'),
+                prereq_min_completion=request.json.get('prereqMinCompletion'),
                 publish=request.json.get('publish'),
                 fields=request.json.get('fields'),
             )
@@ -480,7 +483,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
 
 def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None, nullout=None,
                  grader_type=None, is_prereq=None, prereq_usage_key=None, prereq_min_score=None,
-                 publish=None, fields=None):
+                 prereq_min_completion=None, publish=None, fields=None):
     """
     Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
     nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
@@ -621,7 +624,11 @@ def _save_xblock(user, xblock, data=None, children_strings=None, metadata=None,
 
             if prereq_usage_key is not None:
                 gating_api.set_required_content(
-                    xblock.location.course_key, xblock.location, prereq_usage_key, prereq_min_score
+                    xblock.location.course_key,
+                    xblock.location,
+                    prereq_usage_key,
+                    prereq_min_score,
+                    prereq_min_completion
                 )
 
         # If publish is set to 'republish' and this item is not in direct only categories and has previously been
@@ -1050,12 +1057,13 @@ def _get_gating_info(course, xblock):
         info["prereqs"] = [
             p for p in course.gating_prerequisites if unicode(xblock.location) not in p['namespace']
         ]
-        prereq, prereq_min_score = gating_api.get_required_content(
+        prereq, prereq_min_score, prereq_min_completion = gating_api.get_required_content(
             course.id,
             xblock.location
         )
         info["prereq"] = prereq
         info["prereq_min_score"] = prereq_min_score
+        info["prereq_min_completion"] = prereq_min_completion
         if prereq:
             info["visibility_state"] = VisibilityState.gated
     return info
diff --git a/cms/djangoapps/contentstore/views/tests/test_gating.py b/cms/djangoapps/contentstore/views/tests/test_gating.py
index ebbbf344404..887e56f8eb4 100644
--- a/cms/djangoapps/contentstore/views/tests/test_gating.py
+++ b/cms/djangoapps/contentstore/views/tests/test_gating.py
@@ -1,6 +1,7 @@
 """
 Unit tests for the gating feature in Studio
 """
+import ddt
 import json
 
 from mock import patch
@@ -13,6 +14,7 @@ from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
 from xmodule.modulestore.tests.factories import ItemFactory
 
 
+@ddt.ddt
 class TestSubsectionGating(CourseTestCase):
     """
     Tests for the subsection gating feature
@@ -84,12 +86,13 @@ class TestSubsectionGating(CourseTestCase):
 
         self.client.ajax_post(
             self.seq2_url,
-            data={'prereqUsageKey': unicode(self.seq1.location), 'prereqMinScore': '100'}
+            data={'prereqUsageKey': unicode(self.seq1.location), 'prereqMinScore': '100', 'prereqMinCompletion': '100'}
         )
         mock_set_required_content.assert_called_with(
             self.course.id,
             self.seq2.location,
             unicode(self.seq1.location),
+            '100',
             '100'
         )
 
@@ -101,21 +104,31 @@ class TestSubsectionGating(CourseTestCase):
 
         self.client.ajax_post(
             self.seq2_url,
-            data={'prereqUsageKey': '', 'prereqMinScore': ''}
+            data={'prereqUsageKey': '', 'prereqMinScore': '', 'prereqMinCompletion': ''}
         )
         mock_set_required_content.assert_called_with(
             self.course.id,
             self.seq2.location,
             '',
+            '',
             ''
         )
 
     @patch('contentstore.views.item.gating_api.get_prerequisites')
     @patch('contentstore.views.item.gating_api.get_required_content')
     @patch('contentstore.views.item.gating_api.is_prerequisite')
-    def test_get_prerequisite(self, mock_is_prereq, mock_get_required_content, mock_get_prereqs):
+    @ddt.data(
+        (90, None),
+        (None, 90),
+        (100, 100),
+    )
+    @ddt.unpack
+    def test_get_prerequisite(
+            self, min_score, min_completion,
+            mock_is_prereq, mock_get_required_content, mock_get_prereqs
+    ):
         mock_is_prereq.return_value = True
-        mock_get_required_content.return_value = unicode(self.seq1.location), 100
+        mock_get_required_content.return_value = unicode(self.seq1.location), min_score, min_completion
         mock_get_prereqs.return_value = [
             {'namespace': '{}{}'.format(unicode(self.seq1.location), GATING_NAMESPACE_QUALIFIER)},
             {'namespace': '{}{}'.format(unicode(self.seq2.location), GATING_NAMESPACE_QUALIFIER)}
@@ -126,7 +139,8 @@ class TestSubsectionGating(CourseTestCase):
         mock_get_prereqs.assert_called_with(self.course.id)
         self.assertTrue(resp['is_prereq'])
         self.assertEqual(resp['prereq'], unicode(self.seq1.location))
-        self.assertEqual(resp['prereq_min_score'], 100)
+        self.assertEqual(resp['prereq_min_score'], min_score)
+        self.assertEqual(resp['prereq_min_completion'], min_completion)
         self.assertEqual(resp['visibility_state'], VisibilityState.gated)
 
     @patch('contentstore.signals.handlers.gating_api.set_required_content')
@@ -139,4 +153,4 @@ class TestSubsectionGating(CourseTestCase):
         )
         self.client.delete(reverse_usage_url('xblock_handler', seq3.location))
         mock_remove_prereq.assert_called_with(seq3.location)
-        mock_set_required.assert_called_with(seq3.location.course_key, seq3.location, None, None)
+        mock_set_required.assert_called_with(seq3.location.course_key, seq3.location, None, None, None)
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 95dc045faa1..bb9145e1794 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -80,6 +80,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     prereqs: [],
                     prereq: '',
                     prereq_min_score: '',
+                    prereq_min_completion: '',
                     show_correctness: 'always',
                     child_info: {
                         category: 'vertical',
@@ -1023,9 +1024,10 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     this.$('#is_prereq').prop('checked', true).trigger('change');
                 };
 
-                selectLastPrerequisiteSubsection = function(minScore) {
+                selectLastPrerequisiteSubsection = function(minScore, minCompletion) {
                     this.$('#prereq option:last').prop('selected', true).trigger('change');
                     this.$('#prereq_min_score').val(minScore).trigger('keyup');
+                    this.$('#prereq_min_completion').val(minCompletion).trigger('keyup');
                 };
 
                 // Helper to validate oft-checked additional option fields' visibility
@@ -1045,6 +1047,7 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     delete mockSubsectionJSON.prereqs;
                     delete mockSubsectionJSON.prereq;
                     delete mockSubsectionJSON.prereq_min_score;
+                    delete mockSubsectionJSON.prereq_min_completion;
                     return createMockCourseJSON({
                         enable_proctored_exams: false,
                         enable_timed_exams: false
@@ -1611,10 +1614,12 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     ]);
                     createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
                     outlinePage.$('.outline-subsection .configure-button').click();
-                    selectLastPrerequisiteSubsection('80');
+                    selectLastPrerequisiteSubsection('80', '0');
                     expect($('#prereq_min_score_input').css('display')).not.toBe('none');
                     expect($('#prereq option:selected').val()).toBe('usage_key');
                     expect($('#prereq_min_score').val()).toBe('80');
+                    expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion').val()).toBe('0');
                     $('.wrapper-modal-window .action-save').click();
                 });
 
@@ -1641,7 +1646,8 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                             createMockSubsectionJSON({
                                 prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
                                 prereq: 'usage_key',
-                                prereq_min_score: '80'
+                                prereq_min_score: '80',
+                                prereq_min_completion: '50'
                             }, [])
                         ])
                     ]);
@@ -1651,9 +1657,32 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     expect($('#prereq option:selected').val()).toBe('usage_key');
                     expect($('#prereq_min_score_input').css('display')).not.toBe('none');
                     expect($('#prereq_min_score').val()).toBe('80');
+                    expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion').val()).toBe('50');
                 });
 
-                it('can display validation error on non-integer minimum score', function() {
+                it('can show a saved prerequisite subsection with empty min score correctly', function() {
+                    var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
+                        createMockSectionJSON({}, [
+                            createMockSubsectionJSON({
+                                prereqs: [{block_usage_key: 'usage_key', block_display_name: 'Prereq Subsection 1'}],
+                                prereq: 'usage_key',
+                                prereq_min_score: '',
+                                prereq_min_completion: '50'
+                            }, [])
+                        ])
+                    ]);
+                    createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
+                    outlinePage.$('.outline-subsection .configure-button').click();
+                    expect($('.gating-prereq').length).toBe(1);
+                    expect($('#prereq option:selected').val()).toBe('usage_key');
+                    expect($('#prereq_min_score_input').css('display')).not.toBe('none');
+                    expect($('#prereq_min_score').val()).toBe('100');
+                    expect($('#prereq_min_completion_input').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion').val()).toBe('50');
+                });
+
+                it('can display validation error on non-integer or empty minimum score', function() {
                     var mockCourseWithPreqsJSON = createMockCourseJSON({}, [
                         createMockSectionJSON({}, [
                             createMockSubsectionJSON({
@@ -1663,12 +1692,39 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     ]);
                     createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
                     outlinePage.$('.outline-subsection .configure-button').click();
-                    selectLastPrerequisiteSubsection('abc');
+                    selectLastPrerequisiteSubsection('', '50');
+                    expect($('#prereq_min_score_error').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('50', '');
+                    expect($('#prereq_min_score_error').css('display')).toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).not.toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('', '');
+                    expect($('#prereq_min_score_error').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).not.toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('abc', '50');
                     expect($('#prereq_min_score_error').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
                     expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
                     expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
-                    selectLastPrerequisiteSubsection('5.5');
+                    selectLastPrerequisiteSubsection('50', 'abc');
+                    expect($('#prereq_min_score_error').css('display')).toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).not.toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('5.5', '50');
                     expect($('#prereq_min_score_error').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('50', '5.5');
+                    expect($('#prereq_min_score_error').css('display')).toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).not.toBe('none');
                     expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
                     expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
                 });
@@ -1683,12 +1739,24 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     ]);
                     createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
                     outlinePage.$('.outline-subsection .configure-button').click();
-                    selectLastPrerequisiteSubsection('-5');
+                    selectLastPrerequisiteSubsection('-5', '50');
                     expect($('#prereq_min_score_error').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('50', '-5');
+                    expect($('#prereq_min_score_error').css('display')).toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).not.toBe('none');
                     expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
                     expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
-                    selectLastPrerequisiteSubsection('105');
+                    selectLastPrerequisiteSubsection('105', '50');
                     expect($('#prereq_min_score_error').css('display')).not.toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
+                    expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
+                    expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
+                    selectLastPrerequisiteSubsection('50', '105');
+                    expect($('#prereq_min_score_error').css('display')).toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).not.toBe('none');
                     expect($('.wrapper-modal-window .action-save').prop('disabled')).toBe(true);
                     expect($('.wrapper-modal-window .action-save').hasClass('is-disabled')).toBe(true);
                 });
@@ -1704,14 +1772,15 @@ define(['jquery', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/j
                     createCourseOutlinePage(this, mockCourseWithPreqsJSON, false);
                     outlinePage.$('.outline-subsection .configure-button').click();
                     selectAdvancedSettings();
-                    selectLastPrerequisiteSubsection('');
-                    expect($('#prereq_min_score_error').css('display')).toBe('none');
-                    selectLastPrerequisiteSubsection('80');
+                    selectLastPrerequisiteSubsection('80', '50');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
                     expect($('#prereq_min_score_error').css('display')).toBe('none');
-                    selectLastPrerequisiteSubsection('0');
+                    selectLastPrerequisiteSubsection('0', '0');
                     expect($('#prereq_min_score_error').css('display')).toBe('none');
-                    selectLastPrerequisiteSubsection('100');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
+                    selectLastPrerequisiteSubsection('100', '100');
                     expect($('#prereq_min_score_error').css('display')).toBe('none');
+                    expect($('#prereq_min_completion_error').css('display')).toBe('none');
                 });
 
                 it('release date, due date, grading type, and staff lock can be cleared.', function() {
diff --git a/cms/static/js/views/modals/course_outline_modals.js b/cms/static/js/views/modals/course_outline_modals.js
index 67140eec54c..6bff80e9772 100644
--- a/cms/static/js/views/modals/course_outline_modals.js
+++ b/cms/static/js/views/modals/course_outline_modals.js
@@ -565,50 +565,68 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
         className: 'edit-settings-access',
         events: {
             'change #prereq': 'handlePrereqSelect',
-            'keyup #prereq_min_score': 'validateMinScore'
+            'keyup #prereq_min_completion': 'validateScoreAndCompletion',
+            'keyup #prereq_min_score': 'validateScoreAndCompletion'
         },
         afterRender: function() {
             AbstractEditor.prototype.afterRender.call(this);
             var prereq = this.model.get('prereq') || '';
-            var prereq_min_score = this.model.get('prereq_min_score') || '';
+            var prereqMinScore = this.model.get('prereq_min_score') || '100';
+            var prereqMinCompletion = this.model.get('prereq_min_completion') || '100';
             this.$('#is_prereq').prop('checked', this.model.get('is_prereq'));
             this.$('#prereq option[value="' + prereq + '"]').prop('selected', true);
-            this.$('#prereq_min_score').val(prereq_min_score);
+            this.$('#prereq_min_score').val(prereqMinScore);
             this.$('#prereq_min_score_input').toggle(prereq.length > 0);
+            this.$('#prereq_min_completion').val(prereqMinCompletion);
+            this.$('#prereq_min_completion_input').toggle(prereq.length > 0);
         },
         handlePrereqSelect: function() {
             var showPrereqInput = this.$('#prereq option:selected').val().length > 0;
             this.$('#prereq_min_score_input').toggle(showPrereqInput);
+            this.$('#prereq_min_completion_input').toggle(showPrereqInput);
         },
-        validateMinScore: function() {
+        isValidPercentage: function(val) {
+            var intVal = parseInt(val, 10);
+            return (typeof val !== 'undefined' && val !== '' && intVal >= 0 && intVal <= 100 && String(intVal) === val);
+        },
+        validateScoreAndCompletion: function() {
+            var invalidInput = false;
             var minScore = this.$('#prereq_min_score').val().trim();
-            var minScoreInt = parseInt(minScore);
-            // minScore needs to be an integer between 0 and 100
-            if (
-                minScore &&
-                (
-                    typeof(minScoreInt) === 'undefined' ||
-                    String(minScoreInt) !== minScore ||
-                    minScoreInt < 0 ||
-                    minScoreInt > 100
-                )
-            ) {
+            var minCompletion = this.$('#prereq_min_completion').val().trim();
+
+            if (minScore === '' || !this.isValidPercentage(minScore)) {
+                invalidInput = true;
                 this.$('#prereq_min_score_error').show();
-                BaseModal.prototype.disableActionButton.call(this.parent, 'save');
             } else {
                 this.$('#prereq_min_score_error').hide();
+            }
+            if (minCompletion === '' || !this.isValidPercentage(minCompletion)) {
+                invalidInput = true;
+                this.$('#prereq_min_completion_error').show();
+            } else {
+                this.$('#prereq_min_completion_error').hide();
+            }
+            if (invalidInput) {
+                BaseModal.prototype.disableActionButton.call(this.parent, 'save');
+            } else {
                 BaseModal.prototype.enableActionButton.call(this.parent, 'save');
             }
         },
         getRequestData: function() {
             var minScore = this.$('#prereq_min_score').val();
+            var minCompletion = this.$('#prereq_min_completion').val();
             if (minScore) {
                 minScore = minScore.trim();
             }
+            if (minCompletion) {
+                minCompletion = minCompletion.trim();
+            }
+
             return {
                 isPrereq: this.$('#is_prereq').is(':checked'),
                 prereqUsageKey: this.$('#prereq option:selected').val(),
-                prereqMinScore: minScore
+                prereqMinScore: minScore,
+                prereqMinCompletion: minCompletion
             };
         }
     });
diff --git a/cms/static/sass/elements/_modal-window.scss b/cms/static/sass/elements/_modal-window.scss
index 097a45a74ff..b683518f6ab 100644
--- a/cms/static/sass/elements/_modal-window.scss
+++ b/cms/static/sass/elements/_modal-window.scss
@@ -674,6 +674,11 @@
           width: ($baseline*7);
         }
 
+        input.percentage {
+          display: inline-block;
+          width: ($baseline*3);
+        }
+
         .tip {
           @extend %t-copy-sub1;
 
@@ -810,6 +815,9 @@
     .edit-settings-access {
       .gating-prereq {
         margin-bottom: 10px;
+        .list-fields .field {
+          display: block;
+        }
       }
     }
   }
diff --git a/cms/templates/js/access-editor.underscore b/cms/templates/js/access-editor.underscore
index 7425a0e0179..dfab0c5bda9 100644
--- a/cms/templates/js/access-editor.underscore
+++ b/cms/templates/js/access-editor.underscore
@@ -4,7 +4,7 @@
         <div class="modal-section-content gating-prereq">
             <ul class="list-fields list-input">
                 <p class="field-message">
-                    <%- gettext('Select a prerequisite subsection and enter a minimum score percentage to limit access to this subsection.') %>
+                    <%- gettext('Select a prerequisite subsection and enter a minimum score percentage and minimum completion percentage to limit access to this subsection. Allowed values are 0-100') %>
                 </p>
                 <li class="field field-select">
                     <label class="label">
@@ -18,14 +18,25 @@
                     </label>
                 </li>
                 <li id="prereq_min_score_input" class="field field-input input-cosmetic">
-                    <label class="label">
+                    <label for="prereq_min_score">
                         <%- gettext('Minimum Score:') %>
-                        <input type="text" id="prereq_min_score" name="prereq_min_score" class="input input-text" size="3" />
                     </label>
+                    <input type="number" id="prereq_min_score" aria-describedby="msp" class="input input-text percentage" min="0" max="100" />
+                    <span id="msp">%</span>
+                </li>
+                <li id="prereq_min_completion_input" class="field field-input input-cosmetic">
+                    <label for="prereq_min_completion">
+                        <%- gettext('Minimum Completion:') %>
+                    </label>
+                    <input type="number" id="prereq_min_completion" aria-describedby="mcp" class="input input-text percentage" min="0" max="100" />
+                    <span id="mcp">%</span>
                 </li>
-                <div id="prereq_min_score_error" class="message-status error">
+                <li id="prereq_min_score_error" class="message-status error">
                     <%- gettext('The minimum score percentage must be a whole number between 0 and 100.') %>
-                </div>
+                </li>
+                <li id="prereq_min_completion_error" class="message-status error">
+                    <%- gettext('The minimum completion percentage must be a whole number between 0 and 100.') %>
+                </li>
             </ul>
         </div>
     <% } %>
diff --git a/common/test/acceptance/pages/studio/overview.py b/common/test/acceptance/pages/studio/overview.py
index c190a2692c9..d03609b42b7 100644
--- a/common/test/acceptance/pages/studio/overview.py
+++ b/common/test/acceptance/pages/studio/overview.py
@@ -720,12 +720,13 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
         self.q(css=".action-save").first.click()
         self.wait_for_ajax()
 
-    def add_prerequisite_to_subsection(self, min_score):
+    def add_prerequisite_to_subsection(self, min_score, min_completion):
         """
         Adds a prerequisite to a subsection.
         """
         Select(self.q(css="#prereq")[0]).select_by_index(1)
         self.q(css="#prereq_min_score").fill(min_score)
+        self.q(css="#prereq_min_completion").fill(min_completion)
         self.q(css=".action-save").first.click()
         self.wait_for_ajax()
 
diff --git a/common/test/acceptance/tests/lms/test_lms_gating.py b/common/test/acceptance/tests/lms/test_lms_gating.py
index 51a389c303b..b90d6e7b960 100644
--- a/common/test/acceptance/tests/lms/test_lms_gating.py
+++ b/common/test/acceptance/tests/lms/test_lms_gating.py
@@ -106,7 +106,7 @@ class GatingTest(UniqueCourseTest):
         self.studio_course_outline.visit()
         self.studio_course_outline.open_subsection_settings_dialog(1)
         self.studio_course_outline.select_advanced_tab(desired_item='gated_content')
-        self.studio_course_outline.add_prerequisite_to_subsection("80")
+        self.studio_course_outline.add_prerequisite_to_subsection("80", "")
 
     def _fulfill_prerequisite(self):
         """
diff --git a/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py b/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
index 0a589790b7f..7366cd36091 100644
--- a/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
+++ b/lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py
@@ -49,7 +49,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
             gating_block: The block that must be completed before access is granted
         """
         gating_api.add_prerequisite(self.course.id, unicode(gating_block.location))
-        gating_api.set_required_content(self.course.id, gated_block.location, gating_block.location, 100)
+        gating_api.set_required_content(self.course.id, gated_block.location, gating_block.location, 100, 0)
 
     ALL_BLOCKS = (
         'course', 'A', 'B', 'C', 'ProctoredExam', 'D', 'E', 'PracticeExam', 'F', 'G', 'H', 'I', 'TimedExam', 'J', 'K'
@@ -170,12 +170,11 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
         self.clear_caches()
 
         # this call triggers reevaluation of prerequisites fulfilled by the gating block.
-        with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)):
-            lms_gating_api.evaluate_prerequisite(
-                self.course,
-                Mock(location=self.blocks[gating_block_ref].location),
-                self.user,
-            )
+        lms_gating_api.evaluate_prerequisite(
+            self.course,
+            Mock(location=self.blocks[gating_block_ref].location, percent_graded=1.0),
+            self.user,
+        )
 
         with self.assertNumQueries(6):
             self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
@@ -205,12 +204,11 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
         self.clear_caches()
 
         # this call triggers reevaluation of prerequisites fulfilled by the gating block.
-        with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)):
-            lms_gating_api.evaluate_prerequisite(
-                self.course,
-                Mock(location=self.blocks['A'].location),
-                self.user,
-            )
+        lms_gating_api.evaluate_prerequisite(
+            self.course,
+            Mock(location=self.blocks['A'].location, percent_graded=1.0),
+            self.user,
+        )
         self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS)
 
     def get_blocks_and_check_against_expected(self, user, expected_blocks):
diff --git a/lms/djangoapps/gating/api.py b/lms/djangoapps/gating/api.py
index 84ce71eefec..5155310a06e 100644
--- a/lms/djangoapps/gating/api.py
+++ b/lms/djangoapps/gating/api.py
@@ -18,9 +18,9 @@ log = logging.getLogger(__name__)
 def evaluate_prerequisite(course, subsection_grade, user):
     """
     Evaluates any gating milestone relationships attached to the given
-    subsection. If the subsection_grade meets the minimum score required
-    by dependent subsections, the related milestone will be marked
-    fulfilled for the user.
+    subsection. If the subsection_grade and subsection_completion meets
+    the minimum score required by dependent subsections, the related
+    milestone will be marked fulfilled for the user.
     """
     prereq_milestone = gating_api.get_gating_milestone(course.id, subsection_grade.location, 'fulfills')
     if prereq_milestone:
@@ -30,8 +30,13 @@ def evaluate_prerequisite(course, subsection_grade, user):
 
         gated_content = gated_content_milestones.get(prereq_milestone['id'])
         if gated_content:
+            grade_percentage = subsection_grade.percent_graded * 100.0 \
+                if hasattr(subsection_grade, 'percent_graded') else None
+
             for milestone in gated_content:
-                gating_api.update_milestone(milestone, subsection_grade, prereq_milestone, user.id)
+                gating_api.update_milestone(
+                    milestone, subsection_grade.location, prereq_milestone, user, grade_percentage
+                )
 
 
 def evaluate_entrance_exam(course_grade, user):
diff --git a/lms/djangoapps/gating/signals.py b/lms/djangoapps/gating/signals.py
index f05dd33b2de..f6896cf61ac 100644
--- a/lms/djangoapps/gating/signals.py
+++ b/lms/djangoapps/gating/signals.py
@@ -1,9 +1,12 @@
 """
 Signal handlers for the gating djangoapp
 """
+from django.db import models
 from django.dispatch import receiver
 
+from completion.models import BlockCompletion
 from gating import api as gating_api
+from gating.tasks import task_evaluate_subsection_completion_milestones
 from lms.djangoapps.grades.signals.signals import SUBSECTION_SCORE_CHANGED
 from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED
 
@@ -24,6 +27,19 @@ def evaluate_subsection_gated_milestones(**kwargs):
     gating_api.evaluate_prerequisite(kwargs['course'], subsection_grade, kwargs.get('user'))
 
 
+@receiver(models.signals.post_save, sender=BlockCompletion)
+def evaluate_subsection_completion_milestones(**kwargs):
+    """
+    Receives the BlockCompletion signal and triggers the
+    evaluation of any milestone which can be completed.
+    """
+    instance = kwargs['instance']
+    course_id = unicode(instance.course_key)
+    block_id = unicode(instance.block_key)
+    user_id = instance.user_id
+    task_evaluate_subsection_completion_milestones(course_id, block_id, user_id)
+
+
 @receiver(COURSE_GRADE_CHANGED)
 def evaluate_course_gated_milestones(**kwargs):
     """
diff --git a/lms/djangoapps/gating/tasks.py b/lms/djangoapps/gating/tasks.py
new file mode 100644
index 00000000000..6bf98bee10d
--- /dev/null
+++ b/lms/djangoapps/gating/tasks.py
@@ -0,0 +1,64 @@
+"""
+This file contains celery tasks related to course content gating.
+"""
+import logging
+
+from celery import task
+from django.contrib.auth.models import User
+
+from gating import api as gating_api
+from lms.djangoapps.course_blocks.api import get_course_blocks
+from opaque_keys.edx.keys import CourseKey, UsageKey
+from xmodule.modulestore.django import modulestore
+
+
+log = logging.getLogger(__name__)
+
+
+@task()
+def task_evaluate_subsection_completion_milestones(course_id, block_id, user_id):
+    """
+    Updates users' milestones related to completion of a subsection.
+     Args:
+        course_id(str): Course id which triggered a completion event
+        block_id(str): Id of the completed block
+        user_id(int): Id of the user who completed a block
+    """
+    store = modulestore()
+    course_key = CourseKey.from_string(course_id)
+    with store.bulk_operations(course_key):
+        course = store.get_course(course_key)
+        if not course or not course.enable_subsection_gating:
+            log.debug(
+                "Gating: ignoring evaluation of completion milestone because it disabled for course [%s]", course_id
+            )
+        else:
+            try:
+                user = User.objects.get(id=user_id)
+                course_structure = get_course_blocks(user, store.make_course_usage_key(course_key))
+                completed_block_usage_key = UsageKey.from_string(block_id).map_into_course(course.id)
+                subsection_block = _get_subsection_of_block(completed_block_usage_key, course_structure)
+                subsection = course_structure[subsection_block]
+                log.debug(
+                    "Gating: Evaluating completion milestone for subsection [%s] and user [%s]",
+                    unicode(subsection.location), user.id
+                )
+                gating_api.evaluate_prerequisite(course, subsection, user)
+            except KeyError:
+                log.error("Gating: Given prerequisite subsection [%s] not found in course structure", block_id)
+
+
+def _get_subsection_of_block(usage_key, block_structure):
+    """
+    Finds subsection of a block by recursively iterating over its parents
+    :param usage_key: key of the block
+    :param block_structure: block structure
+    :return: sequential block
+    """
+    parents = block_structure.get_parents(usage_key)
+    if parents:
+        for parent_block in parents:
+            if parent_block.block_type == 'sequential':
+                return parent_block
+            else:
+                return _get_subsection_of_block(parent_block, block_structure)
diff --git a/lms/djangoapps/gating/tests/test_api.py b/lms/djangoapps/gating/tests/test_api.py
index be601e935e8..0dfed52795f 100644
--- a/lms/djangoapps/gating/tests/test_api.py
+++ b/lms/djangoapps/gating/tests/test_api.py
@@ -68,44 +68,58 @@ class TestEvaluatePrerequisite(GatingTestCase, MilestonesTestCaseMixin):
         super(TestEvaluatePrerequisite, self).setUp()
         self.user_dict = {'id': self.user.id}
         self.prereq_milestone = None
-        self.subsection_grade = Mock(location=self.seq1.location)
+        self.subsection_grade = Mock(location=self.seq1.location, percent_graded=0.5)
 
-    def _setup_gating_milestone(self, min_score):
+    def _setup_gating_milestone(self, min_score, min_completion):
         """
         Setup a gating milestone for testing
         """
         gating_api.add_prerequisite(self.course.id, self.seq1.location)
-        gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, min_score)
+        gating_api.set_required_content(
+            self.course.id, self.seq2.location, self.seq1.location, min_score, min_completion
+        )
         self.prereq_milestone = gating_api.get_gating_milestone(self.course.id, self.seq1.location, 'fulfills')
 
-    @patch('openedx.core.lib.gating.api._get_subsection_percentage')
-    @data((50, True), (100, True), (0, False))
+    @patch('openedx.core.lib.gating.api.get_subsection_completion_percentage')
+    @data(
+        (50, 0, 50, 0, True),
+        (50, 0, 10, 0, False),
+        (0, 50, 0, 50, True),
+        (0, 50, 0, 10, False),
+        (50, 50, 50, 10, False),
+        (50, 50, 10, 50, False),
+        (50, 50, 50, 50, True),
+    )
     @unpack
-    def test_min_score_achieved(self, module_score, result, mock_score):
-        self._setup_gating_milestone(50)
-        mock_score.return_value = module_score
+    def test_min_score_achieved(
+            self, min_score, min_completion, module_score, module_completion, result, mock_completion
+    ):
+        self._setup_gating_milestone(min_score, min_completion)
+        mock_completion.return_value = module_completion
+        self.subsection_grade.percent_graded = module_score / 100.0
 
         evaluate_prerequisite(self.course, self.subsection_grade, self.user)
         self.assertEqual(milestones_api.user_has_milestone(self.user_dict, self.prereq_milestone), result)
 
-    @patch('openedx.core.lib.gating.api._get_subsection_percentage')
+    @patch('openedx.core.lib.gating.api.get_subsection_completion_percentage')
     @patch('openedx.core.lib.gating.api._get_minimum_required_percentage')
-    @data((50, False), (100, True))
+    @data((50, 50, False), (100, 50, False), (50, 100, False), (100, 100, True))
     @unpack
-    def test_invalid_min_score(self, module_score, result, mock_min_score, mock_score):
-        self._setup_gating_milestone(None)
-        mock_score.return_value = module_score
-        mock_min_score.return_value = 100
+    def test_invalid_min_score(self, module_score, module_completion, result, mock_min_score, mock_completion):
+        self._setup_gating_milestone(None, None)
+        mock_completion.return_value = module_completion
+        self.subsection_grade.percent_graded = module_score / 100.0
+        mock_min_score.return_value = 100, 100
 
         evaluate_prerequisite(self.course, self.subsection_grade, self.user)
         self.assertEqual(milestones_api.user_has_milestone(self.user_dict, self.prereq_milestone), result)
 
-    @patch('openedx.core.lib.gating.api._get_subsection_percentage')
+    @patch('openedx.core.lib.gating.api.get_subsection_grade_percentage')
     def test_no_prerequisites(self, mock_score):
         evaluate_prerequisite(self.course, self.subsection_grade, self.user)
         self.assertFalse(mock_score.called)
 
-    @patch('openedx.core.lib.gating.api._get_subsection_percentage')
+    @patch('openedx.core.lib.gating.api.get_subsection_grade_percentage')
     def test_no_gated_content(self, mock_score):
         gating_api.add_prerequisite(self.course.id, self.seq1.location)
 
diff --git a/lms/djangoapps/gating/tests/test_integration.py b/lms/djangoapps/gating/tests/test_integration.py
index 6880957b5d5..8ba2ff58c95 100644
--- a/lms/djangoapps/gating/tests/test_integration.py
+++ b/lms/djangoapps/gating/tests/test_integration.py
@@ -2,6 +2,7 @@
 Integration tests for gated content.
 """
 import ddt
+from completion import waffle as completion_waffle
 from milestones import api as milestones_api
 from milestones.tests.utils import MilestonesTestCaseMixin
 from nose.plugins.attrib import attr
@@ -32,7 +33,7 @@ class TestGatedContent(MilestonesTestCaseMixin, SharedModuleStoreTestCase):
 
     def setUp(self):
         super(TestGatedContent, self).setUp()
-        self.setup_gating_milestone(50)
+        self.setup_gating_milestone(50, 100)
         self.non_staff_user = UserFactory()
         self.staff_user = UserFactory(is_staff=True, is_superuser=True)
         self.request = get_mock_request(self.non_staff_user)
@@ -110,14 +111,16 @@ class TestGatedContent(MilestonesTestCaseMixin, SharedModuleStoreTestCase):
                 display_name='problem 3',
             )
 
-    def setup_gating_milestone(self, min_score):
+    def setup_gating_milestone(self, min_score, min_completion):
         """
         Setup a gating milestone for testing.
         Gating content: seq1 (must be fulfilled before access to seq2)
         Gated content: seq2 (requires completion of seq1 before access)
         """
         gating_api.add_prerequisite(self.course.id, str(self.seq1.location))
-        gating_api.set_required_content(self.course.id, str(self.seq2.location), str(self.seq1.location), min_score)
+        gating_api.set_required_content(
+            self.course.id, str(self.seq2.location), str(self.seq1.location), min_score, min_completion
+        )
         self.prereq_milestone = gating_api.get_gating_milestone(self.course.id, self.seq1.location, 'fulfills')
 
     def assert_access_to_gated_content(self, user):
@@ -164,29 +167,30 @@ class TestGatedContent(MilestonesTestCaseMixin, SharedModuleStoreTestCase):
         self.assert_access_to_gated_content(self.staff_user)
 
     def test_gated_content_always_in_grades(self):
-        # start with a grade from a non-gated subsection
-        answer_problem(self.course, self.request, self.prob3, 10, 10)
+        with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True):
+            # start with a grade from a non-gated subsection
+            answer_problem(self.course, self.request, self.prob3, 10, 10)
 
-        # verify gated status and overall course grade percentage
-        self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False)
-        self.assert_access_to_gated_content(self.non_staff_user)
-        self.assert_course_grade(self.non_staff_user, .33)
+            # verify gated status and overall course grade percentage
+            self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False)
+            self.assert_access_to_gated_content(self.non_staff_user)
+            self.assert_course_grade(self.non_staff_user, .33)
 
-        # fulfill the gated requirements
-        answer_problem(self.course, self.request, self.gating_prob1, 10, 10)
+            # fulfill the gated requirements
+            answer_problem(self.course, self.request, self.gating_prob1, 10, 10)
 
-        # verify gated status and overall course grade percentage
-        self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=True)
-        self.assert_access_to_gated_content(self.non_staff_user)
-        self.assert_course_grade(self.non_staff_user, .67)
+            # verify gated status and overall course grade percentage
+            self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=True)
+            self.assert_access_to_gated_content(self.non_staff_user)
+            self.assert_course_grade(self.non_staff_user, .67)
 
     @ddt.data((1, 1, True), (1, 2, True), (1, 3, False), (0, 1, False))
     @ddt.unpack
     def test_ungating_when_fulfilled(self, earned, max_possible, result):
         self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=False)
         self.assert_access_to_gated_content(self.non_staff_user)
+        with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True):
+            answer_problem(self.course, self.request, self.gating_prob1, earned, max_possible)
 
-        answer_problem(self.course, self.request, self.gating_prob1, earned, max_possible)
-
-        self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=result)
-        self.assert_access_to_gated_content(self.non_staff_user)
+            self.assert_user_has_prereq_milestone(self.non_staff_user, expected_has_milestone=result)
+            self.assert_access_to_gated_content(self.non_staff_user)
diff --git a/lms/templates/_gated_content.html b/lms/templates/_gated_content.html
index 561f59e5612..2039baec5ae 100644
--- a/lms/templates/_gated_content.html
+++ b/lms/templates/_gated_content.html
@@ -16,7 +16,7 @@ from openedx.core.djangolib.markup import Text
     </h3>
     <p>
         ${Text(_(
-            "You must earn a passing score for '{prereq_section_name}' to access this content."
+            "You must complete the prerequisites for '{prereq_section_name}' to access this content."
         )).format(prereq_section_name=prereq_section_name)}
         <p>
             <a href="${prereq_url}" class="btn btn-brand">${_("Go to Prerequisite Section")}
diff --git a/openedx/core/lib/gating/api.py b/openedx/core/lib/gating/api.py
index e93ee3d2a20..b9bc9800ecc 100644
--- a/openedx/core/lib/gating/api.py
+++ b/openedx/core/lib/gating/api.py
@@ -7,12 +7,13 @@ import logging
 from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
+
+from completion.models import BlockCompletion
 from lms.djangoapps.courseware.access import _has_access_to_course
 from lms.djangoapps.course_blocks.api import get_course_blocks
 from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactory
 from milestones import api as milestones_api
 from opaque_keys.edx.keys import UsageKey
-from opaque_keys.edx.locator import BlockUsageLocator
 from openedx.core.lib.gating.exceptions import GatingValidationError
 from util import milestones_helpers
 from xmodule.modulestore.django import modulestore
@@ -230,7 +231,7 @@ def is_prerequisite(course_key, prereq_content_key):
     ) is not None
 
 
-def set_required_content(course_key, gated_content_key, prereq_content_key, min_score):
+def set_required_content(course_key, gated_content_key, prereq_content_key, min_score='', min_completion=''):
     """
     Adds a `requires` milestone relationship for the given gated_content_key if a prerequisite
     prereq_content_key is provided. If prereq_content_key is None, removes the `requires`
@@ -241,6 +242,7 @@ def set_required_content(course_key, gated_content_key, prereq_content_key, min_
         gated_content_key (str|UsageKey): The gated content usage key
         prereq_content_key (str|UsageKey): The prerequisite content usage key
         min_score (str|int): The minimum score
+        min_completion (str|int): The minimum completion percentage
 
     Returns:
         None
@@ -254,7 +256,7 @@ def set_required_content(course_key, gated_content_key, prereq_content_key, min_
 
     if prereq_content_key:
         _validate_min_score(min_score)
-        requirements = {'min_score': min_score}
+        requirements = {'min_score': min_score, 'min_completion': min_completion}
         if not milestone:
             milestone = _get_prerequisite_milestone(prereq_content_key)
         milestones_api.add_course_content_milestone(course_key, gated_content_key, 'requires', milestone, requirements)
@@ -262,7 +264,7 @@ def set_required_content(course_key, gated_content_key, prereq_content_key, min_
 
 def get_required_content(course_key, gated_content_key):
     """
-    Returns the prerequisite content usage key and minimum score needed for fulfillment
+    Returns the prerequisite content usage key, minimum score and minimum completion percentage needed for fulfillment
     of that prerequisite for the given gated_content_key.
 
     Args:
@@ -270,16 +272,18 @@ def get_required_content(course_key, gated_content_key):
         gated_content_key (str|UsageKey): The gated content usage key
 
     Returns:
-        tuple: The prerequisite content usage key and minimum score, (None, None) if the content is not gated
+        tuple: The prerequisite content usage key, minimum score and minimum completion percentage,
+        (None, None, None) if the content is not gated
     """
     milestone = get_gating_milestone(course_key, gated_content_key, 'requires')
     if milestone:
         return (
             _get_gating_block_id(milestone),
-            milestone.get('requirements', {}).get('min_score')
+            milestone.get('requirements', {}).get('min_score', None),
+            milestone.get('requirements', {}).get('min_completion', None),
         )
     else:
-        return None, None
+        return None, None, None
 
 
 @gating_enabled(default=[])
@@ -378,41 +382,37 @@ def compute_is_prereq_met(content_id, user_id, recalc_on_unmet=False):
             'url': reverse('jump_to', kwargs={'course_id': course_key, 'location': subsection_usage_key}),
             'display_name': subsection.display_name
         }
-
-        try:
-            subsection_structure = get_course_blocks(student, subsection_usage_key)
-            if any(subsection_structure):
-                subsection_grade_factory = SubsectionGradeFactory(student, course_structure=subsection_structure)
-                if subsection_usage_key in subsection_structure:
-                    # this will force a recalcuation of the subsection grade
-                    subsection_grade = subsection_grade_factory.update(subsection_structure[subsection_usage_key], persist_grade=False)
-                    prereq_met = update_milestone(milestone, subsection_grade, milestone, user_id)
-        except ItemNotFoundError as err:
-            log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err)
+        prereq_met = update_milestone(milestone, subsection_usage_key, milestone, student)
 
     return prereq_met, prereq_meta_info
 
 
-def update_milestone(milestone, subsection_grade, prereq_milestone, user_id):
+def update_milestone(milestone, usage_key, prereq_milestone, user, grade_percentage=None, completion_percentage=None):
     """
     Updates the milestone record based on evaluation of prerequisite met.
 
     Arguments:
         milestone: The gated milestone being evaluated
-        subsection_grade: The grade of the prerequisite subsection
-        prerequisite_milestone: The gating milestone
-        user_id: The id of the user
+        usage_key: Usage key of the prerequisite subsection
+        prereq_milestone: The gating milestone
+        user: The user who has fulfilled milestone
+        grade_percentage: Grade percentage of prerequisite subsection
+        completion_percentage: Completion percentage of prerequisite subsection
 
     Returns:
         True if prerequisite has been met, False if not
     """
-    min_percentage = _get_minimum_required_percentage(milestone)
-    subsection_percentage = _get_subsection_percentage(subsection_grade)
-    if subsection_percentage >= min_percentage:
-        milestones_helpers.add_user_milestone({'id': user_id}, prereq_milestone)
+    min_score, min_completion = _get_minimum_required_percentage(milestone)
+    if not grade_percentage:
+        grade_percentage = get_subsection_grade_percentage(usage_key, user) if min_score > 0 else 0
+    if not completion_percentage:
+        completion_percentage = get_subsection_completion_percentage(usage_key, user) if min_completion > 0 else 0
+
+    if grade_percentage >= min_score and completion_percentage >= min_completion:
+        milestones_helpers.add_user_milestone({'id': user.id}, prereq_milestone)
         return True
     else:
-        milestones_helpers.remove_user_milestone({'id': user_id}, prereq_milestone)
+        milestones_helpers.remove_user_milestone({'id': user.id}, prereq_milestone)
         return False
 
 
@@ -423,12 +423,74 @@ def _get_gating_block_id(milestone):
     return milestone.get('namespace', '').replace(GATING_NAMESPACE_QUALIFIER, '')
 
 
+def get_subsection_grade_percentage(subsection_usage_key, user):
+    """
+    Computes grade percentage for a subsection in a given course for a user
+
+    Arguments:
+        subsection_usage_key: key of subsection
+        user: The user whose grade needs to be computed
+
+    Returns:
+        User's grade percentage for given subsection
+    """
+    subsection_grade_percentage = 0.0
+    try:
+        subsection_structure = get_course_blocks(user, subsection_usage_key)
+        if any(subsection_structure):
+            subsection_grade_factory = SubsectionGradeFactory(user, course_structure=subsection_structure)
+            if subsection_usage_key in subsection_structure:
+                # this will force a recalculation of the subsection grade
+                subsection_grade = subsection_grade_factory.update(
+                    subsection_structure[subsection_usage_key], persist_grade=False
+                )
+                subsection_grade_percentage = subsection_grade.percent_graded * 100.0
+    except ItemNotFoundError as err:
+        log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err)
+    return subsection_grade_percentage
+
+
+def get_subsection_completion_percentage(subsection_usage_key, user):
+    """
+    Computes completion percentage for a subsection in a given course for a user
+    Arguments:
+        subsection_usage_key: key of subsection
+        user: The user whose completion percentage needs to be computed
+    Returns:
+        User's completion percentage for given subsection
+    """
+    subsection_completion_percentage = 0.0
+    try:
+        subsection_structure = get_course_blocks(user, subsection_usage_key)
+        if any(subsection_structure):
+            completable_blocks = [
+                block for block in subsection_structure
+                if block.block_type not in ['chapter', 'sequential', 'vertical']
+            ]
+            if not completable_blocks:
+                return 0
+            subsection_completion_total = 0
+            course_block_completions = BlockCompletion.get_course_completions(user, subsection_usage_key.course_key)
+            for block in completable_blocks:
+                if course_block_completions.get(block):
+                    subsection_completion_total += course_block_completions.get(block)
+            subsection_completion_percentage = min(
+                100 * (subsection_completion_total / float(len(completable_blocks))), 100
+            )
+
+    except ItemNotFoundError as err:
+        log.warning("Could not find course_block for subsection=%s error=%s", subsection_usage_key, err)
+
+    return subsection_completion_percentage
+
+
 def _get_minimum_required_percentage(milestone):
     """
-    Returns the minimum percentage requirement for the given milestone.
+    Returns the minimum score and minimum completion percentage requirement for the given milestone.
     """
-    # Default minimum score to 100
+    # Default minimum score and minimum completion percentage to 100
     min_score = 100
+    min_completion = 100
     requirements = milestone.get('requirements')
     if requirements:
         try:
@@ -438,7 +500,14 @@ def _get_minimum_required_percentage(milestone):
                 u'Gating: Failed to find minimum score for gating milestone %s, defaulting to 100',
                 json.dumps(milestone)
             )
-    return min_score
+        try:
+            min_completion = int(requirements.get('min_completion', 0))
+        except (ValueError, TypeError):
+            log.warning(
+                u'Gating: Failed to find minimum completion percentage for gating milestone %s, defaulting to 100',
+                json.dumps(milestone)
+            )
+    return min_score, min_completion
 
 
 def _get_subsection_percentage(subsection_grade):
diff --git a/openedx/core/lib/gating/tests/test_api.py b/openedx/core/lib/gating/tests/test_api.py
index c54e631ddbe..d0341e9a58f 100644
--- a/openedx/core/lib/gating/tests/test_api.py
+++ b/openedx/core/lib/gating/tests/test_api.py
@@ -5,7 +5,7 @@ import unittest
 
 from mock import patch, Mock
 from nose.plugins.attrib import attr
-from ddt import ddt, data
+from ddt import ddt, data, unpack
 from django.conf import settings
 from lms.djangoapps.gating import api as lms_gating_api
 from milestones.tests.utils import MilestonesTestCaseMixin
@@ -145,17 +145,23 @@ class TestGatingApi(ModuleStoreTestCase, MilestonesTestCaseMixin):
         """ Test test_required_content """
 
         gating_api.add_prerequisite(self.course.id, self.seq1.location)
-        gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100)
+        gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100, 100)
 
-        prereq_content_key, min_score = gating_api.get_required_content(self.course.id, self.seq2.location)
+        prereq_content_key, min_score, min_completion = gating_api.get_required_content(
+            self.course.id, self.seq2.location
+        )
         self.assertEqual(prereq_content_key, unicode(self.seq1.location))
         self.assertEqual(min_score, 100)
+        self.assertEqual(min_completion, 100)
 
-        gating_api.set_required_content(self.course.id, self.seq2.location, None, None)
+        gating_api.set_required_content(self.course.id, self.seq2.location, None, None, None)
 
-        prereq_content_key, min_score = gating_api.get_required_content(self.course.id, self.seq2.location)
+        prereq_content_key, min_score, min_completion = gating_api.get_required_content(
+            self.course.id, self.seq2.location
+        )
         self.assertIsNone(prereq_content_key)
         self.assertIsNone(min_score)
+        self.assertIsNone(min_completion)
 
     def test_get_gated_content(self):
         """
@@ -179,13 +185,25 @@ class TestGatingApi(ModuleStoreTestCase, MilestonesTestCaseMixin):
 
         self.assertEqual(gating_api.get_gated_content(self.course, student), [])
 
-    def test_is_gate_fulfilled(self):
+    @data(
+        (100, 0, 50, 0, False),
+        (100, 0, 100, 0, True),
+        (0, 100, 0, 50, False),
+        (0, 100, 0, 100, True),
+        (100, 100, 50, 100, False),
+        (100, 100, 100, 50, False),
+        (100, 100, 100, 100, True),
+    )
+    @unpack
+    def test_is_gate_fulfilled(self, min_score, min_completion, learner_score, learner_completion, is_gate_fulfilled):
         """
         Test if prereq section has any unfulfilled milestones
         """
         student = UserFactory(is_staff=False)
         gating_api.add_prerequisite(self.course.id, self.seq1.location)
-        gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100)
+        gating_api.set_required_content(
+            self.course.id, self.seq2.location, self.seq1.location, min_score, min_completion
+        )
         milestone = milestones_api.add_milestone(self.generic_milestone)
         milestones_api.add_course_content_milestone(self.course.id, self.seq1.location, 'fulfills', milestone)
 
@@ -193,22 +211,16 @@ class TestGatingApi(ModuleStoreTestCase, MilestonesTestCaseMixin):
 
         # complete the prerequisite to unlock the gated content
         # this call triggers reevaluation of prerequisites fulfilled by the gating block.
-        with patch.object(gating_api, '_get_subsection_percentage') as mock_grade:
-            mock_grade.return_value = 75
+        with patch.object(gating_api, 'get_subsection_completion_percentage') as mock_grade:
+            mock_grade.return_value = learner_completion
             lms_gating_api.evaluate_prerequisite(
                 self.course,
-                Mock(location=self.seq1.location),
+                Mock(location=self.seq1.location, percent_graded=learner_score / 100.0),
                 student,
             )
-            self.assertFalse(gating_api.is_gate_fulfilled(self.course.id, self.seq1.location, student.id))
-
-            mock_grade.return_value = 100
-            lms_gating_api.evaluate_prerequisite(
-                self.course,
-                Mock(location=self.seq1.location),
-                student,
+            self.assertEqual(
+                gating_api.is_gate_fulfilled(self.course.id, self.seq1.location, student.id), is_gate_fulfilled
             )
-            self.assertTrue(gating_api.is_gate_fulfilled(self.course.id, self.seq1.location, student.id))
 
     @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
     def test_compute_is_prereq_met(self):
@@ -217,11 +229,11 @@ class TestGatingApi(ModuleStoreTestCase, MilestonesTestCaseMixin):
         """
         student = UserFactory(is_staff=False)
         gating_api.add_prerequisite(self.course.id, self.seq1.location)
-        gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100)
+        gating_api.set_required_content(self.course.id, self.seq2.location, self.seq1.location, 100, 0)
 
         # complete the prerequisite to unlock the gated content
         # this call triggers reevaluation of prerequisites fulfilled by the gating block.
-        with patch.object(gating_api, '_get_subsection_percentage') as mock_grade:
+        with patch.object(gating_api, 'get_subsection_grade_percentage') as mock_grade:
             mock_grade.return_value = 75
             # don't force recompute
             prereq_met, prereq_meta_info = gating_api.compute_is_prereq_met(self.seq2.location, student.id, False)
diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py
index 8943a8f8f1e..dfbf8aca53d 100644
--- a/openedx/features/course_experience/tests/views/test_course_outline.py
+++ b/openedx/features/course_experience/tests/views/test_course_outline.py
@@ -251,10 +251,10 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston
 
         # complete the prerequisite to unlock the gated content
         # this call triggers reevaluation of prerequisites fulfilled by the gating block.
-        with patch('openedx.core.lib.gating.api._get_subsection_percentage', Mock(return_value=100)):
+        with patch('openedx.core.lib.gating.api.get_subsection_completion_percentage', Mock(return_value=100)):
             lms_gating_api.evaluate_prerequisite(
                 self.course,
-                Mock(location=self.course_blocks['prerequisite'].location),
+                Mock(location=self.course_blocks['prerequisite'].location, percent_graded=1.0),
                 self.user,
             )
 
-- 
GitLab