Commit 659d3603 by Chris Dodge

address review feedback

parent 25a4bcfd
""" """
Django management command to force-send the daily/weekly digest emails Django management command to force-send the daily/weekly digest emails
""" """
import sys
import datetime
import pytz
import logging import logging
import logging.config import logging.config
import sys
# This is specifially placed at the top
# to act as a loggic configuration override for the rest of the
# code
# Have all logging go to stdout with management commands # Have all logging go to stdout with management commands
# this must be up at the top otherwise the # this must be up at the top otherwise the
# configuration does not appear to take affect # configuration does not appear to take affect
import datetime logging.config.dictConfig({
import pytz
from django.conf import settings
from edx_notifications import const
from edx_notifications.digests import send_notifications_digest, send_notifications_namespace_digest
LOGGING = {
'version': 1, 'version': 1,
'handlers': { 'handlers': {
'console': { 'console': {
...@@ -27,16 +25,20 @@ LOGGING = { ...@@ -27,16 +25,20 @@ LOGGING = {
'handlers': ['console'], 'handlers': ['console'],
'level': 'INFO' 'level': 'INFO'
} }
} })
logging.config.dictConfig(LOGGING)
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
log = logging.getLogger(__file__) from edx_notifications import const
from edx_notifications.digests import send_notifications_digest, send_notifications_namespace_digest
from optparse import make_option, OptionParser from optparse import make_option, OptionParser
log = logging.getLogger(__file__)
class Command(BaseCommand): class Command(BaseCommand):
""" """
Django management command to force-send the daily/weekly digest emails Django management command to force-send the daily/weekly digest emails
...@@ -73,18 +75,16 @@ class Command(BaseCommand): ...@@ -73,18 +75,16 @@ class Command(BaseCommand):
), ),
) )
def send_daily_digest(self, namespace='All'): def _send_digest(self, subject, preference_name, day_delta, namespace):
""" """
Sends the daily digest. Sends a digest
""" """
if const.NOTIFICATION_DIGEST_SEND_TIMEFILTERED: if const.NOTIFICATION_DIGEST_SEND_TIMEFILTERED:
from_timestamp = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1) from_timestamp = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=day_delta)
else: else:
from_timestamp = None from_timestamp = None
to_timestamp = datetime.datetime.now(pytz.UTC) to_timestamp = datetime.datetime.now(pytz.UTC)
preference_name = const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME
subject = const.NOTIFICATION_DAILY_DIGEST_SUBJECT
from_email = const.NOTIFICATION_EMAIL_FROM_ADDRESS from_email = const.NOTIFICATION_EMAIL_FROM_ADDRESS
if namespace == "All": if namespace == "All":
...@@ -95,28 +95,29 @@ class Command(BaseCommand): ...@@ -95,28 +95,29 @@ class Command(BaseCommand):
) )
return digests_sent return digests_sent
def send_weekly_digest(self, namespace='All'): def send_daily_digest(self, namespace='All'):
""" """
Sends the weekly digest. Sends the daily digest.
""" """
if const.NOTIFICATION_DIGEST_SEND_TIMEFILTERED: return self._send_digest(
from_timestamp = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7) const.NOTIFICATION_DAILY_DIGEST_SUBJECT,
else: const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME,
from_timestamp = None 1,
namespace
)
to_timestamp = datetime.datetime.now(pytz.UTC) def send_weekly_digest(self, namespace='All'):
preference_name = const.NOTIFICATION_WEEKLY_DIGEST_PREFERENCE_NAME """
subject = const.NOTIFICATION_WEEKLY_DIGEST_SUBJECT Sends the weekly digest.
from_email = const.NOTIFICATION_EMAIL_FROM_ADDRESS """
if namespace == "All": return self._send_digest(
digests_sent = send_notifications_digest(from_timestamp, to_timestamp, preference_name, subject, from_email) const.NOTIFICATION_WEEKLY_DIGEST_SUBJECT,
else: const.NOTIFICATION_WEEKLY_DIGEST_PREFERENCE_NAME,
digests_sent = send_notifications_namespace_digest( 7,
namespace, from_timestamp, to_timestamp, preference_name, subject, from_email namespace
) )
return digests_sent
def handle(self, *args, **options): def handle(self, *args, **options):
""" """
...@@ -128,7 +129,6 @@ class Command(BaseCommand): ...@@ -128,7 +129,6 @@ class Command(BaseCommand):
--ns=NAMESPACE : Sends the notifications for the particular NAMESPACE. --ns=NAMESPACE : Sends the notifications for the particular NAMESPACE.
""" """
if not settings.FEATURES.get('ENABLE_NOTIFICATIONS', False): if not settings.FEATURES.get('ENABLE_NOTIFICATIONS', False):
print 'ENABLE_NOTIFICATIONS not set to "true". Stopping...' print 'ENABLE_NOTIFICATIONS not set to "true". Stopping...'
return return
...@@ -140,16 +140,16 @@ class Command(BaseCommand): ...@@ -140,16 +140,16 @@ class Command(BaseCommand):
if options['send_daily_digest']: if options['send_daily_digest']:
log.info("Sending the daily digest with namespace=%s...", options['namespace']) log.info("Sending the daily digest with namespace=%s...", options['namespace'])
weekly_digests_sent = self.send_daily_digest(options['namespace']) daily_digests_sent = self.send_daily_digest(options['namespace'])
log.info("Successfully sent %s digests...", weekly_digests_sent) log.info("Successfully sent %s digests...", daily_digests_sent)
if options['send_weekly_digest']: if options['send_weekly_digest']:
log.info("Sending the weekly digest with namespace=%s...", options['namespace']) log.info("Sending the weekly digest with namespace=%s...", options['namespace'])
daily_digests_sent = self.send_weekly_digest(options['namespace']) weekly_digests_sent = self.send_weekly_digest(options['namespace'])
log.info("Successfully sent %s digests...", daily_digests_sent) log.info("Successfully sent %s digests...", weekly_digests_sent)
if not options['send_weekly_digest'] and not options['send_daily_digest']: if not options['send_weekly_digest'] and not options['send_daily_digest']:
parser.print_help() parser.print_help()
raise CommandError("Neither Daily, nor Weekly digest specified.") raise CommandError("Neither Daily, nor Weekly digest specified.")
log.info("Completed .") log.info("Completed.")
...@@ -10,8 +10,10 @@ from student.management.commands import force_send_notification_digest ...@@ -10,8 +10,10 @@ from student.management.commands import force_send_notification_digest
@mock.patch.dict(settings.FEATURES, {'ENABLE_NOTIFICATIONS': True}) @mock.patch.dict(settings.FEATURES, {'ENABLE_NOTIFICATIONS': True})
class ForceSendDigestCommandTest(TestCase): class ForceSendDigestCommandTest(TestCase):
def test_command_check(self): def test_command_all(self):
# run the management command for purging notifications. # run the management command for sending notification digests.
force_send_notification_digest.Command().handle(**{'send_daily_digest': True, 'send_weekly_digest': True, 'namespace': 'All'}) force_send_notification_digest.Command().handle(**{'send_daily_digest': True, 'send_weekly_digest': True, 'namespace': 'All'})
def test_command_namespaced(self):
# run the management command for sending notification digests.
force_send_notification_digest.Command().handle(**{'send_daily_digest': True, 'send_weekly_digest': True, 'namespace': 'ABC'}) force_send_notification_digest.Command().handle(**{'send_daily_digest': True, 'send_weekly_digest': True, 'namespace': 'ABC'})
# force_send_digest.Command().handle(**{'send_daily_digest': False, 'send_weekly_digest': False, 'namespace': 'ABC'})
...@@ -15,6 +15,23 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey ...@@ -15,6 +15,23 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def _get_course_key_from_string(course_id):
"""
Helper method to convert a string formatted
course_id into a CourseKey
"""
if not isinstance(course_id, CourseKey):
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
else:
course_key = course_id
return course_key
class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver): class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver):
""" """
Implementation of the NotificationUserScopeResolver abstract Implementation of the NotificationUserScopeResolver abstract
...@@ -35,21 +52,16 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver): ...@@ -35,21 +52,16 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver):
if scope_name != 'course_enrollments': if scope_name != 'course_enrollments':
# we can't resolve any other scopes # we can't resolve any other scopes
# The API expects a None (not an exception) if this
# particular resolver is not able to resolve a scope_name
# which it does not know about.
return None return None
if 'course_id' not in scope_context: if 'course_id' not in scope_context:
# did not receive expected parameters # did not receive expected parameters
return None raise KeyError('Missing course_id in scope_context')
course_id = scope_context['course_id'] course_key = _get_course_key_from_string(scope_context['course_id'])
if not isinstance(course_id , CourseKey):
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
else:
course_key = course_id
return CourseEnrollment.objects.values_list('user_id', flat=True).filter( return CourseEnrollment.objects.values_list('user_id', flat=True).filter(
is_active=1, is_active=1,
...@@ -70,25 +82,23 @@ class NamespaceEnrollmentsScopeResolver(NotificationUserScopeResolver): ...@@ -70,25 +82,23 @@ class NamespaceEnrollmentsScopeResolver(NotificationUserScopeResolver):
def resolve(self, scope_name, scope_context, instance_context): def resolve(self, scope_name, scope_context, instance_context):
""" """
The entry point to resolve a scope_name with a given scope_context The entry point to resolve a scope_name with a given scope_context
scope_context must include a 'namespace' key/value pair to indicate
what course_id needs to be resolved
""" """
if scope_name != 'namespace_scope': if scope_name != 'namespace_scope':
# we can't resolve any other scopes # we can't resolve any other scopes
# The API expects a None (not an exception) if this
# particular resolver is not able to resolve a scope_name
# which it does not know about.
return None return None
if 'namespace' not in scope_context: if 'namespace' not in scope_context:
# did not receive expected parameters # did not receive expected parameters
return None raise KeyError('Missing course_id in scope_context')
course_id = scope_context['namespace']
if not isinstance(course_id , CourseKey): course_key = _get_course_key_from_string(scope_context['namespace'])
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
else:
course_key = course_id
query = User.objects.select_related('courseenrollment') query = User.objects.select_related('courseenrollment')
...@@ -110,7 +120,7 @@ class NamespaceEnrollmentsScopeResolver(NotificationUserScopeResolver): ...@@ -110,7 +120,7 @@ class NamespaceEnrollmentsScopeResolver(NotificationUserScopeResolver):
query = query.values(*fields) query = query.values(*fields)
query = query.filter( query = query.filter(
courseenrollment__is_active=1, courseenrollment__is_active=True,
courseenrollment__course_id=course_key courseenrollment__course_id=course_key
) )
return query return query
...@@ -129,6 +139,9 @@ class StudentEmailScopeResolver(NotificationUserScopeResolver): ...@@ -129,6 +139,9 @@ class StudentEmailScopeResolver(NotificationUserScopeResolver):
if scope_name != 'user_email_resolver': if scope_name != 'user_email_resolver':
# we can't resolve any other scopes # we can't resolve any other scopes
# The API expects a None (not an exception) if this
# particular resolver is not able to resolve a scope_name
# which it does not know about.
return None return None
user_id = scope_context.get('user_id') user_id = scope_context.get('user_id')
......
...@@ -62,7 +62,9 @@ class StudentTasksTestCase(ModuleStoreTestCase): ...@@ -62,7 +62,9 @@ class StudentTasksTestCase(ModuleStoreTestCase):
resolver = CourseEnrollmentsScopeResolver() resolver = CourseEnrollmentsScopeResolver()
self.assertIsNone(resolver.resolve('bad', {'course_id': 'foo'}, None)) self.assertIsNone(resolver.resolve('bad', {'course_id': 'foo'}, None))
self.assertIsNone(resolver.resolve('course_enrollments', {'bad': 'foo'}, None))
with self.assertRaises(KeyError):
self.assertIsNone(resolver.resolve('course_enrollments', {'bad': 'foo'}, None))
def test_namespace_scope(self): def test_namespace_scope(self):
""" """
...@@ -139,7 +141,7 @@ class StudentTasksTestCase(ModuleStoreTestCase): ...@@ -139,7 +141,7 @@ class StudentTasksTestCase(ModuleStoreTestCase):
resolver = StudentEmailScopeResolver() resolver = StudentEmailScopeResolver()
emails_resultset = resolver.resolve( resolved_scopes = resolver.resolve(
'user_email_resolver', 'user_email_resolver',
{ {
'user_id': test_user_1.id, 'user_id': test_user_1.id,
...@@ -147,7 +149,7 @@ class StudentTasksTestCase(ModuleStoreTestCase): ...@@ -147,7 +149,7 @@ class StudentTasksTestCase(ModuleStoreTestCase):
None None
) )
emails = [email['email'] for email in emails_resultset] emails = [resolved_scope['email'] for resolved_scope in resolved_scopes]
self.assertTrue(test_user_1.email in emails) self.assertTrue(test_user_1.email in emails)
......
...@@ -1904,7 +1904,7 @@ class UsersApiTests(ModuleStoreTestCase): ...@@ -1904,7 +1904,7 @@ class UsersApiTests(ModuleStoreTestCase):
# mark as read # mark as read
test_uri = '{}/{}/notifications/{}/'.format(self.users_base_uri, user_id, sent_user_msg.msg.id) test_uri = '{}/{}/notifications/{}/'.format(self.users_base_uri, user_id, sent_user_msg.msg.id)
response = self.do_post(test_uri, {"read": True}) response = self.do_post(test_uri, {"read": True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 201)
# then verify unread count, which should be 0 # then verify unread count, which should be 0
self.assertEqual(get_notifications_count_for_user(user_id, filters={'read': False}), 0) self.assertEqual(get_notifications_count_for_user(user_id, filters={'read': False}), 0)
...@@ -1391,16 +1391,22 @@ class UsersRolesCoursesDetail(SecureAPIView): ...@@ -1391,16 +1391,22 @@ class UsersRolesCoursesDetail(SecureAPIView):
class UsersNotificationsDetail(SecureAPIView): class UsersNotificationsDetail(SecureAPIView):
""" """
Allows for a caller to delete a user's notification, passed in by msg_id. Note that the Allows for a caller to mark a user's notification as read,
user_msg_id must belong to the user_id passed in passed in by msg_id. Note that the user_msg_id must belong
to the user_id passed in
""" """
def post(self, request, user_id, msg_id): def post(self, request, user_id, msg_id):
""" """
POST /api/users/{user_id}/notifications/{msg_id} POST /api/users/{user_id}/notifications/{msg_id}
payload:
{
'read': 'True' or 'False'
}
""" """
read = bool(request.DATA['read']) read = bool(request.DATA['read'])
mark_notification_read(int(user_id), int(msg_id), read=read) mark_notification_read(int(user_id), int(msg_id), read=read)
return Response({}, status=status.HTTP_200_OK) return Response({}, status=status.HTTP_201_CREATED)
...@@ -27,7 +27,7 @@ for pkg_name in ['track.contexts', 'track.middleware', 'dd.dogapi']: ...@@ -27,7 +27,7 @@ for pkg_name in ['track.contexts', 'track.middleware', 'dd.dogapi']:
################################ EMAIL ######################################## ################################ EMAIL ########################################
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses 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['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms)
...@@ -49,7 +49,6 @@ if FEATURES.get('PROFILER'): ...@@ -49,7 +49,6 @@ if FEATURES.get('PROFILER'):
ANALYTICS_DASHBOARD_URL = None ANALYTICS_DASHBOARD_URL = None
################################ DEBUG TOOLBAR ################################ ################################ DEBUG TOOLBAR ################################
FEATURES['DEBUG_TOOLBAR'] = True FEATURES['DEBUG_TOOLBAR'] = True
if FEATURES.get('DEBUG_TOOLBAR'): if FEATURES.get('DEBUG_TOOLBAR'):
......
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