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
9dfb68c2
Commit
9dfb68c2
authored
Apr 18, 2015
by
Nimisha Asthagiri
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7676 from edx/mobile/notifications-backend
Mobile/notifications
parents
2fed57a0
11044ebd
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
357 additions
and
51 deletions
+357
-51
cms/djangoapps/contentstore/admin.py
+2
-1
cms/djangoapps/contentstore/course_info_model.py
+9
-3
cms/djangoapps/contentstore/features/course-updates.py
+11
-11
cms/djangoapps/contentstore/migrations/0003_auto__add_pushnotificationconfig.py
+81
-0
cms/djangoapps/contentstore/models.py
+4
-0
cms/djangoapps/contentstore/push_notification.py
+62
-0
cms/djangoapps/contentstore/tasks.py
+10
-0
cms/djangoapps/contentstore/tests/tests.py
+14
-0
cms/djangoapps/contentstore/views/course.py
+3
-1
cms/djangoapps/contentstore/views/tests/test_course_updates.py
+38
-4
cms/envs/aws.py
+5
-0
cms/static/coffee/spec/views/course_info_spec.coffee
+43
-5
cms/static/js/factories/course_info.js
+3
-2
cms/static/js/models/course_update.js
+3
-1
cms/static/js/views/course_info_edit.js
+2
-1
cms/static/js/views/course_info_update.js
+28
-13
cms/templates/course_info.html
+7
-1
cms/templates/js/course_info_update.underscore
+13
-8
common/djangoapps/config_models/models.py
+5
-0
common/lib/xmodule/xmodule/course_module.py
+10
-0
lms/djangoapps/mobile_api/users/serializers.py
+1
-0
lms/djangoapps/mobile_api/users/tests.py
+1
-0
lms/djangoapps/mobile_api/users/views.py
+1
-0
requirements/edx/github.txt
+1
-0
No files found.
cms/djangoapps/contentstore/admin.py
View file @
9dfb68c2
...
...
@@ -5,6 +5,7 @@ Admin site bindings for contentstore
from
django.contrib
import
admin
from
config_models.admin
import
ConfigurationModelAdmin
from
contentstore.models
import
VideoUploadConfig
from
contentstore.models
import
VideoUploadConfig
,
PushNotificationConfig
admin
.
site
.
register
(
VideoUploadConfig
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
PushNotificationConfig
,
ConfigurationModelAdmin
)
cms/djangoapps/contentstore/course_info_model.py
View file @
9dfb68c2
...
...
@@ -23,6 +23,7 @@ from xmodule.modulestore.django import modulestore
from
xmodule.html_module
import
CourseInfoModule
from
xmodule_modifiers
import
get_course_update_items
from
cms.djangoapps.contentstore.push_notification
import
enqueue_push_course_update
# # This should be in a class which inherits from XmlDescriptor
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -44,9 +45,13 @@ def get_course_updates(location, provided_id, user_id):
def
update_course_updates
(
location
,
update
,
passed_id
=
None
,
user
=
None
):
"""
Either add or update the given course update. It will add it if the passed_id is absent or None. It will update it if
it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index
into the html structure.
Either add or update the given course update.
Add:
If the passed_id is absent or None, the course update is added.
If push_notification_selected is set in the update, a celery task for the push notification is created.
Update:
It will update it if it has a passed_id which has a valid value.
Until updates have distinct values, the passed_id is the location url + an index into the html structure.
"""
try
:
course_updates
=
modulestore
()
.
get_item
(
location
)
...
...
@@ -73,6 +78,7 @@ def update_course_updates(location, update, passed_id=None, user=None):
"status"
:
CourseInfoModule
.
STATUS_VISIBLE
}
course_update_items
.
append
(
course_update_dict
)
enqueue_push_course_update
(
update
,
location
.
course_key
)
# update db record
save_course_update_items
(
location
,
course_updates
,
course_update_items
,
user
)
...
...
cms/djangoapps/contentstore/features/course-updates.py
View file @
9dfb68c2
...
...
@@ -45,37 +45,37 @@ def check_no_update(_step, text):
@step
(
u'I modify the text to "([^"]*)"$'
)
def
modify_update
(
_step
,
text
):
button_css
=
'div.post-preview
a
.edit-button'
button_css
=
'div.post-preview .edit-button'
world
.
css_click
(
button_css
)
change_text
(
text
)
@step
(
u'I change the update from "([^"]*)" to "([^"]*)"$'
)
def
change_existing_update
(
_step
,
before
,
after
):
verify_text_in_editor_and_update
(
'div.post-preview
a
.edit-button'
,
before
,
after
)
verify_text_in_editor_and_update
(
'div.post-preview .edit-button'
,
before
,
after
)
@step
(
u'I change the handout from "([^"]*)" to "([^"]*)"$'
)
def
change_existing_handout
(
_step
,
before
,
after
):
verify_text_in_editor_and_update
(
'div.course-handouts
a
.edit-button'
,
before
,
after
)
verify_text_in_editor_and_update
(
'div.course-handouts .edit-button'
,
before
,
after
)
@step
(
u'I delete the update$'
)
def
click_button
(
_step
):
button_css
=
'div.post-preview
a
.delete-button'
button_css
=
'div.post-preview .delete-button'
world
.
css_click
(
button_css
)
@step
(
u'I edit the date to "([^"]*)"$'
)
def
change_date
(
_step
,
new_date
):
button_css
=
'div.post-preview
a
.edit-button'
button_css
=
'div.post-preview .edit-button'
world
.
css_click
(
button_css
)
date_css
=
'input.date'
date
=
world
.
css_find
(
date_css
)
for
i
in
range
(
len
(
date
.
value
)):
date
.
_element
.
send_keys
(
Keys
.
END
,
Keys
.
BACK_SPACE
)
date
.
_element
.
send_keys
(
new_date
)
save_css
=
'
a
.save-button'
save_css
=
'.save-button'
world
.
css_click
(
save_css
)
...
...
@@ -87,7 +87,7 @@ def check_date(_step, date):
@step
(
u'I modify the handout to "([^"]*)"$'
)
def
edit_handouts
(
_step
,
text
):
edit_css
=
'div.course-handouts >
a
.edit-button'
edit_css
=
'div.course-handouts > .edit-button'
world
.
css_click
(
edit_css
)
change_text
(
text
)
...
...
@@ -114,7 +114,7 @@ def check_handout_error(_step):
@step
(
u'I see handout save button disabled'
)
def
check_handout_error
(
_step
):
handout_save_button
=
'form.edit-handouts-form
a
.save-button'
handout_save_button
=
'form.edit-handouts-form .save-button'
assert
world
.
css_has_class
(
handout_save_button
,
'is-disabled'
)
...
...
@@ -125,19 +125,19 @@ def edit_handouts(_step, text):
@step
(
u'I see handout save button re-enabled'
)
def
check_handout_error
(
_step
):
handout_save_button
=
'form.edit-handouts-form
a
.save-button'
handout_save_button
=
'form.edit-handouts-form .save-button'
assert
not
world
.
css_has_class
(
handout_save_button
,
'is-disabled'
)
@step
(
u'I save handout edit'
)
def
check_handout_error
(
_step
):
save_css
=
'
a
.save-button'
save_css
=
'.save-button'
world
.
css_click
(
save_css
)
def
change_text
(
text
):
type_in_codemirror
(
0
,
text
)
save_css
=
'
a
.save-button'
save_css
=
'.save-button'
world
.
css_click
(
save_css
)
...
...
cms/djangoapps/contentstore/migrations/0003_auto__add_pushnotificationconfig.py
0 → 100644
View file @
9dfb68c2
# -*- coding: utf-8 -*-
from
south.utils
import
datetime_utils
as
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding model 'PushNotificationConfig'
db
.
create_table
(
'contentstore_pushnotificationconfig'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'change_date'
,
self
.
gf
(
'django.db.models.fields.DateTimeField'
)(
auto_now_add
=
True
,
blank
=
True
)),
(
'changed_by'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
to
=
orm
[
'auth.User'
],
null
=
True
,
on_delete
=
models
.
PROTECT
)),
(
'enabled'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
)),
))
db
.
send_create_signal
(
'contentstore'
,
[
'PushNotificationConfig'
])
def
backwards
(
self
,
orm
):
# Deleting model 'PushNotificationConfig'
db
.
delete_table
(
'contentstore_pushnotificationconfig'
)
models
=
{
'auth.group'
:
{
'Meta'
:
{
'object_name'
:
'Group'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'80'
}),
'permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
})
},
'auth.permission'
:
{
'Meta'
:
{
'ordering'
:
"('content_type__app_label', 'content_type__model', 'codename')"
,
'unique_together'
:
"(('content_type', 'codename'),)"
,
'object_name'
:
'Permission'
},
'codename'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'content_type'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['contenttypes.ContentType']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
})
},
'auth.user'
:
{
'Meta'
:
{
'object_name'
:
'User'
},
'date_joined'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'email'
:
(
'django.db.models.fields.EmailField'
,
[],
{
'max_length'
:
'75'
,
'blank'
:
'True'
}),
'first_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'groups'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Group']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'is_staff'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_superuser'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'last_login'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'last_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'password'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
}),
'user_permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'30'
})
},
'contentstore.pushnotificationconfig'
:
{
'Meta'
:
{
'object_name'
:
'PushNotificationConfig'
},
'change_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'changed_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
,
'null'
:
'True'
,
'on_delete'
:
'models.PROTECT'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
})
},
'contentstore.videouploadconfig'
:
{
'Meta'
:
{
'object_name'
:
'VideoUploadConfig'
},
'change_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'changed_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
,
'null'
:
'True'
,
'on_delete'
:
'models.PROTECT'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'profile_whitelist'
:
(
'django.db.models.fields.TextField'
,
[],
{
'blank'
:
'True'
})
},
'contenttypes.contenttype'
:
{
'Meta'
:
{
'ordering'
:
"('name',)"
,
'unique_together'
:
"(('app_label', 'model'),)"
,
'object_name'
:
'ContentType'
,
'db_table'
:
"'django_content_type'"
},
'app_label'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'model'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
})
}
}
complete_apps
=
[
'contentstore'
]
\ No newline at end of file
cms/djangoapps/contentstore/models.py
View file @
9dfb68c2
...
...
@@ -19,3 +19,7 @@ class VideoUploadConfig(ConfigurationModel):
def
get_profile_whitelist
(
cls
):
"""Get the list of profiles to include in the encoding download"""
return
[
profile
for
profile
in
cls
.
current
()
.
profile_whitelist
.
split
(
","
)
if
profile
]
class
PushNotificationConfig
(
ConfigurationModel
):
"""Configuration for mobile push notifications."""
cms/djangoapps/contentstore/push_notification.py
0 → 100644
View file @
9dfb68c2
"""
Helper methods for push notifications from Studio.
"""
from
django.conf
import
settings
from
logging
import
exception
as
log_exception
from
contentstore.tasks
import
push_course_update_task
from
contentstore.models
import
PushNotificationConfig
from
xmodule.modulestore.django
import
modulestore
from
parse_rest.installation
import
Push
from
parse_rest.connection
import
register
from
parse_rest.core
import
ParseError
def
push_notification_enabled
():
"""
Returns whether the push notification feature is enabled.
"""
return
PushNotificationConfig
.
is_enabled
()
def
enqueue_push_course_update
(
update
,
course_key
):
"""
Enqueues a task for push notification for the given update for the given course if
(1) the feature is enabled and
(2) push_notification is selected for the update
"""
if
push_notification_enabled
()
and
update
.
get
(
"push_notification_selected"
):
course
=
modulestore
()
.
get_course
(
course_key
)
if
course
:
push_course_update_task
.
delay
(
unicode
(
course_key
),
course
.
clean_id
(
padding_char
=
'_'
),
course
.
display_name
)
def
send_push_course_update
(
course_key_string
,
course_subscription_id
,
course_display_name
):
"""
Sends a push notification for a course update, given the course's subscription_id and display_name.
"""
if
settings
.
PARSE_KEYS
:
try
:
register
(
settings
.
PARSE_KEYS
[
"APPLICATION_ID"
],
settings
.
PARSE_KEYS
[
"REST_API_KEY"
],
)
Push
.
alert
(
data
=
{
"course-id"
:
course_key_string
,
"action"
:
"course.announcement"
,
"action-loc-key"
:
"VIEW_BUTTON"
,
"loc-key"
:
"COURSE_ANNOUNCEMENT_NOTIFICATION_BODY"
,
"loc-args"
:
[
course_display_name
],
"title-loc-key"
:
"COURSE_ANNOUNCEMENT_NOTIFICATION_TITLE"
,
"title-loc-args"
:
[],
},
channels
=
[
course_subscription_id
],
)
except
ParseError
as
error
:
log_exception
(
error
.
message
)
cms/djangoapps/contentstore/tasks.py
View file @
9dfb68c2
...
...
@@ -115,3 +115,13 @@ def update_library_index(library_id, triggered_time_isoformat):
LOGGER
.
error
(
'Search indexing error for library
%
s -
%
s'
,
library_id
,
unicode
(
exc
))
else
:
LOGGER
.
debug
(
'Search indexing successful for library
%
s'
,
library_id
)
@task
()
def
push_course_update_task
(
course_key_string
,
course_subscription_id
,
course_display_name
):
"""
Sends a push notification for a course update.
"""
# TODO Use edx-notifications library instead (MA-638).
from
.push_notification
import
send_push_course_update
send_push_course_update
(
course_key_string
,
course_subscription_id
,
course_display_name
)
cms/djangoapps/contentstore/tests/tests.py
View file @
9dfb68c2
...
...
@@ -6,12 +6,14 @@ import mock
import
unittest
from
ddt
import
ddt
,
data
,
unpack
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.core.cache
import
cache
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.core.urlresolvers
import
reverse
from
contentstore.models
import
PushNotificationConfig
from
contentstore.tests.utils
import
parse_json
,
user
,
registration
,
AjaxEnabledTestClient
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
contentstore.tests.test_course_settings
import
CourseTestCase
...
...
@@ -349,3 +351,15 @@ class CourseKeyVerificationTestCase(CourseTestCase):
)
resp
=
self
.
client
.
get_html
(
url
)
self
.
assertEqual
(
resp
.
status_code
,
status_code
)
class
PushNotificationConfigTestCase
(
TestCase
):
"""
Tests PushNotificationConfig.
"""
def
test_notifications_defaults
(
self
):
self
.
assertFalse
(
PushNotificationConfig
.
is_enabled
())
def
test_notifications_enabled
(
self
):
PushNotificationConfig
(
enabled
=
True
)
.
save
()
self
.
assertTrue
(
PushNotificationConfig
.
is_enabled
())
cms/djangoapps/contentstore/views/course.py
View file @
9dfb68c2
...
...
@@ -69,6 +69,7 @@ from contentstore.views.entrance_exam import (
from
.library
import
LIBRARIES_ENABLED
from
.item
import
create_xblock_info
from
contentstore.push_notification
import
push_notification_enabled
from
course_creators.views
import
get_course_creator_status
,
add_user_with_status_unrequested
from
contentstore
import
utils
from
student.roles
import
(
...
...
@@ -778,7 +779,8 @@ def course_info_handler(request, course_key_string):
'context_course'
:
course_module
,
'updates_url'
:
reverse_course_url
(
'course_info_update_handler'
,
course_key
),
'handouts_locator'
:
course_key
.
make_usage_key
(
'course_info'
,
'handouts'
),
'base_asset_url'
:
StaticContent
.
get_base_url_path_for_course_assets
(
course_module
.
id
)
'base_asset_url'
:
StaticContent
.
get_base_url_path_for_course_assets
(
course_module
.
id
),
'push_notification_enabled'
:
push_notification_enabled
()
}
)
else
:
...
...
cms/djangoapps/contentstore/views/tests/test_course_updates.py
View file @
9dfb68c2
...
...
@@ -2,7 +2,10 @@
unit tests for course_info views and models.
"""
import
json
from
mock
import
patch
from
django.test.utils
import
override_settings
from
contentstore.models
import
PushNotificationConfig
from
contentstore.tests.test_course_settings
import
CourseTestCase
from
contentstore.utils
import
reverse_course_url
,
reverse_usage_url
from
opaque_keys.edx.keys
import
UsageKey
...
...
@@ -234,18 +237,19 @@ class CourseUpdateTest(CourseTestCase):
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertTrue
(
len
(
payload
)
==
1
)
def
test_post_course_update
(
self
):
def
post_course_update
(
self
,
send_push_notification
=
False
):
"""
Test that a user can successfully post on course updates and handouts of a
course
Posts an update to the
course
"""
course_update_url
=
self
.
create_update_url
(
course_key
=
self
.
course
.
id
)
# create a course via the view handler
self
.
client
.
ajax_post
(
course_update_url
)
block
=
u'updates'
content
=
u"Sample update"
payload
=
{
'content'
:
content
,
'date'
:
'January 8, 2013'
}
if
send_push_notification
:
payload
[
'push_notification_selected'
]
=
True
resp
=
self
.
client
.
ajax_post
(
course_update_url
,
payload
)
# check that response status is 200 not 400
...
...
@@ -254,9 +258,19 @@ class CourseUpdateTest(CourseTestCase):
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertHTMLEqual
(
payload
[
'content'
],
content
)
@patch
(
"contentstore.push_notification.send_push_course_update"
)
def
test_post_course_update
(
self
,
mock_push_update
):
"""
Test that a user can successfully post on course updates and handouts of a course
"""
self
.
post_course_update
()
# check that push notifications are not sent
self
.
assertFalse
(
mock_push_update
.
called
)
updates_location
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'updates'
)
self
.
assertTrue
(
isinstance
(
updates_location
,
UsageKey
))
self
.
assertEqual
(
updates_location
.
name
,
block
)
self
.
assertEqual
(
updates_location
.
name
,
u'updates'
)
# check posting on handouts
handouts_location
=
self
.
course
.
id
.
make_usage_key
(
'course_info'
,
'handouts'
)
...
...
@@ -265,8 +279,28 @@ class CourseUpdateTest(CourseTestCase):
content
=
u"Sample handout"
payload
=
{
'data'
:
content
}
resp
=
self
.
client
.
ajax_post
(
course_handouts_url
,
payload
)
# check that response status is 200 not 500
self
.
assertEqual
(
resp
.
status_code
,
200
)
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertHTMLEqual
(
payload
[
'data'
],
content
)
@patch
(
"contentstore.push_notification.send_push_course_update"
)
def
test_notifications_enabled_but_not_requested
(
self
,
mock_push_update
):
PushNotificationConfig
(
enabled
=
True
)
.
save
()
self
.
post_course_update
()
self
.
assertFalse
(
mock_push_update
.
called
)
@patch
(
"contentstore.push_notification.send_push_course_update"
)
def
test_notifications_enabled_and_sent
(
self
,
mock_push_update
):
PushNotificationConfig
(
enabled
=
True
)
.
save
()
self
.
post_course_update
(
send_push_notification
=
True
)
self
.
assertTrue
(
mock_push_update
.
called
)
@override_settings
(
PARSE_KEYS
=
{
"APPLICATION_ID"
:
"TEST_APPLICATION_ID"
,
"REST_API_KEY"
:
"TEST_REST_API_KEY"
})
@patch
(
"contentstore.push_notification.Push"
)
def
test_notifications_sent_to_parse
(
self
,
mock_parse_push
):
PushNotificationConfig
(
enabled
=
True
)
.
save
()
self
.
post_course_update
(
send_push_notification
=
True
)
self
.
assertTrue
(
mock_parse_push
.
alert
.
called
)
cms/envs/aws.py
View file @
9dfb68c2
...
...
@@ -320,6 +320,11 @@ DEPRECATED_ADVANCED_COMPONENT_TYPES = ENV_TOKENS.get(
VIDEO_UPLOAD_PIPELINE
=
ENV_TOKENS
.
get
(
'VIDEO_UPLOAD_PIPELINE'
,
VIDEO_UPLOAD_PIPELINE
)
################ PUSH NOTIFICATIONS ###############
PARSE_KEYS
=
AUTH_TOKENS
.
get
(
"PARSE_KEYS"
,
{})
#date format the api will be formatting the datetime values
API_DATE_FORMAT
=
'
%
Y-
%
m-
%
d'
API_DATE_FORMAT
=
ENV_TOKENS
.
get
(
'API_DATE_FORMAT'
,
API_DATE_FORMAT
)
...
...
cms/static/coffee/spec/views/course_info_spec.coffee
View file @
9dfb68c2
...
...
@@ -22,7 +22,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
delete
window
.
analytics
delete
window
.
course_location_analytics
describe
"Course Updates"
,
->
describe
"Course Updates
without Push notification
"
,
->
courseInfoTemplate
=
readFixtures
(
'course_info_update.underscore'
)
beforeEach
->
...
...
@@ -100,7 +100,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
else
modalCover
.
click
()
it
"does
not rewrite links
on save"
,
->
it
"does
send expected data
on save"
,
->
requests
=
AjaxHelpers
[
"requests"
](
this
)
# Create a new update, verifying that the model is created
...
...
@@ -116,9 +116,12 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@
courseInfoEdit
.
$el
.
find
(
'.save-button'
).
click
()
expect
(
model
.
save
).
toHaveBeenCalled
()
# Verify content sent to server does not have rewritten links.
contentSaved
=
JSON
.
parse
(
requests
[
requests
.
length
-
1
].
requestBody
).
content
expect
(
contentSaved
).
toEqual
(
'/static/image.jpg'
)
# Verify push_notification_selected is set to false.
requestSent
=
JSON
.
parse
(
requests
[
requests
.
length
-
1
].
requestBody
)
expect
(
requestSent
.
push_notification_selected
).
toEqual
(
false
)
# Verify the link is not rewritten when saved.
expect
(
requestSent
.
content
).
toEqual
(
'/static/image.jpg'
)
it
"does rewrite links for preview"
,
->
# Create a new update.
...
...
@@ -147,6 +150,41 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
it
"does not remove existing course info on click outside modal"
,
->
@
cancelExistingCourseInfo
(
false
)
describe
"Course Updates WITH Push notification"
,
->
courseInfoTemplate
=
readFixtures
(
'course_info_update.underscore'
)
beforeEach
->
setFixtures
(
$
(
"<script>"
,
{
id
:
"course_info_update-tpl"
,
type
:
"text/template"
}).
text
(
courseInfoTemplate
))
appendSetFixtures
courseInfoPage
@
collection
=
new
CourseUpdateCollection
()
@
collection
.
url
=
'course_info_update/'
@
courseInfoEdit
=
new
CourseInfoUpdateView
({
el
:
$
(
'.course-updates'
),
collection
:
@
collection
,
base_asset_url
:
'base-asset-url/'
,
push_notification_enabled
:
true
})
@
courseInfoEdit
.
render
()
@
event
=
{
preventDefault
:
()
->
'no op'
}
@
courseInfoEdit
.
onNew
(
@
event
)
@
requests
=
AjaxHelpers
[
"requests"
](
this
)
it
"shows push notification checkbox as selected by default"
,
->
expect
(
@
courseInfoEdit
.
$el
.
find
(
'.toggle-checkbox'
)).
toBeChecked
()
it
"sends correct default value for push_notification_selected"
,
->
@
courseInfoEdit
.
$el
.
find
(
'.save-button'
).
click
()
requestSent
=
JSON
.
parse
(
@
requests
[
@
requests
.
length
-
1
].
requestBody
)
expect
(
requestSent
.
push_notification_selected
).
toEqual
(
true
)
it
"sends correct value for push_notification_selected when it is unselected"
,
->
# unselect push notification
@
courseInfoEdit
.
$el
.
find
(
'.toggle-checkbox'
).
attr
(
'checked'
,
false
);
@
courseInfoEdit
.
$el
.
find
(
'.save-button'
).
click
()
requestSent
=
JSON
.
parse
(
@
requests
[
@
requests
.
length
-
1
].
requestBody
)
expect
(
requestSent
.
push_notification_selected
).
toEqual
(
false
)
describe
"Course Handouts"
,
->
handoutsTemplate
=
readFixtures
(
'course_info_handouts.underscore'
)
...
...
cms/static/js/factories/course_info.js
View file @
9dfb68c2
...
...
@@ -3,7 +3,7 @@ define([
'js/models/course_info'
,
'js/views/course_info_edit'
],
function
(
$
,
CourseUpdateCollection
,
ModuleInfoModel
,
CourseInfoModel
,
CourseInfoEditView
)
{
'use strict'
;
return
function
(
updatesUrl
,
handoutsLocator
,
baseAssetUrl
)
{
return
function
(
updatesUrl
,
handoutsLocator
,
baseAssetUrl
,
push_notification_enabled
)
{
var
course_updates
=
new
CourseUpdateCollection
(),
course_handouts
,
editor
;
...
...
@@ -18,7 +18,8 @@ define([
updates
:
course_updates
,
base_asset_url
:
baseAssetUrl
,
handouts
:
course_handouts
})
}),
push_notification_enabled
:
push_notification_enabled
});
editor
.
render
();
};
...
...
cms/static/js/models/course_update.js
View file @
9dfb68c2
...
...
@@ -3,7 +3,9 @@ define(["backbone", "jquery", "jquery.ui"], function(Backbone, $) {
var
CourseUpdate
=
Backbone
.
Model
.
extend
({
defaults
:
{
"date"
:
$
.
datepicker
.
formatDate
(
'MM d, yy'
,
new
Date
()),
"content"
:
""
"content"
:
""
,
"push_notification_enabled"
:
false
,
"push_notification_selected"
:
false
}
});
return
CourseUpdate
;
...
...
cms/static/js/views/course_info_edit.js
View file @
9dfb68c2
...
...
@@ -15,7 +15,8 @@ var CourseInfoEdit = BaseView.extend({
new
CourseInfoUpdateView
({
el
:
$
(
'body.updates'
),
collection
:
this
.
model
.
get
(
'updates'
),
base_asset_url
:
this
.
model
.
get
(
'base_asset_url'
)
base_asset_url
:
this
.
model
.
get
(
'base_asset_url'
),
push_notification_enabled
:
this
.
options
.
push_notification_enabled
});
new
CourseInfoHandoutView
({
...
...
cms/static/js/views/course_info_update.js
View file @
9dfb68c2
...
...
@@ -3,6 +3,7 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
function
(
BaseView
,
CodeMirror
,
CourseUpdateModel
,
PromptView
,
NotificationView
,
CourseInfoHelper
,
ModalUtils
)
{
var
CourseInfoUpdateView
=
BaseView
.
extend
({
// collection is CourseUpdateCollection
events
:
{
"click .new-update-button"
:
"onNew"
,
...
...
@@ -29,7 +30,8 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
try
{
CourseInfoHelper
.
changeContentToPreview
(
update
,
'content'
,
self
.
options
[
'base_asset_url'
]);
var
newEle
=
self
.
template
({
updateModel
:
update
});
// push notification is always disabled for existing updates
var
newEle
=
self
.
template
({
updateModel
:
update
,
push_notification_enabled
:
false
});
$
(
updateEle
).
append
(
newEle
);
}
catch
(
e
)
{
// ignore
...
...
@@ -47,7 +49,12 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
var
newModel
=
new
CourseUpdateModel
();
this
.
collection
.
add
(
newModel
,
{
at
:
0
});
var
$newForm
=
$
(
this
.
template
({
updateModel
:
newModel
}));
var
$newForm
=
$
(
this
.
template
({
updateModel
:
newModel
,
push_notification_enabled
:
this
.
options
.
push_notification_enabled
})
);
var
updateEle
=
this
.
$el
.
find
(
"#course-update-list"
);
$
(
updateEle
).
prepend
(
$newForm
);
...
...
@@ -74,7 +81,11 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
onSave
:
function
(
event
)
{
event
.
preventDefault
();
var
targetModel
=
this
.
eventModel
(
event
);
targetModel
.
set
({
date
:
this
.
dateEntry
(
event
).
val
(),
content
:
this
.
$codeMirror
.
getValue
()
});
targetModel
.
set
({
date
:
this
.
dateEntry
(
event
).
val
(),
content
:
this
.
$codeMirror
.
getValue
(),
push_notification_selected
:
this
.
push_notification_selected
(
event
)
});
// push change to display, hide the editor, submit the change
var
saving
=
new
NotificationView
.
Mini
({
title
:
gettext
(
'Saving'
)
...
...
@@ -196,6 +207,11 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
}
this
.
$currentPost
.
find
(
'form'
).
hide
();
this
.
$currentPost
.
find
(
'.CodeMirror'
).
remove
();
// hide the push notification checkbox for subsequent edits to the Post
var
push_notification_ele
=
this
.
$currentPost
.
find
(
".new-update-push-notification"
);
if
(
push_notification_ele
)
push_notification_ele
.
hide
();
}
ModalUtils
.
hideModalCover
(
this
.
$modalCover
);
...
...
@@ -222,16 +238,15 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
if
(
li
)
return
$
(
li
).
find
(
".date"
).
first
();
},
contentEntry
:
function
(
event
)
{
return
$
(
event
.
currentTarget
).
closest
(
"li"
).
find
(
".new-update-content"
).
first
();
},
dateDisplay
:
function
(
event
)
{
return
$
(
event
.
currentTarget
).
closest
(
"li"
).
find
(
"#date-display"
).
first
();
},
contentDisplay
:
function
(
event
)
{
return
$
(
event
.
currentTarget
).
closest
(
"li"
).
find
(
".update-contents"
).
first
();
push_notification_selected
:
function
(
event
)
{
var
push_notification_checkbox
;
var
li
=
$
(
event
.
currentTarget
).
closest
(
"li"
);
if
(
li
)
{
push_notification_checkbox
=
li
.
find
(
".new-update-push-notification .toggle-checkbox"
);
if
(
push_notification_checkbox
)
{
return
push_notification_checkbox
.
is
(
":checked"
);
}
}
}
});
...
...
cms/templates/course_info.html
View file @
9dfb68c2
...
...
@@ -5,6 +5,7 @@
<
%
inherit
file=
"base.html"
/>
<
%
def
name=
"online_help_token()"
><
%
return
"
updates
"
%
></
%
def>
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
<
%!
import
json
%
>
<!-- TODO decode course # from context_course into title -->
<
%
block
name=
"title"
>
${_("Course Updates")}
</
%
block>
...
...
@@ -21,7 +22,12 @@
<
%
block
name=
"requirejs"
>
require(["js/factories/course_info"], function(CourseInfoFactory) {
CourseInfoFactory("${updates_url}", "${handouts_locator | escapejs}", "${base_asset_url}");
CourseInfoFactory(
"${updates_url}",
"${handouts_locator | escapejs}",
"${base_asset_url}",
${json.dumps(push_notification_enabled)}
);
});
</
%
block>
...
...
cms/templates/js/course_info_update.underscore
View file @
9dfb68c2
...
...
@@ -2,23 +2,29 @@
<!-- FIXME what style should we use for initially hidden? --> <!-- TODO decide whether this should use codemirror -->
<form class="new-update-form">
<div class="row">
<label class="inline-label">Date:</label>
<label
for="update-date-<%= updateModel.cid %>"
class="inline-label">Date:</label>
<!-- TODO replace w/ date widget and actual date (problem is that persisted version is "Month day" not an actual date obj -->
<input type="text" class="date" value="<%= updateModel.get('date') %>">
<input
id="update-date-<%= updateModel.cid %>"
type="text" class="date" value="<%= updateModel.get('date') %>">
</div>
<div class="row">
<textarea class="new-update-content text-editor"><%= updateModel.get('content') %></textarea>
</div>
<%if (push_notification_enabled) { %>
<div class="row new-update-push-notification">
<input id="update-notification-checkbox-<%= updateModel.cid %>" type="checkbox" class="toggle-checkbox" data-tooltip="<%= gettext('Send push notification to mobile apps') %>" checked />
<label for="update-notification-checkbox-<%= updateModel.cid %>" class="inline-label"><%= gettext('Send notification to mobile apps') %></label>
</div>
<% } %>
<div class="row">
<!-- cid rather than id b/c new ones have cid's not id's -->
<a href="#" class="save-button" name="<%= updateModel.cid %>">Save</a
>
<a href="#" class="cancel-button" name="<%= updateModel.cid %>">Cancel</a
>
<button class="save-button" name="<%= updateModel.cid %>"><%= gettext('Post') %></button
>
<button class="cancel-button" name="<%= updateModel.cid %>"><%= gettext('Cancel') %></button
>
</div>
</form>
<div class="post-preview">
<div class="post-actions">
<a href="#" class="edit-button" name="<%- updateModel.cid %>"><span class="edit-icon"></span>Edit</a
>
<a href="#" class="delete-button" name="<%- updateModel.cid %>"><span class="delete-icon"></span>Delete</a
>
<button class="edit-button" name="<%= updateModel.cid %>"><span class="edit-icon"></span>Edit</button
>
<button class="delete-button" name="<%= updateModel.cid %>"><span class="delete-icon"></span>Delete</button
>
</div>
<h2>
<span class="calendar-icon"></span><span class="date-display"><%=
...
...
@@ -26,4 +32,4 @@
</h2>
<div class="update-contents"><%= updateModel.get('content') %></div>
</div>
</li>
\ No newline at end of file
</li>
common/djangoapps/config_models/models.py
View file @
9dfb68c2
...
...
@@ -60,3 +60,8 @@ class ConfigurationModel(models.Model):
cache
.
set
(
cls
.
cache_key_name
(),
current
,
cls
.
cache_timeout
)
return
current
@classmethod
def
is_enabled
(
cls
):
"""Returns True if this feature is configured as enabled, else False."""
return
cls
.
current
()
.
enabled
common/lib/xmodule/xmodule/course_module.py
View file @
9dfb68c2
...
...
@@ -10,6 +10,7 @@ import requests
from
datetime
import
datetime
import
dateutil.parser
from
lazy
import
lazy
from
base64
import
b32encode
from
xmodule.exceptions
import
UndefinedContext
from
xmodule.seq_module
import
SequenceDescriptor
,
SequenceModule
...
...
@@ -1398,3 +1399,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
self
.
video_upload_pipeline
is
not
None
and
'course_video_upload_token'
in
self
.
video_upload_pipeline
)
def
clean_id
(
self
,
padding_char
=
'='
):
"""
Returns a unique deterministic base32-encoded ID for the course.
The optional padding_char parameter allows you to override the "=" character used for padding.
"""
return
"course_{}"
.
format
(
b32encode
(
unicode
(
self
.
location
.
course_key
))
.
replace
(
'='
,
padding_char
)
)
lms/djangoapps/mobile_api/users/serializers.py
View file @
9dfb68c2
...
...
@@ -60,6 +60,7 @@ class CourseField(serializers.RelatedField):
"course_updates"
:
course_updates_url
,
"course_handouts"
:
course_handouts_url
,
"course_about"
:
course_about_url
,
"subscription_id"
:
course
.
clean_id
(
padding_char
=
'_'
),
}
...
...
lms/djangoapps/mobile_api/users/tests.py
View file @
9dfb68c2
...
...
@@ -60,6 +60,7 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileEn
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
courses
[
0
][
'mode'
],
'honor'
)
self
.
assertEqual
(
courses
[
0
][
'course'
][
'subscription_id'
],
self
.
course
.
clean_id
(
padding_char
=
'_'
))
def
verify_failure
(
self
,
response
):
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
lms/djangoapps/mobile_api/users/views.py
View file @
9dfb68c2
...
...
@@ -221,6 +221,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
* video_outline: The URI to get the list of all vides the user can
access in the course.
* id: The unique ID of the course.
* subscription_id: A unique "clean" (alphanumeric with '_') ID of the course.
* latest_updates: Reserved for future use.
* end: The end date of the course.
* name: The name of the course.
...
...
requirements/edx/github.txt
View file @
9dfb68c2
...
...
@@ -20,6 +20,7 @@
-e git+https://github.com/jazkarta/edx-jsme.git@c5bfa5d361d6685d8c643838fc0055c25f8b7999#egg=edx-jsme
-e git+https://github.com/pmitros/django-pyfs.git@d175715e0fe3367ec0f1ee429c242d603f6e8b10#egg=djpyfs
git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas
-e git+https://github.com/dgrtwo/ParsePy.git@7949b9f754d1445eff8e8f20d0e967b9a6420639#egg=parse_rest
# Our libraries:
-e git+https://github.com/edx/XBlock.git@aed464a0e2f7478e93157150ac04133a745f5f46#egg=XBlock
...
...
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