From b33b8e8897b99b57cc5d3985de89a2ccd3c5b632 Mon Sep 17 00:00:00 2001 From: Michael Roytman Date: Tue, 14 May 2019 20:15:39 -0400 Subject: [PATCH] introduce Python API for BulkEmail Djangoapp --- .../student/tests/test_bulk_email_settings.py | 7 +-- common/djangoapps/student/tests/test_views.py | 5 +- common/djangoapps/student/views/dashboard.py | 5 +- lms/djangoapps/bulk_email/api.py | 11 +++++ lms/djangoapps/bulk_email/models.py | 7 +++ lms/djangoapps/bulk_email/models_api.py | 48 +++++++++++++++++++ lms/djangoapps/bulk_email/tests/test_email.py | 2 +- lms/djangoapps/bulk_email/tests/test_forms.py | 11 +++-- .../bulk_email/tests/test_models.py | 29 ++++++++--- lms/djangoapps/instructor/tests/test_email.py | 13 ++--- lms/djangoapps/instructor/views/api.py | 5 +- .../instructor/views/instructor_dashboard.py | 4 +- 12 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 lms/djangoapps/bulk_email/api.py create mode 100644 lms/djangoapps/bulk_email/models_api.py diff --git a/common/djangoapps/student/tests/test_bulk_email_settings.py b/common/djangoapps/student/tests/test_bulk_email_settings.py index 785fdc3733..50bd90dc07 100644 --- a/common/djangoapps/student/tests/test_bulk_email_settings.py +++ b/common/djangoapps/student/tests/test_bulk_email_settings.py @@ -13,7 +13,8 @@ from django.urls import reverse # This import is for an lms djangoapp. # Its testcases are only run under lms. -from bulk_email.models import BulkEmailFlag, CourseAuthorization # pylint: disable=import-error +from bulk_email.api import is_bulk_email_feature_enabled +from bulk_email.models import BulkEmailFlag, CourseAuthorization from student.tests.factories import CourseEnrollmentFactory, UserFactory from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -68,7 +69,7 @@ class TestStudentDashboardEmailView(SharedModuleStoreTestCase): 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(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertFalse(is_bulk_email_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) @@ -80,7 +81,7 @@ class TestStudentDashboardEmailView(SharedModuleStoreTestCase): cauth = CourseAuthorization(course_id=self.course.id, email_enabled=True) cauth.save() # Assert that instructor email is enabled for this course - self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_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) diff --git a/common/djangoapps/student/tests/test_views.py b/common/djangoapps/student/tests/test_views.py index 53c32f0259..ffeaa1db3b 100644 --- a/common/djangoapps/student/tests/test_views.py +++ b/common/djangoapps/student/tests/test_views.py @@ -24,7 +24,6 @@ from opaque_keys.edx.keys import CourseKey from pyquery import PyQuery as pq from six.moves import range -from bulk_email.models import BulkEmailFlag from course_modes.models import CourseMode from entitlements.tests.factories import CourseEntitlementFactory from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory @@ -508,7 +507,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, self.assertIn('Related Programs:', response.content) @patch('openedx.core.djangoapps.catalog.utils.get_course_runs_for_course') - @patch.object(BulkEmailFlag, 'feature_enabled') + @patch('student.views.dashboard.is_bulk_email_feature_enabled') def test_email_settings_fulfilled_entitlement(self, mock_email_feature, mock_get_course_runs): """ Assert that the Email Settings action is shown when the user has a fulfilled entitlement. @@ -529,7 +528,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin, self.assertEqual(pq(response.content)(self.EMAIL_SETTINGS_ELEMENT_ID).length, 1) @patch.object(CourseOverview, 'get_from_id') - @patch.object(BulkEmailFlag, 'feature_enabled') + @patch('student.views.dashboard.is_bulk_email_feature_enabled') def test_email_settings_unfulfilled_entitlement(self, mock_email_feature, mock_course_overview): """ Assert that the Email Settings action is not shown when the entitlement is not fulfilled. diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index c07c649cf3..9102752a0c 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -23,7 +23,8 @@ from pytz import UTC from six import iteritems, text_type import track.views -from bulk_email.models import BulkEmailFlag, Optout # pylint: disable=import-error +from bulk_email.api import is_bulk_email_feature_enabled +from bulk_email.models import Optout # pylint: disable=import-error from course_modes.models import CourseMode from courseware.access import has_access from edxmako.shortcuts import render_to_response, render_to_string @@ -755,7 +756,7 @@ def student_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 ( - BulkEmailFlag.feature_enabled(enrollment.course_id) + is_bulk_email_feature_enabled(enrollment.course_id) ) ) diff --git a/lms/djangoapps/bulk_email/api.py b/lms/djangoapps/bulk_email/api.py new file mode 100644 index 0000000000..3d54d5f363 --- /dev/null +++ b/lms/djangoapps/bulk_email/api.py @@ -0,0 +1,11 @@ +# pylint: disable=unused-import +""" +Python APIs exposed by the bulk_email app to other in-process apps. +""" + +# Public Bulk Email Functions +from bulk_email.models_api import ( + is_bulk_email_enabled_for_course, + is_bulk_email_feature_enabled, + is_user_opted_out_for_course, +) diff --git a/lms/djangoapps/bulk_email/models.py b/lms/djangoapps/bulk_email/models.py index f5e7ad787d..2ce7c182c6 100644 --- a/lms/djangoapps/bulk_email/models.py +++ b/lms/djangoapps/bulk_email/models.py @@ -325,6 +325,13 @@ class Optout(models.Model): app_label = "bulk_email" unique_together = ('user', 'course_id') + @classmethod + def is_user_opted_out_for_course(cls, user, course_id): + return cls.objects.filter( + user=user, + course_id=course_id, + ).exists() + # Defines the tag that must appear in a template, to indicate # the location where the email message body is to be inserted. diff --git a/lms/djangoapps/bulk_email/models_api.py b/lms/djangoapps/bulk_email/models_api.py new file mode 100644 index 0000000000..fe76eee19a --- /dev/null +++ b/lms/djangoapps/bulk_email/models_api.py @@ -0,0 +1,48 @@ +""" +Provides Python APIs exposed from Bulk Email models. +""" +from bulk_email.models import BulkEmailFlag, CourseAuthorization, Optout + + +def is_user_opted_out_for_course(user, course_id): + """ + Arguments: + user: user whose opt out status is to be returned + course_id (CourseKey): id of the course + + Returns: + bool: True if user has opted out of e-mails for the course + associated with course_id, False otherwise. + """ + return Optout.is_user_opted_out_for_course(user, course_id) + + +def is_bulk_email_feature_enabled(course_id=None): + """ + Looks at the currently active configuration model to determine whether the bulk email feature is available. + + Arguments: + course_id (string; optional): the course id of the course + + Returns: + bool: True or False, depending on the following: + 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. + """ + return BulkEmailFlag.feature_enabled(course_id) + + +def is_bulk_email_enabled_for_course(course_id): + """ + Arguments: + course_id: the course id of the course + + Returns: + bool: True if the Bulk Email feature is enabled for the course + associated with the course_id; False otherwise + """ + return CourseAuthorization.instructor_email_enabled(course_id) diff --git a/lms/djangoapps/bulk_email/tests/test_email.py b/lms/djangoapps/bulk_email/tests/test_email.py index 89cc7f6fbf..f2870ec9fa 100644 --- a/lms/djangoapps/bulk_email/tests/test_email.py +++ b/lms/djangoapps/bulk_email/tests/test_email.py @@ -95,7 +95,7 @@ class EmailSendFromDashboardTestCase(SharedModuleStoreTestCase): # navigate to a particular email section response = self.client.get(url) email_section = '
' - # If this fails, it is likely because BulkEmailFlag.is_enabled() is set to False + # If this fails, it is likely because bulk_email.api.is_bulk_email_feature_enabled is set to False self.assertIn(email_section, response.content) @classmethod diff --git a/lms/djangoapps/bulk_email/tests/test_forms.py b/lms/djangoapps/bulk_email/tests/test_forms.py index 851a2b7e7e..0243cc666c 100644 --- a/lms/djangoapps/bulk_email/tests/test_forms.py +++ b/lms/djangoapps/bulk_email/tests/test_forms.py @@ -6,6 +6,7 @@ Unit tests for bulk-email-related forms. from opaque_keys.edx.locator import CourseLocator from six import text_type +from bulk_email.api import is_bulk_email_feature_enabled from bulk_email.forms import CourseAuthorizationAdminForm, CourseEmailTemplateForm from bulk_email.models import BulkEmailFlag, CourseEmailTemplate from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -27,7 +28,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): def test_authorize_mongo_course(self): # Initially course shouldn't be authorized - self.assertFalse(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertFalse(is_bulk_email_feature_enabled(self.course.id)) # Test authorizing the course, which should totally work form_data = {'course_id': text_type(self.course.id), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) @@ -35,11 +36,11 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): self.assertTrue(form.is_valid()) form.save() # Check that this course is authorized - self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_feature_enabled(self.course.id)) def test_repeat_course(self): # Initially course shouldn't be authorized - self.assertFalse(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertFalse(is_bulk_email_feature_enabled(self.course.id)) # Test authorizing the course, which should totally work form_data = {'course_id': text_type(self.course.id), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) @@ -47,7 +48,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): self.assertTrue(form.is_valid()) form.save() # Check that this course is authorized - self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_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': text_type(self.course.id), 'email_enabled': False} @@ -65,7 +66,7 @@ class CourseAuthorizationFormTest(ModuleStoreTestCase): form.save() # Course should still be authorized (invalid attempt had no effect) - self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_feature_enabled(self.course.id)) def test_form_typo(self): # Munge course id diff --git a/lms/djangoapps/bulk_email/tests/test_models.py b/lms/djangoapps/bulk_email/tests/test_models.py index c98c612ae1..5178f61736 100644 --- a/lms/djangoapps/bulk_email/tests/test_models.py +++ b/lms/djangoapps/bulk_email/tests/test_models.py @@ -10,6 +10,7 @@ from mock import Mock, patch from opaque_keys.edx.keys import CourseKey from pytz import UTC +from bulk_email.api import is_bulk_email_feature_enabled from bulk_email.models import ( SEND_TO_COHORT, SEND_TO_STAFF, @@ -17,7 +18,8 @@ from bulk_email.models import ( BulkEmailFlag, CourseAuthorization, CourseEmail, - CourseEmailTemplate + CourseEmailTemplate, + Optout, ) from course_modes.models import CourseMode from openedx.core.djangoapps.course_groups.models import CourseCohort @@ -144,6 +146,21 @@ class CourseEmailTest(ModuleStoreTestCase): self.assertEqual(target.long_display(), 'Cohort: test cohort') +class OptoutTest(TestCase): + def test_is_user_opted_out_for_course(self): + user = UserFactory.create() + course_id = CourseKey.from_string('abc/123/doremi') + + self.assertFalse(Optout.is_user_opted_out_for_course(user, course_id)) + + Optout.objects.create( + user=user, + course_id=course_id, + ) + + self.assertTrue(Optout.is_user_opted_out_for_course(user, course_id)) + + class NoCourseEmailTemplateTest(TestCase): """Test the CourseEmailTemplate model without loading the template data.""" @@ -261,13 +278,13 @@ class CourseAuthorizationTest(TestCase): BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=True) course_id = CourseKey.from_string('abc/123/doremi') # Test that course is not authorized by default - self.assertFalse(BulkEmailFlag.feature_enabled(course_id)) + self.assertFalse(is_bulk_email_feature_enabled(course_id)) # Authorize cauth = CourseAuthorization(course_id=course_id, email_enabled=True) cauth.save() # Now, course should be authorized - self.assertTrue(BulkEmailFlag.feature_enabled(course_id)) + self.assertTrue(is_bulk_email_feature_enabled(course_id)) self.assertEqual( cauth.__unicode__(), "Course 'abc/123/doremi': Instructor Email Enabled" @@ -277,7 +294,7 @@ class CourseAuthorizationTest(TestCase): cauth.email_enabled = False cauth.save() # Test that course is now unauthorized - self.assertFalse(BulkEmailFlag.feature_enabled(course_id)) + self.assertFalse(is_bulk_email_feature_enabled(course_id)) self.assertEqual( cauth.__unicode__(), "Course 'abc/123/doremi': Instructor Email Not Enabled" @@ -287,11 +304,11 @@ class CourseAuthorizationTest(TestCase): BulkEmailFlag.objects.create(enabled=True, require_course_email_auth=False) course_id = CourseKey.from_string('blahx/blah101/ehhhhhhh') # Test that course is authorized by default, since auth is turned off - self.assertTrue(BulkEmailFlag.feature_enabled(course_id)) + self.assertTrue(is_bulk_email_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(BulkEmailFlag.feature_enabled(course_id)) + self.assertTrue(is_bulk_email_feature_enabled(course_id)) diff --git a/lms/djangoapps/instructor/tests/test_email.py b/lms/djangoapps/instructor/tests/test_email.py index d44e51aa07..7835b5b963 100644 --- a/lms/djangoapps/instructor/tests/test_email.py +++ b/lms/djangoapps/instructor/tests/test_email.py @@ -11,6 +11,7 @@ from django.urls import reverse from opaque_keys.edx.keys import CourseKey from six import text_type +from bulk_email.api import is_bulk_email_enabled_for_course, is_bulk_email_feature_enabled from bulk_email.models import BulkEmailFlag, CourseAuthorization from student.tests.factories import AdminFactory from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE, SharedModuleStoreTestCase @@ -51,7 +52,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase): 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(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_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) @@ -71,7 +72,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase): 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(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertFalse(is_bulk_email_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.assertNotIn(self.email_link, response.content) @@ -80,7 +81,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase): 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(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertFalse(is_bulk_email_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.assertNotIn(self.email_link, response.content) @@ -90,7 +91,7 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase): cauth.save() # Assert that instructor email is enabled for this course - self.assertTrue(BulkEmailFlag.feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_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) @@ -103,8 +104,8 @@ class TestNewInstructorDashboardEmailViewMongoBacked(SharedModuleStoreTestCase): cauth.save() # 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)) + self.assertFalse(is_bulk_email_feature_enabled(self.course.id)) + self.assertTrue(is_bulk_email_enabled_for_course(self.course.id)) # Assert that the URL for the email view IS NOT in the response response = self.client.get(self.url) self.assertNotIn(self.email_link, response.content) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index ee76af354b..de3544a98e 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -44,7 +44,8 @@ from submissions import api as sub_api # installed from the edx-submissions rep import instructor_analytics.basic import instructor_analytics.csvs import instructor_analytics.distributions -from bulk_email.models import BulkEmailFlag, CourseEmail +from bulk_email.api import is_bulk_email_feature_enabled +from bulk_email.models import CourseEmail from courseware.access import has_access from courseware.courses import get_course_by_id, get_course_with_access from courseware.models import StudentModule @@ -2692,7 +2693,7 @@ def send_email(request, course_id): """ course_id = CourseKey.from_string(course_id) - if not BulkEmailFlag.feature_enabled(course_id): + if not is_bulk_email_feature_enabled(course_id): log.warning(u'Email is not enabled for course %s', course_id) return HttpResponseForbidden("Email is not enabled for this course.") diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 6c5edaecbc..ed9e4347a7 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -25,7 +25,7 @@ from six import text_type from xblock.field_data import DictFieldData from xblock.fields import ScopeIds -from bulk_email.models import BulkEmailFlag +from bulk_email.api import is_bulk_email_feature_enabled from class_dashboard.dashboard_data import get_array_section_has_problem, get_section_display_name from course_modes.models import CourseMode, CourseModesArchive from courseware.access import has_access @@ -164,7 +164,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 BulkEmailFlag.feature_enabled(course_key): + if is_bulk_email_feature_enabled(course_key): sections.append(_section_send_email(course, access)) # Gate access to Metrics tab by featue flag and staff authorization