diff --git a/lms/djangoapps/pocs/__init__.py b/lms/djangoapps/pocs/__init__.py index e69de29bb2..4da31a2dd5 100644 --- a/lms/djangoapps/pocs/__init__.py +++ b/lms/djangoapps/pocs/__init__.py @@ -0,0 +1 @@ +ACTIVE_POC_KEY = '_poc_id' diff --git a/lms/djangoapps/pocs/overrides.py b/lms/djangoapps/pocs/overrides.py index 0c9b61f456..08ed7a30a6 100644 --- a/lms/djangoapps/pocs/overrides.py +++ b/lms/djangoapps/pocs/overrides.py @@ -7,7 +7,9 @@ import threading from contextlib import contextmanager +from courseware.courses import get_request_for_thread from courseware.field_overrides import FieldOverrideProvider +from pocs import ACTIVE_POC_KEY from .models import PocMembership, PocFieldOverride @@ -52,7 +54,11 @@ def poc_context(poc): def get_current_poc(user): """ - TODO Needs to look in user's session + Return the poc that is active for this user + + The user's session data is used to look up the active poc by id + If no poc is active, None is returned and MOOC view will take precedence + Active poc can be overridden by context manager (see `poc_context`) """ # If poc context is explicitly set, that takes precedence over the user's # session. @@ -60,16 +66,22 @@ def get_current_poc(user): if poc: return poc - # Temporary implementation. Final implementation will need to look in - # user's session so user can switch between (potentially multiple) POC and - # MOOC views. See courseware.courses.get_request_for_thread for idea to - # get at the request object. - try: - membership = PocMembership.objects.get(student=user, active=True) - return membership.poc - except PocMembership.DoesNotExist: + request = get_request_for_thread() + if request is None: return None + poc = None + poc_id = request.session.get(ACTIVE_POC_KEY, None) + if poc_id is not None: + try: + membership = PocMembership.objects.get( + student=user, active=True, poc__id__exact=poc_id + ) + poc = membership.poc + except PocMembership.DoesNotExist: + pass + return poc + def get_override_for_poc(poc, block, name, default=None): """ diff --git a/lms/djangoapps/pocs/tests/test_views.py b/lms/djangoapps/pocs/tests/test_views.py index 19ddf442c4..779ed46b76 100644 --- a/lms/djangoapps/pocs/tests/test_views.py +++ b/lms/djangoapps/pocs/tests/test_views.py @@ -23,6 +23,7 @@ from xmodule.modulestore.tests.factories import ( CourseFactory, ItemFactory, ) +from pocs import ACTIVE_POC_KEY from ..models import ( PersonalOnlineCourse, PocMembership, @@ -539,6 +540,154 @@ class TestPocGrades(ModuleStoreTestCase, LoginEnrollmentTestCase): self.assertEqual(len(grades['section_breakdown']), 4) +class TestSwitchActivePoc(ModuleStoreTestCase, LoginEnrollmentTestCase): + """Verify the view for switching which POC is active, if any + """ + def setUp(self): + self.course = course = CourseFactory.create() + coach = AdminFactory.create() + role = CoursePocCoachRole(course.id) + role.add_users(coach) + self.poc = PocFactory(course_id=course.id, coach=coach) + enrollment = CourseEnrollmentFactory.create(course_id=course.id) + self.user = enrollment.user + self.target_url = reverse( + 'course_root', args=[course.id.to_deprecated_string()] + ) + + def register_user_in_poc(self, active=False): + """create registration of self.user in self.poc + + registration will be inactive unless active=True + """ + PocMembershipFactory(poc=self.poc, student=self.user, active=active) + + def verify_active_poc(self, request, id=None): + if id: + id = str(id) + self.assertEqual(id, request.session.get(ACTIVE_POC_KEY, None)) + + def test_unauthorized_cannot_switch_to_poc(self): + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string(), self.poc.id] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + + def test_unauthorized_cannot_switch_to_mooc(self): + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string()] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + + def test_enrolled_inactive_user_cannot_select_poc(self): + self.register_user_in_poc(active=False) + self.client.login(username=self.user.username, password="test") + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string(), self.poc.id] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(self.target_url)) + # if the poc were active, we'd need to pass the ID of the poc here. + self.verify_active_poc(self.client) + + def test_enrolled_user_can_select_poc(self): + self.register_user_in_poc(active=True) + self.client.login(username=self.user.username, password="test") + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string(), self.poc.id] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(self.target_url)) + self.verify_active_poc(self.client, self.poc.id) + + def test_enrolled_user_can_select_mooc(self): + self.register_user_in_poc(active=True) + self.client.login(username=self.user.username, password="test") + # pre-seed the session with the poc id + session = self.client.session + session[ACTIVE_POC_KEY] = str(self.poc.id) + session.save() + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string()] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(self.target_url)) + self.verify_active_poc(self.client) + + def test_unenrolled_user_cannot_select_poc(self): + self.client.login(username=self.user.username, password="test") + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string(), self.poc.id] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(self.target_url)) + # if the poc were active, we'd need to pass the ID of the poc here. + self.verify_active_poc(self.client) + + def test_unenrolled_user_switched_to_mooc(self): + self.client.login(username=self.user.username, password="test") + # pre-seed the session with the poc id + session = self.client.session + session[ACTIVE_POC_KEY] = str(self.poc.id) + session.save() + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string(), self.poc.id] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(self.target_url)) + # we tried to select the poc but are not registered, so we are switched + # back to the mooc view + self.verify_active_poc(self.client) + + def test_unassociated_course_and_poc_not_selected(self): + new_course = CourseFactory.create() + self.client.login(username=self.user.username, password="test") + expected_url = reverse( + 'course_root', args=[new_course.id.to_deprecated_string()] + ) + # the poc and the course are not related. + switch_url = reverse( + 'switch_active_poc', + args=[new_course.id.to_deprecated_string(), self.poc.id] + ) + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(expected_url)) + # the mooc should be active + self.verify_active_poc(self.client) + + def test_missing_poc_cannot_be_selected(self): + self.register_user_in_poc() + self.client.login(username=self.user.username, password="test") + switch_url = reverse( + 'switch_active_poc', + args=[self.course.id.to_deprecated_string(), self.poc.id] + ) + # delete the poc + self.poc.delete() + + response = self.client.get(switch_url) + self.assertEqual(response.status_code, 302) + self.assertTrue(response.get('Location', '').endswith(self.target_url)) + # we tried to select the poc it doesn't exist anymore, so we are + # switched back to the mooc view + self.verify_active_poc(self.client) + + def flatten(seq): """ For [[1, 2], [3, 4]] returns [1, 2, 3, 4]. Does not recurse. diff --git a/lms/djangoapps/pocs/utils.py b/lms/djangoapps/pocs/utils.py index 525deb8de5..b597caafd4 100644 --- a/lms/djangoapps/pocs/utils.py +++ b/lms/djangoapps/pocs/utils.py @@ -17,12 +17,12 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey from microsite_configuration import microsite -from pocs.models import ( +from .models import ( PersonalOnlineCourse, PocMembership, PocFutureMembership, ) - +from .overrides import get_current_poc class EmailEnrollmentState(object): """ Store the complete enrollment state of an email in a class """ @@ -225,8 +225,12 @@ def get_all_pocs_for_user(user): Returns a list of dicts: { poc_name: poc_url: + poc_active: True if this poc is currently the 'active' one + mooc_name: + mooc_url: } """ + current_active_poc = get_current_poc(user) if user.is_anonymous(): return [] active_poc_memberships = PocMembership.objects.filter( @@ -235,13 +239,22 @@ def get_all_pocs_for_user(user): memberships = [] for membership in active_poc_memberships: course = get_course_by_id(membership.poc.course_id) - title = 'POC: {}'.format(get_course_about_section(course, 'title')) + course_title = get_course_about_section(course, 'title') + poc_title = 'POC: {}'.format(course_title) + mooc_title = 'MOOC: {}'.format(course_title) url = reverse( 'switch_active_poc', args=[course.id.to_deprecated_string(), membership.poc.id] ) + mooc_url = reverse( + 'switch_active_poc', + args=[course.id.to_deprecated_string(),] + ) memberships.append({ - 'poc_name': title, - 'poc_url': url + 'poc_name': poc_title, + 'poc_url': url, + 'active': membership.poc == current_active_poc, + 'mooc_name': mooc_title, + 'mooc_url': mooc_url, }) return memberships diff --git a/lms/djangoapps/pocs/views.py b/lms/djangoapps/pocs/views.py index 576102c686..7573dbdd5f 100644 --- a/lms/djangoapps/pocs/views.py +++ b/lms/djangoapps/pocs/views.py @@ -12,15 +12,21 @@ from copy import deepcopy from cStringIO import StringIO from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseForbidden +from django.http import ( + HttpResponse, + HttpResponseForbidden, + HttpResponseRedirect, +) from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.shortcuts import redirect from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_control from django_future.csrf import ensure_csrf_cookie +from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User +from courseware.courses import get_course from courseware.courses import get_course_by_id from courseware.field_overrides import disable_overrides from courseware.grades import iterate_grades_for @@ -41,7 +47,13 @@ from .overrides import ( override_field_for_poc, poc_context, ) -from .utils import enroll_email, unenroll_email +from .utils import ( + enroll_email, + unenroll_email, + get_all_pocs_for_user, +) +from pocs import ACTIVE_POC_KEY + log = logging.getLogger(__name__) TODAY = datetime.datetime.today # for patching in tests @@ -403,7 +415,6 @@ def poc_grades_csv(request, course): course.id, request.user, course, depth=2) course = get_module_for_descriptor( request.user, request, course, field_data_cache, course.id) - poc = get_poc_for_coach(course, request.user) with poc_context(poc): # The grading policy for the MOOC is probably already cached. We need @@ -446,3 +457,38 @@ def poc_grades_csv(request, course): writer.writerow(row) return HttpResponse(buf.getvalue(), content_type='text/plain') + + +@login_required +def swich_active_poc(request, course_id, poc_id=None): + """set the active POC for the logged-in user + """ + user = request.user + if not user.is_authenticated(): + return HttpResponseForbidden( + _('Only registered students may change POC views.') + ) + course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) + # will raise Http404 if course_id is bad + course = get_course_by_id(course_key) + course_url = reverse( + 'course_root', args=[course.id.to_deprecated_string()] + ) + if poc_id is not None: + try: + requested_poc = PersonalOnlineCourse.objects.get(pk=poc_id) + assert requested_poc.course_id.to_deprecated_string() == course_id + if not PocMembership.objects.filter( + poc=requested_poc, student=request.user, active=True + ).exists(): + poc_id = None + except PersonalOnlineCourse.DoesNotExist: + # what to do here? Log the failure? Do we care? + poc_id = None + except AssertionError: + # what to do here? Log the failure? Do we care? + poc_id = None + + request.session[ACTIVE_POC_KEY] = poc_id + + return HttpResponseRedirect(course_url) diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 2e82e73362..e66be26f63 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -13,6 +13,7 @@ from status.status import get_site_status_msg <%! from microsite_configuration import microsite %> <%! from microsite_configuration.templatetags.microsite import platform_name %> +<%! from pocs.utils import get_all_pocs_for_user %> ## Provide a hook for themes to inject branding on top. <%block name="navigation_top" /> @@ -82,6 +83,15 @@ site_status_msg = get_site_status_msg(course_id) % if settings.MKTG_URL_LINK_MAP.get('FAQ'):
  • ${_("Help")}
  • % endif + % if settings.FEATURES.get('PERSONAL_ONLINE_COURSES', False): + %for poc in get_all_pocs_for_user(user): + % if poc['active']: +
  • ${poc['mooc_name']}
  • + % else: +
  • ${poc['poc_name']}
  • + % endif + %endfor + % endif
  • ${_("Sign out")}
  • diff --git a/lms/urls.py b/lms/urls.py index 76ee70f599..cbeea15512 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -359,6 +359,8 @@ if settings.COURSEWARE_ENABLED: 'pocs.views.poc_grades_csv', name='poc_grades_csv'), url(r'^courses/{}/poc_set_grading_policy$'.format(settings.COURSE_ID_PATTERN), 'pocs.views.set_grading_policy', name='poc_set_grading_policy'), + url(r'^courses/{}/swich_poc(?:/(?P[\d]+))?$'.format(settings.COURSE_ID_PATTERN), + 'pocs.views.swich_active_poc', name='switch_active_poc'), url(r'^courses/{}/set_course_mode_price$'.format(settings.COURSE_ID_PATTERN), 'instructor.views.instructor_dashboard.set_course_mode_price', name="set_course_mode_price"), url(r'^courses/{}/instructor/api/'.format(settings.COURSE_ID_PATTERN),