From 69f900dd905a2875ac1243aeef52ff137fe463f4 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 30 Jul 2014 13:54:17 -0400 Subject: [PATCH] Basic notifications handling. LMS-11163 --- cms/djangoapps/contentstore/utils.py | 3 +- cms/djangoapps/contentstore/views/course.py | 95 ++++++++++++++++++- .../views/tests/test_course_index.py | 65 ++++++++++++- cms/urls.py | 1 + .../course_action_state/managers.py | 2 +- 5 files changed, 160 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 0f54190c15..54109d9243 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -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()) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 0f74fd41b2..5c89672f61 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -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, }) diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index ea31cb4f1d..cc82dc6fd5 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -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 diff --git a/cms/urls.py b/cms/urls.py index f351e037a7..badb012c2c 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -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\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'), diff --git a/common/djangoapps/course_action_state/managers.py b/common/djangoapps/course_action_state/managers.py index 12cda9a046..84ba239213 100644 --- a/common/djangoapps/course_action_state/managers.py +++ b/common/djangoapps/course_action_state/managers.py @@ -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):