Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,5 @@ Eugeny Kolpakov <eugeny.kolpakov@gmail.com>
Omar Al-Ithawi <oithawi@qrf.org>
Louis Pilfold <louis@lpil.uk>
Akiva Leffert <akiva@edx.org>
Mike Bifulco <mbifulco@aquent.com>
Mike Bifulco <mbifulco@aquent.com>
Se Won Jang <swjang@stanford.edu>
14 changes: 14 additions & 0 deletions cms/djangoapps/contentstore/tests/test_course_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import copy
import mock
from mock import patch
from smtplib import SMTPException

from django.utils.timezone import UTC
from django.test.utils import override_settings
from django.conf import settings
from django.core.management import call_command

from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
Expand Down Expand Up @@ -47,6 +49,7 @@ def test_virgin_fetch(self):
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
self.assertFalse(details.enable_enrollment_email, "Enrollment Email should be initialized as false")

def test_encoder(self):
details = CourseDetails.fetch(self.course.id)
Expand Down Expand Up @@ -166,6 +169,17 @@ def test_regular_site_fetch(self):
self.assertContains(response, "Course Introduction Video")
self.assertContains(response, "Requirements")

def test_send_test_enrollment_email(self):
call_command("loaddata", "course_email_template.json")
response = self.client.post(reverse_course_url('send_test_enrollment_email', self.course.id), {'subject': 'Test subject', 'message': 'Test body'})
self.assertEquals(response.status_code, 200)

def test_send_test_enrollment_email_failure(self):
call_command("loaddata", "course_email_template.json")
with patch('django.contrib.auth.models.User.email_user', side_effect=SMTPException):
response = self.client.post(reverse_course_url('send_test_enrollment_email', self.course.id), {'subject': 'Test subject', 'message': 'Test body'})
self.assertContains(response, 'Error while sending test email.', status_code=400)


class CourseDetailsViewTest(CourseTestCase):
"""
Expand Down
32 changes: 32 additions & 0 deletions cms/djangoapps/contentstore/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,38 @@ def lms_link_test(self):
link = utils.get_lms_link_for_item(location)
self.assertEquals(link, "//localhost:8000/courses/mitX/101/test/jump_to/i4x://mitX/101/course/test")

def test_dashboard_link(self):
""" Tests get_lms_link_for_dashboard. """
dashboard_link = utils.get_lms_link_for_dashboard()
self.assertEquals(dashboard_link, u"https://localhost:8000/dashboard")

@override_settings(LMS_BASE=None)
def test_dashboard_link_no_lms_base(self):
""" Tests get_lms_link_for_dashboard with no LMS_BASE. """
self.assertEquals(utils.get_lms_link_for_dashboard(), None)

def test_login_link(self):
""" Tests get_lms_link_for_login. """
login_link = utils.get_lms_link_for_login()
self.assertEquals(login_link, u"https://localhost:8000/login")

@override_settings(LMS_BASE=None)
def test_login_link_no_lms_base(self):
""" Tests get_lms_link_for_login with no LMS_BASE. """
self.assertEquals(utils.get_lms_link_for_login(), None)

def test_course_link(self):
""" Tests get_lms_link_for_course. """
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
course_link = utils.get_lms_link_for_course(course_key)
self.assertEquals(course_link, u"https://localhost:8000/courses/mitX/101/test/")

@override_settings(LMS_BASE=None)
def test_course_link_no_lms_base(self):
""" Tests get_lms_link_for_course with no LMS_BASE. """
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
self.assertEquals(utils.get_lms_link_for_course(course_key), None)


class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """
Expand Down
24 changes: 24 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,30 @@ def get_lms_link_for_about_page(course_key):
)


def get_lms_link_for_dashboard():
"""Returns the url to the lms dashboard."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docstrings should describe the behavior that would lead to None being returned.

if settings.LMS_BASE is None:
return None
return u"https://{lms_base}/dashboard".format(lms_base=settings.LMS_BASE)


def get_lms_link_for_login():
"""Returns the url to the lms login page."""
if settings.LMS_BASE is None:
return None
return u"https://{lms_base}/login".format(lms_base=settings.LMS_BASE)


def get_lms_link_for_course(course_key):
"""Returns the url to the lms course."""
if settings.LMS_BASE is None:
return None
return u"https://{lms_base}/courses/{course_key}/".format(
lms_base=settings.LMS_BASE,
course_key=course_key
)


def course_image_url(course):
"""Returns the image url for the course."""
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
Expand Down
51 changes: 46 additions & 5 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest, HttpResponseNotFound, HttpResponse, Http404
from smtplib import SMTPException
from util.json_request import JsonResponse, JsonResponseBadRequest
from util.date_utils import get_default_time_display
from edxmako.shortcuts import render_to_response
Expand All @@ -35,6 +36,10 @@
add_instructor,
initialize_permissions,
get_lms_link_for_item,
get_lms_link_for_about_page,
get_lms_link_for_dashboard,
get_lms_link_for_course,
course_image_url,
add_extra_panel_tab,
remove_extra_panel_tab,
reverse_course_url,
Expand All @@ -58,7 +63,6 @@
from contentstore.tasks import rerun_course
from .item import create_xblock_info
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils
from student.roles import (
CourseInstructorRole, CourseStaffRole, CourseCreatorRole, GlobalStaff, UserBasedRole
)
Expand All @@ -67,6 +71,7 @@
from course_action_state.managers import CourseActionStateItemNotFoundError
from microsite_configuration import microsite
from xmodule.course_module import CourseFields
from bulk_email.models import CourseEmailTemplate


__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
Expand All @@ -76,7 +81,8 @@
'advanced_settings_handler',
'course_notifications_handler',
'textbooks_list_handler', 'textbooks_detail_handler',
'group_configurations_list_handler', 'group_configurations_detail_handler']
'group_configurations_list_handler', 'group_configurations_detail_handler',
'send_test_enrollment_email']

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -730,6 +736,38 @@ def course_info_update_handler(request, course_key_string, provided_id=None):
)


@require_http_methods("POST")
def send_test_enrollment_email(request, course_key_string):
"""
Handles ajax request for sending test enrollment emails to the instructor
"""
course_key = CourseKey.from_string(course_key_string)
course = modulestore().get_course(course_key)
user = request.user
subject = request.POST.get('subject')
subject = ''.join(subject.splitlines())
message = request.POST.get('message')

template_name = microsite.get_value('course_email_template_name')
from_address = microsite.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL)

template = CourseEmailTemplate.get_template(template_name)
email_context = {
'course_title': course.display_name,
'course_url': get_lms_link_for_course(course_key_string),
'account_settings_url': get_lms_link_for_dashboard(),
'platform_name': settings.PLATFORM_NAME,
'email': user.email
}
message = template.render_plaintext(message, email_context)

try:
user.email_user(subject, message, from_address)
except SMTPException:
return HttpResponseBadRequest(_("Error while sending test email."))
return HttpResponse()


@login_required
@ensure_csrf_cookie
@require_http_methods(("GET", "PUT", "POST"))
Expand Down Expand Up @@ -762,12 +800,15 @@ def settings_handler(request, course_key_string):
return render_to_response('settings.html', {
'context_course': course_module,
'course_locator': course_key,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
'course_image_url': utils.course_image_url(course_module),
'lms_link_for_about_page': get_lms_link_for_about_page(course_key),
'course_image_url': course_image_url(course_module),
'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable,
'upload_asset_url': upload_asset_url
'upload_asset_url': upload_asset_url,
'test_email_url': reverse_course_url('send_test_enrollment_email', course_key),
'default_pre_template': CourseDetails.get_default_pre_enrollment_email(course_key),
'default_post_template': CourseDetails.get_default_post_enrollment_email(),
})
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET':
Expand Down
56 changes: 54 additions & 2 deletions cms/djangoapps/models/settings/course_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@
import json
from json.encoder import JSONEncoder

from django.conf import settings
from mako.template import Template
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.utils import course_image_url
from contentstore.utils import (
course_image_url,
get_lms_link_for_about_page,
get_lms_link_for_dashboard,
get_lms_link_for_login,
)
from models.settings import course_grading
from xmodule.fields import Date
from xmodule.modulestore.django import modulestore
from edxmako.shortcuts import render_to_string

# This list represents the attribute keys for a course's 'about' info.
# Note: The 'video' attribute is intentionally excluded as it must be
Expand All @@ -19,14 +28,18 @@
'short_description',
'overview',
'effort',
'pre_enrollment_email',
'post_enrollment_email',
'pre_enrollment_email_subject',
'post_enrollment_email_subject',
]


class CourseDetails(object):
def __init__(self, org, course_id, run):
# still need these for now b/c the client's screen shows these 3 fields
self.org = org
self.course_id = course_id
self.course_id = course_id # This actually holds the course number.
self.run = run
self.start_date = None # 'start'
self.end_date = None # 'end'
Expand All @@ -35,10 +48,15 @@ def __init__(self, org, course_id, run):
self.syllabus = None # a pdf file asset
self.short_description = ""
self.overview = "" # html to render as the overview
self.pre_enrollment_email = CourseDetails.get_default_pre_enrollment_email(CourseKey.from_string(u'/'.join([self.org, self.course_id, self.run])))
self.post_enrollment_email = CourseDetails.get_default_post_enrollment_email()
self.pre_enrollment_email_subject = "Thanks for Enrolling in {}".format(self.course_id)
self.post_enrollment_email_subject = "Thanks for Enrolling in {}".format(self.course_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i18n. Use named placeholders eg {course_name}, not {} or {0}`.

You're going to have to be careful how you i18n these strings so they show up in the user's language. You might need to use lazy translations. I'm not positive - you can test locally though! https://github.com/edx/edx-platform/blob/master/docs/en_us/developers/source/i18n.rst#building-and-testing-your-code

self.intro_video = None # a video pointer
self.effort = None # int hours/week
self.course_image_name = ""
self.course_image_asset_path = "" # URL of the course image
self.enable_enrollment_email = False

@classmethod
def _fetch_about_attribute(cls, course_key, attribute):
Expand Down Expand Up @@ -66,6 +84,7 @@ def fetch(cls, course_key):
course_details.enrollment_end = descriptor.enrollment_end
course_details.course_image_name = descriptor.course_image
course_details.course_image_asset_path = course_image_url(descriptor)
course_details.enable_enrollment_email = descriptor.enable_enrollment_email

for attribute in ABOUT_ATTRIBUTES:
value = cls._fetch_about_attribute(course_key, attribute)
Expand Down Expand Up @@ -116,6 +135,11 @@ def update_from_json(cls, course_key, jsondict, user):
# setter expects as input.
date = Date()

# Added to allow admins to enable/disable enrollment emails
if 'enable_enrollment_email' in jsondict:
descriptor.enable_enrollment_email = jsondict['enable_enrollment_email']
dirty = True

if 'start_date' in jsondict:
converted = date.from_json(jsondict['start_date'])
else:
Expand Down Expand Up @@ -200,6 +224,34 @@ def recompose_video_tag(video_key):
video_key + '?rel=0" frameborder="0" allowfullscreen=""></iframe>'
return result

@staticmethod
def get_default_pre_enrollment_email(course_key):
"""
Returns the rendered default email body on enrolling before course starts.
"""
enroll_email_dict = {
'dashboard_url': get_lms_link_for_dashboard(),
'about_url': u"https:{}".format(get_lms_link_for_about_page(course_key)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We conditionalize use of https elsewhere, depending on a setting of whether or not the site uses http or https. Please make sure you do that here, and anywhere else you're hardcoding in the protocol.

}
if settings.DEFAULT_PRE_ENROLLMENT_EMAIL:
return Template(settings.DEFAULT_PRE_ENROLLMENT_EMAIL).render_unicode(**enroll_email_dict)
else:
return render_to_string('emails/default_pre_enrollment_message.txt', enroll_email_dict)

@staticmethod
def get_default_post_enrollment_email():
"""
Returns the rendered default email body on enrolling after course starts.
"""
enroll_email_dict = {
'dashboard_url': get_lms_link_for_dashboard(),
'signin_url': get_lms_link_for_login(),
}
if settings.DEFAULT_POST_ENROLLMENT_EMAIL:
return Template(settings.DEFAULT_POST_ENROLLMENT_EMAIL).render_unicode(**enroll_email_dict)
else:
return render_to_string('emails/default_post_enrollment_message.txt', enroll_email_dict)


# TODO move to a more general util?
class CourseSettingsEncoder(json.JSONEncoder):
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/models/settings/course_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class CourseMetadata(object):
'tags', # from xblock
'visible_to_staff_only',
'group_access',
'enable_enrollment_email',
]

@classmethod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Test that theming works (for enrollment email template).
"""
from django.test import TestCase
from django.test.utils import override_settings

from opaque_keys.edx.locations import SlashSeparatedCourseKey
from models.settings.course_details import CourseDetails


class EnrollmentEmailThemingTestCase(TestCase):
"""
Tests that theming for enrollment email works.
"""
@override_settings(DEFAULT_PRE_ENROLLMENT_EMAIL='This is a test pre enrollment email template.')
def test_pre_enrollment_email_theming(self):
"""
Test that settings override template is used for default email on enrolling before course start.
"""
course_key = SlashSeparatedCourseKey('mitX', '101', 'test')
template = CourseDetails.get_default_pre_enrollment_email(course_key)
self.assertIn(u'This is a test pre enrollment email template.', template)

@override_settings(DEFAULT_POST_ENROLLMENT_EMAIL='This is a test post enrollment email template.')
def test_post_enrollment_email_theming(self):
"""
Test that settings override template is used for default email on enrolling after course start.
"""
template = CourseDetails.get_default_post_enrollment_email()
self.assertIn(u'This is a test post enrollment email template.', template)
4 changes: 4 additions & 0 deletions cms/envs/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@
if "TRACKING_IGNORE_URL_PATTERNS" in ENV_TOKENS:
TRACKING_IGNORE_URL_PATTERNS = ENV_TOKENS.get("TRACKING_IGNORE_URL_PATTERNS")

# Enrollment email overrides
DEFAULT_PRE_ENROLLMENT_EMAIL = ENV_TOKENS.get("DEFAULT_PRE_ENROLLMENT_EMAIL", None)
DEFAULT_POST_ENROLLMENT_EMAIL = ENV_TOKENS.get("DEFAULT_POST_ENROLLMENT_EMAIL", None)

# Django CAS external authentication settings
CAS_EXTRA_LOGIN_PARAMS = ENV_TOKENS.get("CAS_EXTRA_LOGIN_PARAMS", None)
if FEATURES.get('AUTH_USE_CAS'):
Expand Down
Loading