Commit d9a599ce by Chris Dodge

add a namespace resolver

parent be7723d4
...@@ -16,6 +16,7 @@ from openedx.core.djangoapps.course_groups.scope_resolver import CourseGroupScop ...@@ -16,6 +16,7 @@ from openedx.core.djangoapps.course_groups.scope_resolver import CourseGroupScop
from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver
from projects.scope_resolver import GroupProjectParticipantsScopeResolver from projects.scope_resolver import GroupProjectParticipantsScopeResolver
from edx_notifications.scopes import register_user_scope_resolver from edx_notifications.scopes import register_user_scope_resolver
from util.namespace_resolver import CourseNamespaceResolver
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -88,13 +89,16 @@ def startup_notification_subsystem(): ...@@ -88,13 +89,16 @@ def startup_notification_subsystem():
try: try:
startup.initialize() 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 # to edx-notifications
register_user_scope_resolver('course_enrollments', CourseEnrollmentsScopeResolver()) register_user_scope_resolver('course_enrollments', CourseEnrollmentsScopeResolver())
register_user_scope_resolver('course_group', CourseGroupScopeResolver()) register_user_scope_resolver('course_group', CourseGroupScopeResolver())
register_user_scope_resolver('group_project_participants', GroupProjectParticipantsScopeResolver()) register_user_scope_resolver('group_project_participants', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('group_project_workgroup', GroupProjectParticipantsScopeResolver()) register_user_scope_resolver('group_project_workgroup', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('student_email_resolver', StudentEmailScopeResolver()) register_user_scope_resolver('student_email_resolver', StudentEmailScopeResolver())
# register namespace resolver
register_namespace_resolver(CourseNamespaceResolver())
except Exception, ex: except Exception, ex:
# Note this will fail when we try to run migrations as manage.py will call startup.py # 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. # and startup.initialze() will try to manipulate some database tables.
......
...@@ -33,7 +33,7 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver): ...@@ -33,7 +33,7 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver):
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
""" """
if scope_name != 'course_enrollments' and scope_name != 'namespace_scope': if scope_name != 'course_enrollments':
# we can't resolve any other scopes # we can't resolve any other scopes
return None return None
...@@ -51,37 +51,69 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver): ...@@ -51,37 +51,69 @@ class CourseEnrollmentsScopeResolver(NotificationUserScopeResolver):
else: else:
course_key = course_id course_key = course_id
if scope_name == 'course_enrollments': 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, course_id=course_key
course_id=course_key )
)
elif scope_name == 'namespace_scope':
class NamespaceEnrollmentsScopeResolver(NotificationUserScopeResolver):
query = User.objects.select_related('courseenrollment') """
Implementation of the NotificationUserScopeResolver abstract
if 'fields' in scope_context: interface defined in edx-notifications.
fields = []
if scope_context['fields'].get('id'): We will be passed in a namespace (aka course_id) in the context
fields.append('id') and we must return a Django ORM resultset or None if
we cannot match.
if scope_context['fields'].get('email'): """
fields.append('email')
def resolve(self, scope_name, scope_context, instance_context):
if scope_context['fields'].get('first_name'): """
fields.append('first_name') The entry point to resolve a scope_name with a given scope_context
"""
if scope_context['fields'].get('last_name'):
fields.append('last_name') if scope_name != 'namespace_scope':
else: # we can't resolve any other scopes
fields =['id', 'email', 'first_name', 'last_name'] return None
query = query.values(*fields) if 'namespace' not in scope_context:
query = query.filter( # did not receive expected parameters
courseenrollment__is_active=1, return None
courseenrollment__course_id=course_key
) course_id = scope_context['namespace']
return query
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
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=1,
courseenrollment__course_id=course_key
)
return query
class StudentEmailScopeResolver(NotificationUserScopeResolver): class StudentEmailScopeResolver(NotificationUserScopeResolver):
......
...@@ -7,7 +7,11 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -7,7 +7,11 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver from student.scope_resolver import (
CourseEnrollmentsScopeResolver,
StudentEmailScopeResolver,
NamespaceEnrollmentsScopeResolver
)
class StudentTasksTestCase(ModuleStoreTestCase): class StudentTasksTestCase(ModuleStoreTestCase):
...@@ -89,12 +93,12 @@ class StudentTasksTestCase(ModuleStoreTestCase): ...@@ -89,12 +93,12 @@ class StudentTasksTestCase(ModuleStoreTestCase):
enrollment.is_active = False enrollment.is_active = False
enrollment.save() enrollment.save()
resolver = CourseEnrollmentsScopeResolver() resolver = NamespaceEnrollmentsScopeResolver()
users = resolver.resolve( users = resolver.resolve(
'namespace_scope', 'namespace_scope',
{ {
'course_id': self.course.id, 'namespace': self.course.id,
'fields': { 'fields': {
'id': True, 'id': True,
'email': True, 'email': True,
......
"""
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'])
...@@ -20,6 +20,7 @@ from openedx.core.djangoapps.course_groups.scope_resolver import CourseGroupScop ...@@ -20,6 +20,7 @@ from openedx.core.djangoapps.course_groups.scope_resolver import CourseGroupScop
from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver from student.scope_resolver import CourseEnrollmentsScopeResolver, StudentEmailScopeResolver
from projects.scope_resolver import GroupProjectParticipantsScopeResolver from projects.scope_resolver import GroupProjectParticipantsScopeResolver
from edx_notifications.scopes import register_user_scope_resolver from edx_notifications.scopes import register_user_scope_resolver
from util.namespace_resolver import CourseNamespaceResolver
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -215,13 +216,16 @@ def startup_notification_subsystem(): ...@@ -215,13 +216,16 @@ def startup_notification_subsystem():
try: try:
startup.initialize() 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 # to edx-notifications
register_user_scope_resolver('course_enrollments', CourseEnrollmentsScopeResolver()) register_user_scope_resolver('course_enrollments', CourseEnrollmentsScopeResolver())
register_user_scope_resolver('course_group', CourseGroupScopeResolver()) register_user_scope_resolver('course_group', CourseGroupScopeResolver())
register_user_scope_resolver('group_project_participants', GroupProjectParticipantsScopeResolver()) register_user_scope_resolver('group_project_participants', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('group_project_workgroup', GroupProjectParticipantsScopeResolver()) register_user_scope_resolver('group_project_workgroup', GroupProjectParticipantsScopeResolver())
register_user_scope_resolver('student_email_resolver', StudentEmailScopeResolver()) register_user_scope_resolver('student_email_resolver', StudentEmailScopeResolver())
# register namespace resolver
register_namespace_resolver(CourseNamespaceResolver())
except Exception, ex: except Exception, ex:
# Note this will fail when we try to run migrations as manage.py will call startup.py # 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. # and startup.initialze() will try to manipulate some database tables.
......
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