diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index cf5c9cba88..bbfd5ba898 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -39,7 +39,7 @@ from course_modes.tests.factories import CourseModeFactory from courseware.model_data import set_score from courseware.module_render import toc_for_course from courseware.testutils import RenderXBlockTestMixin -from courseware.tests.factories import StudentModuleFactory +from courseware.tests.factories import StudentModuleFactory, GlobalStaffFactory from courseware.url_helpers import get_redirect_url from courseware.user_state_client import DjangoXBlockUserStateClient from courseware.views.index import render_accordion, CoursewareIndex @@ -196,7 +196,7 @@ class ViewsTestCase(ModuleStoreTestCase): def setUp(self): super(ViewsTestCase, self).setUp() - self.course = CourseFactory.create(display_name=u'teꜱᴛ course') + self.course = CourseFactory.create(display_name=u'teꜱᴛ course', run="Testing_course") self.chapter = ItemFactory.create( category='chapter', parent_location=self.course.location, @@ -323,6 +323,105 @@ class ViewsTestCase(ModuleStoreTestCase): self.assertNotIn('Problem 1', response.content) self.assertNotIn('Problem 2', response.content) + def _create_global_staff_user(self): + """ + Create global staff user and log them in + """ + self.global_staff = GlobalStaffFactory.create() # pylint: disable=attribute-defined-outside-init + self.client.login(username=self.global_staff.username, password='test') + + def _create_url_for_enroll_staff(self): + """ + creates the courseware url and enroll staff url + """ + # create the _next parameter + courseware_url = reverse( + 'courseware_section', + kwargs={ + 'course_id': unicode(self.course_key), + 'chapter': unicode(self.chapter.location.name), + 'section': unicode(self.section.location.name), + } + ) + # create the url for enroll_staff view + enroll_url = "{enroll_url}?next={courseware_url}".format( + enroll_url=reverse('enroll_staff', kwargs={'course_id': unicode(self.course.id)}), + courseware_url=courseware_url + ) + return courseware_url, enroll_url + + @ddt.data( + ({'enroll': "Enroll"}, True), + ({'dont_enroll': "Don't enroll"}, False)) + @ddt.unpack + def test_enroll_staff_redirection(self, data, enrollment): + """ + Verify unenrolled staff is redirected to correct url. + """ + self._create_global_staff_user() + courseware_url, enroll_url = self._create_url_for_enroll_staff() + response = self.client.post(enroll_url, data=data, follow=True) + self.assertEqual(response.status_code, 200) + + # we were redirected to our current location + self.assertIn(302, response.redirect_chain[0]) + self.assertEqual(len(response.redirect_chain), 1) + if enrollment: + self.assertRedirects(response, courseware_url) + else: + self.assertRedirects(response, '/courses/{}/about'.format(unicode(self.course_key))) + + def test_enroll_staff_with_invalid_data(self): + """ + If we try to post with an invalid data pattern, then we'll redirected to + course about page. + """ + self._create_global_staff_user() + __, enroll_url = self._create_url_for_enroll_staff() + response = self.client.post(enroll_url, data={'test': "test"}) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, '/courses/{}/about'.format(unicode(self.course_key))) + + def test_courseware_redirection(self): + """ + Tests that a global staff member is redirected to the staff enrollment page. + + Un-enrolled Staff user should be redirected to the staff enrollment page accessing courseware, + user chooses to enroll in the course. User is enrolled and redirected to the requested url. + + Scenario: + 1. Un-enrolled staff tries to access any course vertical (courseware url). + 2. User is redirected to the staff enrollment page. + 3. User chooses to enroll in the course. + 4. User is enrolled in the course and redirected to the requested courseware url. + """ + self._create_global_staff_user() + courseware_url, enroll_url = self._create_url_for_enroll_staff() + + # Accessing the courseware url in which not enrolled & redirected to staff enrollment page + response = self.client.get(courseware_url, follow=True) + self.assertEqual(response.status_code, 200) + self.assertIn(302, response.redirect_chain[0]) + self.assertEqual(len(response.redirect_chain), 1) + self.assertRedirects(response, enroll_url) + + # Accessing the enroll staff url and verify the correct url + response = self.client.get(enroll_url) + self.assertEqual(response.status_code, 200) + response_content = response.content + self.assertIn('Enroll', response_content) + self.assertIn("dont_enroll", response_content) + + # Post the valid data to enroll the staff in the course + response = self.client.post(enroll_url, data={'enroll': "Enroll"}, follow=True) + self.assertEqual(response.status_code, 200) + self.assertIn(302, response.redirect_chain[0]) + self.assertEqual(len(response.redirect_chain), 1) + self.assertRedirects(response, courseware_url) + + # Verify staff has been enrolled to the given course + self.assertTrue(CourseEnrollment.is_enrolled(self.global_staff, self.course.id)) + @unittest.skipUnless(settings.FEATURES.get('ENABLE_SHOPPING_CART'), "Shopping Cart not enabled in settings") @patch.dict(settings.FEATURES, {'ENABLE_PAID_COURSE_REGISTRATION': True}) def test_course_about_in_cart(self): diff --git a/lms/djangoapps/courseware/url_helpers.py b/lms/djangoapps/courseware/url_helpers.py index fcdf567dd2..9acd931a40 100644 --- a/lms/djangoapps/courseware/url_helpers.py +++ b/lms/djangoapps/courseware/url_helpers.py @@ -42,11 +42,23 @@ def get_redirect_url(course_key, usage_key): # Here we use the navigation_index from the position returned from # path_to_location - we can only navigate to the topmost vertical at the # moment - redirect_url = reverse( 'courseware_position', args=(unicode(course_key), chapter, section, navigation_index(position)) ) - redirect_url += "?{}".format(urlencode({'activate_block_id': unicode(final_target_id)})) return redirect_url + + +def get_redirect_url_for_global_staff(course_key, _next): + """ + Returns the redirect url for staff enrollment + + Args: + course_key(str): Course key string + _next(str): Redirect url of course component + """ + redirect_url = ("{url}?next={redirect}".format( + url=reverse('enroll_staff', args=[unicode(course_key)]), + redirect=_next)) + return redirect_url diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py index 49d5f49200..88de50285f 100644 --- a/lms/djangoapps/courseware/views/index.py +++ b/lms/djangoapps/courseware/views/index.py @@ -15,6 +15,8 @@ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie from django.views.generic import View from django.shortcuts import redirect + +from courseware.url_helpers import get_redirect_url_for_global_staff from edxmako.shortcuts import render_to_response, render_to_string import logging import newrelic.agent @@ -26,7 +28,9 @@ from opaque_keys.edx.keys import CourseKey from openedx.core.lib.gating import api as gating_api from openedx.core.djangoapps.user_api.preferences.api import get_user_preference from shoppingcart.models import CourseRegistrationCode +from student.models import CourseEnrollment from student.views import is_course_blocked +from student.roles import GlobalStaff from util.views import ensure_valid_course_key from xmodule.modulestore.django import modulestore from xmodule.x_module import STUDENT_VIEW @@ -89,6 +93,7 @@ class CoursewareIndex(View): self.section_url_name = section self.position = position self.chapter, self.section = None, None + self.url = request.path try: self._init_new_relic() @@ -221,6 +226,11 @@ class CoursewareIndex(View): self.effective_user, unicode(self.course.id) ) + user_is_global_staff = GlobalStaff().has_user(self.effective_user) + user_is_enrolled = CourseEnrollment.is_enrolled(self.effective_user, self.course_key) + if user_is_global_staff and not user_is_enrolled: + redirect_url = get_redirect_url_for_global_staff(self.course_key, _next=self.url) + raise Redirect(redirect_url) raise Redirect(reverse('about_course', args=[unicode(self.course.id)])) def _redirect_if_needed_for_prereqs(self): diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 9cdeb1d62f..501b816257 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -14,15 +14,18 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User, AnonymousUser from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse +from django.core.context_processors import csrf from django.db import transaction from django.db.models import Q from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden from django.shortcuts import redirect +from django.utils.decorators import method_decorator from django.utils.timezone import UTC from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_GET, require_POST, require_http_methods +from django.views.generic import View from eventtracking import tracker from ipware.ip import get_ip from markupsafe import escape @@ -30,6 +33,7 @@ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.locations import SlashSeparatedCourseKey from rest_framework import status +from instructor.views.api import require_global_staff import shoppingcart import survey.utils @@ -37,6 +41,7 @@ import survey.views from certificates import api as certs_api from openedx.core.djangoapps.models.course_details import CourseDetails from commerce.utils import EcommerceService +from enrollment.api import add_enrollment from course_modes.models import CourseMode from courseware import grades from courseware.access import has_access, has_ccx_coach_role, _adjust_start_date_for_beta_testers @@ -57,7 +62,7 @@ from courseware.courses import ( from courseware.masquerade import setup_masquerade from courseware.model_data import FieldDataCache, ScoresClient from courseware.models import StudentModule, BaseStudentModuleHistory -from courseware.url_helpers import get_redirect_url +from courseware.url_helpers import get_redirect_url, get_redirect_url_for_global_staff from courseware.user_state_client import DjangoXBlockUserStateClient from edxmako.shortcuts import render_to_response, render_to_string, marketing_link from instructor.enrollment import uses_shib @@ -72,6 +77,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers from shoppingcart.utils import is_shopping_cart_enabled from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from student.models import UserTestGroup, CourseEnrollment +from student.roles import GlobalStaff from util.cache import cache, cache_if_anonymous from util.date_utils import strftime_localized from util.db import outer_atomic @@ -239,6 +245,11 @@ def jump_to(_request, course_id, location): raise Http404(u"Invalid course_key or usage_key") try: redirect_url = get_redirect_url(course_key, usage_key) + user = _request.user + user_is_global_staff = GlobalStaff().has_user(user) + user_is_enrolled = CourseEnrollment.is_enrolled(user, course_key) + if user_is_global_staff and not user_is_enrolled: + redirect_url = get_redirect_url_for_global_staff(course_key, _next=redirect_url) except ItemNotFoundError: raise Http404(u"No data at this location: {0}".format(usage_key)) except NoPathToItem: @@ -444,6 +455,63 @@ def get_cosmetic_display_price(course, registration_price): return _('Free') +class EnrollStaffView(View): + """ + Displays view for registering in the course to a global staff user. + + User can either choose to 'Enroll' or 'Don't Enroll' in the course. + Enroll: Enrolls user in course and redirects to the courseware. + Don't Enroll: Redirects user to course about page. + + Arguments: + - request : HTTP request + - course_id : course id + + Returns: + - RedirectResponse + """ + template_name = 'enroll_staff.html' + + @method_decorator(require_global_staff) + @method_decorator(ensure_valid_course_key) + def get(self, request, course_id): + """ + Display enroll staff view to global staff user with `Enroll` and `Don't Enroll` options. + """ + user = request.user + course_key = CourseKey.from_string(course_id) + with modulestore().bulk_operations(course_key): + course = get_course_with_access(user, 'load', course_key) + if not registered_for_course(course, user): + context = { + 'course': course, + 'csrftoken': csrf(request)["csrf_token"] + } + return render_to_response(self.template_name, context) + + @method_decorator(require_global_staff) + @method_decorator(ensure_valid_course_key) + def post(self, request, course_id): + """ + Either enrolls the user in course or redirects user to course about page + depending upon the option (Enroll, Don't Enroll) chosen by the user. + """ + _next = urllib.quote_plus(request.GET.get('next', 'info'), safe='/:?=') + course_key = CourseKey.from_string(course_id) + enroll = 'enroll' in request.POST + if enroll: + add_enrollment(request.user.username, course_id) + log.info( + u"User %s enrolled in %s via `enroll_staff` view", + request.user.username, + course_id + ) + return redirect(_next) + + # In any other case redirect to the course about page. + return redirect(reverse('about_course', args=[unicode(course_key)])) + + @ensure_csrf_cookie @cache_if_anonymous() def course_about(request, course_id): diff --git a/lms/templates/enroll_staff.html b/lms/templates/enroll_staff.html new file mode 100644 index 0000000000..c088de9846 --- /dev/null +++ b/lms/templates/enroll_staff.html @@ -0,0 +1,45 @@ +<%page expression_filter="h" /> +<%inherit file="main.html" /> +<%namespace name='static' file='static_content.html'/> +<%! + from django.utils.translation import ugettext as _ + from courseware.courses import get_course_about_section +%> + +<%block name="headextra"> + + +%block> + +<%block name="pagetitle">${course.display_name_with_default}%block> + + + + + + + + ${_("You should Register before trying to access the Unit")} + + + + ${course.display_name_with_default} + + + + + + ${_('Enroll')}${course.display_name_with_default} + ${_('Don\'t enroll')}${course.display_name_with_default} + + + + + + + + diff --git a/lms/urls.py b/lms/urls.py index 9848cc0620..dfcb5ed2d5 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -10,7 +10,7 @@ from django.conf.urls.static import static from microsite_configuration import microsite import auth_exchange.views - +from courseware.views.views import EnrollStaffView from config_models.views import ConfigurationModelCurrentAPIView from courseware.views.index import CoursewareIndex from openedx.core.djangoapps.programs.models import ProgramsApiConfig @@ -366,6 +366,14 @@ urlpatterns += ( name='about_course', ), + url( + r'^courses/{}/enroll_staff$'.format( + settings.COURSE_ID_PATTERN, + ), + EnrollStaffView.as_view(), + name='enroll_staff', + ), + #Inside the course url( r'^courses/{}/$'.format(
+ ${course.display_name_with_default} +