Added a decorator that prevents access to course pages until the course has started.

This commit is contained in:
Bridger Maxwell
2012-07-13 10:31:36 -04:00
parent 8d7c4ad8de
commit ecc50314f7
8 changed files with 145 additions and 93 deletions

View File

@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, date
import dateutil.parser
from fs.errors import ResourceNotFoundError
import logging
@@ -18,7 +18,14 @@ class CourseDescriptor(SequenceDescriptor):
def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self.start = dateutil.parser.parse(self.metadata["start"])
try:
self.start = dateutil.parser.parse(self.metadata["start"])
except KeyError:
self.start = date.fromtimestamp(0) #The epoch
log.critical("Course loaded without a start date. " + str(self.id))
except ValueError, e:
self.start = date.fromtimestamp(0) #The epoch
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'")
def has_started(self):
return datetime.now() > self.start

View File

@@ -0,0 +1,50 @@
from django.http import Http404
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
def check_course(course_must_be_open=True, course_required=True):
"""
This is a decorator for views that are within a course.
It converts the string passed to the view as 'course_id'
to a course, and then passes it to the view function as
a parameter named 'course'.
This decorator also checks that the course has started.
If the course has not started, it raises a 404. This check
can be skipped by setting course_must_be_open to False.
If course_required is False, course_id is not required. If
course_id is still provided, but is None, course will be
set to None.
"""
def inner_check_course(function):
def wrapped_function(*args, **kwargs):
if course_required or 'course_id' in kwargs:
course_id = kwargs['course_id']
course = None
if course_required or course_id:
try:
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc)
except KeyError:
raise Http404("Course not found.")
if course_must_be_open and not course.has_started():
raise Http404
del kwargs['course_id']
kwargs['course'] = course
return function(*args, **kwargs)
return wrapped_function
# If no arguments were passed to the decorator, the function itself
# will be in course_must_be_open
if hasattr(course_must_be_open, '__call__'):
function = course_must_be_open
course_must_be_open = True
return inner_check_course(function)
else:
return inner_check_course

View File

@@ -16,37 +16,17 @@ from module_render import toc_for_course, get_module, get_section
from models import StudentModuleCache
from student.models import UserProfile
from multicourse import multicourse_settings
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
from util.cache import cache
#from util.cache import cache #TODO: Where did this go? lib/util/cache no longer exists
from student.models import UserTestGroup
from courseware import grades
from courseware.decorators import check_course
from xmodule.modulestore.django import modulestore
log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib}
def check_course(function, course_must_be_open=True):
def inner_function(*args, **kwargs):
course_id = kwargs['course_id']
try:
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc)
except KeyError:
raise Http404("Course not found.")
if course_must_be_open and not course.has_started():
raise Http404
del kwargs['course_id']
kwargs['course'] = course
return function(*args, **kwargs)
return inner_function
def user_groups(user):
if not user.is_authenticated():
return []
@@ -77,20 +57,18 @@ def courses(request):
'csrf': csrf_token}
return render_to_response("courses.html", context)
@check_course
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request, course_id):
def gradebook(request, course):
if 'course_admin' not in user_groups(request.user):
raise Http404
course_location = CourseDescriptor.id_to_location(course_id)
student_objects = User.objects.all()[:100]
student_info = []
for student in student_objects:
student_module_cache = StudentModuleCache(student, modulestore().get_item(course_location))
course, _, _, _ = get_module(request.user, request, course_location, student_module_cache)
student_module_cache = StudentModuleCache(student, course)
course, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
student_info.append({
'username': student.username,
'id': student.id,

View File

@@ -9,20 +9,12 @@ from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response
from courseware.decorators import check_course
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm
import wiki_settings
def get_course(course_id):
if course_id == None:
return None
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc)
# raise Http404("Course not found")
return course
def wiki_reverse(wiki_page, article = None, course = None, namespace=None, args=[], kwargs={}):
kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'}
@@ -55,10 +47,9 @@ def update_template_dictionary(dictionary, request = None, course = None, articl
if request:
dictionary.update(csrf(request))
def view(request, article_path, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def view(request, article_path, course=None):
(article, err) = get_article(request, article_path, course )
if err:
return err
@@ -71,8 +62,9 @@ def view(request, article_path, course_id=None):
update_template_dictionary(d, request, course, article, article.current_revision)
return render_to_response('simplewiki/simplewiki_view.html', d)
def view_revision(request, revision_number, article_path, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def view_revision(request, revision_number, article_path, course=None):
(article, err) = get_article(request, article_path, course )
if err:
return err
@@ -93,24 +85,24 @@ def view_revision(request, revision_number, article_path, course_id=None):
return render_to_response('simplewiki/simplewiki_view.html', d)
def root_redirect(request, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def root_redirect(request, course=None):
#TODO: Add a default namespace to settings.
namespace = course.wiki_namespace if course else "edX"
try:
root = Article.get_root(course.wiki_namespace)
root = Article.get_root(namespace)
return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id' : course_id, 'article_path' : root.get_path()} ))
except:
# If the root is not found, we probably are loading this class for the first time
# We should make sure the namespace exists so the root article can be created.
Namespace.ensure_namespace(course.wiki_namespace)
Namespace.ensure_namespace(namespace)
err = not_found(request, course.wiki_namespace + '/', course)
err = not_found(request, namespace + '/', course)
return err
return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id' : course_id, 'article_path' : root.get_path()} ))
def create(request, article_path, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def create(request, article_path, course=None):
article_path_components = article_path.split('/')
# Ensure the namespace exists
@@ -168,8 +160,8 @@ def create(request, article_path, course_id=None):
return render_to_response('simplewiki/simplewiki_edit.html', d)
def edit(request, article_path, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def edit(request, article_path, course=None):
(article, err) = get_article(request, article_path, course )
if err:
return err
@@ -214,8 +206,8 @@ def edit(request, article_path, course_id=None):
update_template_dictionary(d, request, course, article)
return render_to_response('simplewiki/simplewiki_edit.html', d)
def history(request, article_path, page=1, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def history(request, article_path, page=1, course=None):
(article, err) = get_article(request, article_path, course )
if err:
return err
@@ -295,10 +287,8 @@ def history(request, article_path, page=1, course_id=None):
return render_to_response('simplewiki/simplewiki_history.html', d)
def revision_feed(request, page=1, namespace=None, course_id=None):
course = get_course(course_id)
@check_course(course_required=False)
def revision_feed(request, page=1, namespace=None, course=None):
page_size = 10
if page == None:
@@ -328,7 +318,8 @@ def revision_feed(request, page=1, namespace=None, course_id=None):
return render_to_response('simplewiki/simplewiki_revision_feed.html', d)
def search_articles(request, namespace=None, course_id = None):
@check_course(course_required=False)
def search_articles(request, namespace=None, course = None):
# 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.
# Adding some context to results (eg where matches were) would also be nice.
@@ -339,9 +330,7 @@ def search_articles(request, namespace=None, course_id = None):
querystring = request.GET.get('value', '').strip()
else:
querystring = ""
course = get_course(course_id)
results = Article.objects.all()
if namespace:
results = results.filter(namespace__name__exact = namespace)
@@ -377,8 +366,8 @@ def search_articles(request, namespace=None, course_id = None):
update_template_dictionary(d, request, course)
return render_to_response('simplewiki/simplewiki_searchresults.html', d)
def search_add_related(request, course_id, slug, namespace):
@check_course(course_required=False)
def search_add_related(request, course, slug, namespace):
(article, err) = get_article(request, slug, namespace if namespace else course_id )
if err:
return err
@@ -408,7 +397,8 @@ def search_add_related(request, course_id, slug, namespace):
json = simplejson.dumps({'results': results})
return HttpResponse(json, mimetype='application/json')
def add_related(request, course_id, slug, namespace):
@check_course(course_required=False)
def add_related(request, course, slug, namespace):
(article, err) = get_article(request, slug, namespace if namespace else course_id )
if err:
return err
@@ -429,7 +419,8 @@ def add_related(request, course_id, slug, namespace):
finally:
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def remove_related(request, course_id, namespace, slug, related_id):
@check_course(course_required=False)
def remove_related(request, course, namespace, slug, related_id):
(article, err) = get_article(request, slug, namespace if namespace else course_id )
if err:
@@ -449,8 +440,8 @@ def remove_related(request, course_id, namespace, slug, related_id):
finally:
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def random_article(request, course_id):
course = get_course(course_id)
@check_course(course_required=False)
def random_article(request, course=None):
from random import randint
num_arts = Article.objects.count()
article = Article.objects.all()[randint(0, num_arts-1)]

View File

@@ -1,13 +1,12 @@
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
from courseware.decorators import check_course
@login_required
def index(request, course_id=None, page=0):
course_location = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_location)
@check_course
def index(request, course, page=0):
return render_to_response('staticbook.html',{'page':int(page), 'course': course})
def index_shifted(request, page):
return index(request, int(page)+24)
def index_shifted(request, course_id, page):
return index(request, course_id=course_id, page=int(page)+24)

View File

@@ -20,6 +20,7 @@ from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
from django.core.urlresolvers import reverse
from courseware.decorators import check_course
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
from django_future.csrf import ensure_csrf_cookie
@@ -495,11 +496,10 @@ def accept_name_change(request):
@ensure_csrf_cookie
def course_info(request, course_id):
@check_course(course_must_be_open=False)
def course_info(request, course):
# This is the advertising page for a student to look at the course before signing up
csrf_token = csrf(request)['csrf_token']
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc)
# TODO: Couse should be a model
return render_to_response('portal/course_about.html', {'csrf': csrf_token, 'course': course})
@@ -520,10 +520,11 @@ def help(request):
@login_required
@check_course(course_must_be_open=False)
@ensure_csrf_cookie
def enroll(request, course_id):
def enroll(request, course):
user = request.user
enrollment = CourseEnrollment(user=user,
course_id=course_id)
course_id=course.id)
enrollment.save()
return redirect(reverse('dashboard'))

View File

@@ -42,3 +42,26 @@ CACHES = {
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.version.VersionDebugPanel',
'debug_toolbar.panels.timer.TimerDebugPanel',
'debug_toolbar.panels.settings_vars.SettingsVarsDebugPanel',
'debug_toolbar.panels.headers.HeaderDebugPanel',
'debug_toolbar.panels.request_vars.RequestVarsDebugPanel',
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
)

View File

@@ -57,9 +57,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
url(r'^change_setting$', 'student.views.change_setting'),
url(r'^s/(?P<template>[^/]*)$', 'static_template_view.views.auth_index'),
url(r'^book/(?P<page>[^/]*)$', 'staticbook.views.index'),
url(r'^book-shifted/(?P<page>[^/]*)$', 'staticbook.views.index_shifted'),
url(r'^book*$', 'staticbook.views.index'),
# url(r'^course_info/$', 'student.views.courseinfo'),
# url(r'^show_circuit/(?P<circuit>[^/]*)$', 'circuit.views.show_circuit'),
url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
@@ -69,15 +66,21 @@ if settings.COURSEWARE_ENABLED:
# Multicourse related:
url(r'^courses/?$', 'courseware.views.courses', name="courses"),
#About the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$', 'student.views.course_info', name="about_course"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll$', 'student.views.enroll', name="enroll"),
#Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$', 'courseware.views.course_info', name="info"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$', 'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll$', 'student.views.enroll', name="enroll"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$', 'staticbook.views.index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$', 'staticbook.views.index_shifted'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', 'courseware.views.index', name="courseware"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$', 'courseware.views.profile', name="profile"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$', 'student.views.course_info', name="about_course"),
)
# Multicourse wiki