Commit e6f15bf6 by cahrens

Send notification to admin when a user enters 'pending' state.

Also fixes a bug where message was not sent to user when entering 'denied'
state unless the user was previously in 'granted'.

Send notification to admin when a user enters 'pending' state.

Pylint cleanup.
parent 55f2cbc9
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Studio: Email will be sent to admin address when a user requests course creator
privileges for Studio (edge only).
Studio: Studio course authors (both instructors and staff) will be auto-enrolled Studio: Studio course authors (both instructors and staff) will be auto-enrolled
for their courses so that "View Live" works. for their courses so that "View Live" works.
......
...@@ -85,10 +85,13 @@ def update_creator_group_callback(sender, **kwargs): ...@@ -85,10 +85,13 @@ def update_creator_group_callback(sender, **kwargs):
@receiver(send_user_notification, sender=CourseCreator) @receiver(send_user_notification, sender=CourseCreator)
def send_user_notification_callback(sender, **kwargs): def send_user_notification_callback(sender, **kwargs):
"""
Callback for notifying user about course creator status change.
"""
user = kwargs['user'] user = kwargs['user']
updated_state = kwargs['state'] updated_state = kwargs['state']
studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','') studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL', '')
context = {'studio_request_email': studio_request_email} context = {'studio_request_email': studio_request_email}
subject = render_to_string('emails/course_creator_subject.txt', context) subject = render_to_string('emails/course_creator_subject.txt', context)
...@@ -115,7 +118,7 @@ def send_admin_notification_callback(sender, **kwargs): ...@@ -115,7 +118,7 @@ def send_admin_notification_callback(sender, **kwargs):
""" """
user = kwargs['user'] user = kwargs['user']
studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL','') studio_request_email = settings.MITX_FEATURES.get('STUDIO_REQUEST_EMAIL', '')
context = {'user_name': user.username, 'user_email': user.email} context = {'user_name': user.username, 'user_email': user.email}
subject = render_to_string('emails/course_creator_admin_subject.txt', context) subject = render_to_string('emails/course_creator_admin_subject.txt', context)
......
...@@ -10,7 +10,13 @@ from django.utils import timezone ...@@ -10,7 +10,13 @@ from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
# A signal that will be sent when users should be added or removed from the creator group # A signal that will be sent when users should be added or removed from the creator group
update_creator_state = Signal(providing_args=["caller", "user", "add"]) update_creator_state = Signal(providing_args=["caller", "user", "state"])
# A signal that will be sent when admin should be notified of a pending user request
send_admin_notification = Signal(providing_args=["user"])
# A signal that will be sent when user should be notified of change in course creator privileges
send_user_notification = Signal(providing_args=["user", "state"])
class CourseCreator(models.Model): class CourseCreator(models.Model):
...@@ -60,9 +66,10 @@ def post_save_callback(sender, **kwargs): ...@@ -60,9 +66,10 @@ def post_save_callback(sender, **kwargs):
# We only wish to modify the state_changed time if the state has been modified. We don't wish to # We only wish to modify the state_changed time if the state has been modified. We don't wish to
# modify it for changes to the notes field. # modify it for changes to the notes field.
if instance.state != instance.orig_state: if instance.state != instance.orig_state:
granted_state_change = instance.state == CourseCreator.GRANTED or instance.orig_state == CourseCreator.GRANTED
# If either old or new state is 'granted', we must manipulate the course creator # If either old or new state is 'granted', we must manipulate the course creator
# group maintained by authz. That requires staff permissions (stored admin). # group maintained by authz. That requires staff permissions (stored admin).
if instance.state == CourseCreator.GRANTED or instance.orig_state == CourseCreator.GRANTED: if granted_state_change:
assert hasattr(instance, 'admin'), 'Must have stored staff user to change course creator group' assert hasattr(instance, 'admin'), 'Must have stored staff user to change course creator group'
update_creator_state.send( update_creator_state.send(
sender=sender, sender=sender,
...@@ -71,6 +78,22 @@ def post_save_callback(sender, **kwargs): ...@@ -71,6 +78,22 @@ def post_save_callback(sender, **kwargs):
state=instance.state state=instance.state
) )
# If user has been denied access, granted access, or previously granted access has been
# revoked, send a notification message to the user.
if instance.state == CourseCreator.DENIED or granted_state_change:
send_user_notification.send(
sender=sender,
user=instance.user,
state=instance.state
)
# If the user has gone into the 'pending' state, send a notification to interested admin.
if instance.state == CourseCreator.PENDING:
send_admin_notification.send(
sender=sender,
user=instance.user
)
instance.state_changed = timezone.now() instance.state_changed = timezone.now()
instance.orig_state = instance.state instance.orig_state = instance.state
instance.save() instance.save()
...@@ -11,6 +11,7 @@ import mock ...@@ -11,6 +11,7 @@ import mock
from course_creators.admin import CourseCreatorAdmin from course_creators.admin import CourseCreatorAdmin
from course_creators.models import CourseCreator from course_creators.models import CourseCreator
from auth.authz import is_user_in_creator_group from auth.authz import is_user_in_creator_group
from django.core import mail
def mock_render_to_string(template_name, context): def mock_render_to_string(template_name, context):
...@@ -37,21 +38,25 @@ class CourseCreatorAdminTest(TestCase): ...@@ -37,21 +38,25 @@ class CourseCreatorAdminTest(TestCase):
self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite()) self.creator_admin = CourseCreatorAdmin(self.table_entry, AdminSite())
self.studio_request_email = 'mark@marky.mark'
self.enable_creator_group_patch = {
"ENABLE_CREATOR_GROUP": True,
"STUDIO_REQUEST_EMAIL": self.studio_request_email
}
@mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True)) @mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True))
@mock.patch('django.contrib.auth.models.User.email_user') @mock.patch('django.contrib.auth.models.User.email_user')
def test_change_status(self, email_user): def test_change_status(self, email_user):
""" """
Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent. Tests that updates to state impact the creator group maintained in authz.py and that e-mails are sent.
""" """
STUDIO_REQUEST_EMAIL = 'mark@marky.mark'
def change_state(state, is_creator): def change_state_and_verify_email(state, is_creator):
""" Helper method for changing state """ """ Changes user state, verifies creator status, and verifies e-mail is sent based on transition """
self.table_entry.state = state self._change_state(state)
self.creator_admin.save_model(self.request, self.table_entry, None, True)
self.assertEqual(is_creator, is_user_in_creator_group(self.user)) self.assertEqual(is_creator, is_user_in_creator_group(self.user))
context = {'studio_request_email': STUDIO_REQUEST_EMAIL} context = {'studio_request_email': self.studio_request_email}
if state == CourseCreator.GRANTED: if state == CourseCreator.GRANTED:
template = 'emails/course_creator_granted.txt' template = 'emails/course_creator_granted.txt'
elif state == CourseCreator.DENIED: elif state == CourseCreator.DENIED:
...@@ -61,31 +66,76 @@ class CourseCreatorAdminTest(TestCase): ...@@ -61,31 +66,76 @@ class CourseCreatorAdminTest(TestCase):
email_user.assert_called_with( email_user.assert_called_with(
mock_render_to_string('emails/course_creator_subject.txt', context), mock_render_to_string('emails/course_creator_subject.txt', context),
mock_render_to_string(template, context), mock_render_to_string(template, context),
STUDIO_REQUEST_EMAIL self.studio_request_email
) )
with mock.patch.dict( with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group_patch):
'django.conf.settings.MITX_FEATURES',
{
"ENABLE_CREATOR_GROUP": True,
"STUDIO_REQUEST_EMAIL": STUDIO_REQUEST_EMAIL
}
):
# User is initially unrequested. # User is initially unrequested.
self.assertFalse(is_user_in_creator_group(self.user)) self.assertFalse(is_user_in_creator_group(self.user))
change_state(CourseCreator.GRANTED, True) change_state_and_verify_email(CourseCreator.GRANTED, True)
change_state_and_verify_email(CourseCreator.DENIED, False)
change_state_and_verify_email(CourseCreator.GRANTED, True)
change_state_and_verify_email(CourseCreator.PENDING, False)
change_state(CourseCreator.DENIED, False) change_state_and_verify_email(CourseCreator.GRANTED, True)
change_state(CourseCreator.GRANTED, True) change_state_and_verify_email(CourseCreator.UNREQUESTED, False)
change_state(CourseCreator.PENDING, False) change_state_and_verify_email(CourseCreator.DENIED, False)
change_state(CourseCreator.GRANTED, True) @mock.patch('course_creators.admin.render_to_string', mock.Mock(side_effect=mock_render_to_string, autospec=True))
def test_mail_admin_on_pending(self):
"""
Tests that the admin account is notified when a user is in the 'pending' state.
"""
change_state(CourseCreator.UNREQUESTED, False) def check_admin_message_state(state, expect_sent_to_admin, expect_sent_to_user):
""" Changes user state and verifies e-mail sent to admin address only when pending. """
mail.outbox = []
self._change_state(state)
# If a message is sent to the user about course creator status change, it will be the first
# message sent. Admin message will follow.
base_num_emails = 1 if expect_sent_to_user else 0
if expect_sent_to_admin:
context = {'user_name': "test_user", 'user_email': 'test_user+courses@edx.org'}
self.assertEquals(base_num_emails + 1, len(mail.outbox), 'Expected admin message to be sent')
sent_mail = mail.outbox[base_num_emails]
self.assertEquals(
mock_render_to_string('emails/course_creator_admin_subject.txt', context),
sent_mail.subject
)
self.assertEquals(
mock_render_to_string('emails/course_creator_admin_user_pending.txt', context),
sent_mail.body
)
self.assertEquals(self.studio_request_email, sent_mail.from_email)
self.assertEqual([self.studio_request_email], sent_mail.to)
else:
self.assertEquals(base_num_emails, len(mail.outbox))
with mock.patch.dict('django.conf.settings.MITX_FEATURES', self.enable_creator_group_patch):
# E-mail message should be sent to admin only when new state is PENDING, regardless of what
# previous state was (unless previous state was already PENDING).
# E-mail message sent to user only on transition into and out of GRANTED state.
check_admin_message_state(CourseCreator.UNREQUESTED, expect_sent_to_admin=False, expect_sent_to_user=False)
check_admin_message_state(CourseCreator.PENDING, expect_sent_to_admin=True, expect_sent_to_user=False)
check_admin_message_state(CourseCreator.GRANTED, expect_sent_to_admin=False, expect_sent_to_user=True)
check_admin_message_state(CourseCreator.DENIED, expect_sent_to_admin=False, expect_sent_to_user=True)
check_admin_message_state(CourseCreator.GRANTED, expect_sent_to_admin=False, expect_sent_to_user=True)
check_admin_message_state(CourseCreator.PENDING, expect_sent_to_admin=True, expect_sent_to_user=True)
check_admin_message_state(CourseCreator.PENDING, expect_sent_to_admin=False, expect_sent_to_user=False)
check_admin_message_state(CourseCreator.DENIED, expect_sent_to_admin=False, expect_sent_to_user=True)
def _change_state(self, state):
""" Helper method for changing state """
self.table_entry.state = state
self.creator_admin.save_model(self.request, self.table_entry, None, True)
def test_add_permission(self): def test_add_permission(self):
""" """
......
<%! from django.utils.translation import ugettext as _ %>
${_("{email} has requested Studio course creator privileges on edge".format(email=user_email))}
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
${_("User '{user}' with e-mail {email} has requested Studio course creator privileges on edge.".format(user=user_name, email=user_email))}
${_("To grant or deny this request, use the course creator admin table.")}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment