Merge pull request #20620 from edx/mroytman/bulk-email-python-api
introduce Python API for BulkEmail Djangoapp
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
11
lms/djangoapps/bulk_email/api.py
Normal file
11
lms/djangoapps/bulk_email/api.py
Normal 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,
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
48
lms/djangoapps/bulk_email/models_api.py
Normal file
48
lms/djangoapps/bulk_email/models_api.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user