Basic notifications handling.
LMS-11163
This commit is contained in:
committed by
Julia Hansbrough
parent
a0b4f8b6c6
commit
69f900dd90
@@ -57,8 +57,7 @@ def initialize_permissions(course_key, user_who_created_course):
|
||||
|
||||
def remove_all_instructors(course_key):
|
||||
"""
|
||||
Removes given user as instructor and staff to the given course,
|
||||
after verifying that the requesting_user has permission to do so.
|
||||
Removes all instructor and staff users from the given course.
|
||||
"""
|
||||
staff_role = CourseStaffRole(course_key)
|
||||
staff_role.remove_users(*staff_role.users_with_role())
|
||||
|
||||
@@ -38,6 +38,7 @@ from contentstore.utils import (
|
||||
reverse_course_url,
|
||||
reverse_usage_url,
|
||||
reverse_url,
|
||||
remove_all_instructors,
|
||||
)
|
||||
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
@@ -62,6 +63,7 @@ from student.roles import (
|
||||
)
|
||||
from student import auth
|
||||
from course_action_state.models import CourseRerunState, CourseRerunUIStateManager
|
||||
from course_action_state.managers import CourseActionStateItemNotFoundError
|
||||
|
||||
from microsite_configuration import microsite
|
||||
|
||||
@@ -70,6 +72,7 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
|
||||
'settings_handler',
|
||||
'grading_handler',
|
||||
'advanced_settings_handler',
|
||||
'course_notifications_handler',
|
||||
'textbooks_list_handler', 'textbooks_detail_handler',
|
||||
'group_configurations_list_handler', 'group_configurations_detail_handler']
|
||||
|
||||
@@ -95,6 +98,90 @@ def _get_course_module(course_key, user, depth=0):
|
||||
return course_module
|
||||
|
||||
|
||||
@login_required
|
||||
def course_notifications_handler(request, course_key_string=None, action_state_id=None):
|
||||
"""
|
||||
Handle incoming requests for notifications in a RESTful way.
|
||||
|
||||
course_key_string and action_state_id must both be set; else a HttpBadResponseRequest is returned.
|
||||
|
||||
For each of these operations, the requesting user must have access to the course;
|
||||
else a PermissionDenied error is returned.
|
||||
|
||||
GET
|
||||
json: return json representing information about the notification (action, state, etc)
|
||||
DELETE
|
||||
json: return json repressing success or failure of dismissal/deletion of the notification
|
||||
PUT
|
||||
Raises a NotImplementedError.
|
||||
POST
|
||||
Raises a NotImplementedError.
|
||||
"""
|
||||
# ensure that we have a course and an action state
|
||||
if not course_key_string or not action_state_id:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
response_format = request.REQUEST.get('format', 'html')
|
||||
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
|
||||
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
if not has_course_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
if request.method == 'GET':
|
||||
return _course_notifications_json_get(action_state_id)
|
||||
elif request.method == 'DELETE':
|
||||
# we assume any delete requests dismiss actions from the UI
|
||||
return _dismiss_notification(request, action_state_id)
|
||||
elif request.method == 'PUT':
|
||||
raise NotImplementedError()
|
||||
elif request.method == 'POST':
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
def _course_notifications_json_get(course_action_state_id):
|
||||
"""
|
||||
Return the action and the action state for the given id
|
||||
"""
|
||||
try:
|
||||
action_state = CourseRerunState.objects.find_first(id=course_action_state_id)
|
||||
except CourseActionStateItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
action_state_info = {
|
||||
'action': action_state.action,
|
||||
'state': action_state.state,
|
||||
'should_display': action_state.should_display
|
||||
}
|
||||
return JsonResponse(action_state_info)
|
||||
|
||||
|
||||
def _dismiss_notification(request, course_action_state_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Update the display of the course notification
|
||||
"""
|
||||
try:
|
||||
action_state = CourseRerunState.objects.find_first(id=course_action_state_id)
|
||||
|
||||
except CourseActionStateItemNotFoundError:
|
||||
# Can't dismiss a notification that doesn't exist in the first place
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if action_state.state == CourseRerunUIStateManager.State.FAILED:
|
||||
# We remove all permissions for this course key at this time, since
|
||||
# no further access is required to a course that failed to be created.
|
||||
remove_all_instructors(action_state.course_key)
|
||||
|
||||
# The CourseRerunState is no longer needed by the UI; delete
|
||||
action_state.delete()
|
||||
|
||||
return JsonResponse({'success': True})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@login_required
|
||||
def course_handler(request, course_key_string=None):
|
||||
@@ -297,6 +384,11 @@ def course_index(request, course_key):
|
||||
lms_link = get_lms_link_for_item(course_module.location)
|
||||
sections = course_module.get_children()
|
||||
|
||||
try:
|
||||
current_action = CourseRerunState.objects.find_first(course_key=course_key, should_display=True)
|
||||
except (ItemNotFoundError, CourseActionStateItemNotFoundError):
|
||||
current_action = None
|
||||
|
||||
return render_to_response('overview.html', {
|
||||
'context_course': course_module,
|
||||
'lms_link': lms_link,
|
||||
@@ -307,7 +399,8 @@ def course_index(request, course_key):
|
||||
'new_section_category': 'chapter',
|
||||
'new_subsection_category': 'sequential',
|
||||
'new_unit_category': 'vertical',
|
||||
'category': 'vertical'
|
||||
'category': 'vertical',
|
||||
'rerun_notification_id': current_action.id if current_action else None,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ import json
|
||||
import lxml
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_course_url
|
||||
from contentstore.utils import reverse_course_url, add_instructor
|
||||
from contentstore.views.access import has_course_access
|
||||
from course_action_state.models import CourseRerunState
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from opaque_keys.edx.locator import Locator
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from student.tests.factories import UserFactory
|
||||
from course_action_state.managers import CourseRerunUIStateManager
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -115,6 +119,63 @@ class TestCourseIndex(CourseTestCase):
|
||||
# Finally, validate the entire response for consistency
|
||||
self.assert_correct_json_response(json_response)
|
||||
|
||||
def test_notifications_handler_get(self):
|
||||
state = CourseRerunUIStateManager.State.FAILED
|
||||
action = CourseRerunUIStateManager.ACTION
|
||||
should_display = True
|
||||
|
||||
# try when no notification exists
|
||||
notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
|
||||
'action_state_id': 1,
|
||||
})
|
||||
|
||||
resp = self.client.get(notification_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
# verify that we get an empty dict out
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
# create a test notification
|
||||
rerun_state = CourseRerunState.objects.update_state(course_key=self.course.id, new_state=state, allow_not_found=True)
|
||||
CourseRerunState.objects.update_should_display(entry_id=rerun_state.id, user=UserFactory(), should_display=should_display)
|
||||
|
||||
# try to get information on this notification
|
||||
notification_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
|
||||
'action_state_id': rerun_state.id,
|
||||
})
|
||||
resp = self.client.get(notification_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
json_response = json.loads(resp.content)
|
||||
|
||||
self.assertEquals(json_response['state'], state)
|
||||
self.assertEquals(json_response['action'], action)
|
||||
self.assertEquals(json_response['should_display'], should_display)
|
||||
|
||||
def test_notifications_handler_dismiss(self):
|
||||
state = CourseRerunUIStateManager.State.FAILED
|
||||
should_display = True
|
||||
rerun_course_key = CourseLocator(org='testx', course='test_course', run='test_run')
|
||||
|
||||
# add an instructor to this course
|
||||
user2 = UserFactory()
|
||||
add_instructor(rerun_course_key, self.user, user2)
|
||||
|
||||
# create a test notification
|
||||
rerun_state = CourseRerunState.objects.update_state(course_key=rerun_course_key, new_state=state, allow_not_found=True)
|
||||
CourseRerunState.objects.update_should_display(entry_id=rerun_state.id, user=user2, should_display=should_display)
|
||||
|
||||
# try to get information on this notification
|
||||
notification_dismiss_url = reverse_course_url('course_notifications_handler', self.course.id, kwargs={
|
||||
'action_state_id': rerun_state.id,
|
||||
})
|
||||
resp = self.client.delete(notification_dismiss_url)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
with self.assertRaises(CourseRerunState.DoesNotExist):
|
||||
# delete nofications that are dismissed
|
||||
CourseRerunState.objects.get(id=rerun_state.id)
|
||||
|
||||
self.assertFalse(has_course_access(user2, rerun_course_key))
|
||||
|
||||
def assert_correct_json_response(self, json_response):
|
||||
"""
|
||||
Asserts that the JSON response is syntactically consistent
|
||||
|
||||
@@ -73,6 +73,7 @@ urlpatterns += patterns(
|
||||
'course_info_update_handler'
|
||||
),
|
||||
url(r'^course/{}?$'.format(settings.COURSE_KEY_PATTERN), 'course_handler', name='course_handler'),
|
||||
url(r'^course_notifications/{}/(?P<action_state_id>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'course_notifications_handler'),
|
||||
url(r'^subsection/{}$'.format(settings.USAGE_KEY_PATTERN), 'subsection_handler'),
|
||||
url(r'^unit/{}$'.format(settings.USAGE_KEY_PATTERN), 'unit_handler'),
|
||||
url(r'^container/{}$'.format(settings.USAGE_KEY_PATTERN), 'container_handler'),
|
||||
|
||||
@@ -96,7 +96,7 @@ class CourseActionUIStateManager(CourseActionStateManager):
|
||||
"""
|
||||
Updates the should_display field with the given value for the entry for the given id.
|
||||
"""
|
||||
self.update(id=entry_id, updated_user=user, should_display=should_display)
|
||||
return self.update(id=entry_id, updated_user=user, should_display=should_display)
|
||||
|
||||
|
||||
class CourseRerunUIStateManager(CourseActionUIStateManager):
|
||||
|
||||
Reference in New Issue
Block a user