diff --git a/lms/djangoapps/courseware/url_helpers.py b/lms/djangoapps/courseware/url_helpers.py new file mode 100644 index 0000000..22f4619 --- /dev/null +++ b/lms/djangoapps/courseware/url_helpers.py @@ -0,0 +1,46 @@ +""" +Module to define url helpers functions +""" +from xmodule.modulestore.search import path_to_location, navigation_index +from xmodule.modulestore.django import modulestore +from django.core.urlresolvers import reverse + + +def get_redirect_url(course_key, usage_key): + """ Returns the redirect url back to courseware + + Args: + course_id(str): Course Id string + location(str): The location id of course component + + Raises: + ItemNotFoundError if no data at the location or NoPathToItem if location not in any class + + Returns: + Redirect url string + """ + + (course_key, chapter, section, position) = path_to_location(modulestore(), usage_key) + + # choose the appropriate view (and provide the necessary args) based on the + # args provided by the redirect. + # Rely on index to do all error handling and access control. + if chapter is None: + redirect_url = reverse('courseware', args=(unicode(course_key), )) + elif section is None: + redirect_url = reverse('courseware_chapter', args=(unicode(course_key), chapter)) + elif position is None: + redirect_url = reverse( + 'courseware_section', + args=(unicode(course_key), chapter, section) + ) + else: + # 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)) + ) + return redirect_url diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 7096d67..bfb1f02 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -60,7 +60,6 @@ from util.cache import cache, cache_if_anonymous from xblock.fragment import Fragment from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem -from xmodule.modulestore.search import path_to_location, navigation_index from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEndedGradingTab from xmodule.x_module import STUDENT_VIEW import shoppingcart @@ -82,6 +81,7 @@ import survey.views from util.views import ensure_valid_course_key from eventtracking import tracker import analytics +from courseware.url_helpers import get_redirect_url log = logging.getLogger("edx.courseware") @@ -642,37 +642,13 @@ def jump_to(_request, course_id, location): except InvalidKeyError: raise Http404(u"Invalid course_key or usage_key") try: - (course_key, chapter, section, position) = path_to_location(modulestore(), usage_key) + redirect_url = get_redirect_url(course_key, usage_key) except ItemNotFoundError: raise Http404(u"No data at this location: {0}".format(usage_key)) except NoPathToItem: raise Http404(u"This location is not in any class: {0}".format(usage_key)) - # choose the appropriate view (and provide the necessary args) based on the - # args provided by the redirect. - # Rely on index to do all error handling and access control. - if chapter is None: - return redirect('courseware', course_id=unicode(course_key)) - elif section is None: - return redirect('courseware_chapter', course_id=unicode(course_key), chapter=chapter) - elif position is None: - return redirect( - 'courseware_section', - course_id=unicode(course_key), - chapter=chapter, - section=section - ) - else: - # 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 - return redirect( - 'courseware_position', - course_id=unicode(course_key), - chapter=chapter, - section=section, - position=navigation_index(position) - ) + return redirect(redirect_url) @ensure_csrf_cookie diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py index cc43f38..435cc1e 100644 --- a/lms/djangoapps/verify_student/services.py +++ b/lms/djangoapps/verify_student/services.py @@ -34,7 +34,7 @@ class ReverificationService(object): except ObjectDoesNotExist: return None - def start_verification(self, course_id, checkpoint_name, item_id): # pylint: disable=W0613 + def start_verification(self, course_id, checkpoint_name, item_id): """ Get or create the verification checkpoint and return the re-verification link Args: @@ -46,5 +46,5 @@ class ReverificationService(object): """ course_key = CourseKey.from_string(course_id) VerificationCheckpoint.objects.get_or_create(course_id=course_key, checkpoint_name=checkpoint_name) - re_verification_link = reverse("verify_student_incourse_reverify", args=(course_id, checkpoint_name)) + re_verification_link = reverse("verify_student_incourse_reverify", args=(course_id, checkpoint_name, item_id)) return re_verification_link diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 1a381e3..05000bb 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -20,7 +20,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.core import mail from bs4 import BeautifulSoup from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.django import modulestore from xmodule.modulestore import ModuleStoreEnum from opaque_keys.edx.locations import SlashSeparatedCourseKey @@ -1701,20 +1701,42 @@ class TestInCourseReverifyView(ModuleStoreTestCase): IMAGE_DATA = "abcd,1234" MIDTERM = "midterm" - def setUp(self): - super(TestInCourseReverifyView, self).setUp() - - self.user = UserFactory.create(username="rusty", password="test") - self.client.login(username="rusty", password="test") + def build_course(self): + """ + Build up a course tree with a Reverificaiton xBlock. + """ + # pylint: disable=attribute-defined-outside-init self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") - CourseFactory.create(org='Robot', number='999', display_name='Test Course') + self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') # Create the course modes for mode in ('audit', 'honor', 'verified'): min_price = 0 if mode in ["honor", "audit"] else 1 CourseModeFactory(mode_slug=mode, course_id=self.course_key, min_price=min_price) + # Create the 'edx-reverification-block' in course tree + section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') + subsection = ItemFactory.create(parent=section, category='sequential', display_name='Test Subsection') + vertical = ItemFactory.create(parent=subsection, category='vertical', display_name='Test Unit') + reverification = ItemFactory.create( + parent=vertical, + category='edx-reverification-block', + display_name='Test Verification Block' + ) + self.section_location = section.location + self.subsection_location = subsection.location + self.vertical_location = vertical.location + self.reverification_location = reverification.location + + def setUp(self): + super(TestInCourseReverifyView, self).setUp() + + self.build_course() + + self.user = UserFactory.create(username="rusty", password="test") + self.client.login(username="rusty", password="test") + # Enroll the user in the default mode (honor) to emulate CourseEnrollment.enroll(self.user, self.course_key, mode="verified") self.config = InCourseReverificationConfiguration(enabled=True) @@ -1863,4 +1885,8 @@ class TestInCourseReverifyView(ModuleStoreTestCase): """ return reverse('verify_student_incourse_reverify', - kwargs={"course_id": unicode(course_key), "checkpoint_name": checkpoint}) + kwargs={ + "course_id": unicode(course_key), + "checkpoint_name": checkpoint, + "location": unicode(self.reverification_location) + }) diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py index 03fcb49..00ff3b0 100644 --- a/lms/djangoapps/verify_student/urls.py +++ b/lms/djangoapps/verify_student/urls.py @@ -143,7 +143,7 @@ urlpatterns = patterns( # Users are sent to this end-point from within courseware # to re-verify their identities by re-submitting face photos. url( - r'^reverify/{course_id}/{checkpoint}/$'.format( + r'^reverify/{course_id}/{checkpoint}/(?P<location>.*)/$'.format( course_id=settings.COURSE_ID_PATTERN, checkpoint=settings.CHECKPOINT_PATTERN ), views.InCourseReverifyView.as_view(), diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 39dca7c..1244c05 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -24,9 +24,10 @@ from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _, ugettext_lazy from django.contrib.auth.decorators import login_required from django.core.mail import send_mail -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys import InvalidKeyError from xmodule.modulestore.django import modulestore -from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem from edxmako.shortcuts import render_to_response, render_to_string from openedx.core.djangoapps.user_api.accounts.api import get_account_settings, update_account_settings @@ -55,7 +56,7 @@ from util.json_request import JsonResponse from util.date_utils import get_default_time_display from eventtracking import tracker import analytics - +from courseware.url_helpers import get_redirect_url log = logging.getLogger(__name__) @@ -1089,7 +1090,7 @@ class InCourseReverifyView(View): Does not need to worry about pricing """ @method_decorator(login_required) - def get(self, request, course_id, checkpoint_name): + def get(self, request, course_id, checkpoint_name, location): """ Display the view for face photo submission""" # Check the in-course re-verification is enabled or not incourse_reverify_enabled = InCourseReverificationConfiguration.current().enabled @@ -1120,11 +1121,12 @@ class InCourseReverifyView(View): 'course_name': course.display_name_with_default, 'checkpoint_name': checkpoint_name, 'platform_name': settings.PLATFORM_NAME, + 'location': location } return render_to_response("verify_student/incourse_reverify.html", context) @method_decorator(login_required) - def post(self, request, course_id, checkpoint_name): + def post(self, request, course_id, checkpoint_name, location): """Submits the re-verification attempt to SoftwareSecure Args: @@ -1143,7 +1145,11 @@ class InCourseReverifyView(View): raise Http404 user = request.user - course_key = CourseKey.from_string(course_id) + try: + course_key = CourseKey.from_string(course_id) + usage_key = UsageKey.from_string(location).replace(course_key=course_key) + except InvalidKeyError: + raise Http404(u"Invalid course_key or usage_key") course = modulestore().get_course(course_key) checkpoint = VerificationCheckpoint.get_verification_checkpoint(course_key, checkpoint_name) if checkpoint is None: @@ -1156,6 +1162,7 @@ class InCourseReverifyView(View): 'error': True, 'errorMsg': _("No checkpoint found"), 'platform_name': settings.PLATFORM_NAME, + 'location': location } return render_to_response("verify_student/incourse_reverify.html", context) init_verification = SoftwareSecurePhotoVerification.get_initial_verification(user) @@ -1175,12 +1182,20 @@ class InCourseReverifyView(View): EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, user.id, course_id, checkpoint_name ) - return HttpResponse() + try: + redirect_url = get_redirect_url(course_key, usage_key) + except (ItemNotFoundError, NoPathToItem): + redirect_url = reverse("courseware", args=(unicode(course_key),)) + + return JsonResponse({'url': redirect_url}) + except Http404 as expt: + log.exception("Invalid location during photo verification.") + return HttpResponseBadRequest(expt.message) except IndexError: log.exception("Invalid image data during photo verification.") return HttpResponseBadRequest(_("Invalid image data during photo verification.")) except Exception: # pylint: disable=broad-except - log.exception("Could not submit verification attempt for user {}.").format(request.user.id) + log.exception("Could not submit verification attempt for user %s.", request.user.id) msg = _("Could not submit photos") return HttpResponseBadRequest(msg) diff --git a/lms/static/js/verify_student/incourse_reverify.js b/lms/static/js/verify_student/incourse_reverify.js index d6a65bf..faa0bea 100644 --- a/lms/static/js/verify_student/incourse_reverify.js +++ b/lms/static/js/verify_student/incourse_reverify.js @@ -22,6 +22,7 @@ courseKey: el.data('course-key'), checkpointName: el.data('checkpoint-name'), platformName: el.data('platform-name'), + location: el.data('location'), errorModel: errorView.model }).render(); diff --git a/lms/static/js/verify_student/models/reverification_model.js b/lms/static/js/verify_student/models/reverification_model.js index e9152d8..a5dbc5b 100644 --- a/lms/static/js/verify_student/models/reverification_model.js +++ b/lms/static/js/verify_student/models/reverification_model.js @@ -18,18 +18,20 @@ var edx = edx || {}; courseKey: '', checkpointName: '', faceImage: '', + location: '' }, sync: function( method ) { var model = this; var headers = { 'X-CSRFToken': $.cookie( 'csrftoken' ) }, data = { - face_image: model.get( 'faceImage' ), + face_image: model.get( 'faceImage' ) }, url = _.str.sprintf( - '/verify_student/reverify/%(courseKey)s/%(checkpointName)s/', { + '/verify_student/reverify/%(courseKey)s/%(checkpointName)s/%(location)s/', { courseKey: model.get('courseKey'), - checkpointName: model.get('checkpointName') + checkpointName: model.get('checkpointName'), + location: model.get('location') } ); @@ -38,8 +40,8 @@ var edx = edx || {}; type: 'POST', data: data, headers: headers, - success: function() { - model.trigger( 'sync' ); + success: function(response) { + model.trigger( 'sync', response.url); }, error: function( error ) { model.trigger( 'error', error ); diff --git a/lms/static/js/verify_student/views/incourse_reverify_view.js b/lms/static/js/verify_student/views/incourse_reverify_view.js index 0d9883b..2d6ebf6 100644 --- a/lms/static/js/verify_student/views/incourse_reverify_view.js +++ b/lms/static/js/verify_student/views/incourse_reverify_view.js @@ -28,11 +28,13 @@ this.courseKey = obj.courseKey || null; this.checkpointName = obj.checkpointName || null; this.platformName = obj.platformName || null; + this.location = obj.location || null; this.model = new edx.verify_student.ReverificationModel({ courseKey: this.courseKey, - checkpointName: this.checkpointName + checkpointName: this.checkpointName, + location: this.location }); this.listenTo( this.model, 'sync', _.bind( this.handleSubmitPhotoSuccess, this )); @@ -75,10 +77,10 @@ this.model.save(); }, - handleSubmitPhotoSuccess: function() { + handleSubmitPhotoSuccess: function(redirect_url) { // Eventually this will be a redirect back into the courseware, // but for now we can return to the student dashboard. - window.location.href = '/dashboard'; + window.location.href = redirect_url; }, handleSubmissionError: function(xhr) { diff --git a/lms/templates/verify_student/incourse_reverify.html b/lms/templates/verify_student/incourse_reverify.html index fae9dca..c2228e7 100644 --- a/lms/templates/verify_student/incourse_reverify.html +++ b/lms/templates/verify_student/incourse_reverify.html @@ -46,6 +46,7 @@ checkpoint_name=checkpoint_name)} data-course-key='${course_key}' data-checkpoint-name='${checkpoint_name}' data-platform-name='${platform_name}' + data-location='${location}' ></div> </section> </div>