From a87c881024aaf2416b695dd23d2e9a2404a53d60 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Thu, 9 Apr 2015 15:13:43 -0400 Subject: [PATCH 1/4] MA-612 Mobile Push Notification Studio Django admin setting --- cms/djangoapps/contentstore/admin.py | 3 +- .../0003_auto__add_pushnotificationconfig.py | 80 +++++++++++++++++++ cms/djangoapps/contentstore/models.py | 4 + cms/djangoapps/contentstore/tests/tests.py | 14 ++++ common/djangoapps/config_models/models.py | 5 ++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 cms/djangoapps/contentstore/migrations/0003_auto__add_pushnotificationconfig.py diff --git a/cms/djangoapps/contentstore/admin.py b/cms/djangoapps/contentstore/admin.py index 6fdb475b37..21904959f8 100644 --- a/cms/djangoapps/contentstore/admin.py +++ b/cms/djangoapps/contentstore/admin.py @@ -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) diff --git a/cms/djangoapps/contentstore/migrations/0003_auto__add_pushnotificationconfig.py b/cms/djangoapps/contentstore/migrations/0003_auto__add_pushnotificationconfig.py new file mode 100644 index 0000000000..7963916271 --- /dev/null +++ b/cms/djangoapps/contentstore/migrations/0003_auto__add_pushnotificationconfig.py @@ -0,0 +1,80 @@ +# -*- 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 diff --git a/cms/djangoapps/contentstore/models.py b/cms/djangoapps/contentstore/models.py index d2a7973892..d2112bd9f1 100644 --- a/cms/djangoapps/contentstore/models.py +++ b/cms/djangoapps/contentstore/models.py @@ -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.""" diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 350ef47f17..710351ae4c 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -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()) diff --git a/common/djangoapps/config_models/models.py b/common/djangoapps/config_models/models.py index 3c1d5d6061..effde6e510 100644 --- a/common/djangoapps/config_models/models.py +++ b/common/djangoapps/config_models/models.py @@ -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 From c2a1870fb5ed68c10021f1b34a05fb1a2c416fb4 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Mon, 13 Apr 2015 20:46:46 -0400 Subject: [PATCH 2/4] Clean ID of course_module for push notification channel name. Update Mobile API to include channel_id for course. --- common/lib/xmodule/xmodule/course_module.py | 10 ++++++++++ lms/djangoapps/mobile_api/users/serializers.py | 1 + lms/djangoapps/mobile_api/users/tests.py | 1 + lms/djangoapps/mobile_api/users/views.py | 1 + 4 files changed, 13 insertions(+) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index e304606595..7fa3b50780 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -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) + ) diff --git a/lms/djangoapps/mobile_api/users/serializers.py b/lms/djangoapps/mobile_api/users/serializers.py index 9b12c054bf..e745d95de3 100644 --- a/lms/djangoapps/mobile_api/users/serializers.py +++ b/lms/djangoapps/mobile_api/users/serializers.py @@ -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='_'), } diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index 1095e52a07..c5774fd593 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -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) diff --git a/lms/djangoapps/mobile_api/users/views.py b/lms/djangoapps/mobile_api/users/views.py index 54f0089038..fa1e6c68ee 100644 --- a/lms/djangoapps/mobile_api/users/views.py +++ b/lms/djangoapps/mobile_api/users/views.py @@ -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. From b124485ed68ee5840d58cff986b09a513e4b5799 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Thu, 9 Apr 2015 16:44:23 -0400 Subject: [PATCH 3/4] 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 From 11044ebdf26e4f159a001d6ea611524b6b4e3b07 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Fri, 10 Apr 2015 17:16:09 -0400 Subject: [PATCH 4/4] MA-614 Mobile Push Notification Studio UI --- .../contentstore/features/course-updates.py | 22 ++++----- .../coffee/spec/views/course_info_spec.coffee | 48 +++++++++++++++++-- cms/static/js/factories/course_info.js | 5 +- cms/static/js/models/course_update.js | 4 +- cms/static/js/spec/views/course_info_spec.js | 0 cms/static/js/views/course_info_edit.js | 3 +- cms/static/js/views/course_info_update.js | 41 +++++++++++----- cms/templates/course_info.html | 8 +++- .../js/course_info_update.underscore | 20 +++++--- 9 files changed, 110 insertions(+), 41 deletions(-) delete mode 100644 cms/static/js/spec/views/course_info_spec.js diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index 6a39a29fee..afc02aa424 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -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) diff --git a/cms/static/coffee/spec/views/course_info_spec.coffee b/cms/static/coffee/spec/views/course_info_spec.coffee index e9846b80d7..8bf4e41f36 100644 --- a/cms/static/coffee/spec/views/course_info_spec.coffee +++ b/cms/static/coffee/spec/views/course_info_spec.coffee @@ -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($("