Commit d7f94a05 by Victor Shnayder

Add DARK_LAUNCH functionality

* pass user to check_course
* if dark launch feature enabled, users with staff access to course
can see courseware before start date.  Students still can't.
* tests.
* Remaining: enrollment view has custom access control.  Need to check it.
parent 622eebc4
......@@ -15,11 +15,11 @@ from static_replace import replace_urls, try_staticfiles_lookup
log = logging.getLogger(__name__)
def check_course(course_id, course_must_be_open=True, course_required=True):
def check_course(user, course_id, course_must_be_open=True, course_required=True):
"""
Given a course_id, this returns the course object. By default,
if the course is not found or the course is not open yet, this
method will raise a 404.
Given a django user and a course_id, this returns the course
object. By default, if the course is not found or the course is
not open yet, this method will raise a 404.
If course_must_be_open is False, the course will be returned
without a 404 even if it is not open.
......@@ -27,6 +27,10 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
If course_required is False, a course_id of None is acceptable. The
course returned will be None. Even if the course is not required,
if a course_id is given that does not exist a 404 will be raised.
This behavior is modified by MITX_FEATURES['DARK_LAUNCH']:
if dark launch is enabled, course_must_be_open is ignored for
users that have staff access.
"""
course = None
if course_required or course_id:
......@@ -38,7 +42,13 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
raise Http404("Course not found.")
started = course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']
if course_must_be_open and not started:
must_be_open = course_must_be_open
if (settings.MITX_FEATURES['DARK_LAUNCH'] and
has_staff_access_to_course(user, course)):
must_be_open = False
if must_be_open and not started:
raise Http404("This course has not yet started.")
return course
......
import copy
import json
from path import path
import os
import sys
import time
from pprint import pprint
from nose import SkipTest
from path import path
from pprint import pprint
from django.contrib.auth.models import User, Group
from django.test import TestCase
from django.test.client import Client
from django.conf import settings
......@@ -13,12 +16,11 @@ from django.core.urlresolvers import reverse
from mock import patch, Mock
from override_settings import override_settings
from django.contrib.auth.models import User, Group
import xmodule.modulestore.django
from student.models import Registration
from courseware.courses import course_staff_group_name
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
......@@ -206,13 +208,14 @@ class TestCoursesLoadTestCase(PageLoader):
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class TestInstructorAuth(PageLoader):
"""Check that authentication works properly"""
class TestViewAuth(PageLoader):
"""Check that view authentication works properly"""
# NOTE: setUpClass() runs before override_settings takes effect, so
# can't do imports there without manually hacking settings.
def setUp(self):
print "sys.path: {}".format(sys.path)
xmodule.modulestore.django._MODULESTORES = {}
modulestore().collection.drop()
import_from_xml(modulestore(), TEST_DATA_DIR, ['toy'])
......@@ -237,12 +240,16 @@ class TestInstructorAuth(PageLoader):
# TODO (vshnayder): once we're returning 404s, get rid of this if.
if code != 404:
self.assertEqual(resp.status_code, code)
# And 'page not found' shouldn't be in the returned page
self.assertTrue(resp.content.lower().find('page not found') == -1)
else:
# look for "page not found" instead of the status code
#print resp.content
self.assertTrue(resp.content.lower().find('page not found') != -1)
def test_instructor_page(self):
"Make sure only instructors can load it"
def test_instructor_pages(self):
"""Make sure only instructors for the course or staff can load the instructor
dashboard, the grade views, and student profile pages"""
# First, try with an enrolled student
self.login(self.student, self.password)
......@@ -297,7 +304,125 @@ class TestInstructorAuth(PageLoader):
self.check_for_get_code(200, url)
def test_dark_launch(self):
"""Make sure that when dark launch is on, students can't access course
pages, but instructors can"""
# test.py turns off start dates, enable them and set them correctly.
# Because settings is global, be careful not to mess it up for other tests
# (Can't use override_settings because we're only changing part of the
# MITX_FEATURES dict)
oldDSD = settings.MITX_FEATURES['DISABLE_START_DATES']
oldDL = settings.MITX_FEATURES['DARK_LAUNCH']
try:
settings.MITX_FEATURES['DISABLE_START_DATES'] = False
settings.MITX_FEATURES['DARK_LAUNCH'] = True
self._do_test_dark_launch()
finally:
settings.MITX_FEATURES['DISABLE_START_DATES'] = oldDSD
settings.MITX_FEATURES['DARK_LAUNCH'] = oldDL
def _do_test_dark_launch(self):
"""Actually do the test, relying on settings to be right."""
# Make courses start in the future
tomorrow = time.time() + 24*3600
self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow)
self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow)
self.assertFalse(self.toy.has_started())
self.assertFalse(self.full.has_started())
self.assertFalse(settings.MITX_FEATURES['DISABLE_START_DATES'])
self.assertTrue(settings.MITX_FEATURES['DARK_LAUNCH'])
def reverse_urls(names, course):
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
def dark_student_urls(course):
"""
list of urls that students should be able to see only
after launch, but staff should see before
"""
urls = reverse_urls(['info', 'book', 'courseware', 'profile'], course)
return urls
def light_student_urls(course):
"""
list of urls that students should be able to see before
launch.
"""
urls = reverse_urls(['about_course'], course)
urls.append(reverse('courses'))
# Need separate test for change_enrollment, since it's a POST view
#urls.append(reverse('change_enrollment'))
return urls
def instructor_urls(course):
"""list of urls that only instructors/staff should be able to see"""
urls = reverse_urls(['instructor_dashboard','gradebook','grade_summary'],
course)
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
'student_id': user(self.student).id}))
return urls
def check_non_staff(course):
"""Check that access is right for non-staff in course"""
print '=== Checking non-staff access for {}'.format(course.id)
for url in instructor_urls(course) + dark_student_urls(course):
print 'checking for 404 on {}'.format(url)
self.check_for_get_code(404, url)
for url in light_student_urls(course):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
def check_staff(course):
"""Check that access is right for staff in course"""
print '=== Checking staff access for {}'.format(course.id)
for url in (instructor_urls(course) +
dark_student_urls(course) +
light_student_urls(course)):
print 'checking for 200 on {}'.format(url)
self.check_for_get_code(200, url)
# First, try with an enrolled student
print '=== Testing student access....'
self.login(self.student, self.password)
self.enroll(self.toy)
self.enroll(self.full)
# shouldn't be able to get to anything except the light pages
check_non_staff(self.toy)
check_non_staff(self.full)
print '=== Testing course instructor access....'
# Make the instructor staff in the toy course
group_name = course_staff_group_name(self.toy)
g = Group.objects.create(name=group_name)
g.user_set.add(user(self.instructor))
self.logout()
self.login(self.instructor, self.password)
# Enroll in the classes---can't see courseware otherwise.
self.enroll(self.toy)
self.enroll(self.full)
# should now be able to get to everything for toy course
check_non_staff(self.full)
check_staff(self.toy)
print '=== Testing staff access....'
# now also make the instructor staff
u = user(self.instructor)
u.is_staff = True
u.save()
# and now should be able to load both
check_staff(self.toy)
check_staff(self.full)
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
......
......@@ -110,9 +110,10 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
'''
course = check_course(course_id)
course = check_course(request.user, course_id)
registered = registered_for_course(course, request.user)
if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
return redirect(reverse('about_course', args=[course.id]))
......@@ -203,7 +204,7 @@ def course_info(request, course_id):
Assumes the course_id is in a valid format.
"""
course = check_course(course_id)
course = check_course(request.user, course_id)
return render_to_response('info.html', {'course': course})
......@@ -220,7 +221,7 @@ def registered_for_course(course, user):
@ensure_csrf_cookie
@cache_if_anonymous
def course_about(request, course_id):
course = check_course(course_id, course_must_be_open=False)
course = check_course(request.user, course_id, course_must_be_open=False)
registered = registered_for_course(course, request.user)
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
......@@ -252,7 +253,7 @@ def profile(request, course_id, student_id=None):
Course staff are allowed to see the profiles of students in their class.
"""
course = check_course(course_id)
course = check_course(request.user, course_id)
if student_id is None or student_id == request.user.id:
# always allowed to see your own profile
......@@ -299,7 +300,7 @@ def gradebook(request, course_id):
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(course_id)
course = check_course(request.user, course_id)
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username')
......@@ -324,7 +325,7 @@ def grade_summary(request, course_id):
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(course_id)
course = check_course(request.user, course_id)
# For now, just a static page
context = {'course': course }
......@@ -337,7 +338,7 @@ def instructor_dashboard(request, course_id):
if not has_staff_access_to_course_id(request.user, course_id):
raise Http404
course = check_course(course_id)
course = check_course(request.user, course_id)
# For now, just a static page
context = {'course': course }
......
......@@ -51,7 +51,7 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
def view(request, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -67,7 +67,7 @@ def view(request, article_path, course_id=None):
def view_revision(request, revision_number, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -91,7 +91,7 @@ def view_revision(request, revision_number, article_path, course_id=None):
def root_redirect(request, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
#TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX"
......@@ -109,7 +109,7 @@ def root_redirect(request, course_id=None):
def create(request, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
article_path_components = article_path.split('/')
......@@ -170,7 +170,7 @@ def create(request, article_path, course_id=None):
def edit(request, article_path, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -218,7 +218,7 @@ def edit(request, article_path, course_id=None):
def history(request, article_path, page=1, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, article_path, course)
if err:
......@@ -300,7 +300,7 @@ def history(request, article_path, page=1, course_id=None):
def revision_feed(request, page=1, namespace=None, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
page_size = 10
......@@ -333,7 +333,7 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
def search_articles(request, namespace=None, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
# blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort.
......@@ -382,7 +382,7 @@ def search_articles(request, namespace=None, course_id=None):
def search_add_related(request, course_id, slug, namespace):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
......@@ -415,7 +415,7 @@ def search_add_related(request, course_id, slug, namespace):
def add_related(request, course_id, slug, namespace):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id)
if err:
......@@ -439,7 +439,7 @@ def add_related(request, course_id, slug, namespace):
def remove_related(request, course_id, namespace, slug, related_id):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
(article, err) = get_article(request, slug, namespace if namespace else course_id)
......@@ -462,7 +462,7 @@ def remove_related(request, course_id, namespace, slug, related_id):
def random_article(request, course_id=None):
course = check_course(course_id, course_required=False)
course = check_course(request.user, course_id, course_required=False)
from random import randint
num_arts = Article.objects.count()
......
......@@ -6,7 +6,7 @@ from lxml import etree
@login_required
def index(request, course_id, page=0):
course = check_course(course_id)
course = check_course(request.user, course_id)
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
return render_to_response('staticbook.html', {'page': int(page), 'course': course, 'table_of_contents': table_of_contents})
......
......@@ -48,6 +48,7 @@ MITX_FEATURES = {
## DO NOT SET TO True IN THIS FILE
## Doing so will cause all courses to be released on production
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
'DARK_LAUNCH': False, # When True, courses will be active for staff only
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True,
......
......@@ -97,12 +97,16 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED:
urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users
url(r'^masquerade/', include('masquerade.urls')),
url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
url(r'^xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'),
url(r'^change_setting$', 'student.views.change_setting'),
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.modx_dispatch', name='modx_dispatch'),
url(r'^xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.xqueue_callback', name='xqueue_callback'),
url(r'^change_setting$', 'student.views.change_setting',
name='change_setting'),
# TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment