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
Show whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
356 additions
and
49 deletions
+356
-49
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
+12
-6
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
...
@@ -5,6 +5,7 @@ Admin site bindings for contentstore
from
django.contrib
import
admin
from
django.contrib
import
admin
from
config_models.admin
import
ConfigurationModelAdmin
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
(
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
...
@@ -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
)
...
...
cms/djangoapps/contentstore/features/course-updates.py
View file @
9dfb68c2
...
@@ -45,37 +45,37 @@ def check_no_update(_step, text):
...
@@ -45,37 +45,37 @@ def check_no_update(_step, text):
@step
(
u'I modify the text to "([^"]*)"$'
)
@step
(
u'I modify the text to "([^"]*)"$'
)
def
modify_update
(
_step
,
text
):
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
)
world
.
css_click
(
button_css
)
change_text
(
text
)
change_text
(
text
)
@step
(
u'I change the update from "([^"]*)" to "([^"]*)"$'
)
@step
(
u'I change the update from "([^"]*)" to "([^"]*)"$'
)
def
change_existing_update
(
_step
,
before
,
after
):
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 "([^"]*)"$'
)
@step
(
u'I change the handout from "([^"]*)" to "([^"]*)"$'
)
def
change_existing_handout
(
_step
,
before
,
after
):
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$'
)
@step
(
u'I delete the update$'
)
def
click_button
(
_step
):
def
click_button
(
_step
):
button_css
=
'div.post-preview
a
.delete-button'
button_css
=
'div.post-preview .delete-button'
world
.
css_click
(
button_css
)
world
.
css_click
(
button_css
)
@step
(
u'I edit the date to "([^"]*)"$'
)
@step
(
u'I edit the date to "([^"]*)"$'
)
def
change_date
(
_step
,
new_date
):
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
)
world
.
css_click
(
button_css
)
date_css
=
'input.date'
date_css
=
'input.date'
date
=
world
.
css_find
(
date_css
)
date
=
world
.
css_find
(
date_css
)
for
i
in
range
(
len
(
date
.
value
)):
for
i
in
range
(
len
(
date
.
value
)):
date
.
_element
.
send_keys
(
Keys
.
END
,
Keys
.
BACK_SPACE
)
date
.
_element
.
send_keys
(
Keys
.
END
,
Keys
.
BACK_SPACE
)
date
.
_element
.
send_keys
(
new_date
)
date
.
_element
.
send_keys
(
new_date
)
save_css
=
'
a
.save-button'
save_css
=
'.save-button'
world
.
css_click
(
save_css
)
world
.
css_click
(
save_css
)
...
@@ -87,7 +87,7 @@ def check_date(_step, date):
...
@@ -87,7 +87,7 @@ def check_date(_step, date):
@step
(
u'I modify the handout to "([^"]*)"$'
)
@step
(
u'I modify the handout to "([^"]*)"$'
)
def
edit_handouts
(
_step
,
text
):
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
)
world
.
css_click
(
edit_css
)
change_text
(
text
)
change_text
(
text
)
...
@@ -114,7 +114,7 @@ def check_handout_error(_step):
...
@@ -114,7 +114,7 @@ def check_handout_error(_step):
@step
(
u'I see handout save button disabled'
)
@step
(
u'I see handout save button disabled'
)
def
check_handout_error
(
_step
):
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'
)
assert
world
.
css_has_class
(
handout_save_button
,
'is-disabled'
)
...
@@ -125,19 +125,19 @@ def edit_handouts(_step, text):
...
@@ -125,19 +125,19 @@ def edit_handouts(_step, text):
@step
(
u'I see handout save button re-enabled'
)
@step
(
u'I see handout save button re-enabled'
)
def
check_handout_error
(
_step
):
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'
)
assert
not
world
.
css_has_class
(
handout_save_button
,
'is-disabled'
)
@step
(
u'I save handout edit'
)
@step
(
u'I save handout edit'
)
def
check_handout_error
(
_step
):
def
check_handout_error
(
_step
):
save_css
=
'
a
.save-button'
save_css
=
'.save-button'
world
.
css_click
(
save_css
)
world
.
css_click
(
save_css
)
def
change_text
(
text
):
def
change_text
(
text
):
type_in_codemirror
(
0
,
text
)
type_in_codemirror
(
0
,
text
)
save_css
=
'
a
.save-button'
save_css
=
'.save-button'
world
.
css_click
(
save_css
)
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):
...
@@ -19,3 +19,7 @@ class VideoUploadConfig(ConfigurationModel):
def
get_profile_whitelist
(
cls
):
def
get_profile_whitelist
(
cls
):
"""Get the list of profiles to include in the encoding download"""
"""Get the list of profiles to include in the encoding download"""
return
[
profile
for
profile
in
cls
.
current
()
.
profile_whitelist
.
split
(
","
)
if
profile
]
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):
...
@@ -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
)
cms/djangoapps/contentstore/tests/tests.py
View file @
9dfb68c2
...
@@ -6,12 +6,14 @@ import mock
...
@@ -6,12 +6,14 @@ import mock
import
unittest
import
unittest
from
ddt
import
ddt
,
data
,
unpack
from
ddt
import
ddt
,
data
,
unpack
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
django.core.cache
import
cache
from
django.core.cache
import
cache
from
django.conf
import
settings
from
django.conf
import
settings
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
contentstore.models
import
PushNotificationConfig
from
contentstore.tests.utils
import
parse_json
,
user
,
registration
,
AjaxEnabledTestClient
from
contentstore.tests.utils
import
parse_json
,
user
,
registration
,
AjaxEnabledTestClient
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
contentstore.tests.test_course_settings
import
CourseTestCase
from
contentstore.tests.test_course_settings
import
CourseTestCase
...
@@ -349,3 +351,15 @@ class CourseKeyVerificationTestCase(CourseTestCase):
...
@@ -349,3 +351,15 @@ class CourseKeyVerificationTestCase(CourseTestCase):
)
)
resp
=
self
.
client
.
get_html
(
url
)
resp
=
self
.
client
.
get_html
(
url
)
self
.
assertEqual
(
resp
.
status_code
,
status_code
)
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 (
...
@@ -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
:
...
...
cms/djangoapps/contentstore/views/tests/test_course_updates.py
View file @
9dfb68c2
...
@@ -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
)
cms/envs/aws.py
View file @
9dfb68c2
...
@@ -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
)
...
...
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
...
@@ -22,7 +22,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
delete
window
.
analytics
delete
window
.
analytics
delete
window
.
course_location_analytics
delete
window
.
course_location_analytics
describe
"Course Updates"
,
->
describe
"Course Updates
without Push notification
"
,
->
courseInfoTemplate
=
readFixtures
(
'course_info_update.underscore'
)
courseInfoTemplate
=
readFixtures
(
'course_info_update.underscore'
)
beforeEach
->
beforeEach
->
...
@@ -100,7 +100,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
...
@@ -100,7 +100,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
else
else
modalCover
.
click
()
modalCover
.
click
()
it
"does
not rewrite links
on save"
,
->
it
"does
send expected data
on save"
,
->
requests
=
AjaxHelpers
[
"requests"
](
this
)
requests
=
AjaxHelpers
[
"requests"
](
this
)
# Create a new update, verifying that the model is created
# 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
...
@@ -116,9 +116,12 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@
courseInfoEdit
.
$el
.
find
(
'.save-button'
).
click
()
@
courseInfoEdit
.
$el
.
find
(
'.save-button'
).
click
()
expect
(
model
.
save
).
toHaveBeenCalled
()
expect
(
model
.
save
).
toHaveBeenCalled
()
# Verify content sent to server does not have rewritten links.
# Verify push_notification_selected is set to false.
contentSaved
=
JSON
.
parse
(
requests
[
requests
.
length
-
1
].
requestBody
).
content
requestSent
=
JSON
.
parse
(
requests
[
requests
.
length
-
1
].
requestBody
)
expect
(
contentSaved
).
toEqual
(
'/static/image.jpg'
)
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"
,
->
it
"does rewrite links for preview"
,
->
# Create a new update.
# Create a new update.
...
@@ -147,6 +150,41 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
...
@@ -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"
,
->
it
"does not remove existing course info on click outside modal"
,
->
@
cancelExistingCourseInfo
(
false
)
@
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"
,
->
describe
"Course Handouts"
,
->
handoutsTemplate
=
readFixtures
(
'course_info_handouts.underscore'
)
handoutsTemplate
=
readFixtures
(
'course_info_handouts.underscore'
)
...
...
cms/static/js/factories/course_info.js
View file @
9dfb68c2
...
@@ -3,7 +3,7 @@ define([
...
@@ -3,7 +3,7 @@ define([
'js/models/course_info'
,
'js/views/course_info_edit'
'js/models/course_info'
,
'js/views/course_info_edit'
],
function
(
$
,
CourseUpdateCollection
,
ModuleInfoModel
,
CourseInfoModel
,
CourseInfoEditView
)
{
],
function
(
$
,
CourseUpdateCollection
,
ModuleInfoModel
,
CourseInfoModel
,
CourseInfoEditView
)
{
'use strict'
;
'use strict'
;
return
function
(
updatesUrl
,
handoutsLocator
,
baseAssetUrl
)
{
return
function
(
updatesUrl
,
handoutsLocator
,
baseAssetUrl
,
push_notification_enabled
)
{
var
course_updates
=
new
CourseUpdateCollection
(),
var
course_updates
=
new
CourseUpdateCollection
(),
course_handouts
,
editor
;
course_handouts
,
editor
;
...
@@ -18,7 +18,8 @@ define([
...
@@ -18,7 +18,8 @@ define([
updates
:
course_updates
,
updates
:
course_updates
,
base_asset_url
:
baseAssetUrl
,
base_asset_url
:
baseAssetUrl
,
handouts
:
course_handouts
handouts
:
course_handouts
})
}),
push_notification_enabled
:
push_notification_enabled
});
});
editor
.
render
();
editor
.
render
();
};
};
...
...
cms/static/js/models/course_update.js
View file @
9dfb68c2
...
@@ -3,7 +3,9 @@ define(["backbone", "jquery", "jquery.ui"], function(Backbone, $) {
...
@@ -3,7 +3,9 @@ define(["backbone", "jquery", "jquery.ui"], function(Backbone, $) {
var
CourseUpdate
=
Backbone
.
Model
.
extend
({
var
CourseUpdate
=
Backbone
.
Model
.
extend
({
defaults
:
{
defaults
:
{
"date"
:
$
.
datepicker
.
formatDate
(
'MM d, yy'
,
new
Date
()),
"date"
:
$
.
datepicker
.
formatDate
(
'MM d, yy'
,
new
Date
()),
"content"
:
""
"content"
:
""
,
"push_notification_enabled"
:
false
,
"push_notification_selected"
:
false
}
}
});
});
return
CourseUpdate
;
return
CourseUpdate
;
...
...
cms/static/js/views/course_info_edit.js
View file @
9dfb68c2
...
@@ -15,7 +15,8 @@ var CourseInfoEdit = BaseView.extend({
...
@@ -15,7 +15,8 @@ var CourseInfoEdit = BaseView.extend({
new
CourseInfoUpdateView
({
new
CourseInfoUpdateView
({
el
:
$
(
'body.updates'
),
el
:
$
(
'body.updates'
),
collection
:
this
.
model
.
get
(
'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
({
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",
...
@@ -3,6 +3,7 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
function
(
BaseView
,
CodeMirror
,
CourseUpdateModel
,
PromptView
,
NotificationView
,
CourseInfoHelper
,
ModalUtils
)
{
function
(
BaseView
,
CodeMirror
,
CourseUpdateModel
,
PromptView
,
NotificationView
,
CourseInfoHelper
,
ModalUtils
)
{
var
CourseInfoUpdateView
=
BaseView
.
extend
({
var
CourseInfoUpdateView
=
BaseView
.
extend
({
// collection is CourseUpdateCollection
// collection is CourseUpdateCollection
events
:
{
events
:
{
"click .new-update-button"
:
"onNew"
,
"click .new-update-button"
:
"onNew"
,
...
@@ -29,7 +30,8 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
...
@@ -29,7 +30,8 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
try
{
try
{
CourseInfoHelper
.
changeContentToPreview
(
CourseInfoHelper
.
changeContentToPreview
(
update
,
'content'
,
self
.
options
[
'base_asset_url'
]);
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
);
$
(
updateEle
).
append
(
newEle
);
}
catch
(
e
)
{
}
catch
(
e
)
{
// ignore
// ignore
...
@@ -47,7 +49,12 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
...
@@ -47,7 +49,12 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
var
newModel
=
new
CourseUpdateModel
();
var
newModel
=
new
CourseUpdateModel
();
this
.
collection
.
add
(
newModel
,
{
at
:
0
});
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"
);
var
updateEle
=
this
.
$el
.
find
(
"#course-update-list"
);
$
(
updateEle
).
prepend
(
$newForm
);
$
(
updateEle
).
prepend
(
$newForm
);
...
@@ -74,7 +81,11 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
...
@@ -74,7 +81,11 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
onSave
:
function
(
event
)
{
onSave
:
function
(
event
)
{
event
.
preventDefault
();
event
.
preventDefault
();
var
targetModel
=
this
.
eventModel
(
event
);
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
// push change to display, hide the editor, submit the change
var
saving
=
new
NotificationView
.
Mini
({
var
saving
=
new
NotificationView
.
Mini
({
title
:
gettext
(
'Saving'
)
title
:
gettext
(
'Saving'
)
...
@@ -196,6 +207,11 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
...
@@ -196,6 +207,11 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
}
}
this
.
$currentPost
.
find
(
'form'
).
hide
();
this
.
$currentPost
.
find
(
'form'
).
hide
();
this
.
$currentPost
.
find
(
'.CodeMirror'
).
remove
();
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
);
ModalUtils
.
hideModalCover
(
this
.
$modalCover
);
...
@@ -222,16 +238,15 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
...
@@ -222,16 +238,15 @@ define(["js/views/baseview", "codemirror", "js/models/course_update",
if
(
li
)
return
$
(
li
).
find
(
".date"
).
first
();
if
(
li
)
return
$
(
li
).
find
(
".date"
).
first
();
},
},
contentEntry
:
function
(
event
)
{
push_notification_selected
:
function
(
event
)
{
return
$
(
event
.
currentTarget
).
closest
(
"li"
).
find
(
".new-update-content"
).
first
();
var
push_notification_checkbox
;
},
var
li
=
$
(
event
.
currentTarget
).
closest
(
"li"
);
if
(
li
)
{
dateDisplay
:
function
(
event
)
{
push_notification_checkbox
=
li
.
find
(
".new-update-push-notification .toggle-checkbox"
);
return
$
(
event
.
currentTarget
).
closest
(
"li"
).
find
(
"#date-display"
).
first
();
if
(
push_notification_checkbox
)
{
},
return
push_notification_checkbox
.
is
(
":checked"
);
}
contentDisplay
:
function
(
event
)
{
}
return
$
(
event
.
currentTarget
).
closest
(
"li"
).
find
(
".update-contents"
).
first
();
}
}
});
});
...
...
cms/templates/course_info.html
View file @
9dfb68c2
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
<
%
inherit
file=
"base.html"
/>
<
%
inherit
file=
"base.html"
/>
<
%
def
name=
"online_help_token()"
><
%
return
"
updates
"
%
></
%
def>
<
%
def
name=
"online_help_token()"
><
%
return
"
updates
"
%
></
%
def>
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
<
%!
import
json
%
>
<!-- TODO decode course # from context_course into title -->
<!-- TODO decode course # from context_course into title -->
<
%
block
name=
"title"
>
${_("Course Updates")}
</
%
block>
<
%
block
name=
"title"
>
${_("Course Updates")}
</
%
block>
...
@@ -21,7 +22,12 @@
...
@@ -21,7 +22,12 @@
<
%
block
name=
"requirejs"
>
<
%
block
name=
"requirejs"
>
require(["js/factories/course_info"], function(CourseInfoFactory) {
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>
</
%
block>
...
...
cms/templates/js/course_info_update.underscore
View file @
9dfb68c2
...
@@ -2,23 +2,29 @@
...
@@ -2,23 +2,29 @@
<!-- FIXME what style should we use for initially hidden? --> <!-- TODO decide whether this should use codemirror -->
<!-- FIXME what style should we use for initially hidden? --> <!-- TODO decide whether this should use codemirror -->
<form class="new-update-form">
<form class="new-update-form">
<div class="row">
<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 -->
<!-- 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>
<div class="row">
<div class="row">
<textarea class="new-update-content text-editor"><%= updateModel.get('content') %></textarea>
<textarea class="new-update-content text-editor"><%= updateModel.get('content') %></textarea>
</div>
</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">
<div class="row">
<!-- cid rather than id b/c new ones have cid's not id's -->
<!-- cid rather than id b/c new ones have cid's not id's -->
<a href="#" class="save-button" name="<%= updateModel.cid %>">Save</a
>
<button class="save-button" name="<%= updateModel.cid %>"><%= gettext('Post') %></button
>
<a href="#" class="cancel-button" name="<%= updateModel.cid %>">Cancel</a
>
<button class="cancel-button" name="<%= updateModel.cid %>"><%= gettext('Cancel') %></button
>
</div>
</div>
</form>
</form>
<div class="post-preview">
<div class="post-preview">
<div class="post-actions">
<div class="post-actions">
<a href="#" class="edit-button" name="<%- updateModel.cid %>"><span class="edit-icon"></span>Edit</a
>
<button class="edit-button" name="<%= updateModel.cid %>"><span class="edit-icon"></span>Edit</button
>
<a href="#" class="delete-button" name="<%- updateModel.cid %>"><span class="delete-icon"></span>Delete</a
>
<button class="delete-button" name="<%= updateModel.cid %>"><span class="delete-icon"></span>Delete</button
>
</div>
</div>
<h2>
<h2>
<span class="calendar-icon"></span><span class="date-display"><%=
<span class="calendar-icon"></span><span class="date-display"><%=
...
...
common/djangoapps/config_models/models.py
View file @
9dfb68c2
...
@@ -60,3 +60,8 @@ class ConfigurationModel(models.Model):
...
@@ -60,3 +60,8 @@ class ConfigurationModel(models.Model):
cache
.
set
(
cls
.
cache_key_name
(),
current
,
cls
.
cache_timeout
)
cache
.
set
(
cls
.
cache_key_name
(),
current
,
cls
.
cache_timeout
)
return
current
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
...
@@ -10,6 +10,7 @@ import requests
from
datetime
import
datetime
from
datetime
import
datetime
import
dateutil.parser
import
dateutil.parser
from
lazy
import
lazy
from
lazy
import
lazy
from
base64
import
b32encode
from
xmodule.exceptions
import
UndefinedContext
from
xmodule.exceptions
import
UndefinedContext
from
xmodule.seq_module
import
SequenceDescriptor
,
SequenceModule
from
xmodule.seq_module
import
SequenceDescriptor
,
SequenceModule
...
@@ -1398,3 +1399,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
...
@@ -1398,3 +1399,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
self
.
video_upload_pipeline
is
not
None
and
self
.
video_upload_pipeline
is
not
None
and
'course_video_upload_token'
in
self
.
video_upload_pipeline
'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):
...
@@ -60,6 +60,7 @@ class CourseField(serializers.RelatedField):
"course_updates"
:
course_updates_url
,
"course_updates"
:
course_updates_url
,
"course_handouts"
:
course_handouts_url
,
"course_handouts"
:
course_handouts_url
,
"course_about"
:
course_about_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
...
@@ -60,6 +60,7 @@ class TestUserEnrollmentApi(MobileAPITestCase, MobileAuthUserTestMixin, MobileEn
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertTrue
(
'course_handouts'
in
found_course
)
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
found_course
[
'id'
],
unicode
(
self
.
course
.
id
))
self
.
assertEqual
(
courses
[
0
][
'mode'
],
'honor'
)
self
.
assertEqual
(
courses
[
0
][
'mode'
],
'honor'
)
self
.
assertEqual
(
courses
[
0
][
'course'
][
'subscription_id'
],
self
.
course
.
clean_id
(
padding_char
=
'_'
))
def
verify_failure
(
self
,
response
):
def
verify_failure
(
self
,
response
):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
lms/djangoapps/mobile_api/users/views.py
View file @
9dfb68c2
...
@@ -221,6 +221,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
...
@@ -221,6 +221,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
* video_outline: The URI to get the list of all vides the user can
* video_outline: The URI to get the list of all vides the user can
access in the course.
access in the course.
* id: The unique ID of 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.
* latest_updates: Reserved for future use.
* end: The end date of the course.
* end: The end date of the course.
* name: The name of the course.
* name: The name of the course.
...
...
requirements/edx/github.txt
View file @
9dfb68c2
...
@@ -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
...
...
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