Commit b124485e by Nimisha Asthagiri

MA-611, MA-613 Mobile Push Notification Studio Backend; Integration with Parse

parent c2a1870f
...@@ -23,6 +23,7 @@ from xmodule.modulestore.django import modulestore ...@@ -23,6 +23,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.html_module import CourseInfoModule from xmodule.html_module import CourseInfoModule
from xmodule_modifiers import get_course_update_items 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 # # This should be in a class which inherits from XmlDescriptor
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -44,9 +45,13 @@ def get_course_updates(location, provided_id, user_id): ...@@ -44,9 +45,13 @@ def get_course_updates(location, provided_id, user_id):
def update_course_updates(location, update, passed_id=None, user=None): 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 Either add or update the given course update.
it has an passed_id which has a valid value. Until updates have distinct values, the passed_id is the location url + an index Add:
into the html structure. 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: try:
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
...@@ -73,6 +78,7 @@ def update_course_updates(location, update, passed_id=None, user=None): ...@@ -73,6 +78,7 @@ def update_course_updates(location, update, passed_id=None, user=None):
"status": CourseInfoModule.STATUS_VISIBLE "status": CourseInfoModule.STATUS_VISIBLE
} }
course_update_items.append(course_update_dict) course_update_items.append(course_update_dict)
enqueue_push_course_update(update, location.course_key)
# update db record # update db record
save_course_update_items(location, course_updates, course_update_items, user) save_course_update_items(location, course_updates, course_update_items, user)
......
"""
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)
...@@ -115,3 +115,13 @@ def update_library_index(library_id, triggered_time_isoformat): ...@@ -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)) LOGGER.error('Search indexing error for library %s - %s', library_id, unicode(exc))
else: else:
LOGGER.debug('Search indexing successful for library %s', library_id) 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)
...@@ -69,6 +69,7 @@ from contentstore.views.entrance_exam import ( ...@@ -69,6 +69,7 @@ from contentstore.views.entrance_exam import (
from .library import LIBRARIES_ENABLED from .library import LIBRARIES_ENABLED
from .item import create_xblock_info 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 course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from contentstore import utils from contentstore import utils
from student.roles import ( from student.roles import (
...@@ -778,7 +779,8 @@ def course_info_handler(request, course_key_string): ...@@ -778,7 +779,8 @@ def course_info_handler(request, course_key_string):
'context_course': course_module, 'context_course': course_module,
'updates_url': reverse_course_url('course_info_update_handler', course_key), 'updates_url': reverse_course_url('course_info_update_handler', course_key),
'handouts_locator': course_key.make_usage_key('course_info', 'handouts'), '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: else:
......
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
unit tests for course_info views and models. unit tests for course_info views and models.
""" """
import json 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.tests.test_course_settings import CourseTestCase
from contentstore.utils import reverse_course_url, reverse_usage_url from contentstore.utils import reverse_course_url, reverse_usage_url
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
...@@ -234,18 +237,19 @@ class CourseUpdateTest(CourseTestCase): ...@@ -234,18 +237,19 @@ class CourseUpdateTest(CourseTestCase):
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertTrue(len(payload) == 1) 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) course_update_url = self.create_update_url(course_key=self.course.id)
# create a course via the view handler # create a course via the view handler
self.client.ajax_post(course_update_url) self.client.ajax_post(course_update_url)
block = u'updates'
content = u"Sample update" content = u"Sample update"
payload = {'content': content, 'date': 'January 8, 2013'} 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) resp = self.client.ajax_post(course_update_url, payload)
# check that response status is 200 not 400 # check that response status is 200 not 400
...@@ -254,9 +258,19 @@ class CourseUpdateTest(CourseTestCase): ...@@ -254,9 +258,19 @@ class CourseUpdateTest(CourseTestCase):
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertHTMLEqual(payload['content'], 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') updates_location = self.course.id.make_usage_key('course_info', 'updates')
self.assertTrue(isinstance(updates_location, UsageKey)) self.assertTrue(isinstance(updates_location, UsageKey))
self.assertEqual(updates_location.name, block) self.assertEqual(updates_location.name, u'updates')
# check posting on handouts # check posting on handouts
handouts_location = self.course.id.make_usage_key('course_info', 'handouts') handouts_location = self.course.id.make_usage_key('course_info', 'handouts')
...@@ -265,8 +279,28 @@ class CourseUpdateTest(CourseTestCase): ...@@ -265,8 +279,28 @@ class CourseUpdateTest(CourseTestCase):
content = u"Sample handout" content = u"Sample handout"
payload = {'data': content} payload = {'data': content}
resp = self.client.ajax_post(course_handouts_url, payload) resp = self.client.ajax_post(course_handouts_url, payload)
# check that response status is 200 not 500 # check that response status is 200 not 500
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
payload = json.loads(resp.content) payload = json.loads(resp.content)
self.assertHTMLEqual(payload['data'], 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)
...@@ -320,6 +320,11 @@ DEPRECATED_ADVANCED_COMPONENT_TYPES = ENV_TOKENS.get( ...@@ -320,6 +320,11 @@ DEPRECATED_ADVANCED_COMPONENT_TYPES = ENV_TOKENS.get(
VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIPELINE) 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 #date format the api will be formatting the datetime values
API_DATE_FORMAT = '%Y-%m-%d' API_DATE_FORMAT = '%Y-%m-%d'
API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT) API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT)
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
-e git+https://github.com/jazkarta/edx-jsme.git@c5bfa5d361d6685d8c643838fc0055c25f8b7999#egg=edx-jsme -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 -e git+https://github.com/pmitros/django-pyfs.git@d175715e0fe3367ec0f1ee429c242d603f6e8b10#egg=djpyfs
git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas 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: # Our libraries:
-e git+https://github.com/edx/XBlock.git@aed464a0e2f7478e93157150ac04133a745f5f46#egg=XBlock -e git+https://github.com/edx/XBlock.git@aed464a0e2f7478e93157150ac04133a745f5f46#egg=XBlock
......
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