Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
91c31e1
Implemented bulk email interface for new dashboard
Oct 3, 2013
d69748c
disable buttons for large courses on legacy and beta instr dash
adampalay Sep 24, 2013
867d3ba
Implemented bulk email interface for new dashboard
Oct 3, 2013
d8a857d
Changed GET to POST and xmodule HTML editor call, section CSS
Oct 4, 2013
3f88b87
Added acceptance tests for bulk email (through beta dashboard)
Oct 2, 2013
b8aff26
Merged in email confirmation modal
Oct 1, 2013
c7d4270
API tests, email tests, working notifications
Oct 7, 2013
fd54b06
added self to authors style, changed GET to POST
Oct 4, 2013
8a30e9b
Legacy email tests, removed duplicate code, updated comments, fixed CSS
Oct 7, 2013
9c94263
Removed email acceptance test
Oct 10, 2013
67a8ee1
Revert remnants of disable-button, and how html editor is invoked.
brianhw Oct 16, 2013
8fddcdf
Initial refactoring for bulk_email monitoring.
brianhw Sep 11, 2013
ffbb228
Add support for counting and reporting skips in background tasks.
brianhw Sep 17, 2013
2f4774f
Pass status into course_email for tracking retry status.
brianhw Sep 18, 2013
01611c3
Refactor instructor_task tests, and add handling for general errors i…
brianhw Sep 19, 2013
5c29530
Factor out subtask-specific code into subtasks.py.
brianhw Sep 20, 2013
7988b71
Move updates for InstructorTask into BaseInstructorTask abstract class.
brianhw Sep 24, 2013
0fd7518
Update handling of bulk-email retries to update InstructorTask before…
brianhw Sep 24, 2013
c133fd9
Use HIGH_PRIORITY_QUEUE for send_course_email.
brianhw Sep 25, 2013
08a0844
Add some handling for SES exceptions.
brianhw Sep 25, 2013
7b7afd4
Incorporate changes in max_retry logic, adding subtask_status as bulk…
brianhw Sep 26, 2013
a4c35ac
Use separate retry count for calculating retry delay.
brianhw Sep 30, 2013
04f90fe
Fix subtask code to handle (tests) running in eager mode.
brianhw Oct 3, 2013
df0fba9
Add more task-level tests for retries and other errors. Respond to i…
brianhw Oct 4, 2013
eaec962
Internationalize task progress.
brianhw Oct 8, 2013
41fcd96
Don't send emails to students who haven't activated.
brianhw Oct 8, 2013
c5debc2
Add settings to cap infinite retries.
brianhw Oct 8, 2013
4505fb4
Update InstructorTask before performing a retry.
brianhw Oct 8, 2013
e75dd46
Move subtask update logic that was only needed for tests into the tes…
brianhw Oct 8, 2013
bc599a0
Update tests with more complete coverage.
brianhw Oct 9, 2013
87a72b7
Rename some constants, and refactor bulk email task flow.
brianhw Oct 10, 2013
22285da
Switch to 0.2.6 version of diff-cover.
brianhw Oct 10, 2013
9861c93
Change calls in beta instructor dash.
brianhw Oct 10, 2013
b823906
Check that email subtasks are known to the InstructorTask before exec…
brianhw Oct 15, 2013
86c4a03
admin-console support for enabling email per course
Oct 8, 2013
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ Akshay Jagadeesh <akjags@gmail.com>
Nick Parlante <nick.parlante@cs.stanford.edu>
Marko Seric <marko.seric@math.uzh.ch>
Felipe Montoya <felipe.montoya@edunext.co>
Julia Hansbrough <julia@edx.org>
8 changes: 6 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.


LMS: Fix issue with CourseMode expiration dates

LMS: Add PaidCourseRegistration mode, where payment is required before course registration.
LMS: Ported bulk emailing to the beta instructor dashboard.

LMS: Add monitoring of bulk email subtasks to display progress on instructor dash.

LMS: Add PaidCourseRegistration mode, where payment is required before course
registration.

LMS: Add split testing functionality for internal use.

Expand Down
23 changes: 19 additions & 4 deletions common/djangoapps/terrain/course_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# pylint: disable=W0621

from lettuce import world
from django.contrib.auth.models import User
from django.contrib.auth.models import User, Group
from student.models import CourseEnrollment
from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore
Expand Down Expand Up @@ -41,15 +41,30 @@ def log_in(username='robot', password='test', email='robot@edx.org', name='Robot


@world.absorb
def register_by_course_id(course_id, is_staff=False):
create_user('robot', 'password')
u = User.objects.get(username='robot')
def register_by_course_id(course_id, username='robot', password='test', is_staff=False):
create_user(username, password)
user = User.objects.get(username=username)
if is_staff:
u.is_staff = True
u.save()
CourseEnrollment.enroll(u, course_id)


@world.absorb
def add_to_course_staff(username, course_num):
"""
Add the user with `username` to the course staff group
for `course_num`.
"""
# Based on code in lms/djangoapps/courseware/access.py
group_name = "instructor_{}".format(course_num)
group, _ = Group.objects.get_or_create(name=group_name)
group.save()

user = User.objects.get(username=username)
user.groups.add(group)


@world.absorb
def clear_courses():
# Flush and initialize the module store
Expand Down
42 changes: 40 additions & 2 deletions common/djangoapps/terrain/ui_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,46 @@ def is_css_not_present(css_selector, wait_time=5):
world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)

@world.absorb
def css_has_text(css_selector, text, index=0):
return world.css_text(css_selector, index=index) == text
def css_has_text(css_selector, text, index=0, strip=False):
"""
Return a boolean indicating whether the element with `css_selector`
has `text`.

If `strip` is True, strip whitespace at beginning/end of both
strings before comparing.

If there are multiple elements matching the css selector,
use `index` to indicate which one.
"""
# If we're expecting a non-empty string, give the page
# a chance to fill in text fields.
if text:
world.wait_for(lambda _: world.css_text(css_selector, index=index))

actual_text = world.css_text(css_selector, index=index)

if strip:
actual_text = actual_text.strip()
text = text.strip()

return actual_text == text


@world.absorb
def css_has_value(css_selector, value, index=0):
"""
Return a boolean indicating whether the element with
`css_selector` has the specified `value`.

If there are multiple elements matching the css selector,
use `index` to indicate which one.
"""
# If we're expecting a non-empty string, give the page
# a chance to fill in values
if value:
world.wait_for(lambda _: world.css_value(css_selector, index=index))

return world.css_value(css_selector, index=index) == value


@world.absorb
Expand Down
21 changes: 19 additions & 2 deletions lms/djangoapps/bulk_email/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"""
from django.contrib import admin

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


class CourseEmailAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -57,6 +57,23 @@ def has_delete_permission(self, request, obj=None):
return False


class CourseAuthorizationAdmin(admin.ModelAdmin):
"""Admin for enabling email on a course-by-course basis."""
form = CourseAuthorizationAdminForm
fieldsets = (
(None, {
'fields': ('course_id', 'email_enabled'),
'description': '''
Enter a course id in the following form: Org/Course/CourseRun, eg MITx/6.002x/2012_Fall
Do not enter leading or trailing slashes. There is no need to surround the course ID with quotes.
Validation will be performed on the course name, and if it is invalid, an error message will display.

To enable email for the course, check the "Email enabled" box, then click "Save".
'''
}),
)

admin.site.register(CourseEmail, CourseEmailAdmin)
admin.site.register(Optout, OptoutAdmin)
admin.site.register(CourseEmailTemplate, CourseEmailTemplateAdmin)
admin.site.register(CourseAuthorization, CourseAuthorizationAdmin)
37 changes: 35 additions & 2 deletions lms/djangoapps/bulk_email/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
from django import forms
from django.core.exceptions import ValidationError

from bulk_email.models import CourseEmailTemplate, COURSE_EMAIL_MESSAGE_BODY_TAG
from bulk_email.models import CourseEmailTemplate, COURSE_EMAIL_MESSAGE_BODY_TAG, CourseAuthorization

from courseware.courses import get_course_by_id
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore

log = logging.getLogger(__name__)


class CourseEmailTemplateForm(forms.ModelForm):
class CourseEmailTemplateForm(forms.ModelForm): # pylint: disable=R0924
"""Form providing validation of CourseEmail templates."""

class Meta: # pylint: disable=C0111
Expand Down Expand Up @@ -43,3 +47,32 @@ def clean_plain_template(self):
template = self.cleaned_data["plain_template"]
self._validate_template(template)
return template


class CourseAuthorizationAdminForm(forms.ModelForm): # pylint: disable=R0924
"""Input form for email enabling, allowing us to verify data."""

class Meta: # pylint: disable=C0111
model = CourseAuthorization

def clean_course_id(self):
"""Validate the course id"""
course_id = self.cleaned_data["course_id"]
try:
# Just try to get the course descriptor.
# If we can do that, it's a real course.
get_course_by_id(course_id, depth=1)
except Exception as exc:
msg = 'Error encountered ({0})'.format(str(exc).capitalize())
msg += ' --- Entered course id was: "{0}". '.format(course_id)
msg += 'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN'
raise forms.ValidationError(msg)

# Now, try and discern if it is a Studio course - HTML editor doesn't work with XML courses
is_studio_course = modulestore().get_modulestore_type(course_id) == MONGO_MODULESTORE_TYPE
if not is_studio_course:
msg = "Course Email feature is only available for courses authored in Studio. "
msg += '"{0}" appears to be an XML backed course.'.format(course_id)
raise forms.ValidationError(msg)

return course_id
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding model 'CourseAuthorization'
db.create_table('bulk_email_courseauthorization', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('email_enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('bulk_email', ['CourseAuthorization'])


def backwards(self, orm):
# Deleting model 'CourseAuthorization'
db.delete_table('bulk_email_courseauthorization')


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'})
},
'bulk_email.courseauthorization': {
'Meta': {'object_name': 'CourseAuthorization'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'email_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'bulk_email.courseemail': {
'Meta': {'object_name': 'CourseEmail'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'html_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'sender': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'subject': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'text_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'to_option': ('django.db.models.fields.CharField', [], {'default': "'myself'", 'max_length': '64'})
},
'bulk_email.courseemailtemplate': {
'Meta': {'object_name': 'CourseEmailTemplate'},
'html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'plain_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'bulk_email.optout': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'Optout'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
},
'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'})
}
}

complete_apps = ['bulk_email']
Loading