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
659d3603
Commit
659d3603
authored
May 13, 2015
by
Chris Dodge
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
address review feedback
parent
25a4bcfd
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
95 additions
and
73 deletions
+95
-73
common/djangoapps/student/management/commands/force_send_notification_digest.py
+40
-40
common/djangoapps/student/management/commands/tests/test_force_send_notification_digest.py
+5
-3
common/djangoapps/student/scope_resolver.py
+34
-21
common/djangoapps/student/tests/test_scope_resolver.py
+5
-3
lms/djangoapps/api_manager/users/tests.py
+1
-1
lms/djangoapps/api_manager/users/views.py
+9
-3
lms/envs/devstack.py
+1
-2
No files found.
common/djangoapps/student/management/commands/force_send_notification_digest.py
View file @
659d3603
"""
"""
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_
week
ly_digest
(
self
,
namespace
=
'All'
):
def
send_
dai
ly_digest
(
self
,
namespace
=
'All'
):
"""
"""
Sends the
week
ly digest.
Sends the
dai
ly 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'
])
week
ly_digests_sent
=
self
.
send_daily_digest
(
options
[
'namespace'
])
dai
ly_digests_sent
=
self
.
send_daily_digest
(
options
[
'namespace'
])
log
.
info
(
"Successfully sent
%
s digests..."
,
week
ly_digests_sent
)
log
.
info
(
"Successfully sent
%
s digests..."
,
dai
ly_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'
])
dai
ly_digests_sent
=
self
.
send_weekly_digest
(
options
[
'namespace'
])
week
ly_digests_sent
=
self
.
send_weekly_digest
(
options
[
'namespace'
])
log
.
info
(
"Successfully sent
%
s digests..."
,
dai
ly_digests_sent
)
log
.
info
(
"Successfully sent
%
s digests..."
,
week
ly_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."
)
common/djangoapps/student/management/commands/tests/test_force_send_notification_digest.py
View file @
659d3603
...
@@ -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 notification
s.
# run the management command for
sending notification digest
s.
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'})
common/djangoapps/student/scope_resolver.py
View file @
659d3603
...
@@ -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
r
eturn
None
r
aise
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'
)
...
...
common/djangoapps/student/tests/test_scope_resolver.py
View file @
659d3603
...
@@ -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
)
...
...
lms/djangoapps/api_manager/users/tests.py
View file @
659d3603
...
@@ -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
,
20
0
)
self
.
assertEqual
(
response
.
status_code
,
20
1
)
# 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
)
lms/djangoapps/api_manager/users/views.py
View file @
659d3603
...
@@ -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_20
0_OK
)
return
Response
({},
status
=
status
.
HTTP_20
1_CREATED
)
lms/envs/devstack.py
View file @
659d3603
...
@@ -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'
):
...
...
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