ECOM-1524: Display credit availability on the dashboard
This commit is contained in:
@@ -7,8 +7,10 @@ import uuid
|
||||
import time
|
||||
import json
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
from collections import defaultdict
|
||||
from pytz import UTC
|
||||
from requests import HTTPError
|
||||
from ipware.ip import get_ip
|
||||
|
||||
from django.conf import settings
|
||||
@@ -25,21 +27,19 @@ from django.db import IntegrityError, transaction
|
||||
from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden,
|
||||
HttpResponseServerError, Http404)
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.http import cookie_date, base36_to_int
|
||||
from django.utils.translation import ugettext as _, get_language
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
from requests import HTTPError
|
||||
|
||||
from social.apps.django_app import utils as social_utils
|
||||
from social.backends import oauth as social_oauth
|
||||
@@ -123,13 +123,12 @@ from notification_prefs.views import enable_notifications
|
||||
|
||||
# Note that this lives in openedx, so this dependency should be refactored.
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.credit.api import get_credit_eligibility, get_purchased_credit_courses
|
||||
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
|
||||
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=invalid-name
|
||||
|
||||
SETTING_CHANGE_INITIATED = 'edx.user.settings.change_initiated'
|
||||
|
||||
|
||||
@@ -523,6 +522,13 @@ def dashboard(request):
|
||||
course_enrollment_pairs, course_modes_by_course
|
||||
)
|
||||
|
||||
# Retrieve the course modes for each course
|
||||
enrolled_courses_dict = {}
|
||||
for course, __ in course_enrollment_pairs:
|
||||
enrolled_courses_dict[unicode(course.id)] = course
|
||||
|
||||
credit_messages = _create_credit_availability_message(enrolled_courses_dict, user)
|
||||
|
||||
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
|
||||
|
||||
message = ""
|
||||
@@ -628,6 +634,7 @@ def dashboard(request):
|
||||
|
||||
context = {
|
||||
'enrollment_message': enrollment_message,
|
||||
'credit_messages': credit_messages,
|
||||
'course_enrollment_pairs': course_enrollment_pairs,
|
||||
'course_optouts': course_optouts,
|
||||
'message': message,
|
||||
@@ -692,6 +699,47 @@ def _create_recent_enrollment_message(course_enrollment_pairs, course_modes):
|
||||
)
|
||||
|
||||
|
||||
def _create_credit_availability_message(enrolled_courses_dict, user): # pylint: disable=invalid-name
|
||||
"""Builds a dict of credit availability for courses.
|
||||
|
||||
Construct a for courses user has completed and has not purchased credit
|
||||
from the credit provider yet.
|
||||
|
||||
Args:
|
||||
course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information.
|
||||
user (User): User object.
|
||||
|
||||
Returns:
|
||||
A dict of courses user is eligible for credit.
|
||||
|
||||
"""
|
||||
user_eligibilities = get_credit_eligibility(user.username)
|
||||
user_purchased_credit = get_purchased_credit_courses(user.username)
|
||||
|
||||
eligibility_messages = {}
|
||||
for course_id, eligibility in user_eligibilities.iteritems():
|
||||
if course_id not in user_purchased_credit:
|
||||
duration = eligibility["seconds_good_for_display"]
|
||||
curr_time = timezone.now()
|
||||
validity_till = eligibility["created_at"] + timedelta(seconds=duration)
|
||||
if validity_till > curr_time:
|
||||
diff = validity_till - curr_time
|
||||
urgent = diff.days <= 30
|
||||
eligibility_messages[course_id] = {
|
||||
"user_id": user.id,
|
||||
"course_id": course_id,
|
||||
"course_name": enrolled_courses_dict[course_id].display_name,
|
||||
"providers": eligibility["providers"],
|
||||
"status": eligibility["status"],
|
||||
"provider": eligibility.get("provider"),
|
||||
"urgent": urgent,
|
||||
"user_full_name": user.get_full_name(),
|
||||
"expiry": validity_till
|
||||
}
|
||||
|
||||
return eligibility_messages
|
||||
|
||||
|
||||
def _get_recently_enrolled_courses(course_enrollment_pairs):
|
||||
"""Checks to see if the student has recently enrolled in courses.
|
||||
|
||||
|
||||
@@ -1339,6 +1339,12 @@ certificates_web_view_js = [
|
||||
'js/src/logger.js',
|
||||
]
|
||||
|
||||
credit_web_view_js = [
|
||||
'js/vendor/jquery.min.js',
|
||||
'js/vendor/jquery.cookie.js',
|
||||
'js/src/logger.js',
|
||||
]
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'style-vendor': {
|
||||
'source_filenames': [
|
||||
@@ -1578,6 +1584,10 @@ PIPELINE_JS = {
|
||||
'utility': {
|
||||
'source_filenames': ['js/src/utility.js'],
|
||||
'output_filename': 'js/utility.js'
|
||||
},
|
||||
'credit_wv': {
|
||||
'source_filenames': credit_web_view_js,
|
||||
'output_filename': 'js/credit/web_view.js'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -531,6 +531,10 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.purchase_credit {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend %ui-depth1;
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -83,7 +83,8 @@ from django.core.urlresolvers import reverse
|
||||
<% is_course_blocked = (course.id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(course.id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(course.id) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings" />
|
||||
<% credit_message = credit_messages.get(unicode(course.id)) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option = show_refund_option, is_paid_course = is_paid_course, is_course_blocked = is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, credit_message=credit_message, user=user" />
|
||||
% endfor
|
||||
|
||||
% if settings.FEATURES.get('CUSTOM_COURSES_EDX', False):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings" />
|
||||
<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, credit_message" />
|
||||
|
||||
<%!
|
||||
import urllib
|
||||
@@ -273,7 +273,11 @@ from student.helpers import (
|
||||
<ul class="messages-list">
|
||||
% if course.may_certify() and cert_status:
|
||||
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% if credit_message:
|
||||
<%include file='_dashboard_credit_information.html' args='credit_message=credit_message'/>
|
||||
% endif
|
||||
|
||||
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY] and not is_course_blocked:
|
||||
<div class="message message-status wrapper-message-primary is-shown">
|
||||
|
||||
70
lms/templates/dashboard/_dashboard_credit_information.html
Normal file
70
lms/templates/dashboard/_dashboard_credit_information.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<%page args="credit_message" />
|
||||
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from course_modes.models import CourseMode
|
||||
from util.date_utils import get_default_time_display
|
||||
%>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="js_extra" args="credit_message">
|
||||
<%static:js group='credit_wv'/>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRFToken': $.cookie('csrftoken')
|
||||
},
|
||||
dataType: 'json'
|
||||
});
|
||||
$(".purchase-credit-btn").click(function() {
|
||||
var data = {
|
||||
user_id: "${credit_message['user_id']}",
|
||||
course_id: "${credit_message['course_id']}"
|
||||
};
|
||||
Logger.log('edx.credit.shared', data);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<div class="message message-status is-shown">
|
||||
<p>
|
||||
% if credit_message["status"] == "requirements_meet":
|
||||
<span>
|
||||
% if credit_message["urgent"]:
|
||||
${_("{username}, your eligibility for credit expires on {expiry}. Don't miss out!").format(
|
||||
username=credit_message["user_full_name"],
|
||||
expiry=get_default_time_display(credit_message["expiry"])
|
||||
)
|
||||
}
|
||||
% else:
|
||||
${_("{congrats} {username}, You have meet requirements for credit.").format(
|
||||
congrats="<b>Congratulations</b>",
|
||||
username=credit_message["user_full_name"]
|
||||
)
|
||||
}
|
||||
% endif
|
||||
</span>
|
||||
<span class="purchase_credit"> <a class="btn purchase-credit-btn" href="" target="_blank">${_("Purchase Credit")}</a> </span>
|
||||
|
||||
% elif credit_message["status"] == "pending":
|
||||
${_("Thank you, your payment is complete, your credit is processing. Please see {provider_link} for more information.").format(
|
||||
provider_link='<a href="#" target="_blank">{}</a>'.format(credit_message["provider"]["display_name"])
|
||||
)
|
||||
}
|
||||
% elif credit_message["status"] == "approved":
|
||||
${_("Thank you, your credit is approved. Please see {provider_link} for more information.").format(
|
||||
provider_link='<a href="#" target="_blank">{}</a>'.format(credit_message["provider"]["display_name"])
|
||||
)
|
||||
}
|
||||
% elif credit_message["status"] == "rejected":
|
||||
${_("Your credit has been denied. Please contact {provider_link} for more information.").format(
|
||||
provider_link='<a href="#" target="_blank">{}</a>'.format(credit_message["provider"]["display_name"])
|
||||
)
|
||||
}
|
||||
% endif
|
||||
|
||||
</p>
|
||||
|
||||
</div>
|
||||
@@ -26,6 +26,7 @@ from .exceptions import (
|
||||
)
|
||||
from .models import (
|
||||
CreditCourse,
|
||||
CreditProvider,
|
||||
CreditRequirement,
|
||||
CreditRequirementStatus,
|
||||
CreditRequest,
|
||||
@@ -33,6 +34,7 @@ from .models import (
|
||||
)
|
||||
from .signature import signature, get_shared_secret_key
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -211,14 +213,13 @@ def create_credit_request(course_key, provider_id, username):
|
||||
|
||||
"""
|
||||
try:
|
||||
user_eligibility = CreditEligibility.objects.select_related('course', 'provider').get(
|
||||
user_eligibility = CreditEligibility.objects.select_related('course').get(
|
||||
username=username,
|
||||
course__course_key=course_key,
|
||||
provider__provider_id=provider_id
|
||||
course__course_key=course_key
|
||||
)
|
||||
credit_course = user_eligibility.course
|
||||
credit_provider = user_eligibility.provider
|
||||
except CreditEligibility.DoesNotExist:
|
||||
credit_provider = credit_course.providers.get(provider_id=provider_id)
|
||||
except (CreditEligibility.DoesNotExist, CreditProvider.DoesNotExist):
|
||||
log.warning(u'User tried to initiate a request for credit, but the user is not eligible for credit')
|
||||
raise UserIsNotEligible
|
||||
|
||||
@@ -614,3 +615,132 @@ def is_credit_course(course_key):
|
||||
return False
|
||||
|
||||
return CreditCourse.is_credit_course(course_key=course_key)
|
||||
|
||||
|
||||
def get_credit_request_status(username, course_key):
|
||||
"""Get the credit request status.
|
||||
|
||||
This function returns the status of credit request of user for given course.
|
||||
It returns the latest request status for the any credit provider.
|
||||
The valid status are 'pending', 'approved' or 'rejected'.
|
||||
|
||||
Args:
|
||||
username(str): The username of user
|
||||
course_key(CourseKey): The course locator key
|
||||
|
||||
Returns:
|
||||
A dictionary of credit request user has made if any
|
||||
|
||||
"""
|
||||
credit_request = CreditRequest.get_user_request_status(username, course_key)
|
||||
if credit_request:
|
||||
credit_status = {
|
||||
"uuid": credit_request.uuid,
|
||||
"timestamp": credit_request.modified,
|
||||
"course_key": credit_request.course.course_key,
|
||||
"provider": {
|
||||
"id": credit_request.provider.provider_id,
|
||||
"display_name": credit_request.provider.display_name
|
||||
},
|
||||
"status": credit_request.status
|
||||
}
|
||||
else:
|
||||
credit_status = {}
|
||||
return credit_status
|
||||
|
||||
|
||||
def _get_duration_and_providers(credit_course):
|
||||
"""Returns the credit providers and eligibility durations.
|
||||
|
||||
The eligibility_duration is the max of the credit duration of
|
||||
all the credit providers of given course.
|
||||
|
||||
Args:
|
||||
credit_course(CreditCourse): The CreditCourse object
|
||||
|
||||
Returns:
|
||||
Tuple of eligibility_duration and credit providers of given course
|
||||
|
||||
"""
|
||||
providers = credit_course.providers.all()
|
||||
seconds_good_for_display = 0
|
||||
providers_list = []
|
||||
for provider in providers:
|
||||
providers_list.append(
|
||||
{
|
||||
"id": provider.provider_id,
|
||||
"display_name": provider.display_name,
|
||||
"eligibility_duration": provider.eligibility_duration,
|
||||
"provider_url": provider.provider_url
|
||||
}
|
||||
)
|
||||
eligibility_duration = int(provider.eligibility_duration) if provider.eligibility_duration else 0
|
||||
seconds_good_for_display = max(eligibility_duration, seconds_good_for_display)
|
||||
|
||||
return seconds_good_for_display, providers_list
|
||||
|
||||
|
||||
def get_credit_eligibility(username):
|
||||
"""
|
||||
Returns the all the eligibility the user has meet.
|
||||
|
||||
Args:
|
||||
username(str): The username of user
|
||||
|
||||
Example:
|
||||
>> get_credit_eligibility('Aamir'):
|
||||
{
|
||||
"edX/DemoX/Demo_Course": {
|
||||
"created_at": "2015-12-21",
|
||||
"providers": [
|
||||
"id": 12,
|
||||
"display_name": "Arizona State University",
|
||||
"eligibility_duration": 60,
|
||||
"provider_url": "http://arizona/provideere/link"
|
||||
],
|
||||
"seconds_good_for_display": 90
|
||||
}
|
||||
}
|
||||
|
||||
Returns:
|
||||
A dict of eligibilities
|
||||
"""
|
||||
eligibilities = CreditEligibility.get_user_eligibility(username)
|
||||
user_credit_requests = get_credit_requests_for_user(username)
|
||||
request_dict = {}
|
||||
# Change the list to dict for iteration
|
||||
for request in user_credit_requests:
|
||||
request_dict[unicode(request["course_key"])] = request
|
||||
user_eligibilities = {}
|
||||
for eligibility in eligibilities:
|
||||
course_key = eligibility.course.course_key
|
||||
duration, providers_list = _get_duration_and_providers(eligibility.course)
|
||||
user_eligibilities[unicode(course_key)] = {
|
||||
"created_at": eligibility.created,
|
||||
"seconds_good_for_display": duration,
|
||||
"providers": providers_list,
|
||||
}
|
||||
|
||||
# Default status is requirements_meet
|
||||
user_eligibilities[unicode(course_key)]["status"] = "requirements_meet"
|
||||
# If there is some request user has made for this eligibility then update the status
|
||||
if unicode(course_key) in request_dict:
|
||||
user_eligibilities[unicode(course_key)]["status"] = request_dict[unicode(course_key)]["status"]
|
||||
user_eligibilities[unicode(course_key)]["provider"] = request_dict[unicode(course_key)]["provider"]
|
||||
|
||||
return user_eligibilities
|
||||
|
||||
|
||||
def get_purchased_credit_courses(username): # pylint: disable=unused-argument
|
||||
"""
|
||||
Returns the purchased credit courses.
|
||||
|
||||
Args:
|
||||
username(str): Username of the student
|
||||
|
||||
Returns:
|
||||
A dict of courses user has purchased from the credit provider after completion
|
||||
|
||||
"""
|
||||
# TODO: How to track the purchased courses. It requires Will's work for credit provider integration
|
||||
return {}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Deleting field 'CreditEligibility.provider'
|
||||
db.delete_column('credit_crediteligibility', 'provider_id')
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding field 'CreditEligibility.provider'
|
||||
db.add_column('credit_crediteligibility', 'provider',
|
||||
self.gf('django.db.models.fields.related.ForeignKey')(default=1, related_name='eligibilities', to=orm['credit.CreditProvider']),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'credit.creditcourse': {
|
||||
'Meta': {'object_name': 'CreditCourse'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'providers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['credit.CreditProvider']", 'symmetrical': 'False'})
|
||||
},
|
||||
'credit.crediteligibility': {
|
||||
'Meta': {'unique_together': "(('username', 'course'),)", 'object_name': 'CreditEligibility'},
|
||||
'course': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'eligibilities'", 'to': "orm['credit.CreditCourse']"}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
|
||||
},
|
||||
'credit.creditprovider': {
|
||||
'Meta': {'object_name': 'CreditProvider'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'eligibility_duration': ('django.db.models.fields.PositiveIntegerField', [], {'default': '31556970'}),
|
||||
'enable_integration': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'provider_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'provider_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'})
|
||||
},
|
||||
'credit.creditrequest': {
|
||||
'Meta': {'unique_together': "(('username', 'course', 'provider'),)", 'object_name': 'CreditRequest'},
|
||||
'course': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credit_requests'", 'to': "orm['credit.CreditCourse']"}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'parameters': ('jsonfield.fields.JSONField', [], {}),
|
||||
'provider': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credit_requests'", 'to': "orm['credit.CreditProvider']"}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '255'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'})
|
||||
},
|
||||
'credit.creditrequirement': {
|
||||
'Meta': {'unique_together': "(('namespace', 'name', 'course'),)", 'object_name': 'CreditRequirement'},
|
||||
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'course': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'credit_requirements'", 'to': "orm['credit.CreditCourse']"}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'criteria': ('jsonfield.fields.JSONField', [], {}),
|
||||
'display_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'namespace': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
'credit.creditrequirementstatus': {
|
||||
'Meta': {'object_name': 'CreditRequirementStatus'},
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'reason': ('jsonfield.fields.JSONField', [], {'default': '{}'}),
|
||||
'requirement': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'statuses'", 'to': "orm['credit.CreditRequirement']"}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
|
||||
},
|
||||
'credit.historicalcreditrequest': {
|
||||
'Meta': {'ordering': "(u'-history_date', u'-history_id')", 'object_name': 'HistoricalCreditRequest'},
|
||||
'course': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.DO_NOTHING', 'to': "orm['credit.CreditCourse']"}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
u'history_date': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
u'history_id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
u'history_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
|
||||
u'history_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'blank': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'parameters': ('jsonfield.fields.JSONField', [], {}),
|
||||
'provider': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.DO_NOTHING', 'to': "orm['credit.CreditProvider']"}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '255'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['credit']
|
||||
@@ -307,11 +307,23 @@ class CreditEligibility(TimeStampedModel):
|
||||
"""
|
||||
username = models.CharField(max_length=255, db_index=True)
|
||||
course = models.ForeignKey(CreditCourse, related_name="eligibilities")
|
||||
provider = models.ForeignKey(CreditProvider, related_name="eligibilities")
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
unique_together = ('username', 'course')
|
||||
|
||||
@classmethod
|
||||
def get_user_eligibility(cls, username):
|
||||
"""Returns the eligibilities of given user.
|
||||
|
||||
Args:
|
||||
username(str): Username of the user
|
||||
|
||||
Returns:
|
||||
CreditEligibility queryset for the user
|
||||
|
||||
"""
|
||||
return cls.objects.filter(username=username).select_related('course').prefetch_related('course__providers')
|
||||
|
||||
@classmethod
|
||||
def is_user_eligible_for_credit(cls, course_key, username):
|
||||
"""Check if the given user is eligible for the provided credit course
|
||||
@@ -361,6 +373,12 @@ class CreditRequest(TimeStampedModel):
|
||||
|
||||
history = HistoricalRecords()
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
# Enforce the constraint that each user can have exactly one outstanding
|
||||
# request to a given provider. Multiple requests use the same UUID.
|
||||
unique_together = ('username', 'course', 'provider')
|
||||
get_latest_by = 'created'
|
||||
|
||||
@classmethod
|
||||
def credit_requests_for_user(cls, username):
|
||||
"""
|
||||
@@ -402,7 +420,21 @@ class CreditRequest(TimeStampedModel):
|
||||
for request in cls.objects.select_related('course', 'provider').filter(username=username)
|
||||
]
|
||||
|
||||
class Meta(object): # pylint: disable=missing-docstring
|
||||
# Enforce the constraint that each user can have exactly one outstanding
|
||||
# request to a given provider. Multiple requests use the same UUID.
|
||||
unique_together = ('username', 'course', 'provider')
|
||||
@classmethod
|
||||
def get_user_request_status(cls, username, course_key):
|
||||
"""Returns the latest credit request of user against the given course.
|
||||
|
||||
Args:
|
||||
username(str): The username of requesting user
|
||||
course_key(CourseKey): The course identifier
|
||||
|
||||
Returns:
|
||||
CreditRequest if any otherwise None
|
||||
|
||||
"""
|
||||
try:
|
||||
return cls.objects.filter(
|
||||
username=username, course__course_key=course_key
|
||||
).select_related('course', 'provider').latest()
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"""
|
||||
Tests for the API functions in the credit app.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import datetime
|
||||
import ddt
|
||||
import pytz
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.db import connection, transaction
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from util.date_utils import from_timestamp
|
||||
from openedx.core.djangoapps.credit import api
|
||||
from openedx.core.djangoapps.credit.exceptions import (
|
||||
@@ -34,13 +35,20 @@ from openedx.core.djangoapps.credit.api import (
|
||||
set_credit_requirement_status,
|
||||
get_credit_requirement
|
||||
)
|
||||
from student.models import CourseEnrollment
|
||||
from student.views import _create_credit_availability_message
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
TEST_CREDIT_PROVIDER_SECRET_KEY = "931433d583c84ca7ba41784bad3232e6"
|
||||
|
||||
|
||||
@override_settings(CREDIT_PROVIDER_SECRET_KEYS={
|
||||
"hogwarts": TEST_CREDIT_PROVIDER_SECRET_KEY
|
||||
"hogwarts": TEST_CREDIT_PROVIDER_SECRET_KEY,
|
||||
"ASU": TEST_CREDIT_PROVIDER_SECRET_KEY,
|
||||
"MIT": TEST_CREDIT_PROVIDER_SECRET_KEY
|
||||
})
|
||||
class CreditApiTestBase(TestCase):
|
||||
"""
|
||||
@@ -212,7 +220,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
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)
|
||||
course=credit_course, username="staff"
|
||||
)
|
||||
is_eligible = api.is_user_eligible_for_credit('staff', credit_course.course_key)
|
||||
self.assertTrue(is_eligible)
|
||||
@@ -380,19 +388,26 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
|
||||
# Initial status should be "pending"
|
||||
self._assert_credit_status("pending")
|
||||
|
||||
credit_request_status = api.get_credit_request_status(self.USER_INFO['username'], self.course_key)
|
||||
self.assertEqual(credit_request_status["status"], "pending")
|
||||
|
||||
# Update the status
|
||||
api.update_credit_request_status(request["parameters"]["request_uuid"], self.PROVIDER_ID, status)
|
||||
self._assert_credit_status(status)
|
||||
|
||||
credit_request_status = api.get_credit_request_status(self.USER_INFO['username'], self.course_key)
|
||||
self.assertEqual(credit_request_status["status"], status)
|
||||
|
||||
def test_query_counts(self):
|
||||
# Yes, this is a lot of queries, but this API call is also doing a lot of work :)
|
||||
# - 1 query: Check the user's eligibility and retrieve the credit course and provider.
|
||||
# - 1 query: Check the user's eligibility and retrieve the credit course
|
||||
# - 1 Get the provider of the credit course.
|
||||
# - 2 queries: Get-or-create the credit request.
|
||||
# - 1 query: Retrieve user account and profile information from the user API.
|
||||
# - 1 query: Look up the user's final grade from the credit requirements table.
|
||||
# - 2 queries: Update the request.
|
||||
# - 2 queries: Update the history table for the request.
|
||||
with self.assertNumQueries(9):
|
||||
with self.assertNumQueries(10):
|
||||
request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username'])
|
||||
|
||||
# - 3 queries: Retrieve and update the request
|
||||
@@ -522,12 +537,131 @@ class CreditProviderIntegrationApiTests(CreditApiTestBase):
|
||||
status.save()
|
||||
|
||||
CreditEligibility.objects.create(
|
||||
username=self.USER_INFO["username"],
|
||||
course=CreditCourse.objects.get(course_key=self.course_key),
|
||||
provider=CreditProvider.objects.get(provider_id=self.PROVIDER_ID)
|
||||
username=self.USER_INFO['username'],
|
||||
course=CreditCourse.objects.get(course_key=self.course_key)
|
||||
)
|
||||
|
||||
def _assert_credit_status(self, expected_status):
|
||||
"""Check the user's credit status. """
|
||||
statuses = api.get_credit_requests_for_user(self.USER_INFO["username"])
|
||||
self.assertEqual(statuses[0]["status"], expected_status)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CreditMessagesTests(ModuleStoreTestCase, CreditApiTestBase):
|
||||
"""
|
||||
Test dashboard messages of credit course.
|
||||
"""
|
||||
|
||||
FINAL_GRADE = 0.8
|
||||
|
||||
def setUp(self):
|
||||
super(CreditMessagesTests, self).setUp()
|
||||
self.student = UserFactory()
|
||||
self.student.set_password('test') # pylint: disable=no-member
|
||||
self.student.save() # pylint: disable=no-member
|
||||
|
||||
self.client.login(username=self.student.username, password='test')
|
||||
# New Course
|
||||
self.course = CourseFactory.create()
|
||||
self.enrollment = CourseEnrollment.enroll(self.student, self.course.id)
|
||||
|
||||
def _set_creditcourse(self):
|
||||
"""
|
||||
Mark the course to credit
|
||||
|
||||
"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.first_provider = CreditProvider.objects.create(
|
||||
provider_id="ASU",
|
||||
display_name="Arizona State University",
|
||||
provider_url="google.com",
|
||||
enable_integration=True
|
||||
) # pylint: disable=attribute-defined-outside-init
|
||||
self.second_provider = CreditProvider.objects.create(
|
||||
provider_id="MIT",
|
||||
display_name="Massachusetts Institute of Technology",
|
||||
provider_url="MIT.com",
|
||||
enable_integration=True
|
||||
) # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
self.credit_course = CreditCourse.objects.create(course_key=self.course.id, enabled=True) # pylint: disable=attribute-defined-outside-init
|
||||
self.credit_course.providers.add(self.first_provider)
|
||||
self.credit_course.providers.add(self.second_provider)
|
||||
|
||||
def _set_user_eligible(self, credit_course, username):
|
||||
"""
|
||||
Mark the user eligible for credit for the given credit course.
|
||||
"""
|
||||
self.eligibility = CreditEligibility.objects.create(username=username, course=credit_course) # pylint: disable=attribute-defined-outside-init
|
||||
|
||||
def test_user_request_status(self):
|
||||
request_status = api.get_credit_request_status(self.student.username, self.course.id)
|
||||
self.assertEqual(len(request_status), 0)
|
||||
|
||||
def test_credit_messages(self):
|
||||
self._set_creditcourse()
|
||||
|
||||
requirement = CreditRequirement.objects.create(
|
||||
course=self.credit_course,
|
||||
namespace="grade",
|
||||
name="grade",
|
||||
active=True
|
||||
)
|
||||
status = CreditRequirementStatus.objects.create(
|
||||
username=self.student.username,
|
||||
requirement=requirement,
|
||||
)
|
||||
status.status = "satisfied"
|
||||
status.reason = {"final_grade": self.FINAL_GRADE}
|
||||
status.save()
|
||||
|
||||
self._set_user_eligible(self.credit_course, self.student.username)
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
self.assertContains(
|
||||
response,
|
||||
"<b>Congratulations</b> {}, You have meet requirements for credit.".format(
|
||||
self.student.get_full_name() # pylint: disable=no-member
|
||||
)
|
||||
)
|
||||
|
||||
api.create_credit_request(self.course.id, self.first_provider.provider_id, self.student.username)
|
||||
|
||||
response = self.client.get(reverse("dashboard"))
|
||||
self.assertContains(
|
||||
response,
|
||||
'Thank you, your payment is complete, your credit is processing. '
|
||||
'Please see {provider_link} for more information.'.format(
|
||||
provider_link='<a href="#" target="_blank">{provider_name}</a>'.format(
|
||||
provider_name=self.first_provider.display_name
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def test_query_counts(self):
|
||||
# This check the number of queries executed while rendering the
|
||||
# credit message to display on the dashboard.
|
||||
# - 1 query: Check the user's eligibility.
|
||||
# - 1 query: Get the user credit requests.
|
||||
|
||||
self._set_creditcourse()
|
||||
|
||||
requirement = CreditRequirement.objects.create(
|
||||
course=self.credit_course,
|
||||
namespace="grade",
|
||||
name="grade",
|
||||
active=True
|
||||
)
|
||||
status = CreditRequirementStatus.objects.create(
|
||||
username=self.student.username,
|
||||
requirement=requirement,
|
||||
)
|
||||
status.status = "satisfied"
|
||||
status.reason = {"final_grade": self.FINAL_GRADE}
|
||||
status.save()
|
||||
|
||||
with self.assertNumQueries(2):
|
||||
enrollment_dict = {unicode(self.course.id): self.course}
|
||||
_create_credit_availability_message(
|
||||
enrollment_dict, self.student
|
||||
)
|
||||
|
||||
@@ -99,7 +99,6 @@ class CreditProviderViewTests(UrlResetMixin, TestCase):
|
||||
CreditEligibility.objects.create(
|
||||
username=self.USERNAME,
|
||||
course=credit_course,
|
||||
provider=credit_provider,
|
||||
)
|
||||
|
||||
def test_credit_request_and_response(self):
|
||||
|
||||
Reference in New Issue
Block a user