Skip to content
Merged
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
2 changes: 2 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ Mukul Goyal <miki@edx.org>
Robert Marks <rmarks@edx.org>
Yarko Tymciurak <yarkot1@gmail.com>
Miles Steele <miles@milessteele.com>
Kevin Luo <kevluo@edx.org>
Akshay Jagadeesh <akjags@gmail.com>
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ logic has been consolidated into the model -- you should use new class methods
to `enroll()`, `unenroll()`, and to check `is_enrolled()`, instead of creating
CourseEnrollment objects or querying them directly.

LMS: Added bulk email for course feature, with option to optout of individual
course emails.

Studio: Email will be sent to admin address when a user requests course creator
privileges for Studio (edge only).

Expand Down
85 changes: 58 additions & 27 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Student Views
"""
import datetime
import feedparser
import json
Expand Down Expand Up @@ -27,6 +30,7 @@
from django.utils.http import cookie_date
from django.utils.http import base36_to_int
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST

from ratelimitbackend.exceptions import RateLimitException

Expand Down Expand Up @@ -54,6 +58,10 @@

from external_auth.models import ExternalAuthMap

from bulk_email.models import Optout

import track.views

from statsd import statsd
from pytz import UTC

Expand All @@ -64,8 +72,7 @@


def csrf_token(context):
''' A csrf token that can be included in a form.
'''
"""A csrf token that can be included in a form."""
csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED':
return ''
Expand All @@ -78,12 +85,12 @@ def csrf_token(context):
# This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed)
def index(request, extra_context={}, user=None):
'''
"""
Render the edX main page.

extra_context is used to allow immediate display of certain modal windows, eg signup,
as used by external_auth.
'''
"""

# The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
Expand Down Expand Up @@ -267,6 +274,8 @@ def dashboard(request):
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))

course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)

message = ""
if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
Expand Down Expand Up @@ -294,6 +303,7 @@ def dashboard(request):
pass

context = {'courses': courses,
'course_optouts': course_optouts,
'message': message,
'external_auth_map': external_auth_map,
'staff_access': staff_access,
Expand Down Expand Up @@ -404,7 +414,7 @@ def accounts_login(request, error=""):
# Need different levels of logging
@ensure_csrf_cookie
def login_user(request, error=""):
''' AJAX request to log in the user. '''
"""AJAX request to log in the user."""
if 'email' not in request.POST or 'password' not in request.POST:
return HttpResponse(json.dumps({'success': False,
'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message
Expand Down Expand Up @@ -487,11 +497,11 @@ def login_user(request, error=""):

@ensure_csrf_cookie
def logout_user(request):
'''
"""
HTTP request to log out the user. Redirects to marketing page.
Deletes both the CSRF and sessionid cookies so the marketing
site can determine the logged in state of the user
'''
"""
# We do not log here, because we have a handler registered
# to perform logging on successful logouts.
logout(request)
Expand All @@ -505,8 +515,7 @@ def logout_user(request):
@login_required
@ensure_csrf_cookie
def change_setting(request):
''' JSON call to change a profile setting: Right now, location
'''
"""JSON call to change a profile setting: Right now, location"""
# TODO (vshnayder): location is no longer used
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
if 'location' in request.POST:
Expand Down Expand Up @@ -574,10 +583,10 @@ def _do_create_account(post_vars):

@ensure_csrf_cookie
def create_account(request, post_override=None):
'''
"""
JSON call to create new edX account.
Used by form in signup_modal.html, which is included into navigation.html
'''
"""
js = {'success': False}

post_vars = post_override if post_override else request.POST
Expand Down Expand Up @@ -811,10 +820,10 @@ def begin_exam_registration(request, course_id):

@ensure_csrf_cookie
def create_exam_registration(request, post_override=None):
'''
"""
JSON call to create a test center exam registration.
Called by form in test_center_register.html
'''
"""
post_vars = post_override if post_override else request.POST

# first determine if we need to create a new TestCenterUser, or if we are making any update
Expand Down Expand Up @@ -967,8 +976,7 @@ def get_dummy_post_data(username, password, email, name):

@ensure_csrf_cookie
def activate_account(request, key):
''' When link in activation e-mail is clicked
'''
"""When link in activation e-mail is clicked"""
r = Registration.objects.filter(activation_key=key)
if len(r) == 1:
user_logged_in = request.user.is_authenticated()
Expand Down Expand Up @@ -1003,7 +1011,7 @@ def activate_account(request, key):

@ensure_csrf_cookie
def password_reset(request):
''' Attempts to send a password reset e-mail. '''
""" Attempts to send a password reset e-mail. """
if request.method != "POST":
raise Http404

Expand All @@ -1025,9 +1033,9 @@ def password_reset_confirm_wrapper(
uidb36=None,
token=None,
):
''' A wrapper around django.contrib.auth.views.password_reset_confirm.
""" A wrapper around django.contrib.auth.views.password_reset_confirm.
Needed because we want to set the user as active at this step.
'''
"""
# cribbed from django.contrib.auth.views.password_reset_confirm
try:
uid_int = base36_to_int(uidb36)
Expand Down Expand Up @@ -1069,8 +1077,8 @@ def reactivation_email_for_user(user):

@ensure_csrf_cookie
def change_email_request(request):
''' AJAX call from the profile page. User wants a new e-mail.
'''
""" AJAX call from the profile page. User wants a new e-mail.
"""
## Make sure it checks for existing e-mail conflicts
if not request.user.is_authenticated:
raise Http404
Expand Down Expand Up @@ -1125,9 +1133,9 @@ def change_email_request(request):
@ensure_csrf_cookie
@transaction.commit_manually
def confirm_email_change(request, key):
''' User requested a new e-mail. This is called when the activation
""" User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update
'''
"""
try:
try:
pec = PendingEmailChange.objects.get(activation_key=key)
Expand Down Expand Up @@ -1184,7 +1192,7 @@ def confirm_email_change(request, key):

@ensure_csrf_cookie
def change_name_request(request):
''' Log a request for a new name. '''
""" Log a request for a new name. """
if not request.user.is_authenticated:
raise Http404

Expand All @@ -1208,7 +1216,7 @@ def change_name_request(request):

@ensure_csrf_cookie
def pending_name_changes(request):
''' Web page which allows staff to approve or reject name changes. '''
""" Web page which allows staff to approve or reject name changes. """
if not request.user.is_staff:
raise Http404

Expand All @@ -1224,7 +1232,7 @@ def pending_name_changes(request):

@ensure_csrf_cookie
def reject_name_change(request):
''' JSON: Name change process. Course staff clicks 'reject' on a given name change '''
""" JSON: Name change process. Course staff clicks 'reject' on a given name change """
if not request.user.is_staff:
raise Http404

Expand Down Expand Up @@ -1262,13 +1270,36 @@ def accept_name_change_by_id(id):

@ensure_csrf_cookie
def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change
""" JSON: Name change process. Course staff clicks 'accept' on a given name change

We used this during the prototype but now we simply record name changes instead
of manually approving them. Still keeping this around in case we want to go
back to this approval method.
'''
"""
if not request.user.is_staff:
raise Http404

return accept_name_change_by_id(int(request.POST['id']))


@require_POST
@login_required
@ensure_csrf_cookie
def change_email_settings(request):
"""Modify logged-in user's setting for receiving emails from a course."""
user = request.user

course_id = request.POST.get("course_id")
receive_emails = request.POST.get("receive_emails")
if receive_emails:
optout_object = Optout.objects.filter(user=user, course_id=course_id)
if optout_object:
optout_object.delete()
log.info(u"User {0} ({1}) opted in to receive emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
else:
Optout.objects.get_or_create(user=user, course_id=course_id)
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')

return HttpResponse(json.dumps({'success': True}))
27 changes: 27 additions & 0 deletions common/lib/html_to_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Provides a function to convert html to plaintext."""
import logging
from subprocess import Popen, PIPE

log = logging.getLogger(__name__)


def html_to_text(html_message):
"""
Converts an html message to plaintext.
Currently uses lynx in a subprocess; should be refactored to
use something more pythonic.
"""
process = Popen(
['lynx', '-stdin', '-display_charset=UTF-8', '-assume_charset=UTF-8', '-dump'],
stdin=PIPE,
stdout=PIPE
)
# use lynx to get plaintext
(plaintext, err_from_stderr) = process.communicate(
input=html_message.encode('utf-8')
)

if err_from_stderr:
log.info(err_from_stderr)

return plaintext
File renamed without changes.
Empty file.
61 changes: 61 additions & 0 deletions lms/djangoapps/bulk_email/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Django admin page for bulk email models
"""
from django.contrib import admin

from bulk_email.models import CourseEmail, Optout, CourseEmailTemplate
from bulk_email.forms import CourseEmailTemplateForm


class CourseEmailAdmin(admin.ModelAdmin):
"""Admin for course email."""
readonly_fields = ('sender',)


class OptoutAdmin(admin.ModelAdmin):
"""Admin for optouts."""
list_display = ('user', 'course_id')


class CourseEmailTemplateAdmin(admin.ModelAdmin):
form = CourseEmailTemplateForm
fieldsets = (
(None, {
# make the HTML template display above the plain template:
'fields': ('html_template', 'plain_template'),
'description': '''
Enter template to be used by course staff when sending emails to enrolled students.

The HTML template is for HTML email, and may contain HTML markup. The plain template is
for plaintext email. Both templates should contain the string '{{message_body}}' (with
two curly braces on each side), to indicate where the email text is to be inserted.

Other tags that may be used (surrounded by one curly brace on each side):
{platform_name} : the name of the platform
{course_title} : the name of the course
{course_url} : the course's full URL
{email} : the user's email address
{account_settings_url} : URL at which users can change email preferences
{course_image_url} : URL for the course's course image.
Will return a broken link if course doesn't have a course image set.

Note that there is currently NO validation on tags, so be careful. Typos or use of
unsupported tags will cause email sending to fail.
'''
}),
)
# Turn off the action bar (we have no bulk actions)
actions = None

def has_add_permission(self, request):
"""Disables the ability to add new templates, as we want to maintain a Singleton."""
return False

def has_delete_permission(self, request, obj=None):
"""Disables the ability to remove existing templates, as we want to maintain a Singleton."""
return False


admin.site.register(CourseEmail, CourseEmailAdmin)
admin.site.register(Optout, OptoutAdmin)
admin.site.register(CourseEmailTemplate, CourseEmailTemplateAdmin)
10 changes: 10 additions & 0 deletions lms/djangoapps/bulk_email/fixtures/course_email_template.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Loading