diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 88f81a42b1..82fe0b8097 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -167,10 +167,10 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { 'no_overrides': [ - (26, 7, 19), (134, 7, 131), (594, 7, 537) + (27, 7, 19), (135, 7, 131), (595, 7, 537) ], 'ccx': [ - (26, 7, 47), (134, 7, 455), (594, 7, 2037) + (27, 7, 47), (135, 7, 455), (595, 7, 2037) ], } @@ -184,9 +184,9 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): TEST_DATA = { 'no_overrides': [ - (26, 4, 9), (134, 19, 54), (594, 84, 215) + (27, 4, 9), (135, 19, 54), (595, 84, 215) ], 'ccx': [ - (26, 4, 9), (134, 19, 54), (594, 84, 215) + (27, 4, 9), (135, 19, 54), (595, 84, 215) ] } diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 7bf6a2e503..7d0dcdb329 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -38,6 +38,11 @@ from courseware.courses import ( sort_by_start_date, ) from courseware.masquerade import setup_masquerade +from openedx.core.djangoapps.credit.api import ( + get_credit_requirement_status, + is_user_eligible_for_credit, + is_credit_course +) from courseware.model_data import FieldDataCache from .module_render import toc_for_course, get_module_for_descriptor, get_module, get_module_by_usage_id from .entrance_exams import ( @@ -1050,6 +1055,21 @@ def _progress(request, course_key, student_id): # checking certificate generation configuration show_generate_cert_btn = certs_api.cert_generation_enabled(course_key) + if is_credit_course(course_key): + requirement_statuses = get_credit_requirement_status(course_key, student.username) + if any(requirement['status'] == 'failed' for requirement in requirement_statuses): + eligibility_status = "not_eligible" + elif is_user_eligible_for_credit(student.username, course_key): + eligibility_status = "eligible" + else: + eligibility_status = "partial_eligible" + credit_course = { + 'eligibility_status': eligibility_status, + 'requirements': requirement_statuses + } + else: + credit_course = None + context = { 'course': course, 'courseware_summary': courseware_summary, @@ -1058,7 +1078,8 @@ def _progress(request, course_key, student_id): 'staff_access': staff_access, 'student': student, '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 } if show_generate_cert_btn: diff --git a/lms/static/images/correct-icon.png b/lms/static/images/correct-icon.png new file mode 100644 index 0000000000..5ae7de2a14 Binary files /dev/null and b/lms/static/images/correct-icon.png differ diff --git a/lms/static/images/incorrect-icon.png b/lms/static/images/incorrect-icon.png new file mode 100644 index 0000000000..0926486ec9 Binary files /dev/null and b/lms/static/images/incorrect-icon.png differ diff --git a/lms/static/js/courseware/credit_progress.js b/lms/static/js/courseware/credit_progress.js new file mode 100644 index 0000000000..d0600b85c0 --- /dev/null +++ b/lms/static/js/courseware/credit_progress.js @@ -0,0 +1,11 @@ +$(document).ready(function() { + $('.detail-collapse').on('click', function() { + var el = $(this); + $('.requirement-container').toggleClass('is-hidden'); + el.find('.fa').toggleClass('fa-caret-down fa-caret-up'); + el.find('.requirement-detail').text(function(i, text){ + return text === gettext('More') ? gettext('Less') : gettext('More'); + }); + }); + +}); diff --git a/lms/static/sass/course/_profile.scss b/lms/static/sass/course/_profile.scss index 783e2dcd1b..32ff94b641 100644 --- a/lms/static/sass/course/_profile.scss +++ b/lms/static/sass/course/_profile.scss @@ -179,6 +179,66 @@ width: 100%; } + > .credit-eligibility{ + border-top: 1px solid $lightGrey; + margin-top: lh(); + @include padding-left(0); + + > .credit-eligibility-container { + padding: lh(); + > .credit-help { + background: $blue; + color: $white; + width: lh(); + margin: 0; + padding: 0; + border-radius: lh(0.9); + border-color: $white; + text-shadow: None; + } + > .detail-collapse{ + border: none; + box-shadow: none; + background: $white; + padding: 0; + color: $blue; + > i { + padding: lh(0.25); + } + > span{ + color: inherit; + } + } + > .requirement-container{ + padding: lh(); + > .requirement{ + border-bottom: 1px solid $lightGrey; + padding: lh(0.5); + > .requirement-name { + width: bi-app-invert-percentage(30%); + display: inline-block; + } + > .requirement-status{ + width: bi-app-invert-percentage(70%); + @include float(right); + display: inline-block; + .fa-times{ + @extend %t-icon6; + color: $alert-color; + } + .fa-check{ + @extend %t-icon6; + color: $success-color; + } + > .not-achieve{ + color: $lightGrey; + } + } + } + } + } + } + > .chapters { border-top: 1px solid #e3e3e3; margin-top: lh(); diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 019c0aa399..975756d823 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -3,7 +3,7 @@ <%! from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from util.date_utils import get_time_display +from util.date_utils import get_time_display, DEFAULT_LONG_DATE_FORMAT from django.conf import settings from django.utils.http import urlquote_plus %> @@ -24,6 +24,7 @@ from django.utils.http import urlquote_plus + @@ -104,6 +105,51 @@ from django.utils.http import urlquote_plus %endif + %if credit_course is not None: +
+
+
+

${_("Requirements for Course Credit")}

+
+ %if credit_course['eligibility_status'] == 'not_eligible': + ${student.username}, ${_("You are no longer eligible for this course.")} + %elif credit_course['eligibility_status'] == 'eligible': + ${student.username}, ${_("You have met the requirements for credit in this course.")} + ${_("Go to your dashboard")} ${_("to purchase course credit.")} + + %elif credit_course['eligibility_status'] == 'partial_eligible': + ${student.username}, ${_("You have not yet met the requirements for credit.")} + %endif +
+ + +
+
+ %endif +
%for chapter in courseware_summary: %if not chapter['display_name'] == "hidden": diff --git a/openedx/core/djangoapps/credit/api.py b/openedx/core/djangoapps/credit/api.py index 4fa4e55bd3..b54eb5504c 100644 --- a/openedx/core/djangoapps/credit/api.py +++ b/openedx/core/djangoapps/credit/api.py @@ -406,6 +406,88 @@ def get_credit_requests_for_user(username): return CreditRequest.credit_requests_for_user(username) +def get_credit_requirement_status(course_key, username): + """ Retrieve the user's status for each credit requirement in the course. + + Args: + course_key (CourseKey): The identifier for course + username (str): The identifier of the user + + Example: + >>> get_credit_requirement_status("course-v1-edX-DemoX-1T2015", "john") + + [ + { + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "criteria": {}, + "status": "satisfied", + }, + { + "namespace": "reverification", + "name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", + "criteria": {}, + "status": "Not satisfied", + }, + { + "namespace": "proctored_exam", + "name": "i4x://edX/DemoX/proctoring-block/final_uuid", + "criteria": {}, + "status": "error", + }, + { + "namespace": "grade", + "name": "i4x://edX/DemoX/proctoring-block/final_uuid", + "criteria": {"min_grade": 0.8}, + "status": None, + }, + ] + + Returns: + list of requirement statuses + """ + requirements = CreditRequirement.get_course_requirements(course_key) + requirement_statuses = CreditRequirementStatus.get_statuses(requirements, username) + requirement_statuses = dict((o.requirement, o) for o in requirement_statuses) + statuses = [] + for requirement in requirements: + requirement_status = requirement_statuses.get(requirement) + statuses.append({ + "namespace": requirement.namespace, + "name": requirement.name, + "criteria": requirement.criteria, + "status": requirement_status.status if requirement_status else None, + "status_date": requirement_status.modified if requirement_status else None, + }) + return statuses + + +def is_user_eligible_for_credit(username, course_key): + """Returns a boolean indicating if the user is eligible for credit for + the given course + + Args: + username(str): The identifier for user + course_key (CourseKey): The identifier for course + + Returns: + True if user is eligible for the course else False + """ + 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_requirements_to_disable(old_requirements, new_requirements): """ Get the ids of 'CreditRequirement' entries to be disabled that are diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py index 8aff5cfcf1..eb632a5253 100644 --- a/openedx/core/djangoapps/credit/models.py +++ b/openedx/core/djangoapps/credit/models.py @@ -244,6 +244,19 @@ class CreditRequirementStatus(TimeStampedModel): class Meta(object): # pylint: disable=missing-docstring get_latest_by = "created" + @classmethod + def get_statuses(cls, requirements, username): + """ Get credit requirement statuses of given requirement and username + + Args: + requirement(CreditRequirement): The identifier for a requirement + username(str): username of the user + + Returns: + Queryset 'CreditRequirementStatus' objects + """ + return cls.objects.filter(requirement__in=requirements, username=username) + class CreditEligibility(TimeStampedModel): """ @@ -258,6 +271,19 @@ class CreditEligibility(TimeStampedModel): class Meta(object): # pylint: disable=missing-docstring unique_together = ('username', 'course') + @classmethod + def is_user_eligible_for_credit(cls, course_key, username): + """Check if the given user is eligible for the provided credit course + + Args: + course_key(CourseKey): The course identifier + username(str): The username of the user + + Returns: + Bool True if the user eligible for credit course else False + """ + return cls.objects.filter(course__course_key=course_key, username=username).exists() + class CreditRequest(TimeStampedModel): """ @@ -321,6 +347,7 @@ class CreditRequest(TimeStampedModel): ] """ + return [ { "uuid": request.uuid, diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 57474f485b..6e5845fd98 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -204,6 +204,17 @@ class CreditRequirementApiTests(CreditApiTestBase): self.assertEqual(len(grade_req), 1) self.assertEqual(grade_req[0].active, False) + def test_is_user_eligible_for_credit(self): + credit_course = self.add_credit_course() + CreditEligibility.objects.create( + course=credit_course, username="staff", provider=CreditProvider.objects.get(provider_id=self.PROVIDER_ID) + ) + is_eligible = api.is_user_eligible_for_credit('staff', credit_course.course_key) + self.assertTrue(is_eligible) + + is_eligible = api.is_user_eligible_for_credit('abc', credit_course.course_key) + self.assertFalse(is_eligible) + @ddt.ddt class CreditProviderIntegrationApiTests(CreditApiTestBase):