Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
f7b6bd83
Commit
f7b6bd83
authored
May 14, 2015
by
chrisndodge
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #441 from edx-solutions/cdodge/digest-support
Cdodge/digest support
parents
0c740649
659d3603
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
626 additions
and
33 deletions
+626
-33
cms/startup.py
+6
-1
common/djangoapps/student/management/commands/force_send_notification_digest.py
+155
-0
common/djangoapps/student/management/commands/tests/test_force_send_notification_digest.py
+19
-0
common/djangoapps/student/scope_resolver.py
+100
-14
common/djangoapps/student/tests/test_scope_resolver.py
+80
-5
common/djangoapps/util/namespace_resolver.py
+66
-0
common/djangoapps/util/tests/test_namespace_resolver.py
+76
-0
lms/djangoapps/api_manager/users/tests.py
+34
-0
lms/djangoapps/api_manager/users/urls.py
+1
-0
lms/djangoapps/api_manager/users/views.py
+24
-1
lms/envs/aws.py
+45
-4
lms/envs/common.py
+12
-4
lms/envs/devstack.py
+0
-1
lms/startup.py
+7
-2
requirements/edx/custom.txt
+1
-1
No files found.
cms/startup.py
View file @
f7b6bd83
...
...
@@ -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.
...
...
common/djangoapps/student/management/commands/force_send_notification_digest.py
0 → 100644
View file @
f7b6bd83
"""
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."
)
common/djangoapps/student/management/commands/tests/test_force_send_notification_digest.py
0 → 100644
View file @
f7b6bd83
"""
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'
})
common/djangoapps/student/scope_resolver.py
View file @
f7b6bd83
...
...
@@ -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
)
common/djangoapps/student/tests/test_scope_resolver.py
View file @
f7b6bd83
...
...
@@ -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
):
"""
...
...
common/djangoapps/util/namespace_resolver.py
0 → 100644
View file @
f7b6bd83
"""
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
(),
}
common/djangoapps/util/tests/test_namespace_resolver.py
0 → 100644
View file @
f7b6bd83
"""
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'
])
lms/djangoapps/api_manager/users/tests.py
View file @
f7b6bd83
...
...
@@ -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
)
lms/djangoapps/api_manager/users/urls.py
View file @
f7b6bd83
...
...
@@ -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'
),
)
...
...
lms/djangoapps/api_manager/users/views.py
View file @
f7b6bd83
...
...
@@ -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
)
lms/envs/aws.py
View file @
f7b6bd83
...
...
@@ -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'
,
{})
lms/envs/common.py
View file @
f7b6bd83
...
...
@@ -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
=
{
...
...
lms/envs/devstack.py
View file @
f7b6bd83
...
...
@@ -48,7 +48,6 @@ if FEATURES.get('PROFILER'):
# dashboard to the Analytics Dashboard.
ANALYTICS_DASHBOARD_URL
=
None
################################ DEBUG TOOLBAR ################################
FEATURES
[
'DEBUG_TOOLBAR'
]
=
True
...
...
lms/startup.py
View file @
f7b6bd83
...
...
@@ -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.
...
...
requirements/edx/custom.txt
View file @
f7b6bd83
...
...
@@ -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@
1496385f16d2571bc5d958c17e30f3eac3869b8
e#egg=edx-notifications
-e git+https://github.com/edx/edx-notifications.git@
3266015191c1d99f4a692d8bf6f43a1a702cb9e
e#egg=edx-notifications
-e git+https://github.com/open-craft/problem-builder.git@5e00f92d78da0b28ae7f39c3f03596f49bec7119#egg=problem-builder
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment