credit eligibility and payment receipt email
ECOM-1796 ECOM-1525
This commit is contained in:
@@ -331,6 +331,10 @@ FOOTER_ORGANIZATION_IMAGE = ENV_TOKENS.get('FOOTER_ORGANIZATION_IMAGE', FOOTER_O
|
||||
FOOTER_CACHE_TIMEOUT = ENV_TOKENS.get('FOOTER_CACHE_TIMEOUT', FOOTER_CACHE_TIMEOUT)
|
||||
FOOTER_BROWSER_CACHE_MAX_AGE = ENV_TOKENS.get('FOOTER_BROWSER_CACHE_MAX_AGE', FOOTER_BROWSER_CACHE_MAX_AGE)
|
||||
|
||||
# Credit notifications settings
|
||||
NOTIFICATION_EMAIL_CSS = ENV_TOKENS.get('NOTIFICATION_EMAIL_CSS', NOTIFICATION_EMAIL_CSS)
|
||||
NOTIFICATION_EMAIL_EDX_LOGO = ENV_TOKENS.get('NOTIFICATION_EMAIL_EDX_LOGO', NOTIFICATION_EMAIL_EDX_LOGO)
|
||||
|
||||
############# CORS headers for cross-domain requests #################
|
||||
|
||||
if FEATURES.get('ENABLE_CORS_HEADERS') or FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'):
|
||||
|
||||
@@ -1097,6 +1097,9 @@ FOOTER_CACHE_TIMEOUT = 30 * 60
|
||||
# Max age cache control header for the footer (controls browser caching).
|
||||
FOOTER_BROWSER_CACHE_MAX_AGE = 5 * 60
|
||||
|
||||
# Credit api notification cache timeout
|
||||
CREDIT_NOTIFICATION_CACHE_TIMEOUT = 5 * 60 * 60
|
||||
|
||||
################################# Deprecation warnings #####################
|
||||
|
||||
# Ignore deprecation warnings (so we don't clutter Jenkins builds/production)
|
||||
@@ -2572,3 +2575,7 @@ LTI_USER_EMAIL_DOMAIN = 'lti.example.com'
|
||||
# Number of seconds before JWT tokens expire
|
||||
JWT_EXPIRATION = 30
|
||||
JWT_ISSUER = None
|
||||
|
||||
# Credit notifications settings
|
||||
NOTIFICATION_EMAIL_CSS = "templates/credit_notifications/credit_notification.css"
|
||||
NOTIFICATION_EMAIL_EDX_LOGO = "templates/credit_notifications/edx-logo-header.png"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head lang="en">
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<table class="cn-container">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="cn-body">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="cn-img-wrapper">
|
||||
<a target="_blank" title="" href="#">
|
||||
<img class="cn-img" src="cid:${branded_logo}">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td class="cn-content-clear"></td></tr>
|
||||
|
||||
<tr>
|
||||
<td class="cn-content">
|
||||
<p>
|
||||
${_("Hi {name},").format(name=full_name)}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
${_("Congratulations! You are eligible to receive university credit from edX partners! Click {link} to get your credit now.").format(
|
||||
link=u'<a href="{dashboard_url}">here</a>'.format(
|
||||
dashboard_url=dashboard_link
|
||||
))
|
||||
}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
${_("Credit from can help you get a jump start on your university degree, finish a degree already started, or fulfill requirements at a different academic institution.")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
${_('To get university credit for {course_name}, simply go to your {link} and click the yellow "Get Credit" button. No application, transcript, or grade report is required.').format(
|
||||
course_name=course_name,
|
||||
link=u'<a href="{dashboard_url}">edX dashboard</a>'.format(
|
||||
dashboard_url=dashboard_link
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
${_("We hope you enjoyed the course, and we hope to see you in future edX courses!")}<br/>
|
||||
${_("The edX team")}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr><td class="cn-content-clear cn-footer"></td></tr>
|
||||
|
||||
<tr>
|
||||
<td class="cn-footer-content">
|
||||
<p>
|
||||
<a href="${credit_course_link}"> ${_("Find more edX courses you can take for university credit.")} </a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<img src="${tracking_pixel}"/>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
${_("Hi {name},").format(name=full_name)}
|
||||
|
||||
${_("Congratulations! You are eligible to receive university credit from edX and our partners!")}
|
||||
|
||||
${_("Click on the link below to get your credit now")}
|
||||
|
||||
${dashboard_link}
|
||||
|
||||
${_("Credit from can help you get a jump start on your university degree, finish a degree already started, or fulfill requirements at a different academic institution.")}
|
||||
|
||||
${_('To get university credit for {course_name}, simply go to your edX dashboard and click the yellow "Get Credit" button. No application, transcript, or grade report is required.').format(course_name=course_name)}
|
||||
|
||||
${_("We hope you enjoyed the course, and we hope to see you in future edX courses!")}
|
||||
|
||||
${_("The edX team")}
|
||||
38
lms/templates/credit_notifications/credit_notification.css
Normal file
38
lms/templates/credit_notifications/credit_notification.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.cn-container {
|
||||
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
width: 600px;
|
||||
font-size: 14px;
|
||||
line-height: 150%;
|
||||
border: 2px solid #eeeeee;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cn-container .cn-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cn-img-wrapper {
|
||||
padding: 9px 0;
|
||||
}
|
||||
|
||||
.cn-img-wrapper .cn-img {
|
||||
float: left;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.cn-content-clear {
|
||||
padding: 0px 18px 18px;
|
||||
border-top: 3px solid #1d9fd9;
|
||||
}
|
||||
|
||||
.cn-content-clear .cn-footer {
|
||||
border-top-width: 6px;
|
||||
}
|
||||
|
||||
.cn-content p {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.cn-footer-content p {
|
||||
padding: 5px 0;
|
||||
}
|
||||
BIN
lms/templates/credit_notifications/edx-logo-header.png
Normal file
BIN
lms/templates/credit_notifications/edx-logo-header.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -6,6 +6,7 @@ whether a user has satisfied those requirements.
|
||||
import logging
|
||||
|
||||
from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements, InvalidCreditCourse
|
||||
from openedx.core.djangoapps.credit.email_utils import send_credit_notifications
|
||||
from openedx.core.djangoapps.credit.models import (
|
||||
CreditCourse,
|
||||
CreditRequirement,
|
||||
@@ -275,7 +276,12 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name,
|
||||
# If we're marking this requirement as "satisfied", there's a chance
|
||||
# that the user has met all eligibility requirements.
|
||||
if status == "satisfied":
|
||||
CreditEligibility.update_eligibility(reqs, username, course_key)
|
||||
is_eligible, eligibility_record_created = CreditEligibility.update_eligibility(reqs, username, course_key)
|
||||
if eligibility_record_created and is_eligible:
|
||||
try:
|
||||
send_credit_notifications(username, course_key)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.error("Error sending email")
|
||||
|
||||
|
||||
def get_credit_requirement_status(course_key, username, namespace=None, name=None):
|
||||
|
||||
150
openedx/core/djangoapps/credit/email_utils.py
Normal file
150
openedx/core/djangoapps/credit/email_utils.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
This file contains utility functions which will responsible for sending emails.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import logging
|
||||
import pynliner
|
||||
import urlparse
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.cache import cache
|
||||
from django.core.mail import EmailMessage
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from email.mime.image import MIMEImage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from eventtracking import tracker
|
||||
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from microsite_configuration import microsite
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_credit_notifications(username, course_key):
|
||||
"""Sends email notification to user on different phases during credit
|
||||
course e.g., credit eligibility, credit payment etc.
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
log.error('No user with %s exist', username)
|
||||
return
|
||||
|
||||
course = modulestore().get_course(course_key, depth=0)
|
||||
course_display_name = course.display_name
|
||||
branded_logo = dict(title='Logo', path=settings.NOTIFICATION_EMAIL_EDX_LOGO, cid=str(uuid.uuid4()))
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
tracking_id = str(tracking_context.get('user_id'))
|
||||
client_id = str(tracking_context.get('client_id'))
|
||||
events = '&t=event&ec=email&ea=open'
|
||||
tracking_pixel = 'https://www.google-analytics.com/collect?v=1&tid' + tracking_id + '&cid' + client_id + events
|
||||
dashboard_link = _email_url_parser('dashboard')
|
||||
credit_course_link = _email_url_parser('courses', "?type=credit")
|
||||
context = {
|
||||
'full_name': user.get_full_name(),
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
'course_name': course_display_name,
|
||||
'branded_logo': branded_logo['cid'],
|
||||
'dashboard_link': dashboard_link,
|
||||
'credit_course_link': credit_course_link,
|
||||
'tracking_pixel': tracking_pixel,
|
||||
}
|
||||
|
||||
# create the root email message
|
||||
notification_msg = MIMEMultipart('related')
|
||||
# add 'alternative' part to root email message to encapsulate the plain and
|
||||
# HTML versions, so message agents can decide which they want to display.
|
||||
msg_alternative = MIMEMultipart('alternative')
|
||||
notification_msg.attach(msg_alternative)
|
||||
# render the credit notification templates
|
||||
subject = _("Course Credit Eligibility")
|
||||
|
||||
# add alternative plain text message
|
||||
email_body_plain = render_to_string('credit_notifications/credit_eligibility_email.txt', context)
|
||||
msg_alternative.attach(MIMEText(email_body_plain, _subtype='plain'))
|
||||
|
||||
# add alternative html message
|
||||
email_body = cache.get('css-email-body')
|
||||
if not email_body:
|
||||
email_body = with_inline_css(
|
||||
render_to_string("credit_notifications/credit_eligibility_email.html", context)
|
||||
)
|
||||
cache.set('css-email-body', email_body, settings.CREDIT_NOTIFICATION_CACHE_TIMEOUT)
|
||||
|
||||
msg_alternative.attach(MIMEText(email_body, _subtype='html'))
|
||||
# add images
|
||||
logo_image = cache.get('attached-logo-email')
|
||||
if not logo_image:
|
||||
logo_image = attach_image(branded_logo, 'Header Logo')
|
||||
if logo_image:
|
||||
notification_msg.attach(logo_image)
|
||||
cache.set('attached-logo-email', logo_image, settings.CREDIT_NOTIFICATION_CACHE_TIMEOUT)
|
||||
|
||||
from_address = microsite.get_value('default_from_email', settings.DEFAULT_FROM_EMAIL)
|
||||
to_address = user.email
|
||||
|
||||
# send the root email message
|
||||
msg = EmailMessage(subject, None, from_address, [to_address])
|
||||
msg.attach(notification_msg)
|
||||
msg.send()
|
||||
|
||||
|
||||
def with_inline_css(html_without_css):
|
||||
"""Returns html with inline css if the css file path exists
|
||||
else returns html with out the inline css.
|
||||
"""
|
||||
css_filepath = settings.NOTIFICATION_EMAIL_CSS
|
||||
if not css_filepath.startswith('/'):
|
||||
css_filepath = finders.FileSystemFinder().find(settings.NOTIFICATION_EMAIL_CSS)
|
||||
|
||||
if css_filepath:
|
||||
with open(css_filepath, "r") as _file:
|
||||
css_content = _file.read()
|
||||
|
||||
# insert style tag in the html and run pyliner.
|
||||
html_with_inline_css = pynliner.fromString('<style>' + css_content + '</style>' + html_without_css)
|
||||
return html_with_inline_css
|
||||
|
||||
return html_without_css
|
||||
|
||||
|
||||
def attach_image(img_dict, filename):
|
||||
"""
|
||||
Attach images in the email headers.
|
||||
"""
|
||||
img_path = img_dict['path']
|
||||
if not img_path.startswith('/'):
|
||||
img_path = finders.FileSystemFinder().find(img_path)
|
||||
|
||||
if img_path:
|
||||
with open(img_path, 'rb') as img:
|
||||
msg_image = MIMEImage(img.read(), name=os.path.basename(img_path))
|
||||
msg_image.add_header('Content-ID', '<{}>'.format(img_dict['cid']))
|
||||
msg_image.add_header("Content-Disposition", "inline", filename=filename)
|
||||
return msg_image
|
||||
|
||||
|
||||
def _email_url_parser(url_name, extra_param=None):
|
||||
"""Parse url according to 'SITE_NAME' which will be used in the mail.
|
||||
|
||||
Args:
|
||||
url_name(str): Name of the url to be parsed
|
||||
extra_param(str): Any extra parameters to be added with url if any
|
||||
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
site_name = microsite.get_value('SITE_NAME', settings.SITE_NAME)
|
||||
dashboard_url_path = reverse(url_name) + extra_param if extra_param else reverse(url_name)
|
||||
dashboard_link_parts = ("https", site_name, dashboard_url_path, '', '', '')
|
||||
return urlparse.urlunparse(dashboard_link_parts)
|
||||
@@ -0,0 +1,165 @@
|
||||
# -*- 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):
|
||||
# Adding field 'CreditProvider.eligibility_email_message'
|
||||
db.add_column('credit_creditprovider', 'eligibility_email_message',
|
||||
self.gf('django.db.models.fields.TextField')(default=''),
|
||||
keep_default=False)
|
||||
|
||||
# Adding field 'CreditProvider.receipt_email_message'
|
||||
db.add_column('credit_creditprovider', 'receipt_email_message',
|
||||
self.gf('django.db.models.fields.TextField')(default=''),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'CreditProvider.eligibility_email_message'
|
||||
db.delete_column('credit_creditprovider', 'eligibility_email_message')
|
||||
|
||||
# Deleting field 'CreditProvider.receipt_email_message'
|
||||
db.delete_column('credit_creditprovider', 'receipt_email_message')
|
||||
|
||||
|
||||
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'})
|
||||
},
|
||||
'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'}),
|
||||
'deadline': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2016, 7, 9, 0, 0)'}),
|
||||
'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_email_message': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'enable_integration': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'fulfillment_instructions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'provider_description': ('django.db.models.fields.TextField', [], {'default': "''"}),
|
||||
'provider_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'provider_status_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'}),
|
||||
'provider_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'}),
|
||||
'receipt_email_message': ('django.db.models.fields.TextField', [], {'default': "''"})
|
||||
},
|
||||
'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'}),
|
||||
'order': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
|
||||
},
|
||||
'credit.creditrequirementstatus': {
|
||||
'Meta': {'unique_together': "(('username', 'requirement'),)", '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'})
|
||||
},
|
||||
'credit.historicalcreditrequirementstatus': {
|
||||
'Meta': {'ordering': "(u'-history_date', u'-history_id')", 'object_name': 'HistoricalCreditRequirementStatus'},
|
||||
'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'}),
|
||||
'reason': ('jsonfield.fields.JSONField', [], {'default': '{}'}),
|
||||
'requirement': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.DO_NOTHING', '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'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['credit']
|
||||
@@ -111,6 +111,24 @@ class CreditProvider(TimeStampedModel):
|
||||
)
|
||||
)
|
||||
|
||||
eligibility_email_message = models.TextField(
|
||||
default="",
|
||||
help_text=ugettext_lazy(
|
||||
"Plain text or html content for displaying custom message inside "
|
||||
"credit eligibility email content which is sent when user has met "
|
||||
"all credit eligibility requirements."
|
||||
)
|
||||
)
|
||||
|
||||
receipt_email_message = models.TextField(
|
||||
default="",
|
||||
help_text=ugettext_lazy(
|
||||
"Plain text or html content for displaying custom message inside "
|
||||
"credit receipt email content which is sent *after* paying to get "
|
||||
"credit for a credit course."
|
||||
)
|
||||
)
|
||||
|
||||
CREDIT_PROVIDERS_CACHE_KEY = "credit.providers.list"
|
||||
|
||||
@classmethod
|
||||
@@ -479,6 +497,7 @@ class CreditEligibility(TimeStampedModel):
|
||||
username (str): Identifier of the user being updated.
|
||||
course_key (CourseKey): Identifier of the course.
|
||||
|
||||
Returns: tuple
|
||||
"""
|
||||
# Check all requirements for the course to determine if the user
|
||||
# is eligible. We need to check all the *requirements*
|
||||
@@ -497,8 +516,11 @@ class CreditEligibility(TimeStampedModel):
|
||||
username=username,
|
||||
course=CreditCourse.objects.get(course_key=course_key),
|
||||
)
|
||||
return is_eligible, True
|
||||
except IntegrityError:
|
||||
pass
|
||||
return is_eligible, False
|
||||
else:
|
||||
return is_eligible, False
|
||||
|
||||
@classmethod
|
||||
def get_user_eligibilities(cls, username):
|
||||
|
||||
@@ -9,10 +9,12 @@ import pytz
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.db import connection, transaction
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from unittest import skipUnless
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
@@ -34,6 +36,8 @@ from openedx.core.djangoapps.credit.models import (
|
||||
CreditEligibility
|
||||
)
|
||||
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"
|
||||
@@ -46,7 +50,7 @@ from util.testing import UrlResetMixin
|
||||
"ASU": TEST_CREDIT_PROVIDER_SECRET_KEY,
|
||||
"MIT": TEST_CREDIT_PROVIDER_SECRET_KEY
|
||||
})
|
||||
class CreditApiTestBase(TestCase):
|
||||
class CreditApiTestBase(ModuleStoreTestCase):
|
||||
"""
|
||||
Base class for test cases of the credit API.
|
||||
"""
|
||||
@@ -58,6 +62,14 @@ class CreditApiTestBase(TestCase):
|
||||
PROVIDER_DESCRIPTION = "A new model for the Witchcraft and Wizardry School System."
|
||||
ENABLE_INTEGRATION = True
|
||||
FULFILLMENT_INSTRUCTIONS = "Sample fulfillment instruction for credit completion."
|
||||
USER_INFO = {
|
||||
"username": "bob",
|
||||
"email": "bob@example.com",
|
||||
"password": "test_bob",
|
||||
"full_name": "Bob",
|
||||
"mailing_address": "123 Fake Street, Cambridge MA",
|
||||
"country": "US",
|
||||
}
|
||||
|
||||
def setUp(self, **kwargs):
|
||||
super(CreditApiTestBase, self).setUp()
|
||||
@@ -80,6 +92,7 @@ class CreditApiTestBase(TestCase):
|
||||
return credit_course
|
||||
|
||||
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
|
||||
@ddt.ddt
|
||||
class CreditRequirementApiTests(CreditApiTestBase):
|
||||
"""
|
||||
@@ -305,6 +318,8 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
def test_satisfy_all_requirements(self):
|
||||
# Configure a course with two credit requirements
|
||||
self.add_credit_course()
|
||||
CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
|
||||
|
||||
requirements = [
|
||||
{
|
||||
"namespace": "grade",
|
||||
@@ -323,10 +338,12 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
]
|
||||
api.set_credit_requirements(self.course_key, requirements)
|
||||
|
||||
user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password'])
|
||||
|
||||
# Satisfy one of the requirements, but not the other
|
||||
with self.assertNumQueries(7):
|
||||
api.set_credit_requirement_status(
|
||||
"bob",
|
||||
user.username,
|
||||
self.course_key,
|
||||
requirements[0]["namespace"],
|
||||
requirements[0]["name"]
|
||||
@@ -336,7 +353,7 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key))
|
||||
|
||||
# Satisfy the other requirement
|
||||
with self.assertNumQueries(10):
|
||||
with self.assertNumQueries(11):
|
||||
api.set_credit_requirement_status(
|
||||
"bob",
|
||||
self.course_key,
|
||||
@@ -347,6 +364,10 @@ class CreditRequirementApiTests(CreditApiTestBase):
|
||||
# Now the user should be eligible
|
||||
self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key))
|
||||
|
||||
# Credit eligible mail should be sent
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
self.assertEqual(mail.outbox[0].subject, 'Course Credit Eligibility')
|
||||
|
||||
# The user should remain eligible even if the requirement status is later changed
|
||||
api.set_credit_requirement_status(
|
||||
"bob",
|
||||
|
||||
@@ -6,7 +6,9 @@ import pytz
|
||||
import ddt
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.test.client import RequestFactory
|
||||
from unittest import skipUnless
|
||||
|
||||
from openedx.core.djangoapps.credit.api import (
|
||||
set_credit_requirements, get_credit_requirement_status
|
||||
@@ -19,6 +21,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
|
||||
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
|
||||
@ddt.ddt
|
||||
class TestMinGradedRequirementStatus(ModuleStoreTestCase):
|
||||
"""Test cases to check the minimum grade requirement status updated.
|
||||
|
||||
@@ -156,3 +156,6 @@ analytics-python==0.4.4
|
||||
# Needed for mailchimp(mailing djangoapp)
|
||||
mailsnake==1.6.2
|
||||
jsonfield==1.0.3
|
||||
|
||||
# Inlines CSS styles into HTML for email notifications.
|
||||
pynliner==0.5.2
|
||||
|
||||
Reference in New Issue
Block a user