Commit 0cf3e39c by Eric Fischer

Replace bulk email settings with admin config models

Moves ENABLE_INSTRUCTOR_EMAIL and REQUIRE_COURSE_EMAIL_AUTH from settings files
to admin-accessible configuration models. This allows for the bulk email settings
to be modified without a new AMI deploy. See TNL-4504.

Also updates tests:
    -python tests mock out the new configurations in place of the old settings
    -lettuce test has been moved to bokchoy
        (note that there was some loss of coverage here - the lettuce tests had
        been doing some voodoo to allow for cross-process inspection of emails
        messages being "sent" by the server, from the client! In discussion with
        testeng, this seems outside the realm of a visual acceptance test. So,
        the bokchoy test simply confirm the successful queueing of the message,
        and leaves the validation of sending messages to the relevant unit tests.)
    -bok choy fixture has been added, to replace the settings in acceptance.py
    -lettuce and bok choy databases have been updated to reflect the backend changes

The new default is to have bulk_email disabled, we'll need to call this out in the
next OpenEdx release to ensure administrators enable this feature if needed.
parent 140fd85e
......@@ -8,7 +8,6 @@ import unittest
from django.conf import settings
from django.core.urlresolvers import reverse
from mock import patch
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.tests.factories import UserFactory, CourseEnrollmentFactory
......@@ -18,7 +17,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
# This import is for an lms djangoapp.
# Its testcases are only run under lms.
from bulk_email.models import CourseAuthorization # pylint: disable=import-error
from bulk_email.models import CourseAuthorization, BulkEmailFlag # pylint: disable=import-error
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
......@@ -51,34 +50,38 @@ class TestStudentDashboardEmailView(SharedModuleStoreTestCase):
name=self.course.display_name.replace(' ', '_'),
)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def tearDown(self):
super(TestStudentDashboardEmailView, self).tearDown()
BulkEmailFlag.objects.all().delete()
def test_email_flag_true(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
# Assert that the URL for the email view is in the response
response = self.client.get(self.url)
self.assertTrue(self.email_modal_link in response.content)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
def test_email_flag_false(self):
BulkEmailFlag.objects.create(enabled=False)
# Assert that the URL for the email view is not in the response
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
self.assertNotIn(self.email_modal_link, response.content)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_unauthorized(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
# Assert that instructor email is not enabled for this course
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertFalse(BulkEmailFlag.feature_enabled(self.course.id))
# Assert that the URL for the email view is not in the response
# if this course isn't authorized
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
self.assertNotIn(self.email_modal_link, response.content)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_authorized(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
# 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))
self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id))
# Assert that the URL for the email view is not in the response
# if this course isn't authorized
response = self.client.get(self.url)
......@@ -117,15 +120,15 @@ class TestStudentDashboardEmailViewXMLBacked(SharedModuleStoreTestCase):
name='2012_Fall',
)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_true_xml_store(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
# 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)
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_false_xml_store(self):
BulkEmailFlag.objects.create(enabled=False, require_course_email_auth=False)
# Email disabled, shouldn't see link.
response = self.client.get(self.url)
self.assertFalse(self.email_modal_link in response.content)
......@@ -56,6 +56,7 @@ from student.models import (
from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
from bulk_email.models import Optout, BulkEmailFlag # pylint: disable=import-error
from certificates.models import CertificateStatuses, certificate_status_for_student
from certificates.api import ( # pylint: disable=import-error
get_certificate_url,
......@@ -83,7 +84,6 @@ from external_auth.login_and_register import (
register as external_auth_register
)
from bulk_email.models import Optout, CourseAuthorization
from lang_pref import LANGUAGE_KEY
import track.views
......@@ -649,8 +649,7 @@ def dashboard(request):
# only show email settings for Mongo course and when bulk email is turned on
show_email_settings_for = frozenset(
enrollment.course_id for enrollment in course_enrollments if (
settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and
CourseAuthorization.instructor_email_enabled(enrollment.course_id)
BulkEmailFlag.feature_enabled(enrollment.course_id)
)
)
......
......@@ -75,6 +75,15 @@ class InstructorDashboardPage(CoursePage):
timed_exam_section.wait_for_page()
return timed_exam_section
def select_bulk_email(self):
"""
Selects the email tab and returns the bulk email section
"""
self.q(css='a[data-section=send_email]').first.click()
email_section = BulkEmailPage(self.browser)
email_section.wait_for_page()
return email_section
@staticmethod
def get_asset_path(file_name):
"""
......@@ -98,6 +107,62 @@ class InstructorDashboardPage(CoursePage):
return os.sep.join(folders_list_in_path)
class BulkEmailPage(PageObject):
"""
Bulk email section of the instructor dashboard.
This feature is controlled by an admin panel feature flag, which is turned on via database fixture for testing.
"""
url = None
def is_browser_on_page(self):
return self.q(css='a[data-section=send_email].active-section').present
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to the bulk-email context.
"""
return '.send-email {}'.format(selector)
def _select_recipient(self, recipient):
"""
Selects the specified recipient from the selector. Assumes that recipient is not None.
"""
recipient_selector_css = "select[name='send_to']"
select_option_by_text(
self.q(css=self._bounded_selector(recipient_selector_css)), recipient
)
def send_message(self, recipient):
"""
Send a test message to the specified recipient.
"""
send_css = "input[name='send']"
test_subject = "Hello"
test_body = "This is a test email"
self._select_recipient(recipient)
self.q(css=self._bounded_selector("input[name='subject']")).fill(test_subject)
self.q(css=self._bounded_selector("iframe#mce_0_ifr"))[0].click()
self.q(css=self._bounded_selector("iframe#mce_0_ifr"))[0].send_keys(test_body)
with self.handle_alert(confirm=True):
self.q(css=self._bounded_selector(send_css)).click()
def verify_message_queued_successfully(self):
"""
Verifies that the "you email was queued" message appears.
Note that this does NOT ensure the message gets sent successfully, that functionality
is covered by the bulk_email unit tests.
"""
confirmation_selector = self._bounded_selector(".msg-confirm")
expected_text = u"Your email was successfully queued for sending."
EmptyPromise(
lambda: expected_text in self.q(css=confirmation_selector)[0].text,
"Message Queued Confirmation"
).fulfill()
class MembershipPage(PageObject):
"""
Membership section of the Instructor dashboard.
......
......@@ -46,6 +46,25 @@ class BaseInstructorDashboardTest(EventsTestMixin, UniqueCourseTest):
return instructor_dashboard_page
@ddt.ddt
class BulkEmailTest(BaseInstructorDashboardTest):
"""
End-to-end tests for bulk emailing from instructor dash.
"""
def setUp(self):
super(BulkEmailTest, self).setUp()
self.course_fixture = CourseFixture(**self.course_info).install()
self.log_in_as_instructor()
instructor_dashboard_page = self.visit_instructor_dashboard()
self.send_email_page = instructor_dashboard_page.select_bulk_email()
@ddt.data("Myself", "Staff and admins", "All (students, staff, and admins)")
def test_email_queued_for_sending(self, recipient):
self.assertTrue(self.send_email_page.is_browser_on_page())
self.send_email_page.send_message(recipient)
self.send_email_page.verify_message_queued_successfully()
@attr('shard_7')
class AutoEnrollmentWithCSVTest(BaseInstructorDashboardTest):
"""
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -35,7 +35,7 @@ CREATE TABLE `django_migrations` (
`name` varchar(255) NOT NULL,
`applied` datetime(6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=139 DEFAULT CHARSET=utf8;
) ENGINE=InnoDB AUTO_INCREMENT=155 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
......
[
{
"pk": 1,
"model": "bulk_email.bulkemailflag",
"fields": {
"enabled": true,
"require_course_email_auth": false,
"change_date": "2016-05-01"
}
}
]
......@@ -3,7 +3,9 @@ Django admin page for bulk email models
"""
from django.contrib import admin
from bulk_email.models import CourseEmail, Optout, CourseEmailTemplate, CourseAuthorization
from config_models.admin import ConfigurationModelAdmin
from bulk_email.models import CourseEmail, Optout, CourseEmailTemplate, CourseAuthorization, BulkEmailFlag
from bulk_email.forms import CourseEmailTemplateForm, CourseAuthorizationAdminForm
......@@ -80,3 +82,4 @@ admin.site.register(CourseEmail, CourseEmailAdmin)
admin.site.register(Optout, OptoutAdmin)
admin.site.register(CourseEmailTemplate, CourseEmailTemplateAdmin)
admin.site.register(CourseAuthorization, CourseAuthorizationAdmin)
admin.site.register(BulkEmailFlag, ConfigurationModelAdmin)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('bulk_email', '0002_data__load_course_email_template'),
]
operations = [
migrations.CreateModel(
name='BulkEmailFlag',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('require_course_email_auth', models.BooleanField(default=True)),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
]
......@@ -20,6 +20,8 @@ from django.db import models
from openedx.core.lib.html_to_text import html_to_text
from openedx.core.lib.mail_utils import wrap_message
from config_models.models import ConfigurationModel
from xmodule_django.models import CourseKeyField
from util.keyword_substitution import substitute_keywords_with_data
......@@ -240,14 +242,7 @@ class CourseAuthorization(models.Model):
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.FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] is
# set to False, then we enable email for every course.
if not settings.FEATURES['REQUIRE_COURSE_EMAIL_AUTH']:
return True
try:
record = cls.objects.get(course_id=course_id)
return record.email_enabled
......@@ -260,3 +255,47 @@ class CourseAuthorization(models.Model):
not_en = ""
# pylint: disable=no-member
return u"Course '{}': Instructor Email {}Enabled".format(self.course_id.to_deprecated_string(), not_en)
class BulkEmailFlag(ConfigurationModel):
"""
Enables site-wide configuration for the bulk_email feature.
Staff can only send bulk email for a course if all the following conditions are true:
1. BulkEmailFlag is enabled.
2. Course-specific authorization not required, or course authorized to use bulk email.
"""
# boolean field 'enabled' inherited from parent ConfigurationModel
require_course_email_auth = models.BooleanField(default=True)
@classmethod
def feature_enabled(cls, course_id=None):
"""
Looks at the currently active configuration model to determine whether the bulk email feature is available.
If the flag is not enabled, the feature is not available.
If the flag is enabled, course-specific authorization is required, and the course_id is either not provided
or not authorixed, the feature is not available.
If the flag is enabled, course-specific authorization is required, and the provided course_id is authorized,
the feature is available.
If the flag is enabled and course-specific authorization is not required, the feature is available.
"""
if not BulkEmailFlag.is_enabled():
return False
elif BulkEmailFlag.current().require_course_email_auth:
if course_id is None:
return False
else:
return CourseAuthorization.instructor_email_enabled(course_id)
else: # implies enabled == True and require_course_email == False, so email is globally enabled
return True
class Meta(object):
app_label = "bulk_email"
def __unicode__(self):
current_model = BulkEmailFlag.current()
return u"<BulkEmailFlag: enabled {}, require_course_email_auth: {}>".format(
current_model.is_enabled(),
current_model.require_course_email_auth
)
......@@ -15,6 +15,7 @@ from student.tests.factories import UserFactory, AdminFactory, CourseEnrollmentF
from student.models import CourseEnrollment
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from bulk_email.models import BulkEmailFlag
@attr('shard_1')
......@@ -42,6 +43,11 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
'course_id': self.course.id.to_deprecated_string(),
'success': True,
}
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
def tearDown(self):
super(TestOptoutCourseEmails, self).tearDown()
BulkEmailFlag.objects.all().delete()
def navigate_to_email_view(self):
"""Navigate to the instructor dash's email view"""
......@@ -49,10 +55,9 @@ class TestOptoutCourseEmails(ModuleStoreTestCase):
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url)
email_section = '<div class="vert-left send-email" id="section-send-email">'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
# If this fails, it is likely because BulkEmailFlag.is_enabled() is set to False
self.assertTrue(email_section in response.content)
@patch.dict(settings.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.
......@@ -80,7 +85,6 @@ 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.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_optin_course(self):
"""
Make sure student receives course email after opting in.
......
......@@ -15,7 +15,7 @@ from django.core.urlresolvers import reverse
from django.core.management import call_command
from django.test.utils import override_settings
from bulk_email.models import Optout
from bulk_email.models import Optout, BulkEmailFlag
from bulk_email.tasks import _get_source_address
from courseware.tests.factories import StaffFactory, InstructorFactory
from instructor_task.subtasks import update_subtask_status
......@@ -79,7 +79,6 @@ class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase):
"""
self.client.login(username=user.username, password="test")
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def goto_instructor_dash_email_view(self):
"""
Goes to the instructor dashboard to verify that the email section is
......@@ -90,7 +89,7 @@ class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase):
# navigate to a particular email section
response = self.client.get(url)
email_section = '<div class="vert-left send-email" id="section-send-email">'
# If this fails, it is likely because ENABLE_INSTRUCTOR_EMAIL is set to False
# If this fails, it is likely because BulkEmailFlag.is_enabled() is set to False
self.assertIn(email_section, response.content)
@classmethod
......@@ -104,6 +103,7 @@ class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase):
def setUp(self):
super(EmailSendFromDashboardTestCase, self).setUp()
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
self.create_staff_and_instructor()
self.create_students()
......@@ -121,19 +121,22 @@ class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase):
'success': True,
}
def tearDown(self):
super(EmailSendFromDashboardTestCase, self).tearDown()
BulkEmailFlag.objects.all().delete()
@attr('shard_1')
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
class TestEmailSendFromDashboardMockedHtmlToText(EmailSendFromDashboardTestCase):
"""
Tests email sending with mocked html_to_text.
"""
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_email_disabled(self):
"""
Test response when email is disabled for course.
"""
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
test_email = {
'action': 'Send email',
'send_to': 'myself',
......@@ -402,7 +405,6 @@ class TestEmailSendFromDashboardMockedHtmlToText(EmailSendFromDashboardTestCase)
@attr('shard_1')
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
@skipIf(os.environ.get("TRAVIS") == 'true', "Skip this test in Travis CI.")
class TestEmailSendFromDashboard(EmailSendFromDashboardTestCase):
"""
......
......@@ -14,7 +14,7 @@ from mock import patch, Mock
from nose.plugins.attrib import attr
from smtplib import SMTPDataError, SMTPServerDisconnected, SMTPConnectError
from bulk_email.models import CourseEmail, SEND_TO_ALL
from bulk_email.models import CourseEmail, SEND_TO_ALL, BulkEmailFlag
from bulk_email.tasks import perform_delegate_email_batches, send_course_email
from instructor_task.models import InstructorTask
from instructor_task.subtasks import (
......@@ -38,7 +38,6 @@ class EmailTestException(Exception):
@attr('shard_1')
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
class TestEmailErrors(ModuleStoreTestCase):
"""
Test that errors from sending email are handled properly.
......@@ -61,6 +60,11 @@ class TestEmailErrors(ModuleStoreTestCase):
'course_id': self.course.id.to_deprecated_string(),
'success': True,
}
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
def tearDown(self):
super(TestEmailErrors, self).tearDown()
BulkEmailFlag.objects.all().delete()
@patch('bulk_email.tasks.get_connection', autospec=True)
@patch('bulk_email.tasks.send_course_email.retry')
......
......@@ -3,10 +3,9 @@
Unit tests for bulk-email-related forms.
"""
from django.conf import settings
from mock import patch
from nose.plugins.attrib import attr
from bulk_email.models import CourseAuthorization, CourseEmailTemplate
from bulk_email.models import CourseEmailTemplate, BulkEmailFlag
from bulk_email.forms import CourseAuthorizationAdminForm, CourseEmailTemplateForm
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -23,11 +22,15 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
super(CourseAuthorizationFormTest, self).setUp()
course_title = u"ẗëṡẗ title イ乇丂イ ᄊ乇丂丂ムg乇 キo尺 ムレレ тэѕт мэѕѕаБэ"
self.course = CourseFactory.create(display_name=course_title)
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
def tearDown(self):
super(CourseAuthorizationFormTest, self).tearDown()
BulkEmailFlag.objects.all().delete()
@patch.dict(settings.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))
self.assertFalse(BulkEmailFlag.feature_enabled(self.course.id))
# Test authorizing the course, which should totally work
form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': True}
form = CourseAuthorizationAdminForm(data=form_data)
......@@ -35,12 +38,11 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
self.assertTrue(form.is_valid())
form.save()
# Check that this course is authorized
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id))
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_repeat_course(self):
# Initially course shouldn't be authorized
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertFalse(BulkEmailFlag.feature_enabled(self.course.id))
# Test authorizing the course, which should totally work
form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': True}
form = CourseAuthorizationAdminForm(data=form_data)
......@@ -48,7 +50,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
self.assertTrue(form.is_valid())
form.save()
# Check that this course is authorized
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id))
# Now make a new course authorization with the same course id that tries to turn email off
form_data = {'course_id': self.course.id.to_deprecated_string(), 'email_enabled': False}
......@@ -66,9 +68,8 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
form.save()
# Course should still be authorized (invalid attempt had no effect)
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id))
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_form_typo(self):
# Munge course id
bad_id = SlashSeparatedCourseKey(u'Broken{}'.format(self.course.id.org), 'hello', self.course.id.run + '_typo')
......@@ -89,7 +90,6 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
):
form.save()
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_form_invalid_key(self):
form_data = {'course_id': "asd::**!@#$%^&*())//foobar!!", 'email_enabled': True}
form = CourseAuthorizationAdminForm(data=form_data)
......@@ -107,7 +107,6 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase):
):
form.save()
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_course_name_only(self):
# Munge course id - common
form_data = {'course_id': self.course.id.run, 'email_enabled': True}
......
......@@ -10,7 +10,7 @@ from student.tests.factories import UserFactory
from mock import patch, Mock
from nose.plugins.attrib import attr
from bulk_email.models import CourseEmail, SEND_TO_STAFF, CourseEmailTemplate, CourseAuthorization
from bulk_email.models import CourseEmail, SEND_TO_STAFF, CourseEmailTemplate, CourseAuthorization, BulkEmailFlag
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......@@ -173,17 +173,21 @@ class CourseEmailTemplateTest(TestCase):
class CourseAuthorizationTest(TestCase):
"""Test the CourseAuthorization model."""
@patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': True})
def tearDown(self):
super(CourseAuthorizationTest, self).tearDown()
BulkEmailFlag.objects.all().delete()
def test_creation_auth_on(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
course_id = SlashSeparatedCourseKey('abc', '123', 'doremi')
# Test that course is not authorized by default
self.assertFalse(CourseAuthorization.instructor_email_enabled(course_id))
self.assertFalse(BulkEmailFlag.feature_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.assertTrue(BulkEmailFlag.feature_enabled(course_id))
self.assertEquals(
cauth.__unicode__(),
"Course 'abc/123/doremi': Instructor Email Enabled"
......@@ -193,21 +197,21 @@ class CourseAuthorizationTest(TestCase):
cauth.email_enabled = False
cauth.save()
# Test that course is now unauthorized
self.assertFalse(CourseAuthorization.instructor_email_enabled(course_id))
self.assertFalse(BulkEmailFlag.feature_enabled(course_id))
self.assertEquals(
cauth.__unicode__(),
"Course 'abc/123/doremi': Instructor Email Not Enabled"
)
@patch.dict(settings.FEATURES, {'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_creation_auth_off(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
course_id = SlashSeparatedCourseKey('blahx', 'blah101', 'ehhhhhhh')
# Test that course is authorized by default, since auth is turned off
self.assertTrue(CourseAuthorization.instructor_email_enabled(course_id))
self.assertTrue(BulkEmailFlag.feature_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))
self.assertTrue(BulkEmailFlag.feature_enabled(course_id))
@shard_2
Feature: LMS.Instructor Dash Bulk Email
As an instructor or course staff,
In order to communicate with students and staff
I want to send email to staff and students in a course.
Scenario: Send bulk email
Given there is a course with a staff, instructor and student
And I am logged in to the course as "<Role>"
When I send email to "<Recipient>"
Then Email is sent to "<Recipient>"
Examples:
| Role | Recipient |
| instructor | myself |
| instructor | course staff |
| instructor | students, staff, and instructors |
| staff | myself |
| staff | course staff |
| staff | students, staff, and instructors |
"""
Define steps for bulk email acceptance test.
"""
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
from lettuce import world, step
from lettuce.django import mail
from nose.tools import assert_in, assert_equal
from django.core.management import call_command
from django.conf import settings
from courseware.tests.factories import StaffFactory, InstructorFactory
@step(u'Given there is a course with a staff, instructor and student')
def make_populated_course(step): # pylint: disable=unused-argument
## This is different than the function defined in common.py because it enrolls
## a staff, instructor, and student member regardless of what `role` is, then
## logs `role` in. This is to ensure we have 3 class participants to email.
# Clear existing courses to avoid conflicts
world.clear_courses()
# Create a new course
course = world.CourseFactory.create(
org='edx',
number='888',
display_name='Bulk Email Test Course'
)
world.bulk_email_course_key = course.id
try:
# See if we've defined the instructor & staff user yet
world.bulk_email_instructor
except AttributeError:
# Make & register an instructor for the course
world.bulk_email_instructor = InstructorFactory(course_key=world.bulk_email_course_key)
world.enroll_user(world.bulk_email_instructor, world.bulk_email_course_key)
# Make & register a staff member
world.bulk_email_staff = StaffFactory(course_key=course.id)
world.enroll_user(world.bulk_email_staff, world.bulk_email_course_key)
# Make & register a student
world.register_by_course_key(
course.id,
username='student',
password='test',
is_staff=False
)
# Store the expected recipients
# given each "send to" option
staff_emails = [world.bulk_email_staff.email, world.bulk_email_instructor.email]
world.expected_addresses = {
'course staff': staff_emails,
'students, staff, and instructors': staff_emails + ['student@edx.org']
}
# Dictionary mapping a description of the email recipient
# to the corresponding <option> value in the UI.
SEND_TO_OPTIONS = {
'myself': 'myself',
'course staff': 'staff',
'students, staff, and instructors': 'all'
}
@step(u'I am logged in to the course as "([^"]*)"')
def log_into_the_course(step, role): # pylint: disable=unused-argument
# Store the role
assert_in(role, ['instructor', 'staff'])
# Log in as the an instructor or staff for the course
my_email = world.bulk_email_instructor.email
if role == 'instructor':
world.log_in(
username=world.bulk_email_instructor.username,
password='test',
email=my_email,
name=world.bulk_email_instructor.profile.name
)
else:
my_email = world.bulk_email_staff.email
world.log_in(
username=world.bulk_email_staff.username,
password='test',
email=my_email,
name=world.bulk_email_staff.profile.name
)
# Store the "myself" send to option
world.expected_addresses['myself'] = [my_email]
@step(u'I send email to "([^"]*)"')
def when_i_send_an_email(step, recipient): # pylint: disable=unused-argument
# Check that the recipient is valid
assert_in(
recipient, SEND_TO_OPTIONS,
msg="Invalid recipient: {}".format(recipient)
)
# Clear the queue of existing emails
while not mail.queue.empty(): # pylint: disable=no-member
mail.queue.get() # pylint: disable=no-member
# Because we flush the database before each run,
# we need to ensure that the email template fixture
# is re-loaded into the database
call_command('loaddata', 'course_email_template.json')
# Go to the email section of the instructor dash
url = '/courses/{}'.format(world.bulk_email_course_key)
world.visit(url)
world.css_click('a[href="{}/instructor"]'.format(url))
world.css_click('a[data-section="send_email"]')
# Select the recipient
world.select_option('send_to', SEND_TO_OPTIONS[recipient])
# Enter subject and message
world.css_fill('input#id_subject', 'Hello')
with world.browser.get_iframe('mce_0_ifr') as iframe:
editor = iframe.find_by_id('tinymce')[0]
editor.fill('test message')
# Click send
world.css_click('input[name="send"]', dismiss_alert=True)
# Expect to see a message that the email was sent
expected_msg = "Your email was successfully queued for sending."
world.wait_for_visible('#request-response')
assert_in(
expected_msg, world.css_text('#request-response'),
msg="Could not find email success message."
)
UNSUBSCRIBE_MSG = 'To stop receiving email like this'
@step(u'Email is sent to "([^"]*)"')
def then_the_email_is_sent(step, recipient): # pylint: disable=unused-argument
# Check that the recipient is valid
assert_in(
recipient, SEND_TO_OPTIONS,
msg="Invalid recipient: {}".format(recipient)
)
# Retrieve messages. Because we are using celery in "always eager"
# mode, we expect all messages to be sent by this point.
messages = []
while not mail.queue.empty(): # pylint: disable=no-member
messages.append(mail.queue.get()) # pylint: disable=no-member
# Check that we got the right number of messages
assert_equal(
len(messages), len(world.expected_addresses[recipient]),
msg="Received {0} instead of {1} messages for {2}".format(
len(messages), len(world.expected_addresses[recipient]), recipient
)
)
# Check that the message properties were correct
recipients = []
for msg in messages:
assert_in('Hello', msg.subject)
assert_in(settings.BULK_EMAIL_DEFAULT_FROM_EMAIL, msg.from_email)
# Message body should have the message we sent
# and an unsubscribe message
assert_in('test message', msg.body)
assert_in(UNSUBSCRIBE_MSG, msg.body)
# Should have alternative HTML form
assert_equal(len(msg.alternatives), 1)
content, mime_type = msg.alternatives[0]
assert_equal(mime_type, 'text/html')
assert_in('test message', content)
assert_in(UNSUBSCRIBE_MSG, content)
# Store the recipient address so we can verify later
recipients.extend(msg.recipients())
# Check that the messages were sent to the right people
# Because "myself" can vary based on who sent the message,
# we use the world.expected_addresses dict we configured
# in an earlier step.
for addr in world.expected_addresses[recipient]:
assert_in(addr, recipients)
......@@ -31,6 +31,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import UsageKey
from xmodule.modulestore import ModuleStoreEnum
from bulk_email.models import BulkEmailFlag
from course_modes.models import CourseMode
from courseware.models import StudentModule
from courseware.tests.factories import StaffFactory, InstructorFactory, BetaTesterFactory, UserProfileFactory
......@@ -192,7 +193,6 @@ class TestCommonExceptions400(TestCase):
@attr('shard_1')
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Ensure that users cannot access endpoints they shouldn't be able to.
......@@ -207,6 +207,12 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
'robot-some-problem-urlname'
)
cls.problem_urlname = cls.problem_location.to_deprecated_string()
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
@classmethod
def tearDownClass(cls):
super(TestInstructorAPIDenyLevels, cls).tearDownClass()
BulkEmailFlag.objects.all().delete()
def setUp(self):
super(TestInstructorAPIDenyLevels, self).setUp()
......@@ -3391,7 +3397,6 @@ class TestEntranceExamInstructorAPIRegradeTask(SharedModuleStoreTestCase, LoginE
@attr('shard_1')
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
class TestInstructorSendEmail(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Checks that only instructors have access to email endpoints, and that
......@@ -3409,6 +3414,12 @@ class TestInstructorSendEmail(SharedModuleStoreTestCase, LoginEnrollmentTestCase
'subject': test_subject,
'message': test_message,
}
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
@classmethod
def tearDownClass(cls):
super(TestInstructorSendEmail, cls).tearDownClass()
BulkEmailFlag.objects.all().delete()
def setUp(self):
super(TestInstructorSendEmail, self).setUp()
......
......@@ -6,11 +6,10 @@ that the view is conditionally available when Course Auth is turned on.
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from mock import patch
from nose.plugins.attrib import attr
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from bulk_email.models import CourseAuthorization
from bulk_email.models import CourseAuthorization, BulkEmailFlag
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MIXED_MODULESTORE, SharedModuleStoreTestCase
)
......@@ -41,14 +40,18 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase):
instructor = AdminFactory.create()
self.client.login(username=instructor.username, password="test")
# In order for bulk email to work, we must have both the ENABLE_INSTRUCTOR_EMAIL_FLAG
def tearDown(self):
super(TestNewInstructorDashboardEmailViewMongoBacked, self).tearDown()
BulkEmailFlag.objects.all().delete()
# In order for bulk email to work, we must have both the BulkEmailFlag.is_enabled()
# 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.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_true_mongo_true(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
# 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))
self.assertTrue(BulkEmailFlag.feature_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)
......@@ -58,26 +61,26 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase):
self.assertEqual(response.status_code, 200)
# The course is Mongo-backed but the flag is disabled (should not work)
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False})
def test_email_flag_false_mongo_true(self):
BulkEmailFlag.objects.create(enabled=False)
# 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, but we require course auth and haven't turned it on for this course
@patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_course_not_authorized(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
# Assert that instructor email is not enabled for this course
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertFalse(BulkEmailFlag.feature_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.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_course_authorized(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True)
# Assert that instructor email is not enabled for this course
self.assertFalse(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertFalse(BulkEmailFlag.feature_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)
......@@ -87,19 +90,20 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase):
cauth.save()
# Assert that instructor email is enabled for this course
self.assertTrue(CourseAuthorization.instructor_email_enabled(self.course.id))
self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id))
# Assert that the URL for the email view is 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.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False, 'REQUIRE_COURSE_EMAIL_AUTH': True})
def test_course_authorized_feature_off(self):
BulkEmailFlag.objects.create(enabled=False, require_course_email_auth=True)
# 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
# Assert that this course is authorized for instructor email, but the feature is not enabled
self.assertFalse(BulkEmailFlag.feature_enabled(self.course.id))
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)
......@@ -136,15 +140,19 @@ class TestNewInstructorDashboardEmailViewXMLBacked(SharedModuleStoreTestCase):
# URL for email view
self.email_link = '<a href="" data-section="send_email">Email</a>'
def tearDown(self):
super(TestNewInstructorDashboardEmailViewXMLBacked, self).tearDown()
BulkEmailFlag.objects.all().delete()
# 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.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_true_mongo_false(self):
BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False)
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.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False, 'REQUIRE_COURSE_EMAIL_AUTH': False})
def test_email_flag_false_mongo_false(self):
BulkEmailFlag.objects.create(enabled=False, require_course_email_auth=False)
response = self.client.get(self.url)
self.assertFalse(self.email_link in response.content)
......@@ -91,7 +91,7 @@ from submissions import api as sub_api # installed from the edx-submissions rep
from certificates import api as certs_api
from certificates.models import CertificateWhitelist, GeneratedCertificate, CertificateStatuses, CertificateInvalidation
from bulk_email.models import CourseEmail
from bulk_email.models import CourseEmail, BulkEmailFlag
from student.models import get_user_by_username_or_email
from .tools import (
......@@ -104,7 +104,6 @@ from .tools import (
parse_datetime,
set_due_date_extension,
strip_if_string,
bulk_email_is_enabled_for_course,
)
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......@@ -2487,7 +2486,7 @@ def send_email(request, course_id):
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
if not bulk_email_is_enabled_for_course(course_id):
if not BulkEmailFlag.feature_enabled(course_id):
return HttpResponseForbidden("Email is not enabled for this course.")
send_to = request.POST.get("send_to")
......
......@@ -46,10 +46,11 @@ from certificates.models import (
CertificateInvalidation,
)
from certificates import api as certs_api
from bulk_email.models import BulkEmailFlag
from util.date_utils import get_default_time_display
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
from .tools import get_units_with_due_date, title_or_url, bulk_email_is_enabled_for_course
from .tools import get_units_with_due_date, title_or_url
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from openedx.core.djangolib.markup import Text, HTML
......@@ -140,7 +141,7 @@ def instructor_dashboard_2(request, course_id):
sections.insert(3, _section_extensions(course))
# Gate access to course email by feature flag & by course-specific authorization
if bulk_email_is_enabled_for_course(course_key):
if BulkEmailFlag.feature_enabled(course_key):
sections.append(_section_send_email(course, access))
# Gate access to Metrics tab by featue flag and staff authorization
......
......@@ -22,8 +22,6 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import UsageKey
from bulk_email.models import CourseAuthorization
DATE_FIELD = Date()
......@@ -57,23 +55,6 @@ def handle_dashboard_error(view):
return wrapper
def bulk_email_is_enabled_for_course(course_id):
"""
Staff can only send bulk email for a course if all the following conditions are true:
1. Bulk email feature flag is on.
2. It is a studio course.
3. Bulk email is enabled for the course.
"""
bulk_email_enabled_globally = (settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] is True)
bulk_email_enabled_for_course = CourseAuthorization.instructor_email_enabled(course_id)
if bulk_email_enabled_globally and bulk_email_enabled_for_course:
return True
return False
def strip_if_string(value):
if isinstance(value, basestring):
return value.strip()
......
......@@ -127,10 +127,7 @@ THIRD_PARTY_AUTH = {
# Enable fake payment processing page
FEATURES['ENABLE_PAYMENT_FAKE'] = True
# Enable email on the instructor dash
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False
# Enable special exams
FEATURES['ENABLE_SPECIAL_EXAMS'] = True
# Don't actually send any requests to Software Secure for student identity
......
......@@ -61,7 +61,7 @@
"CONTACT_EMAIL": "info@example.com",
"DEFAULT_FEEDBACK_EMAIL": "feedback@example.com",
"DEFAULT_FROM_EMAIL": "registration@example.com",
"EMAIL_BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"EMAIL_BACKEND": "django.core.mail.backends.dummy.EmailBackend",
"SOCIAL_SHARING_SETTINGS": {
"CUSTOM_COURSE_URLS": true,
"DASHBOARD_FACEBOOK": true,
......
......@@ -130,14 +130,6 @@ FEATURES = {
# Enables ability to restrict enrollment in specific courses by the user account login method
'RESTRICT_ENROLL_BY_REG_METHOD': 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 Mongo-backed courses.
'REQUIRE_COURSE_EMAIL_AUTH': True,
# enable analytics server.
# WARNING: THIS SHOULD ALWAYS BE SET TO FALSE UNDER NORMAL
# LMS OPERATION. See analytics.py for details about what
......
......@@ -22,8 +22,6 @@ FEATURES['DISABLE_START_DATES'] = False
FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True
FEATURES['ENABLE_SERVICE_STATUS'] = True
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
FEATURES['ENABLE_SHOPPING_CART'] = True
FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True
FEATURES['ENABLE_S3_GRADE_DOWNLOADS'] = True
......
......@@ -36,9 +36,6 @@ for log_name, log_level in LOG_OVERRIDES:
################################ EMAIL ########################################
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses
FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
########################## ANALYTICS TESTING ########################
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment