Merge pull request #20620 from edx/mroytman/bulk-email-python-api

introduce Python API for BulkEmail Djangoapp
This commit is contained in:
Michael Roytman
2019-05-21 18:38:24 -04:00
committed by GitHub
12 changed files with 117 additions and 30 deletions

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)
)
)

View File

@@ -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,
)

View File

@@ -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.

View File

@@ -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)

View File

@@ -99,7 +99,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 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

View File

@@ -8,6 +8,7 @@ from __future__ import absolute_import
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
@@ -29,7 +30,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)
@@ -37,11 +38,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)
@@ -49,7 +50,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}
@@ -67,7 +68,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

View File

@@ -12,6 +12,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,
@@ -19,7 +20,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
@@ -146,6 +148,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."""
@@ -263,13 +280,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"
@@ -279,7 +296,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"
@@ -289,11 +306,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))

View File

@@ -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)

View File

@@ -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.")

View File

@@ -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