From b124485ed68ee5840d58cff986b09a513e4b5799 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Thu, 9 Apr 2015 16:44:23 -0400 Subject: [PATCH] MA-611, MA-613 Mobile Push Notification Studio Backend; Integration with Parse --- .../contentstore/course_info_model.py | 12 +++- .../contentstore/push_notification.py | 62 +++++++++++++++++++ cms/djangoapps/contentstore/tasks.py | 10 +++ cms/djangoapps/contentstore/views/course.py | 4 +- .../views/tests/test_course_updates.py | 42 +++++++++++-- cms/envs/aws.py | 5 ++ cms/static/js/spec/views/course_info_spec.js | 0 requirements/edx/github.txt | 1 + 8 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 cms/djangoapps/contentstore/push_notification.py create mode 100644 cms/static/js/spec/views/course_info_spec.js diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py index 692f7a9dd4..7ba75ff215 100644 --- a/cms/djangoapps/contentstore/course_info_model.py +++ b/cms/djangoapps/contentstore/course_info_model.py @@ -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) diff --git a/cms/djangoapps/contentstore/push_notification.py b/cms/djangoapps/contentstore/push_notification.py new file mode 100644 index 0000000000..d6e8f65737 --- /dev/null +++ b/cms/djangoapps/contentstore/push_notification.py @@ -0,0 +1,62 @@ +""" +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) diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 9b3c24900c..b67600e238 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -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) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index d205457faa..916435fa05 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -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: diff --git a/cms/djangoapps/contentstore/views/tests/test_course_updates.py b/cms/djangoapps/contentstore/views/tests/test_course_updates.py index a45c7b1db5..cb6a725a6c 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_updates.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_updates.py @@ -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) diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 3eb5dc99b8..319ed99e46 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -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) diff --git a/cms/static/js/spec/views/course_info_spec.js b/cms/static/js/spec/views/course_info_spec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 5ae2f11002..4356c9f624 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -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