Merge pull request #12376 from edx/efischer/bulk_email_flag
Move ENABLE_INSTRUCTOR_EMAIL to admin panel
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,6 +9,19 @@
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
DROP TABLE IF EXISTS `api_admin_apiaccessconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `api_admin_apiaccessconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `api_admin_apiacce_changed_by_id_771a504ee92a076c_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `api_admin_apiacce_changed_by_id_771a504ee92a076c_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `api_admin_apiaccessrequest`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -20,9 +33,15 @@ CREATE TABLE `api_admin_apiaccessrequest` (
|
||||
`website` varchar(200) NOT NULL,
|
||||
`reason` longtext NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`company_address` varchar(255) NOT NULL,
|
||||
`company_name` varchar(255) NOT NULL,
|
||||
`contacted` tinyint(1) NOT NULL,
|
||||
`site_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `api_admin_apiaccessrequ_user_id_6753e50e296cabc7_fk_auth_user_id` (`user_id`),
|
||||
UNIQUE KEY `api_admin_apiaccessrequest_user_id_6753e50e296cabc7_uniq` (`user_id`),
|
||||
KEY `api_admin_apiaccessrequest_9acb4454` (`status`),
|
||||
KEY `api_admin_apiaccessrequest_9365d6e7` (`site_id`),
|
||||
CONSTRAINT `api_admin_apiaccessre_site_id_7963330a765f8041_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`),
|
||||
CONSTRAINT `api_admin_apiaccessrequ_user_id_6753e50e296cabc7_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
@@ -41,10 +60,15 @@ CREATE TABLE `api_admin_historicalapiaccessrequest` (
|
||||
`history_type` varchar(1) NOT NULL,
|
||||
`history_user_id` int(11) DEFAULT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`company_address` varchar(255) NOT NULL,
|
||||
`company_name` varchar(255) NOT NULL,
|
||||
`contacted` tinyint(1) NOT NULL,
|
||||
`site_id` int(11),
|
||||
PRIMARY KEY (`history_id`),
|
||||
KEY `api_admin_histo_history_user_id_73c59297a81bcd02_fk_auth_user_id` (`history_user_id`),
|
||||
KEY `api_admin_historicalapiaccessrequest_b80bb774` (`id`),
|
||||
KEY `api_admin_historicalapiaccessrequest_9acb4454` (`status`),
|
||||
KEY `api_admin_historicalapiaccessrequest_9365d6e7` (`site_id`),
|
||||
CONSTRAINT `api_admin_histo_history_user_id_73c59297a81bcd02_fk_auth_user_id` FOREIGN KEY (`history_user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
@@ -451,7 +475,7 @@ CREATE TABLE `auth_permission` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
|
||||
CONSTRAINT `auth__content_type_id_508cf46651277a81_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=806 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=824 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -677,6 +701,20 @@ CREATE TABLE `branding_brandinginfoconfig` (
|
||||
CONSTRAINT `branding_branding_changed_by_id_298e4241fae118cc_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `bulk_email_bulkemailflag`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `bulk_email_bulkemailflag` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`require_course_email_auth` tinyint(1) NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `bulk_email_bulkem_changed_by_id_67960d6511f876aa_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `bulk_email_bulkem_changed_by_id_67960d6511f876aa_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `bulk_email_courseauthorization`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -736,6 +774,50 @@ CREATE TABLE `bulk_email_optout` (
|
||||
CONSTRAINT `bulk_email_optout_user_id_5d6e4a037bcf14bd_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `ccx_ccxfieldoverride`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `ccx_ccxfieldoverride` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`location` varchar(255) NOT NULL,
|
||||
`field` varchar(255) NOT NULL,
|
||||
`value` longtext NOT NULL,
|
||||
`ccx_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `ccx_ccxfieldoverride_ccx_id_432b832e71334ab2_uniq` (`ccx_id`,`location`,`field`),
|
||||
KEY `ccx_ccxfieldoverride_d5189de0` (`location`),
|
||||
KEY `ccx_ccxfieldoverride_5b9c1ccd` (`ccx_id`),
|
||||
CONSTRAINT `ccx_ccxfield_ccx_id_9266d91ee561fcc_fk_ccx_customcourseforedx_id` FOREIGN KEY (`ccx_id`) REFERENCES `ccx_customcourseforedx` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `ccx_customcourseforedx`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `ccx_customcourseforedx` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`course_id` varchar(255) NOT NULL,
|
||||
`display_name` varchar(255) NOT NULL,
|
||||
`coach_id` int(11) NOT NULL,
|
||||
`structure_json` longtext,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ccx_customcourseforedx_coach_id_ad6ec0656b3bae_fk_auth_user_id` (`coach_id`),
|
||||
KEY `ccx_customcourseforedx_ea134da7` (`course_id`),
|
||||
CONSTRAINT `ccx_customcourseforedx_coach_id_ad6ec0656b3bae_fk_auth_user_id` FOREIGN KEY (`coach_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `ccxcon_ccxcon`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `ccxcon_ccxcon` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`url` varchar(200) NOT NULL,
|
||||
`oauth_client_id` varchar(255) NOT NULL,
|
||||
`oauth_client_secret` varchar(255) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `url` (`url`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `celery_taskmeta`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -967,6 +1049,20 @@ CREATE TABLE `commerce_commerceconfiguration` (
|
||||
CONSTRAINT `commerce_commerce_changed_by_id_7441951d1c97c1d7_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `contentserver_cdnuseragentsconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `contentserver_cdnuseragentsconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`change_date` datetime(6) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`cdn_user_agents` longtext NOT NULL,
|
||||
`changed_by_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `contentserver_cdn_changed_by_id_36fe2b67b2c7f0ba_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `contentserver_cdn_changed_by_id_36fe2b67b2c7f0ba_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `contentserver_courseassetcachettlconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -1755,7 +1851,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `django_content_type_app_label_45f3b1d93ec8c61c_uniq` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=268 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=274 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_migrations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1766,7 +1862,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 */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2012,7 +2108,7 @@ CREATE TABLE `embargo_country` (
|
||||
`country` varchar(2) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `country` (`country`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=250 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=251 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `embargo_countryaccessrule`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2160,24 +2256,6 @@ CREATE TABLE `lms_xblock_xblockasidesconfig` (
|
||||
CONSTRAINT `lms_xblock_xblocka_changed_by_id_eabf5ef3e34dfb8_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `mentoring_answer`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `mentoring_answer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`student_id` varchar(32) NOT NULL,
|
||||
`course_id` varchar(50) NOT NULL,
|
||||
`student_input` longtext NOT NULL,
|
||||
`created_on` datetime(6) NOT NULL,
|
||||
`modified_on` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `student_id` (`student_id`,`course_id`,`name`),
|
||||
KEY `mentoring_answer_b068931c` (`name`),
|
||||
KEY `mentoring_answer_30a811f6` (`student_id`),
|
||||
KEY `mentoring_answer_ea134da7` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `microsite_configuration_historicalmicrositeorganizationmapping`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2371,6 +2449,24 @@ CREATE TABLE `milestones_usermilestone` (
|
||||
CONSTRAINT `milesto_milestone_id_4fe38e3e9994f15c_fk_milestones_milestone_id` FOREIGN KEY (`milestone_id`) REFERENCES `milestones_milestone` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `mobile_api_appversionconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `mobile_api_appversionconfig` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`platform` varchar(50) NOT NULL,
|
||||
`version` varchar(50) NOT NULL,
|
||||
`major_version` int(11) NOT NULL,
|
||||
`minor_version` int(11) NOT NULL,
|
||||
`patch_version` int(11) NOT NULL,
|
||||
`expire_at` datetime(6) DEFAULT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`created_at` datetime(6) NOT NULL,
|
||||
`updated_at` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `mobile_api_appversionconfig_platform_d34993f68d46008_uniq` (`platform`,`version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `mobile_api_mobileapiconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2726,43 +2822,6 @@ CREATE TABLE `organizations_organizationcourse` (
|
||||
CONSTRAINT `a7b04b16eba98e518fbe21d390bd8e3e` FOREIGN KEY (`organization_id`) REFERENCES `organizations_organization` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `problem_builder_answer`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `problem_builder_answer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(50) NOT NULL,
|
||||
`student_id` varchar(32) NOT NULL,
|
||||
`course_id` varchar(50) NOT NULL,
|
||||
`student_input` longtext NOT NULL,
|
||||
`created_on` datetime(6) NOT NULL,
|
||||
`modified_on` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `problem_builder_answer_student_id_2f6847a9fb3e9385_uniq` (`student_id`,`course_id`,`name`),
|
||||
KEY `problem_builder_answer_b068931c` (`name`),
|
||||
KEY `problem_builder_answer_30a811f6` (`student_id`),
|
||||
KEY `problem_builder_answer_ea134da7` (`course_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `problem_builder_share`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `problem_builder_share` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`submission_uid` varchar(32) NOT NULL,
|
||||
`block_id` varchar(255) NOT NULL,
|
||||
`notified` tinyint(1) NOT NULL,
|
||||
`shared_by_id` int(11) NOT NULL,
|
||||
`shared_with_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `problem_builder_share_shared_by_id_4e845ea266d66e1_uniq` (`shared_by_id`,`shared_with_id`,`block_id`),
|
||||
KEY `problem_builder__shared_with_id_573844d7dca07647_fk_auth_user_id` (`shared_with_id`),
|
||||
KEY `problem_builder_share_7e53bca2` (`block_id`),
|
||||
KEY `problem_builder_share_e559ad34` (`notified`),
|
||||
CONSTRAINT `problem_builder__shared_with_id_573844d7dca07647_fk_auth_user_id` FOREIGN KEY (`shared_with_id`) REFERENCES `auth_user` (`id`),
|
||||
CONSTRAINT `problem_builder_sh_shared_by_id_35201b15adc664ce_fk_auth_user_id` FOREIGN KEY (`shared_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `proctoring_proctoredexam`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -2779,6 +2838,7 @@ CREATE TABLE `proctoring_proctoredexam` (
|
||||
`is_proctored` tinyint(1) NOT NULL,
|
||||
`is_practice_exam` tinyint(1) NOT NULL,
|
||||
`is_active` tinyint(1) NOT NULL,
|
||||
`hide_after_due` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `proctoring_proctoredexam_course_id_7d8ab189323890c0_uniq` (`course_id`,`content_id`),
|
||||
KEY `proctoring_proctoredexam_ea134da7` (`course_id`),
|
||||
@@ -3007,6 +3067,7 @@ CREATE TABLE `programs_programsapiconfig` (
|
||||
`enable_certification` tinyint(1) NOT NULL,
|
||||
`max_retries` int(10) unsigned NOT NULL,
|
||||
`xseries_ad_enabled` tinyint(1) NOT NULL,
|
||||
`program_listing_enabled` tinyint(1) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` (`changed_by_id`),
|
||||
CONSTRAINT `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
@@ -3948,6 +4009,29 @@ CREATE TABLE `survey_surveyform` (
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `tagging_tagavailablevalues`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `tagging_tagavailablevalues` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`value` varchar(255) NOT NULL,
|
||||
`category_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `tagging_tagavailablevalues_b583a629` (`category_id`),
|
||||
CONSTRAINT `tagging_category_id_40780d45c76e4f97_fk_tagging_tagcategories_id` FOREIGN KEY (`category_id`) REFERENCES `tagging_tagcategories` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `tagging_tagcategories`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `tagging_tagcategories` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `teams_courseteam`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -3990,18 +4074,6 @@ CREATE TABLE `teams_courseteammembership` (
|
||||
CONSTRAINT `teams_courseteammembers_user_id_2d93b28be22c3c40_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `theming_sitetheme`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `theming_sitetheme` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`theme_dir_name` varchar(255) NOT NULL,
|
||||
`site_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `theming_sitetheme_site_id_4fccdacaebfeb01f_fk_django_site_id` (`site_id`),
|
||||
CONSTRAINT `theming_sitetheme_site_id_4fccdacaebfeb01f_fk_django_site_id` FOREIGN KEY (`site_id`) REFERENCES `django_site` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `third_party_auth_ltiproviderconfig`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
@@ -4222,6 +4294,7 @@ CREATE TABLE `verified_track_content_verifiedtrackcohortedcourse` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`course_key` varchar(255) NOT NULL,
|
||||
`enabled` tinyint(1) NOT NULL,
|
||||
`verified_cohort_name` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `course_key` (`course_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
@@ -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 */;
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
11
common/test/db_fixtures/bulk_email_flag.json
Normal file
11
common/test/db_fixtures/bulk_email_flag.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"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)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- 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))
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
@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 |
|
||||
@@ -1,196 +0,0 @@
|
||||
"""
|
||||
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 ########################
|
||||
|
||||
|
||||
Reference in New Issue
Block a user