Commit 9dfb68c2 by Nimisha Asthagiri

Merge pull request #7676 from edx/mobile/notifications-backend

Mobile/notifications
parents 2fed57a0 11044ebd
......@@ -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)
......@@ -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)
......
......@@ -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)
......
# -*- 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
......@@ -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."""
"""
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):
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)
......@@ -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())
......@@ -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:
......
......@@ -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)
......@@ -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)
......
......@@ -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')
......
......@@ -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();
};
......
......@@ -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;
......
......@@ -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({
......
......@@ -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");
}
}
}
});
......
......@@ -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>
......
......@@ -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>
......@@ -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
......@@ -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)
)
......@@ -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='_'),
}
......
......@@ -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)
......
......@@ -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.
......
......@@ -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
......
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