Commit 6fdff766 by Ahsan Ulhaq

Credit eligibility requirements display on student progress page

ECOM-1523
parent 473c6886
...@@ -42,6 +42,9 @@ from lms.envs.common import ( ...@@ -42,6 +42,9 @@ from lms.envs.common import (
# technically accessible through the CMS via legacy URLs. # technically accessible through the CMS via legacy URLs.
PROFILE_IMAGE_BACKEND, PROFILE_IMAGE_DEFAULT_FILENAME, PROFILE_IMAGE_DEFAULT_FILE_EXTENSION, PROFILE_IMAGE_BACKEND, PROFILE_IMAGE_DEFAULT_FILENAME, PROFILE_IMAGE_DEFAULT_FILE_EXTENSION,
PROFILE_IMAGE_SECRET_KEY, PROFILE_IMAGE_MIN_BYTES, PROFILE_IMAGE_MAX_BYTES, PROFILE_IMAGE_SECRET_KEY, PROFILE_IMAGE_MIN_BYTES, PROFILE_IMAGE_MAX_BYTES,
# The following setting is included as it is used to check whether to
# display credit eligibility table on the CMS or not.
ENABLE_CREDIT_ELIGIBILITY
) )
from path import path from path import path
from warnings import simplefilter from warnings import simplefilter
...@@ -174,7 +177,7 @@ FEATURES = { ...@@ -174,7 +177,7 @@ FEATURES = {
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
# Enable credit eligibility feature # Enable credit eligibility feature
'ENABLE_CREDIT_ELIGIBILITY': False, 'ENABLE_CREDIT_ELIGIBILITY': ENABLE_CREDIT_ELIGIBILITY,
# Can the visibility of the discussion tab be configured on a per-course basis? # Can the visibility of the discussion tab be configured on a per-course basis?
'ALLOW_HIDING_DISCUSSION_TAB': False, 'ALLOW_HIDING_DISCUSSION_TAB': False,
...@@ -248,7 +251,6 @@ from lms.envs.common import ( ...@@ -248,7 +251,6 @@ from lms.envs.common import (
COURSE_KEY_PATTERN, COURSE_ID_PATTERN, USAGE_KEY_PATTERN, ASSET_KEY_PATTERN COURSE_KEY_PATTERN, COURSE_ID_PATTERN, USAGE_KEY_PATTERN, ASSET_KEY_PATTERN
) )
######################### CSRF ######################################### ######################### CSRF #########################################
# Forwards-compatibility with Django 1.7 # Forwards-compatibility with Django 1.7
......
...@@ -102,7 +102,7 @@ var GradingView = ValidatingView.extend({ ...@@ -102,7 +102,7 @@ var GradingView = ValidatingView.extend({
renderMinimumGradeCredit: function() { renderMinimumGradeCredit: function() {
var minimum_grade_credit = this.model.get('minimum_grade_credit'); var minimum_grade_credit = this.model.get('minimum_grade_credit');
this.$el.find('#course-minimum_grade_credit').val( this.$el.find('#course-minimum_grade_credit').val(
parseFloat(minimum_grade_credit) * 100 + '%' Math.round(parseFloat(minimum_grade_credit) * 100) + '%'
); );
}, },
setGracePeriod : function(event) { setGracePeriod : function(event) {
......
...@@ -173,18 +173,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -173,18 +173,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
TEST_DATA = { TEST_DATA = {
# (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks # (providers, course_width, enable_ccx): # of sql queries, # of mongo queries, # of xblocks
('no_overrides', 1, True): (27, 7, 19), ('no_overrides', 1, True): (26, 7, 19),
('no_overrides', 2, True): (135, 7, 131), ('no_overrides', 2, True): (134, 7, 131),
('no_overrides', 3, True): (595, 7, 537), ('no_overrides', 3, True): (594, 7, 537),
('ccx', 1, True): (27, 7, 47), ('ccx', 1, True): (26, 7, 47),
('ccx', 2, True): (135, 7, 455), ('ccx', 2, True): (134, 7, 455),
('ccx', 3, True): (595, 7, 2037), ('ccx', 3, True): (594, 7, 2037),
('no_overrides', 1, False): (27, 7, 19), ('no_overrides', 1, False): (26, 7, 19),
('no_overrides', 2, False): (135, 7, 131), ('no_overrides', 2, False): (134, 7, 131),
('no_overrides', 3, False): (595, 7, 537), ('no_overrides', 3, False): (594, 7, 537),
('ccx', 1, False): (27, 7, 19), ('ccx', 1, False): (26, 7, 19),
('ccx', 2, False): (135, 7, 131), ('ccx', 2, False): (134, 7, 131),
('ccx', 3, False): (595, 7, 537), ('ccx', 3, False): (594, 7, 537),
} }
...@@ -196,16 +196,16 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): ...@@ -196,16 +196,16 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True): (27, 4, 9), ('no_overrides', 1, True): (26, 4, 9),
('no_overrides', 2, True): (135, 19, 54), ('no_overrides', 2, True): (134, 19, 54),
('no_overrides', 3, True): (595, 84, 215), ('no_overrides', 3, True): (594, 84, 215),
('ccx', 1, True): (27, 4, 9), ('ccx', 1, True): (26, 4, 9),
('ccx', 2, True): (135, 19, 54), ('ccx', 2, True): (134, 19, 54),
('ccx', 3, True): (595, 84, 215), ('ccx', 3, True): (594, 84, 215),
('no_overrides', 1, False): (27, 4, 9), ('no_overrides', 1, False): (26, 4, 9),
('no_overrides', 2, False): (135, 19, 54), ('no_overrides', 2, False): (134, 19, 54),
('no_overrides', 3, False): (595, 84, 215), ('no_overrides', 3, False): (594, 84, 215),
('ccx', 1, False): (27, 4, 9), ('ccx', 1, False): (26, 4, 9),
('ccx', 2, False): (135, 19, 54), ('ccx', 2, False): (134, 19, 54),
('ccx', 3, False): (595, 84, 215), ('ccx', 3, False): (594, 84, 215),
} }
...@@ -7,6 +7,7 @@ import urllib ...@@ -7,6 +7,7 @@ import urllib
import json import json
import cgi import cgi
from collections import OrderedDict
from datetime import datetime from datetime import datetime
from django.utils import translation from django.utils import translation
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -1065,7 +1066,9 @@ def _progress(request, course_key, student_id): ...@@ -1065,7 +1066,9 @@ def _progress(request, course_key, student_id):
# checking certificate generation configuration # checking certificate generation configuration
show_generate_cert_btn = certs_api.cert_generation_enabled(course_key) show_generate_cert_btn = certs_api.cert_generation_enabled(course_key)
if is_credit_course(course_key): credit_course_requirements = None
is_course_credit = settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course(course_key)
if is_course_credit:
requirement_statuses = get_credit_requirement_status(course_key, student.username) requirement_statuses = get_credit_requirement_status(course_key, student.username)
if any(requirement['status'] == 'failed' for requirement in requirement_statuses): if any(requirement['status'] == 'failed' for requirement in requirement_statuses):
eligibility_status = "not_eligible" eligibility_status = "not_eligible"
...@@ -1073,12 +1076,16 @@ def _progress(request, course_key, student_id): ...@@ -1073,12 +1076,16 @@ def _progress(request, course_key, student_id):
eligibility_status = "eligible" eligibility_status = "eligible"
else: else:
eligibility_status = "partial_eligible" eligibility_status = "partial_eligible"
credit_course = {
paired_requirements = {}
for requirement in requirement_statuses:
namespace = requirement.pop("namespace")
paired_requirements.setdefault(namespace, []).append(requirement)
credit_course_requirements = {
'eligibility_status': eligibility_status, 'eligibility_status': eligibility_status,
'requirements': requirement_statuses 'requirements': OrderedDict(sorted(paired_requirements.items(), reverse=True))
} }
else:
credit_course = None
context = { context = {
'course': course, 'course': course,
...@@ -1089,7 +1096,8 @@ def _progress(request, course_key, student_id): ...@@ -1089,7 +1096,8 @@ def _progress(request, course_key, student_id):
'student': student, 'student': student,
'passed': is_course_passed(course, grade_summary), 'passed': is_course_passed(course, grade_summary),
'show_generate_cert_btn': show_generate_cert_btn, 'show_generate_cert_btn': show_generate_cert_btn,
'credit_course': credit_course 'credit_course_requirements': credit_course_requirements,
'is_credit_course': is_course_credit,
} }
if show_generate_cert_btn: if show_generate_cert_btn:
......
...@@ -2021,6 +2021,10 @@ FEATURES['CLASS_DASHBOARD'] = False ...@@ -2021,6 +2021,10 @@ FEATURES['CLASS_DASHBOARD'] = False
if FEATURES.get('CLASS_DASHBOARD'): if FEATURES.get('CLASS_DASHBOARD'):
INSTALLED_APPS += ('class_dashboard',) INSTALLED_APPS += ('class_dashboard',)
################ Enable credit eligibility feature ####################
ENABLE_CREDIT_ELIGIBILITY = False
FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY
######################## CAS authentication ########################### ######################## CAS authentication ###########################
if FEATURES.get('AUTH_USE_CAS'): if FEATURES.get('AUTH_USE_CAS'):
......
$(document).ready(function() { $(document).ready(function() {
var container = $('.requirement-container');
var collapse = container.data('eligible');
if (collapse == 'not_eligible') {
container.addClass('is-hidden');
$('.detail-collapse').find('.fa').toggleClass('fa-caret-up fa-caret-down');
$('.requirement-detail').text(gettext('More'));
}
$('.detail-collapse').on('click', function() { $('.detail-collapse').on('click', function() {
var el = $(this); var el = $(this);
$('.requirement-container').toggleClass('is-hidden'); container.toggleClass('is-hidden');
el.find('.fa').toggleClass('fa-caret-down fa-caret-up'); el.find('.fa').toggleClass('fa-caret-up fa-caret-down');
el.find('.requirement-detail').text(function(i, text){ el.find('.requirement-detail').text(function(i, text){
return text === gettext('More') ? gettext('Less') : gettext('More'); return text === gettext('Less') ? gettext('More') : gettext('Less');
}); });
}); });
}); });
...@@ -215,11 +215,11 @@ ...@@ -215,11 +215,11 @@
border-bottom: 1px solid $lightGrey; border-bottom: 1px solid $lightGrey;
padding: lh(0.5); padding: lh(0.5);
> .requirement-name { > .requirement-name {
width: bi-app-invert-percentage(30%); width: bi-app-invert-percentage(40%);
display: inline-block; display: inline-block;
} }
> .requirement-status{ > .requirement-status{
width: bi-app-invert-percentage(70%); width: bi-app-invert-percentage(60%);
@include float(right); @include float(right);
display: inline-block; display: inline-block;
.fa-times{ .fa-times{
...@@ -231,7 +231,7 @@ ...@@ -231,7 +231,7 @@
color: $success-color; color: $success-color;
} }
> .not-achieve{ > .not-achieve{
color: $lightGrey; color: $darkGrey;
} }
} }
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from util.date_utils import get_time_display, DEFAULT_LONG_DATE_FORMAT from util.date_utils import get_time_display, DEFAULT_SHORT_DATE_FORMAT
from django.conf import settings from django.conf import settings
from django.utils.http import urlquote_plus from django.utils.http import urlquote_plus
%> %>
...@@ -103,26 +103,27 @@ from django.utils.http import urlquote_plus ...@@ -103,26 +103,27 @@ from django.utils.http import urlquote_plus
<div class="grade-detail-graph" id="grade-detail-graph" aria-hidden="true"></div> <div class="grade-detail-graph" id="grade-detail-graph" aria-hidden="true"></div>
%endif %endif
%if credit_course is not None: % if is_credit_course:
<section class="credit-eligibility"> <section class="credit-eligibility">
<div class="credit-eligibility-container"> <div class="credit-eligibility-container">
<div class="eligibility-heading"> <div class="eligibility-heading">
<h2>${_("Requirements for Course Credit")}</h2> <h2>${_("Requirements for Course Credit")}</h2>
</div> </div>
%if credit_course['eligibility_status'] == 'not_eligible': %if credit_course_requirements['eligibility_status'] == 'not_eligible':
<span class="eligibility_msg">${student.username}, ${_("You are no longer eligible for this course.")}</span> <span class="eligibility_msg">${student.get_full_name()}, ${_("You are no longer eligible for this course.")}</span>
%elif credit_course['eligibility_status'] == 'eligible': %elif credit_course_requirements['eligibility_status'] == 'eligible':
<span class="eligibility_msg">${student.username}, ${_("You have met the requirements for credit in this course.")} <span class="eligibility_msg">${student.get_full_name()}, ${_("You have met the requirements for credit in this course.")}
<a href="#">${_("Go to your dashboard")}</a> ${_("to purchase course credit.")} ${_("{link} to purchase course credit.").format(link="<a href={url}>{url_name}</a>".format(url = reverse('dashboard'), url_name = _('Go to your dashboard')))}
</span> </span>
%elif credit_course['eligibility_status'] == 'partial_eligible': %elif credit_course_requirements['eligibility_status'] == 'partial_eligible':
<span>${student.username}, ${_("You have not yet met the requirements for credit.")}</span> <span>${student.get_full_name()}, ${_("You have not yet met the requirements for credit.")}</span>
%endif %endif
<button class="credit-help"><i class="fa fa-question"></i><span class="sr">Help regarding credit requirement</span></button><br> <button class="credit-help"><i class="fa fa-question"></i><span class="sr">Help regarding credit requirement</span></button><br>
<div class="requirement-container is-hidden"> <div class="requirement-container" data-eligible="${credit_course_requirements['eligibility_status']}">
%for requirement in credit_course['requirements']: %for namespace in credit_course_requirements['requirements']:
%for requirement in credit_course_requirements['requirements'][namespace]:
<div class="requirement"> <div class="requirement">
<div class="requirement-name">${_(requirement['name'])}</div> <div class="requirement-name">${_(requirement['display_name'])}</div>
<div class="requirement-status"> <div class="requirement-status">
%if requirement['status']: %if requirement['status']:
%if requirement['status'] == 'submitted': %if requirement['status'] == 'submitted':
...@@ -132,7 +133,7 @@ from django.utils.http import urlquote_plus ...@@ -132,7 +133,7 @@ from django.utils.http import urlquote_plus
<span>${_("Verification Failed" )}</span> <span>${_("Verification Failed" )}</span>
%elif requirement['status'] == 'satisfied': %elif requirement['status'] == 'satisfied':
<i class="fa fa-check"></i> <i class="fa fa-check"></i>
<span>Verified on ${get_time_display(requirement['status_date'], DEFAULT_LONG_DATE_FORMAT, settings.TIME_ZONE)}</span> <span>Verified on ${get_time_display(requirement['status_date'], DEFAULT_SHORT_DATE_FORMAT, settings.TIME_ZONE)}</span>
%endif %endif
%else: %else:
<span class="not-achieve">${_("Upcoming")}</span> <span class="not-achieve">${_("Upcoming")}</span>
...@@ -140,9 +141,10 @@ from django.utils.http import urlquote_plus ...@@ -140,9 +141,10 @@ from django.utils.http import urlquote_plus
</div> </div>
</div> </div>
%endfor %endfor
%endfor
</div> </div>
<button class="detail-collapse" aria-live="polite"><i class="fa fa-caret-down"></i> <button class="detail-collapse" aria-live="polite"><i class="fa fa-caret-up"></i>
<span class="requirement-detail">${_("More")}</span><span class="sr"> detail</span> <span class="requirement-detail">${_("Less")}</span>
</button> </button>
</div> </div>
</section> </section>
......
...@@ -419,26 +419,23 @@ def get_credit_requirement_status(course_key, username): ...@@ -419,26 +419,23 @@ def get_credit_requirement_status(course_key, username):
{ {
"namespace": "reverification", "namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"display_name": "In Course Reverification",
"criteria": {}, "criteria": {},
"status": "satisfied", "status": "failed",
},
{
"namespace": "reverification",
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
"criteria": {},
"status": "Not satisfied",
}, },
{ {
"namespace": "proctored_exam", "namespace": "proctored_exam",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid", "name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Proctored Mid Term Exam",
"criteria": {}, "criteria": {},
"status": "error", "status": "satisfied",
}, },
{ {
"namespace": "grade", "namespace": "grade",
"name": "i4x://edX/DemoX/proctoring-block/final_uuid", "name": "i4x://edX/DemoX/proctoring-block/final_uuid",
"display_name": "Minimum Passing Grade",
"criteria": {"min_grade": 0.8}, "criteria": {"min_grade": 0.8},
"status": None, "status": "failed",
}, },
] ]
...@@ -454,6 +451,7 @@ def get_credit_requirement_status(course_key, username): ...@@ -454,6 +451,7 @@ def get_credit_requirement_status(course_key, username):
statuses.append({ statuses.append({
"namespace": requirement.namespace, "namespace": requirement.namespace,
"name": requirement.name, "name": requirement.name,
"display_name": requirement.display_name,
"criteria": requirement.criteria, "criteria": requirement.criteria,
"status": requirement_status.status if requirement_status else None, "status": requirement_status.status if requirement_status else None,
"status_date": requirement_status.modified if requirement_status else None, "status_date": requirement_status.modified if requirement_status else None,
...@@ -475,18 +473,6 @@ def is_user_eligible_for_credit(username, course_key): ...@@ -475,18 +473,6 @@ def is_user_eligible_for_credit(username, course_key):
return CreditEligibility.is_user_eligible_for_credit(course_key, username) return CreditEligibility.is_user_eligible_for_credit(course_key, username)
def is_credit_course(course_key):
"""Check if the given course is a credit course
Arg:
course_key (CourseKey): The identifier for course
Returns:
True if course is credit course else False
"""
return CreditCourse.is_credit_course(course_key)
def get_credit_requirement(course_key, namespace, name): def get_credit_requirement(course_key, namespace, name):
"""Returns the requirement of a given course, namespace and name. """Returns the requirement of a given course, namespace and name.
......
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