Commit f7b6bd83 by chrisndodge

Merge pull request #441 from edx-solutions/cdodge/digest-support

Cdodge/digest support
parents 0c740649 659d3603
......@@ -16,6 +16,8 @@ from openedx.core.djangoapps.course_groups.scope_resolver import CourseGroupScop
from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver
from projects.scope_resolver import GroupProjectParticipantsScopeResolver
from edx_notifications.scopes import register_user_scope_resolver
from edx_notifications.namespaces import register_namespace_resolver
from util.namespace_resolver import CourseNamespaceResolver
log = logging.getLogger(__name__)
......@@ -88,13 +90,16 @@ def startup_notification_subsystem():
try:
startup.initialize()
# register the two scope resolvers that the LMS will be providing
# register the scope resolvers that the runtime will be providing
# to edx-notifications
register_user_scope_resolver('course_enrollments', CourseEnrollmentsScopeResolver())
register_user_scope_resolver('course_group', CourseGroupScopeResolver())
register_user_scope_resolver('group_project_participants', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('group_project_workgroup', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('student_email_resolver', StudentEmailScopeResolver())
# register namespace resolver
register_namespace_resolver(CourseNamespaceResolver())
except Exception, ex:
# Note this will fail when we try to run migrations as manage.py will call startup.py
# and startup.initialze() will try to manipulate some database tables.
......
"""
Django management command to force-send the daily/weekly digest emails
"""
import sys
import datetime
import pytz
import logging
import logging.config
# 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
# this must be up at the top otherwise the
# configuration does not appear to take affect
logging.config.dictConfig({
'version': 1,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'stream': sys.stdout,
}
},
'root': {
'handlers': ['console'],
'level': 'INFO'
}
})
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from edx_notifications import const
from edx_notifications.digests import send_notifications_digest, send_notifications_namespace_digest
from optparse import make_option, OptionParser
log = logging.getLogger(__file__)
class Command(BaseCommand):
"""
Django management command to force-send the daily/weekly digest emails
"""
help = 'Command to force-send the daily/weekly digest emails'
option_list = BaseCommand.option_list + (
make_option(
'--daily',
action='store_true',
dest='send_daily_digest',
default=False,
help='Force send daily digest email.'
),
)
option_list = option_list + (
make_option(
'--weekly',
action='store_true',
dest='send_weekly_digest',
default=False,
help='Force send weekly digest email.'
),
)
option_list = option_list + (
make_option(
'--ns',
dest='namespace',
default='All',
help='Specify the namespace. Default = All.'
),
)
def _send_digest(self, subject, preference_name, day_delta, namespace):
"""
Sends a digest
"""
if const.NOTIFICATION_DIGEST_SEND_TIMEFILTERED:
from_timestamp = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=day_delta)
else:
from_timestamp = None
to_timestamp = datetime.datetime.now(pytz.UTC)
from_email = const.NOTIFICATION_EMAIL_FROM_ADDRESS
if namespace == "All":
digests_sent = send_notifications_digest(from_timestamp, to_timestamp, preference_name, subject, from_email)
else:
digests_sent = send_notifications_namespace_digest(
namespace, from_timestamp, to_timestamp, preference_name, subject, from_email
)
return digests_sent
def send_daily_digest(self, namespace='All'):
"""
Sends the daily digest.
"""
return self._send_digest(
const.NOTIFICATION_DAILY_DIGEST_SUBJECT,
const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME,
1,
namespace
)
def send_weekly_digest(self, namespace='All'):
"""
Sends the weekly digest.
"""
return self._send_digest(
const.NOTIFICATION_WEEKLY_DIGEST_SUBJECT,
const.NOTIFICATION_WEEKLY_DIGEST_PREFERENCE_NAME,
7,
namespace
)
def handle(self, *args, **options):
"""
Management command entry point, simply call into the send_notifications_digest or the
send_notifications_namespace_digest depending on the passed the parameters.
The expected command line arguments are:
--daily: Sends the daily digest.
--weekly: Sends the weekly digest.
--ns=NAMESPACE : Sends the notifications for the particular NAMESPACE.
"""
if not settings.FEATURES.get('ENABLE_NOTIFICATIONS', False):
print 'ENABLE_NOTIFICATIONS not set to "true". Stopping...'
return
usage = "usage: %prog [--daily] [--weekly] [--ns=NAMESPACE]"
parser = OptionParser(usage=usage)
log.info("Running management command ...")
if options['send_daily_digest']:
log.info("Sending the daily digest with namespace=%s...", options['namespace'])
daily_digests_sent = self.send_daily_digest(options['namespace'])
log.info("Successfully sent %s digests...", daily_digests_sent)
if options['send_weekly_digest']:
log.info("Sending the weekly digest with namespace=%s...", options['namespace'])
weekly_digests_sent = self.send_weekly_digest(options['namespace'])
log.info("Successfully sent %s digests...", weekly_digests_sent)
if not options['send_weekly_digest'] and not options['send_daily_digest']:
parser.print_help()
raise CommandError("Neither Daily, nor Weekly digest specified.")
log.info("Completed.")
"""
Tests for the Django management command force_send_digest
"""
import mock
from django.conf import settings
from django.test import TestCase
from student.management.commands import force_send_notification_digest
@mock.patch.dict(settings.FEATURES, {'ENABLE_NOTIFICATIONS': True})
class ForceSendDigestCommandTest(TestCase):
def test_command_all(self):
# run the management command for sending notification digests.
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'})
......@@ -15,6 +15,23 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
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):
"""
Implementation of the NotificationUserScopeResolver abstract
......@@ -35,21 +52,16 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver):
if scope_name != 'course_enrollments':
# 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
if 'course_id' not in scope_context:
# did not receive expected parameters
return None
course_id = scope_context['course_id']
raise KeyError('Missing course_id in scope_context')
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
course_key = _get_course_key_from_string(scope_context['course_id'])
return CourseEnrollment.objects.values_list('user_id', flat=True).filter(
is_active=1,
......@@ -57,6 +69,63 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver):
)
class NamespaceEnrollmentsScopeResolver(NotificationUserScopeResolver):
"""
Implementation of the NotificationUserScopeResolver abstract
interface defined in edx-notifications.
We will be passed in a namespace (aka course_id) in the context
and we must return a Django ORM resultset or None if
we cannot match.
"""
def resolve(self, scope_name, scope_context, instance_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':
# 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
if 'namespace' not in scope_context:
# did not receive expected parameters
raise KeyError('Missing course_id in scope_context')
course_key = _get_course_key_from_string(scope_context['namespace'])
query = User.objects.select_related('courseenrollment')
if 'fields' in scope_context:
fields = []
if scope_context['fields'].get('id'):
fields.append('id')
if scope_context['fields'].get('email'):
fields.append('email')
if scope_context['fields'].get('first_name'):
fields.append('first_name')
if scope_context['fields'].get('last_name'):
fields.append('last_name')
else:
fields = ['id', 'email', 'first_name', 'last_name']
query = query.values(*fields)
query = query.filter(
courseenrollment__is_active=True,
courseenrollment__course_id=course_key
)
return query
class StudentEmailScopeResolver(NotificationUserScopeResolver):
"""
Implementation of the NotificationUserScopeResolver to
......@@ -68,14 +137,31 @@ class StudentEmailScopeResolver(NotificationUserScopeResolver):
The entry point to resolve a scope_name with a given scope_context
"""
if scope_name != 'student_email_resolver':
if scope_name != 'user_email_resolver':
# 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
user_id = scope_context.get('user_id')
if not user_id:
return None
return User.objects.values_list('email', flat=True).filter(
id=user_id
)
if 'fields' in scope_context:
fields = []
if scope_context['fields'].get('id'):
fields.append('id')
if scope_context['fields'].get('email'):
fields.append('email')
if scope_context['fields'].get('first_name'):
fields.append('first_name')
if scope_context['fields'].get('last_name'):
fields.append('last_name')
else:
fields = ['id', 'email', 'first_name', 'last_name']
return User.objects.values(*fields).filter(id=user_id)
......@@ -7,7 +7,11 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.models import CourseEnrollment
from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver
from student.scope_resolver import (
CourseEnrollmentsScopeResolver,
StudentEmailScopeResolver,
NamespaceEnrollmentsScopeResolver
)
class StudentTasksTestCase(ModuleStoreTestCase):
......@@ -58,7 +62,76 @@ class StudentTasksTestCase(ModuleStoreTestCase):
resolver = CourseEnrollmentsScopeResolver()
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):
"""
Make sure that we handle resolving namespaces correctly
"""
test_user_1 = UserFactory.create(
password='test_pass',
email='user1@foo.com',
first_name='user',
last_name='one'
)
CourseEnrollmentFactory(user=test_user_1, course_id=self.course.id)
test_user_2 = UserFactory.create(
password='test_pass',
email='user2@foo.com',
first_name='John',
last_name='Smith'
)
CourseEnrollmentFactory(user=test_user_2, course_id=self.course.id)
test_user_3 = UserFactory.create(password='test_pass')
enrollment = CourseEnrollmentFactory(user=test_user_3, course_id=self.course.id)
# unenroll #3
enrollment.is_active = False
enrollment.save()
resolver = NamespaceEnrollmentsScopeResolver()
users = resolver.resolve(
'namespace_scope',
{
'namespace': self.course.id,
'fields': {
'id': True,
'email': True,
'first_name': True,
'last_name': True,
}
},
None
)
_users = [user for user in users]
self.assertEqual(len(_users), 2)
self.assertIn('id', _users[0])
self.assertIn('email', _users[0])
self.assertIn('first_name', _users[0])
self.assertIn('last_name', _users[0])
self.assertEquals(_users[0]['id'], test_user_1.id)
self.assertEquals(_users[0]['email'], test_user_1.email)
self.assertEquals(_users[0]['first_name'], test_user_1.first_name)
self.assertEquals(_users[0]['last_name'], test_user_1.last_name)
self.assertIn('id', _users[1])
self.assertIn('email', _users[1])
self.assertIn('first_name', _users[1])
self.assertIn('last_name', _users[1])
self.assertEquals(_users[1]['id'], test_user_2.id)
self.assertEquals(_users[1]['email'], test_user_2.email)
self.assertEquals(_users[1]['first_name'], test_user_2.first_name)
self.assertEquals(_users[1]['last_name'], test_user_2.last_name)
def test_email_resolver(self):
"""
......@@ -68,15 +141,17 @@ class StudentTasksTestCase(ModuleStoreTestCase):
resolver = StudentEmailScopeResolver()
emails_resultset = resolver.resolve(
'student_email_resolver',
resolved_scopes = resolver.resolve(
'user_email_resolver',
{
'user_id': test_user_1.id,
},
None
)
self.assertTrue(test_user_1.email in emails_resultset)
emails = [resolved_scope['email'] for resolved_scope in resolved_scopes]
self.assertTrue(test_user_1.email in emails)
def test_bad_email_resolver(self):
"""
......
"""
A namespace resolver for edx-notifications. This basically translates a namespace
into information about the namespace
"""
from xmodule.modulestore.django import modulestore
from student.scope_resolver import NamespaceEnrollmentsScopeResolver
from edx_notifications.namespaces import NotificationNamespaceResolver
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import SlashSeparatedCourseKey
class CourseNamespaceResolver(NotificationNamespaceResolver):
"""
An implementation of NotificationNamespaceResolver which treats
namespaces as courses
"""
def resolve(self, namespace, instance_context):
"""
Namespace resolvers will return this information as a dict:
{
'namespace': <String> ,
'display_name': <String representing a human readible name for the namespace>,
'features': {
'digests': <boolean, saying if namespace supports a digest>
},
'default_user_resolver': <pointer to a UserScopeResolver instance>
}
or None if the handler cannot resolve it
"""
# namespace = course_id
course_id = namespace
if not isinstance(course_id, CourseKey):
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except InvalidKeyError:
return None
else:
course_key = course_id
course = modulestore().get_course(course_key)
if not course:
# not found, we can't resolve it
return None
# return expected results to caller per the interface contract
return {
'namespace': course_id,
'display_name': course.display_name,
'features': {
'digests': course.has_started() and not course.has_ended(),
},
'default_user_resolver': NamespaceEnrollmentsScopeResolver(),
}
"""
Unit tests for namespace_resolver.py
"""
from django.test import TestCase
from datetime import datetime
from xmodule.modulestore.tests.factories import CourseFactory
from util.namespace_resolver import CourseNamespaceResolver
from student.scope_resolver import NamespaceEnrollmentsScopeResolver
class NamespaceResolverTests(TestCase):
"""
Tests for the CourseNamespaceResolver
"""
def setUp(self):
"""
Test initialization
"""
self.course = CourseFactory(
org='foo',
start=datetime(1980, 1, 1),
end=datetime(2200, 1, 1)
)
self.closed_course = CourseFactory(
org='bar',
start=datetime(1975, 1, 1),
end=datetime(1980, 1, 1)
)
self.not_open_course = CourseFactory(
org='baz',
start=datetime(2200, 1, 1),
end=datetime(2222, 1, 1)
)
def test_resolve_namespace(self):
"""
Make sure the interface is properly implemented
"""
resolver = CourseNamespaceResolver()
# can't resolve a non existing course
self.assertIsNone(resolver.resolve('foo', None))
# happy path
result = resolver.resolve(self.course.id, None)
self.assertIsNotNone(result)
self.assertEqual(result['namespace'], self.course.id)
self.assertEqual(result['display_name'], self.course.display_name)
self.assertTrue(isinstance(result['default_user_resolver'], NamespaceEnrollmentsScopeResolver))
self.assertTrue(result['features']['digests'])
# course that is closed
result = resolver.resolve(self.closed_course.id, None)
self.assertIsNotNone(result)
self.assertEqual(result['namespace'], self.closed_course.id)
self.assertEqual(result['display_name'], self.closed_course.display_name)
self.assertTrue(isinstance(result['default_user_resolver'], NamespaceEnrollmentsScopeResolver))
self.assertFalse(result['features']['digests'])
# course that has not opened
result = resolver.resolve(self.not_open_course.id, None)
self.assertIsNotNone(result)
self.assertEqual(result['namespace'], self.not_open_course.id)
self.assertEqual(result['display_name'], self.not_open_course.display_name)
self.assertTrue(isinstance(result['default_user_resolver'], NamespaceEnrollmentsScopeResolver))
self.assertFalse(result['features']['digests'])
......@@ -40,6 +40,10 @@ from xmodule.modulestore import Location
from django.contrib.auth.models import User
from notification_prefs import NOTIFICATION_PREF_KEY
from edx_notifications.lib.publisher import register_notification_type, publish_notification_to_user
from edx_notifications.lib.consumer import get_notifications_count_for_user
from edx_notifications.data import NotificationMessage, NotificationType
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
TEST_API_KEY = str(uuid.uuid4())
......@@ -1874,3 +1878,33 @@ class UsersApiTests(ModuleStoreTestCase):
delete_uri = '{}invalid_role/courses/{}'.format(test_uri, unicode(self.course.id))
response = self.do_delete(delete_uri)
self.assertEqual(response.status_code, 404)
def test_mark_notification_as_read(self):
user_id = self._create_test_user()
msg_type = NotificationType(
name='open-edx.edx_notifications.lib.tests.test_publisher',
renderer='edx_notifications.renderers.basic.BasicSubjectBodyRenderer',
)
register_notification_type(msg_type)
msg = NotificationMessage(
namespace='test-runner',
msg_type=msg_type,
payload={
'foo': 'bar'
}
)
# now do happy path
sent_user_msg = publish_notification_to_user(user_id, msg)
# verify unread count
self.assertEqual(get_notifications_count_for_user(user_id, filters={'read': False}), 1)
# mark as read
test_uri = '{}/{}/notifications/{}/'.format(self.users_base_uri, user_id, sent_user_msg.msg.id)
response = self.do_post(test_uri, {"read": True})
self.assertEqual(response.status_code, 201)
# then verify unread count, which should be 0
self.assertEqual(get_notifications_count_for_user(user_id, filters={'read': False}), 0)
......@@ -26,6 +26,7 @@ urlpatterns = patterns(
url(r'^(?P<user_id>[a-zA-Z0-9]+)/roles/(?P<role>[a-z_]+)/courses/(?P<course_id>[a-zA-Z0-9_+\/:]+)$', users_views.UsersRolesCoursesDetail.as_view(), name='users-roles-courses-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/roles/*$', users_views.UsersRolesList.as_view(), name='users-roles-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/workgroups/$', users_views.UsersWorkgroupsList.as_view(), name='users-workgroups-list'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)/notifications/(?P<msg_id>[0-9]+)/$', users_views.UsersNotificationsDetail.as_view(), name='users-notifications-detail'),
url(r'^(?P<user_id>[a-zA-Z0-9]+)$', users_views.UsersDetail.as_view(), name='apimgr-users-detail'),
url(r'/*$^', users_views.UsersList.as_view(), name='apimgr-users-list'),
)
......
......@@ -57,7 +57,7 @@ from api_manager.utils import generate_base_uri, dict_has_items, extract_data_pa
from projects.serializers import BasicWorkgroupSerializer
from .serializers import UserSerializer, UserCountByCitySerializer, UserRolesSerializer
from edx_notifications.lib.consumer import mark_notification_read
log = logging.getLogger(__name__)
AUDIT_LOG = logging.getLogger("audit")
......@@ -1387,3 +1387,26 @@ class UsersRolesCoursesDetail(SecureAPIView):
return Response({}, status=status.HTTP_404_NOT_FOUND)
return Response({}, status=status.HTTP_204_NO_CONTENT)
class UsersNotificationsDetail(SecureAPIView):
"""
Allows for a caller to mark a user's notification as read,
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):
"""
POST /api/users/{user_id}/notifications/{msg_id}
payload:
{
'read': 'True' or 'False'
}
"""
read = bool(request.DATA['read'])
mark_notification_read(int(user_id), int(msg_id), read=read)
return Response({}, status=status.HTTP_201_CREATED)
......@@ -565,13 +565,54 @@ MIDDLEWARE_CLASSES = tuple(_class for _class in MIDDLEWARE_CLASSES if _class not
##### EDX-NOTIFICATIONS ######
NOTIFICATION_CLICK_LINK_URL_MAPS = ENV_TOKENS.get('NOTIFICATION_CLICK_LINK_URL_MAPS', NOTIFICATION_CLICK_LINK_URL_MAPS)
NOTIFICATION_STORE_PROVIDER = ENV_TOKENS.get('NOTIFICATION_STORE_PROVIDER', NOTIFICATION_STORE_PROVIDER)
NOTIFICATION_CHANNEL_PROVIDERS = ENV_TOKENS.get('NOTIFICATION_CHANNEL_PROVIDERS', NOTIFICATION_CHANNEL_PROVIDERS)
NOTIFICATION_CLICK_LINK_URL_MAPS = ENV_TOKENS.get(
'NOTIFICATION_CLICK_LINK_URL_MAPS',
NOTIFICATION_CLICK_LINK_URL_MAPS
)
NOTIFICATION_STORE_PROVIDER = ENV_TOKENS.get(
'NOTIFICATION_STORE_PROVIDER',
NOTIFICATION_STORE_PROVIDER
)
NOTIFICATION_CHANNEL_PROVIDERS = ENV_TOKENS.get(
'NOTIFICATION_CHANNEL_PROVIDERS',
NOTIFICATION_CHANNEL_PROVIDERS
)
NOTIFICATION_CHANNEL_PROVIDER_TYPE_MAPS = ENV_TOKENS.get(
'NOTIFICATION_CHANNEL_PROVIDER_TYPE_MAPS',
NOTIFICATION_CHANNEL_PROVIDER_TYPE_MAPS
)
NOTIFICATION_MAX_LIST_SIZE = ENV_TOKENS.get('NOTIFICATION_MAX_LIST_SIZE', NOTIFICATION_MAX_LIST_SIZE)
NOTIFICATION_MAX_LIST_SIZE = ENV_TOKENS.get(
'NOTIFICATION_MAX_LIST_SIZE',
NOTIFICATION_MAX_LIST_SIZE
)
NOTIFICATION_DAILY_DIGEST_SUBJECT = ENV_TOKENS.get(
'NOTIFICATION_DAILY_DIGEST_SUBJECT',
NOTIFICATION_DAILY_DIGEST_SUBJECT
)
NOTIFICATION_WEEKLY_DIGEST_SUBJECT = ENV_TOKENS.get(
'NOTIFICATION_WEEKLY_DIGEST_SUBJECT',
NOTIFICATION_WEEKLY_DIGEST_SUBJECT
)
NOTIFICATION_BRANDED_DEFAULT_LOGO = ENV_TOKENS.get(
'NOTIFICATION_BRANDED_DEFAULT_LOGO',
NOTIFICATION_BRANDED_DEFAULT_LOGO
)
NOTIFICATION_EMAIL_FROM_ADDRESS = ENV_TOKENS.get(
'NOTIFICATION_EMAIL_FROM_ADDRESS',
NOTIFICATION_EMAIL_FROM_ADDRESS
)
NOTIFICATION_APP_HOSTNAME = ENV_TOKENS.get(
'NOTIFICATION_APP_HOSTNAME',
SITE_NAME
)
NOTIFICATION_EMAIL_CLICK_LINK_URL_FORMAT = ENV_TOKENS.get(
'NOTIFICATION_EMAIL_CLICK_LINK_URL_FORMAT',
NOTIFICATION_EMAIL_CLICK_LINK_URL_FORMAT
)
NOTIFICATION_DIGEST_SEND_TIMEFILTERED = ENV_TOKENS.get(
'NOTIFICATION_DIGEST_SEND_TIMEFILTERED',
NOTIFICATION_DIGEST_SEND_TIMEFILTERED
)
XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {})
......@@ -296,11 +296,11 @@ FEATURES = {
# Enables the new navigation template and styles. This should be enabled
# when the styles appropriately match the edX.org website.
'ENABLE_NEW_EDX_HEADER': False,
# When a logged in user goes to the homepage ('/') should the user be
# redirected to the dashboard - this is default Open edX behavior. Set to
# False to not redirect the user
'ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER': True,
'ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER': True,
# When a user goes to the homepage ('/') the user see the
# courses listed in the announcement dates order - this is default Open edX behavior.
......@@ -1650,12 +1650,12 @@ INSTALLED_APPS = (
'survey',
'lms.djangoapps.lms_xblock',
# EDX API application
'api_manager',
# Social Engagement
'social_engagement',
'social_engagement',
)
######################### MARKETING SITE ###############################
......@@ -2100,6 +2100,14 @@ NOTIFICATION_CHANNEL_PROVIDER_TYPE_MAPS = {
'*': 'durable', # default global mapping
}
NOTIFICATION_DAILY_DIGEST_SUBJECT = "Your unread notifications for '{display_name}'"
NOTIFICATION_WEEKLY_DIGEST_SUBJECT = "Your unread notifications for '{display_name}'"
NOTIFICATION_BRANDED_DEFAULT_LOGO = 'edx_notifications/img/edx-openedx-logo-tag.png'
NOTIFICATION_EMAIL_FROM_ADDRESS = ''
NOTIFICATION_APP_HOSTNAME = SITE_NAME
NOTIFICATION_EMAIL_CLICK_LINK_URL_FORMAT = "http://{hostname}{url_path}"
NOTIFICATION_DIGEST_SEND_TIMEFILTERED = True
# Country code overrides
# Used by django-countries
COUNTRIES_OVERRIDE = {
......
......@@ -48,7 +48,6 @@ if FEATURES.get('PROFILER'):
# dashboard to the Analytics Dashboard.
ANALYTICS_DASHBOARD_URL = None
################################ DEBUG TOOLBAR ################################
FEATURES['DEBUG_TOOLBAR'] = True
......
......@@ -20,6 +20,8 @@ from openedx.core.djangoapps.course_groups.scope_resolver import CourseGroupScop
from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver
from projects.scope_resolver import GroupProjectParticipantsScopeResolver
from edx_notifications.scopes import register_user_scope_resolver
from edx_notifications.namespaces import register_namespace_resolver
from util.namespace_resolver import CourseNamespaceResolver
log = logging.getLogger(__name__)
......@@ -215,13 +217,16 @@ def startup_notification_subsystem():
try:
startup.initialize()
# register the two scope resolvers that the LMS will be providing
# register the scope resolvers that the runtime will be providing
# to edx-notifications
register_user_scope_resolver('course_enrollments', CourseEnrollmentsScopeResolver())
register_user_scope_resolver('course_group', CourseGroupScopeResolver())
register_user_scope_resolver('group_project_participants', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('group_project_workgroup', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('student_email_resolver', StudentEmailScopeResolver())
register_user_scope_resolver('user_email_resolver', StudentEmailScopeResolver())
# register namespace resolver
register_namespace_resolver(CourseNamespaceResolver())
except Exception, ex:
# Note this will fail when we try to run migrations as manage.py will call startup.py
# and startup.initialze() will try to manipulate some database tables.
......
......@@ -10,5 +10,5 @@
-e git+https://github.com/edx-solutions/xblock-adventure.git@f908e087923231477499e2c455d356d286293641#egg=xblock-adventure
-e git+https://github.com/mckinseyacademy/xblock-poll.git@ca0e6eb4ef10c128d573c3cec015dcfee7984730#egg=xblock-poll
-e git+https://github.com/OfficeDev/xblock-officemix/@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock_officemix-master
-e git+https://github.com/edx/edx-notifications.git@1496385f16d2571bc5d958c17e30f3eac3869b8e#egg=edx-notifications
-e git+https://github.com/edx/edx-notifications.git@3266015191c1d99f4a692d8bf6f43a1a702cb9ee#egg=edx-notifications
-e git+https://github.com/open-craft/problem-builder.git@5e00f92d78da0b28ae7f39c3f03596f49bec7119#egg=problem-builder
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