Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
c24087a9
Commit
c24087a9
authored
Oct 07, 2013
by
Sarina Canelake
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
django-admin command for enabling email per course
parent
56e0c6ba
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
425 additions
and
38 deletions
+425
-38
lms/djangoapps/bulk_email/admin.py
+19
-2
lms/djangoapps/bulk_email/forms.py
+35
-2
lms/djangoapps/bulk_email/migrations/0008_add_course_authorizations.py
+96
-0
lms/djangoapps/bulk_email/models.py
+44
-6
lms/djangoapps/bulk_email/tasks.py
+2
-6
lms/djangoapps/bulk_email/tests/test_course_optout.py
+2
-2
lms/djangoapps/bulk_email/tests/test_email.py
+1
-1
lms/djangoapps/bulk_email/tests/test_forms.py
+105
-0
lms/djangoapps/bulk_email/tests/test_models.py
+47
-1
lms/djangoapps/instructor/tests/test_email.py
+51
-4
lms/djangoapps/instructor/tests/test_legacy_email.py
+1
-1
lms/djangoapps/instructor/views/instructor_dashboard.py
+4
-1
lms/djangoapps/instructor/views/legacy.py
+12
-10
lms/envs/common.py
+6
-2
No files found.
lms/djangoapps/bulk_email/admin.py
View file @
c24087a9
...
...
@@ -3,8 +3,8 @@ 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
from
bulk_email.models
import
CourseEmail
,
Optout
,
CourseEmailTemplate
,
CourseAuthorization
from
bulk_email.forms
import
CourseEmailTemplateForm
,
CourseAuthorizationAdminForm
class
CourseEmailAdmin
(
admin
.
ModelAdmin
):
...
...
@@ -57,6 +57,23 @@ unsupported tags will cause email sending to fail.
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
)
lms/djangoapps/bulk_email/forms.py
View file @
c24087a9
...
...
@@ -6,12 +6,16 @@ import logging
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
...
...
@@ -43,3 +47,32 @@ class CourseEmailTemplateForm(forms.ModelForm):
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
lms/djangoapps/bulk_email/migrations/0008_add_course_authorizations.py
0 → 100644
View file @
c24087a9
# -*- 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'
]
\ No newline at end of file
lms/djangoapps/bulk_email/models.py
View file @
c24087a9
...
...
@@ -16,8 +16,17 @@ from django.db import models, transaction
from
django.contrib.auth.models
import
User
from
html_to_text
import
html_to_text
from
django.conf
import
settings
log
=
logging
.
getLogger
(
__name__
)
# Bulk email to_options - the send to options that users can
# select from when they send email.
SEND_TO_MYSELF
=
'myself'
SEND_TO_STAFF
=
'staff'
SEND_TO_ALL
=
'all'
TO_OPTIONS
=
[
SEND_TO_MYSELF
,
SEND_TO_STAFF
,
SEND_TO_ALL
]
class
Email
(
models
.
Model
):
"""
...
...
@@ -35,12 +44,6 @@ class Email(models.Model):
abstract
=
True
SEND_TO_MYSELF
=
'myself'
SEND_TO_STAFF
=
'staff'
SEND_TO_ALL
=
'all'
TO_OPTIONS
=
[
SEND_TO_MYSELF
,
SEND_TO_STAFF
,
SEND_TO_ALL
]
class
CourseEmail
(
Email
):
"""
Stores information for an email to a course.
...
...
@@ -209,3 +212,38 @@ class CourseEmailTemplate(models.Model):
stored HTML template and the provided `context` dict.
"""
return
CourseEmailTemplate
.
_render
(
self
.
html_template
,
htmltext
,
context
)
class
CourseAuthorization
(
models
.
Model
):
"""
Enable the course email feature on a course-by-course basis.
"""
# The course that these features are attached to.
course_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
# Whether or not to enable instructor email
email_enabled
=
models
.
BooleanField
(
default
=
False
)
@classmethod
def
instructor_email_enabled
(
cls
,
course_id
):
"""
Returns whether or not email is enabled for the given course id.
If email has not been explicitly enabled, returns False.
"""
# If settings.MITX_FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] is
# set to False, then we enable email for every course.
if
not
settings
.
MITX_FEATURES
[
'REQUIRE_COURSE_EMAIL_AUTH'
]:
return
True
try
:
record
=
cls
.
objects
.
get
(
course_id
=
course_id
)
return
record
.
email_enabled
except
cls
.
DoesNotExist
:
return
False
def
__unicode__
(
self
):
not_en
=
"Not "
if
self
.
email_enabled
:
not_en
=
""
return
u"Course '{}': Instructor Email {}Enabled"
.
format
(
self
.
course_id
,
not_en
)
lms/djangoapps/bulk_email/tasks.py
View file @
c24087a9
...
...
@@ -8,9 +8,6 @@ import random
from
uuid
import
uuid4
from
time
import
sleep
from
sys
import
exc_info
from
traceback
import
format_exc
from
dogapi
import
dog_stats_api
from
smtplib
import
SMTPServerDisconnected
,
SMTPDataError
,
SMTPConnectError
,
SMTPException
from
boto.ses.exceptions
import
(
...
...
@@ -30,7 +27,6 @@ from celery.exceptions import RetryTaskError
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
,
Group
from
django.core.mail
import
EmailMultiAlternatives
,
get_connection
from
django.http
import
Http404
from
django.core.urlresolvers
import
reverse
from
bulk_email.models
import
(
...
...
@@ -387,8 +383,8 @@ def _get_source_address(course_id, course_title):
# in an email address, by substituting a '_' anywhere a non-(ascii, period, or dash)
# character appears.
course_num
=
course_id
.
split
(
'/'
)[
1
]
INVALID_CHARS
=
re
.
compile
(
r"[^\w.-]"
)
course_num
=
INVALID_CHARS
.
sub
(
'_'
,
course_num
)
invalid_chars
=
re
.
compile
(
r"[^\w.-]"
)
course_num
=
invalid_chars
.
sub
(
'_'
,
course_num
)
from_addr
=
'"{0}" Course Staff <{1}-{2}>'
.
format
(
course_title_no_quotes
,
course_num
,
settings
.
BULK_EMAIL_DEFAULT_FROM_EMAIL
)
return
from_addr
...
...
lms/djangoapps/bulk_email/tests/test_course_optout.py
View file @
c24087a9
...
...
@@ -59,7 +59,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
selected_email_link
=
'<a href="#" onclick="goto(
\'
Email
\'
)" class="selectedmode">Email</a>'
self
.
assertTrue
(
selected_email_link
in
response
.
content
)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
})
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_optout_course
(
self
):
"""
Make sure student does not receive course email after opting out.
...
...
@@ -88,7 +88,7 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
# Assert that self.student.email not in mail.to, outbox should be empty
self
.
assertEqual
(
len
(
mail
.
outbox
),
0
)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
})
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_optin_course
(
self
):
"""
Make sure student receives course email after opting in.
...
...
lms/djangoapps/bulk_email/tests/test_email.py
View file @
c24087a9
...
...
@@ -44,7 +44,7 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
Test that emails send correctly.
"""
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
})
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
setUp
(
self
):
self
.
course
=
CourseFactory
.
create
()
self
.
instructor
=
UserFactory
.
create
(
username
=
"instructor"
,
email
=
"robot+instructor@edx.org"
)
...
...
lms/djangoapps/bulk_email/tests/test_forms.py
0 → 100644
View file @
c24087a9
"""
Unit tests for bulk-email-related forms.
"""
from
django.test.utils
import
override_settings
from
django.conf
import
settings
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
courseware.tests.modulestore_config
import
TEST_DATA_MIXED_MODULESTORE
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore
import
MONGO_MODULESTORE_TYPE
from
mock
import
patch
from
bulk_email.models
import
CourseAuthorization
from
bulk_email.forms
import
CourseAuthorizationAdminForm
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
CourseAuthorizationFormTest
(
ModuleStoreTestCase
):
"""Test the CourseAuthorizationAdminForm form for Mongo-backed courses."""
def
setUp
(
self
):
# Make a mongo course
self
.
course
=
CourseFactory
.
create
()
def
tearDown
(
self
):
"""
Undo all patches.
"""
patch
.
stopall
()
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_authorize_mongo_course
(
self
):
# Initially course shouldn't be authorized
self
.
assertFalse
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
# Test authorizing the course, which should totally work
form_data
=
{
'course_id'
:
self
.
course
.
id
,
'email_enabled'
:
True
}
form
=
CourseAuthorizationAdminForm
(
data
=
form_data
)
# Validation should work
self
.
assertTrue
(
form
.
is_valid
())
form
.
save
()
# Check that this course is authorized
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_form_typo
(
self
):
# Munge course id
bad_id
=
self
.
course
.
id
+
'_typo'
form_data
=
{
'course_id'
:
bad_id
,
'email_enabled'
:
True
}
form
=
CourseAuthorizationAdminForm
(
data
=
form_data
)
# Validation shouldn't work
self
.
assertFalse
(
form
.
is_valid
())
msg
=
u'Error encountered (Course not found.)'
msg
+=
' --- Entered course id was: "{0}". '
.
format
(
bad_id
)
msg
+=
'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN'
self
.
assertEquals
(
msg
,
form
.
_errors
[
'course_id'
][
0
])
# pylint: disable=protected-access
with
self
.
assertRaisesRegexp
(
ValueError
,
"The CourseAuthorization could not be created because the data didn't validate."
):
form
.
save
()
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_course_name_only
(
self
):
# Munge course id - common
bad_id
=
self
.
course
.
id
.
split
(
'/'
)[
-
1
]
form_data
=
{
'course_id'
:
bad_id
,
'email_enabled'
:
True
}
form
=
CourseAuthorizationAdminForm
(
data
=
form_data
)
# Validation shouldn't work
self
.
assertFalse
(
form
.
is_valid
())
msg
=
u'Error encountered (Need more than 1 value to unpack)'
msg
+=
' --- Entered course id was: "{0}". '
.
format
(
bad_id
)
msg
+=
'Please recheck that you have supplied a course id in the format: ORG/COURSE/RUN'
self
.
assertEquals
(
msg
,
form
.
_errors
[
'course_id'
][
0
])
# pylint: disable=protected-access
with
self
.
assertRaisesRegexp
(
ValueError
,
"The CourseAuthorization could not be created because the data didn't validate."
):
form
.
save
()
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
class
CourseAuthorizationXMLFormTest
(
ModuleStoreTestCase
):
"""Check that XML courses cannot be authorized for email."""
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_xml_course_authorization
(
self
):
course_id
=
'edX/toy/2012_Fall'
# Assert this is an XML course
self
.
assertTrue
(
modulestore
()
.
get_modulestore_type
(
course_id
)
!=
MONGO_MODULESTORE_TYPE
)
form_data
=
{
'course_id'
:
course_id
,
'email_enabled'
:
True
}
form
=
CourseAuthorizationAdminForm
(
data
=
form_data
)
# Validation shouldn't work
self
.
assertFalse
(
form
.
is_valid
())
msg
=
u"Course Email feature is only available for courses authored in Studio. "
msg
+=
'"{0}" appears to be an XML backed course.'
.
format
(
course_id
)
self
.
assertEquals
(
msg
,
form
.
_errors
[
'course_id'
][
0
])
# pylint: disable=protected-access
with
self
.
assertRaisesRegexp
(
ValueError
,
"The CourseAuthorization could not be created because the data didn't validate."
):
form
.
save
()
lms/djangoapps/bulk_email/tests/test_models.py
View file @
c24087a9
...
...
@@ -3,10 +3,13 @@ Unit tests for bulk-email-related models.
"""
from
django.test
import
TestCase
from
django.core.management
import
call_command
from
django.conf
import
settings
from
student.tests.factories
import
UserFactory
from
bulk_email.models
import
CourseEmail
,
SEND_TO_STAFF
,
CourseEmailTemplate
from
mock
import
patch
from
bulk_email.models
import
CourseEmail
,
SEND_TO_STAFF
,
CourseEmailTemplate
,
CourseAuthorization
class
CourseEmailTest
(
TestCase
):
...
...
@@ -99,3 +102,46 @@ class CourseEmailTemplateTest(TestCase):
template
=
CourseEmailTemplate
.
get_template
()
context
=
self
.
_get_sample_plain_context
()
template
.
render_plaintext
(
"My new plain text."
,
context
)
class
CourseAuthorizationTest
(
TestCase
):
"""Test the CourseAuthorization model."""
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_creation_auth_on
(
self
):
course_id
=
'abc/123/doremi'
# Test that course is not authorized by default
self
.
assertFalse
(
CourseAuthorization
.
instructor_email_enabled
(
course_id
))
# Authorize
cauth
=
CourseAuthorization
(
course_id
=
course_id
,
email_enabled
=
True
)
cauth
.
save
()
# Now, course should be authorized
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
course_id
))
self
.
assertEquals
(
cauth
.
__unicode__
(),
"Course 'abc/123/doremi': Instructor Email Enabled"
)
# Unauthorize by explicitly setting email_enabled to False
cauth
.
email_enabled
=
False
cauth
.
save
()
# Test that course is now unauthorized
self
.
assertFalse
(
CourseAuthorization
.
instructor_email_enabled
(
course_id
))
self
.
assertEquals
(
cauth
.
__unicode__
(),
"Course 'abc/123/doremi': Instructor Email Not Enabled"
)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_creation_auth_off
(
self
):
course_id
=
'blahx/blah101/ehhhhhhh'
# Test that course is authorized by default, since auth is turned off
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
course_id
))
# Use the admin interface to unauthorize the course
cauth
=
CourseAuthorization
(
course_id
=
course_id
,
email_enabled
=
False
)
cauth
.
save
()
# Now, course should STILL be authorized!
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
course_id
))
lms/djangoapps/instructor/tests/test_email.py
View file @
c24087a9
...
...
@@ -16,6 +16,8 @@ from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from
mock
import
patch
from
bulk_email.models
import
CourseAuthorization
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestNewInstructorDashboardEmailViewMongoBacked
(
ModuleStoreTestCase
):
...
...
@@ -44,8 +46,11 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase):
# In order for bulk email to work, we must have both the ENABLE_INSTRUCTOR_EMAIL_FLAG
# set to True and for the course to be Mongo-backed.
# The flag is enabled and the course is Mongo-backed (should work)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
})
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_email_flag_true_mongo_true
(
self
):
# Assert that instructor email is enabled for this course - since REQUIRE_COURSE_EMAIL_AUTH is False,
# all courses should be authorized to use email.
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
# Assert that the URL for the email view is in the response
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertIn
(
self
.
email_link
,
response
.
content
)
...
...
@@ -61,6 +66,47 @@ class TestNewInstructorDashboardEmailViewMongoBacked(ModuleStoreTestCase):
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertFalse
(
self
.
email_link
in
response
.
content
)
# Flag is enabled, but we require course auth and haven't turned it on for this course
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_course_not_authorized
(
self
):
# Assert that instructor email is not enabled for this course
self
.
assertFalse
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
# Assert that the URL for the email view is not in the response
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertFalse
(
self
.
email_link
in
response
.
content
)
# Flag is enabled, we require course auth and turn it on for this course
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_course_authorized
(
self
):
# Assert that instructor email is not enabled for this course
self
.
assertFalse
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
# Assert that the URL for the email view is not in the response
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertFalse
(
self
.
email_link
in
response
.
content
)
# Authorize the course to use email
cauth
=
CourseAuthorization
(
course_id
=
self
.
course
.
id
,
email_enabled
=
True
)
cauth
.
save
()
# Assert that instructor email is enabled for this course
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
# Assert that the URL for the email view is not in the response
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertTrue
(
self
.
email_link
in
response
.
content
)
# Flag is disabled, but course is authorized
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
False
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
})
def
test_course_authorized_feature_off
(
self
):
# Authorize the course to use email
cauth
=
CourseAuthorization
(
course_id
=
self
.
course
.
id
,
email_enabled
=
True
)
cauth
.
save
()
# Assert that instructor email IS enabled for this course
self
.
assertTrue
(
CourseAuthorization
.
instructor_email_enabled
(
self
.
course
.
id
))
# Assert that the URL for the email view IS NOT in the response
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertFalse
(
self
.
email_link
in
response
.
content
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
class
TestNewInstructorDashboardEmailViewXMLBacked
(
ModuleStoreTestCase
):
...
...
@@ -79,14 +125,15 @@ class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase):
# URL for email view
self
.
email_link
=
'<a href="" data-section="send_email">Email</a>'
# The flag is enabled but the course is not Mongo-backed (should not work)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
})
# The flag is enabled, and since REQUIRE_COURSE_EMAIL_AUTH is False, all courses should
# be authorized to use email. But the course is not Mongo-backed (should not work)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_email_flag_true_mongo_false
(
self
):
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertFalse
(
self
.
email_link
in
response
.
content
)
# The flag is disabled and the course is not Mongo-backed (should not work)
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
False
})
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
False
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_email_flag_false_mongo_false
(
self
):
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertFalse
(
self
.
email_link
in
response
.
content
)
lms/djangoapps/instructor/tests/test_legacy_email.py
View file @
c24087a9
...
...
@@ -41,7 +41,7 @@ class TestInstructorDashboardEmailView(ModuleStoreTestCase):
"""
patch
.
stopall
()
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
})
@patch.dict
(
settings
.
MITX_FEATURES
,
{
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
'REQUIRE_COURSE_EMAIL_AUTH'
:
False
})
def
test_email_flag_true
(
self
):
# Assert that the URL for the email view is in the response
response
=
self
.
client
.
get
(
self
.
url
)
...
...
lms/djangoapps/instructor/views/instructor_dashboard.py
View file @
c24087a9
...
...
@@ -22,6 +22,7 @@ from courseware.courses import get_course_by_id
from
django_comment_client.utils
import
has_forum_access
from
django_comment_common.models
import
FORUM_ROLE_ADMINISTRATOR
from
student.models
import
CourseEnrollment
from
bulk_email.models
import
CourseAuthorization
@ensure_csrf_cookie
@cache_control
(
no_cache
=
True
,
no_store
=
True
,
must_revalidate
=
True
)
...
...
@@ -57,7 +58,9 @@ def instructor_dashboard_2(request, course_id):
if
max_enrollment_for_buttons
is
not
None
:
disable_buttons
=
enrollment_count
>
max_enrollment_for_buttons
if
settings
.
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
and
is_studio_course
:
# Gate access by feature flag & by course-specific authorization
if
settings
.
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
and
\
is_studio_course
and
CourseAuthorization
.
instructor_email_enabled
(
course_id
):
sections
.
append
(
_section_send_email
(
course_id
,
access
,
course
))
context
=
{
...
...
lms/djangoapps/instructor/views/legacy.py
View file @
c24087a9
...
...
@@ -30,7 +30,7 @@ from xmodule.modulestore.django import modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.html_module
import
HtmlDescriptor
from
bulk_email.models
import
CourseEmail
from
bulk_email.models
import
CourseEmail
,
CourseAuthorization
from
courseware
import
grades
from
courseware.access
import
(
has_access
,
get_access_group_name
,
course_beta_test_group_name
)
...
...
@@ -801,10 +801,13 @@ def instructor_dashboard(request, course_id):
else
:
instructor_tasks
=
None
# determine if this is a studio-backed course so we can 1) provide a link to edit this course in studio
# 2) enable course email
# determine if this is a studio-backed course so we can provide a link to edit this course in studio
is_studio_course
=
modulestore
()
.
get_modulestore_type
(
course_id
)
==
MONGO_MODULESTORE_TYPE
studio_url
=
None
if
is_studio_course
:
studio_url
=
get_cms_course_link_by_id
(
course_id
)
email_editor
=
None
# HTML editor for email
if
idash_mode
==
'Email'
and
is_studio_course
:
...
...
@@ -813,13 +816,12 @@ def instructor_dashboard(request, course_id):
fragment
=
wrap_xmodule
(
'xmodule_edit.html'
,
html_module
,
'studio_view'
,
fragment
,
None
)
email_editor
=
fragment
.
content
studio_url
=
None
if
is_studio_course
:
studio_url
=
get_cms_course_link_by_id
(
course_id
)
# Flag for whether or not we display the email tab (depending upon
# what backing store this course using (Mongo vs. XML))
if
settings
.
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
and
is_studio_course
:
# Enable instructor email only if the following conditions are met:
# 1. Feature flag is on
# 2. We have explicitly enabled email for the given course via django-admin
# 3. It is NOT an XML course
if
settings
.
MITX_FEATURES
[
'ENABLE_INSTRUCTOR_EMAIL'
]
and
\
CourseAuthorization
.
instructor_email_enabled
(
course_id
)
and
is_studio_course
:
show_email_tab
=
True
# display course stats only if there is no other table to display:
...
...
lms/envs/common.py
View file @
c24087a9
...
...
@@ -114,8 +114,12 @@ MITX_FEATURES = {
# analytics experiments
'ENABLE_INSTRUCTOR_ANALYTICS'
:
False
,
# bulk email available to instructors:
'ENABLE_INSTRUCTOR_EMAIL'
:
False
,
# Enables the LMS bulk email feature for course staff
'ENABLE_INSTRUCTOR_EMAIL'
:
True
,
# If True and ENABLE_INSTRUCTOR_EMAIL: Forces email to be explicitly turned on
# for each course via django-admin interface.
# If False and ENABLE_INSTRUCTOR_EMAIL: Email will be turned on by default for all courses.
'REQUIRE_COURSE_EMAIL_AUTH'
:
True
,
# enable analytics server.
# WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment