From 06b085f8b496a42db1d1c0ff012869408102241c Mon Sep 17 00:00:00 2001
From: Ahsan Ulhaq <ahsan.haq@arbisoft.com>
Date: Thu, 9 Jul 2015 14:32:28 +0500
Subject: [PATCH] credit eligibility and payment receipt email

ECOM-1796
ECOM-1525
---
 lms/envs/aws.py                               |   4 +
 lms/envs/common.py                            |   7 +
 .../credit_eligibility_email.html             |  80 +++++++++
 .../credit_eligibility_email.txt              |  16 ++
 .../credit_notification.css                   |  38 ++++
 .../credit_notifications/edx-logo-header.png  | Bin 0 -> 1894 bytes
 .../core/djangoapps/credit/api/eligibility.py |   8 +-
 openedx/core/djangoapps/credit/email_utils.py | 150 ++++++++++++++++
 ...eligibility_email_message__add_field_cr.py | 165 ++++++++++++++++++
 openedx/core/djangoapps/credit/models.py      |  24 ++-
 .../core/djangoapps/credit/tests/test_api.py  |  27 ++-
 .../djangoapps/credit/tests/test_signals.py   |   3 +
 requirements/edx/base.txt                     |   3 +
 13 files changed, 520 insertions(+), 5 deletions(-)
 create mode 100644 lms/templates/credit_notifications/credit_eligibility_email.html
 create mode 100644 lms/templates/credit_notifications/credit_eligibility_email.txt
 create mode 100644 lms/templates/credit_notifications/credit_notification.css
 create mode 100644 lms/templates/credit_notifications/edx-logo-header.png
 create mode 100644 openedx/core/djangoapps/credit/email_utils.py
 create mode 100644 openedx/core/djangoapps/credit/migrations/0016_auto__add_field_creditprovider_eligibility_email_message__add_field_cr.py

diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 4952abed45b..bf0deab099f 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -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'):
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 0189b7dbc75..36b6fbcfe90 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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"
diff --git a/lms/templates/credit_notifications/credit_eligibility_email.html b/lms/templates/credit_notifications/credit_eligibility_email.html
new file mode 100644
index 00000000000..3d5ee38bbb1
--- /dev/null
+++ b/lms/templates/credit_notifications/credit_eligibility_email.html
@@ -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>
diff --git a/lms/templates/credit_notifications/credit_eligibility_email.txt b/lms/templates/credit_notifications/credit_eligibility_email.txt
new file mode 100644
index 00000000000..2dde4b702f3
--- /dev/null
+++ b/lms/templates/credit_notifications/credit_eligibility_email.txt
@@ -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")}
diff --git a/lms/templates/credit_notifications/credit_notification.css b/lms/templates/credit_notifications/credit_notification.css
new file mode 100644
index 00000000000..ae80551d6ca
--- /dev/null
+++ b/lms/templates/credit_notifications/credit_notification.css
@@ -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;
+}
diff --git a/lms/templates/credit_notifications/edx-logo-header.png b/lms/templates/credit_notifications/edx-logo-header.png
new file mode 100644
index 0000000000000000000000000000000000000000..486771df260fdc64862dedf186d4816b30fd96a7
GIT binary patch
literal 1894
zcmV-s2buVZP)<h;3K|Lk000e1NJLTq003P8001ut1^@s6ft$t&0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU(2uVaiRCwC#T1{*lRS<s8Pm=~W-Inx#
zXsA;P$^juq98pzU<xsd_ONb+}FK}!uRlUZ!R!DY^oV?-4C58)!g1iAPRIR#ITTua`
zD@;XIMPvyHB*ck%vpbut?cIH|Kl#N|y>9D$o?U-$-pn^MFV<`}Sy%Q}`rxlvEOw|~
z`o_nl6u+dL?tQg1#BTDjEyEIPjwk&+AU9g<_)M-R;7-U_SDKS&mA*r2ECBfQF6u8j
ze3pplnMzN0v+uohLGOaz1-&a>(2p2pJcJ?x3uybS27kSAVx65ozOnt`XKTak53xR#
zj{;#%8viZvQNgR1RQZ^-{a-UEIwVJjji<YCcgHMmu{W;WICVuL05s#s&mEV%UUSP6
zIu<E6s@p#VyhB;~m#==5K>iA%zy0GUYQ#V$ZD48FuN5@EIz~2EG_<yP!PIFErp7^%
z1%OuwfCJFkcEBz1W$7Gb0AZ4kq{o9b-1-ZA5oDhrXZYn^)Sd9KAkaU@7eN(SI?Uzs
z(j2TJA-U1fI#n0IbLdt?0y-3@8wTDK&^g#v2AJ|`XgXS1nJ<9u0yJ3i6dzX!E}#QY
za0?t%MF_eH8d!Tqpc9N{qQgq%IvCnr!aEiCcgt=!g`jh=CES*&ke&j5NS*_)I9PVc
z!>XxL^#Icjx`Y8YDO}9lXxauieD5*AC}jS<yw?Q4yGP(k9!CJURkx^dokbhqC0x)%
z1o|_iS*G=7aR++Wc1+)GUnmjast9mT%E-Z?`YyjR&2^u<|KYd+!M@O(+^EKZ%vu!F
zYbKw!z^|7GJ43h8N&m?aT6k%<e)CFmHMe%};r;-=1H3}u<qQpv@~&w&rXmC#eK=(+
zwFfiBBK}^|;C~)89<kMLzh@gyo>F6h;*bbM0el&gtgw=IWjjRoQXv8kFFV`L%aONd
zkco8o>Yp1LTWhYH7XS4(?9>PEy8%6e0Z?<`?N=jnP2ht~R+0>;p)BO}UOXLXVU1wb
zz(O<u#gvd@PHj8A<}y<^9<<DhzjcufjK9*76>^}3Ss{X7A%eBTUYxl%m6>=m9HO+d
zm#ZS{=3u8qfP(-h=c8b3J<EJI>mBp{zSTl|^Ax~AFeyr7q7>HJeFZ_=Zp;RKWKWx0
zaHT^W#JpeSn32lhQuM7qJMM2Sudv3VQI0BbAbo99KHmaMj{tbpZXjV&d_RFy#B;MD
zep2DL)2r^m_11lr16U5C7=bUaXhdgwBo^qG1a3tO`T*eMt5;rk&zgSi3-x1hhiIWt
zFoP*IFEPuyW-JIK7JKmuNJ;K~YC#{c`RPy6o$(dNdxNn7cBbd~C^FtT7PM1uf;aCo
zkXnnB4Fh^Y@PNS5ySCXLFn3HQb~36RtFYk5BZ8J9GDhW+T7&n>NTU{@if#gyj)6SI
z_Cw@)DP(O4YVer@2l{~(vo<~zfu;(^go6WmJT~N67O*Jk9s^DdoWP8#gupuQC$RR}
z$iACjQ^U}y*x-OPl$=NcA8)Sz+j4Ie7UUUI+!`D{1Dl_~Ng)QZQJ<0PGGgQPj0DDp
zbX1@#m|X@E=nNfaY{`{ULQ--qwCzs%m`kgTTl^oAAi16=d@p6hh}>v_>Rvwg%Ga=#
z5|l^l0v;XeP$r(>O%kn~Qm^UEIL(ZeLJy}fR**oi3X_3w;-x=_zy;bxj9U9Q!N?J2
zrj15NXQ@D8)Ec}Xu-dDPqO%+Y?q+<Y>)3&s+#Kwc9-Vxa)VEscV{G6UJMqqC%evC`
z?miK8N|QmkKJwlS(U0eeM=)}P-7@A>pJ}f$&IlBhIEWRhtKsveaEGLLf|PvmX+PU|
z@_WnZ8#!&?O*L#(lPsK%d5YyO_QtIsM+uxVk<o~Z!!ua7oCY9RyA^a{fvQfdgX^%G
z7#(`l#>4GuBUm{!G2rTuT^h$KT-=GT;~L*JUtnLHc~dkEn#vPC8Bn0p0Ubv$u*7@U
zM;$`Jt)$y%mFst*#Cv<=p25$-tsFalPP9c;8?%xq0rSpZgY5Ro0DF1RJkNM@d6=>1
z;;oa0;;i}71$SEP)SNK=bg7LdQpyK*-E+;!vvNzj4yuV9_=1nX=_>$2+W%VrIQ!wk
zx(IM<i~0_*ON}1(%Rl|buu?F#EKCC^>?hFswCP@O%t*2EkY~FjQfyd`U%t4v8R!8M
z$1=b!0xx3+DoseQ|6icnfS+Jt8w5N4xnQ;YAYtMo%cAT0s7C^H1Ri>$W5`y5)<=xR
zbK>~erW<3y&!6bXbS4fH=%n-M9Izt46fxPcjxtLE%L8UJD{A%O*_J$~gRrm}wsRC)
zl;J|@qKah(cH}^Bb2&27OGkRxelpibjRwFk<6thO0o@~OP46;RY6Q>Fwv(f-93Y?G
gl`iN<M*b6E0FoQksGlIam;e9(07*qoM6N<$f*kjTwg3PC

literal 0
HcmV?d00001

diff --git a/openedx/core/djangoapps/credit/api/eligibility.py b/openedx/core/djangoapps/credit/api/eligibility.py
index 551b978a5b2..91aa3f79e4c 100644
--- a/openedx/core/djangoapps/credit/api/eligibility.py
+++ b/openedx/core/djangoapps/credit/api/eligibility.py
@@ -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):
diff --git a/openedx/core/djangoapps/credit/email_utils.py b/openedx/core/djangoapps/credit/email_utils.py
new file mode 100644
index 00000000000..d3dd58ff384
--- /dev/null
+++ b/openedx/core/djangoapps/credit/email_utils.py
@@ -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)
diff --git a/openedx/core/djangoapps/credit/migrations/0016_auto__add_field_creditprovider_eligibility_email_message__add_field_cr.py b/openedx/core/djangoapps/credit/migrations/0016_auto__add_field_creditprovider_eligibility_email_message__add_field_cr.py
new file mode 100644
index 00000000000..64417e83981
--- /dev/null
+++ b/openedx/core/djangoapps/credit/migrations/0016_auto__add_field_creditprovider_eligibility_email_message__add_field_cr.py
@@ -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']
\ No newline at end of file
diff --git a/openedx/core/djangoapps/credit/models.py b/openedx/core/djangoapps/credit/models.py
index a9c936f63ae..11c82d6e132 100644
--- a/openedx/core/djangoapps/credit/models.py
+++ b/openedx/core/djangoapps/credit/models.py
@@ -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):
diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py
index 7a2c7f6e8a7..1711d6fad7a 100644
--- a/openedx/core/djangoapps/credit/tests/test_api.py
+++ b/openedx/core/djangoapps/credit/tests/test_api.py
@@ -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",
diff --git a/openedx/core/djangoapps/credit/tests/test_signals.py b/openedx/core/djangoapps/credit/tests/test_signals.py
index 8499a9267d2..f6013f6e25a 100644
--- a/openedx/core/djangoapps/credit/tests/test_signals.py
+++ b/openedx/core/djangoapps/credit/tests/test_signals.py
@@ -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.
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index efa4f34121f..f3f4239b681 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -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
-- 
GitLab