diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index ef46408fde..c06cba62aa 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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.courses import check_course from django_future.csrf import ensure_csrf_cookie from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment from util.cache import cache_if_anonymous @@ -496,10 +497,9 @@ def accept_name_change(request): @ensure_csrf_cookie @cache_if_anonymous def course_info(request, course_id): + course = check_course(course_id, course_must_be_open=False) # 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', {'course': course}) @@ -507,8 +507,10 @@ def course_info(request, course_id): @login_required @ensure_csrf_cookie def enroll(request, course_id): + course = check_course(course_id, course_must_be_open=False) + user = request.user enrollment = CourseEnrollment(user=user, - course_id=course_id) + course_id=course.id) enrollment.save() return redirect(reverse('dashboard')) diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 5f74873465..884dc40df2 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -1,3 +1,5 @@ +import time +import dateutil.parser from fs.errors import ResourceNotFoundError import logging from path import path @@ -13,6 +15,21 @@ log = logging.getLogger(__name__) class CourseDescriptor(SequenceDescriptor): module_class = SequenceModule + def __init__(self, system, definition=None, **kwargs): + super(CourseDescriptor, self).__init__(system, definition, **kwargs) + + try: + self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") + except KeyError: + self.start = time.gmtime(0) #The epoch + log.critical("Course loaded without a start date. " + str(self.id)) + except ValueError, e: + self.start = time.gmtime(0) #The epoch + log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'") + + def has_started(self): + return time.gmtime() > self.start + @classmethod def id_to_location(cls, course_id): org, course, name = course_id.split('/') @@ -25,7 +42,7 @@ class CourseDescriptor(SequenceDescriptor): @property def title(self): return self.metadata['display_name'] - + @property def number(self): return self.location.course diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index ed678fdd0e..e1ad27beea 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -214,7 +214,7 @@ class XModuleDescriptor(Plugin): # A list of metadata that this module can inherit from its parent module inheritable_metadata = ( - 'graded', 'due', 'graceperiod', 'showanswer', 'rerandomize', + 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', # This is used by the XMLModuleStore to provide for locations for static files, # and will need to be removed when that code is removed @@ -251,6 +251,7 @@ class XModuleDescriptor(Plugin): display_name: The name to use for displaying this module to the user format: The format of this module ('Homework', 'Lab', etc) graded (bool): Whether this module is should be graded or not + start (string): The date for which this module will be available due (string): The due date for this module graceperiod (string): The amount of grace period to allow when enforcing the due date showanswer (string): When to show answers for this module diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index ce41ff68bc..eeceeb3c1c 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -88,7 +88,7 @@ class XmlDescriptor(XModuleDescriptor): # The attributes will be removed from the definition xml passed # to definition_from_xml, and from the xml returned by definition_to_xml metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', - 'due', 'graded', 'name', 'slug') + 'start', 'due', 'graded', 'name', 'slug') # A dictionary mapping xml attribute names to functions of the value # that return the metadata key and value diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 858cdb1c87..b6853ae12a 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -1,62 +1,33 @@ -from collections import namedtuple -import logging -import os +from functools import wraps -from path import path -import yaml +from django.http import Http404 -log = logging.getLogger('mitx.courseware.courses') +from xmodule.course_module import CourseDescriptor +from xmodule.modulestore.django import modulestore -_FIELDS = ['number', # 6.002x - 'title', # Circuits and Electronics - 'short_title', # Circuits - 'run_id', # Spring 2012 - 'path', # /some/absolute/filepath/6.002x --> course.xml is in here. - 'instructors', # ['Anant Agarwal'] - 'institution', # "MIT" - 'wiki_namespace', - 'grader', # a courseware.graders.CourseGrader object - #'start', # These should be datetime fields - #'end' - ] - -class CourseInfoLoadError(Exception): - pass - -class Course(namedtuple('Course', _FIELDS)): - """Course objects encapsulate general information about a given run of a - course. This includes things like name, grading policy, etc. +def check_course(course_id, course_must_be_open=True, course_required=True): """ - -def load_courses(courses_path): - """Given a directory of courses, returns a list of Course objects. For the - sake of backwards compatibility, if you point it at the top level of a - specific course, it will return a list with one Course object in it. + 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. + + If course_must_be_open is False, the course will be returned + without a 404 even if it is not open. + + 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. """ - courses_path = path(courses_path) - def _is_course_path(p): - return os.path.exists(p / "course_info.yaml") - - log.info("Loading courses from {0}".format(courses_path)) - - # Compatibility: courses_path is the path for a single course - if _is_course_path(courses_path): - log.warning("course_info.yaml found in top-level ({0})" - .format(courses_path) + - " -- assuming there is only a single course.") - return [Course.load_from_path(courses_path)] - - # Default: Each dir in courses_path is a separate course - courses = [] - log.info("Reading courses from {0}".format(courses_path)) - for course_dir_name in os.listdir(courses_path): - course_path = courses_path / course_dir_name - if _is_course_path(course_path): - log.info("Initializing course {0}".format(course_path)) - courses.append(Course.load_from_path(course_path)) - - return courses - -def create_lookup_table(courses): - return dict((c.id, c) for c in courses) + 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("This course has not yet started.") + + return course diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 60dc8c0c5f..c8391e0483 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -16,18 +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, cache_if_anonymous from student.models import UserTestGroup from courseware import grades +from courseware.courses import check_course +from xmodule.modulestore.django import modulestore log = logging.getLogger("mitx.courseware") template_imports = {'urllib': urllib} - def user_groups(user): if not user.is_authenticated(): return [] @@ -57,20 +56,19 @@ def courses(request): context = {'courses': modulestore().get_courses()} return render_to_response("courses.html", context) - @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request, course_id): if 'course_admin' not in user_groups(request.user): raise Http404 - - course_location = CourseDescriptor.id_to_location(course_id) - + course = check_course(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, @@ -87,8 +85,8 @@ def gradebook(request, course_id): def profile(request, course_id, student_id=None): ''' User profile. Show username, location, etc, as well as grades . We need to allow the user to change some of these settings .''' + course = check_course(course_id) - course_location = CourseDescriptor.id_to_location(course_id) if student_id is None: student = request.user else: @@ -98,8 +96,8 @@ def profile(request, course_id, student_id=None): user_info = UserProfile.objects.get(user=student) - student_module_cache = StudentModuleCache(request.user, modulestore().get_item(course_location)) - course, _, _, _ = get_module(request.user, request, course_location, student_module_cache) + student_module_cache = StudentModuleCache(request.user, course) + course, _, _, _ = get_module(request.user, request, course.location, student_module_cache) context = {'name': user_info.name, 'username': student.username, @@ -142,7 +140,7 @@ def render_accordion(request, course, chapter, section): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -def index(request, course_id=None, chapter=None, section=None, +def index(request, course_id, chapter=None, section=None, position=None): ''' Displays courseware accordion, and any associated content. If course, chapter, and section aren't all specified, just returns @@ -161,6 +159,8 @@ def index(request, course_id=None, chapter=None, section=None, - HTTPresponse ''' + course = check_course(course_id) + def clean(s): ''' Fixes URLs -- we convert spaces to _ in URLs to prevent funny encoding characters and keep the URLs readable. This undoes @@ -168,9 +168,6 @@ def index(request, course_id=None, chapter=None, section=None, ''' return s.replace('_', ' ') if s is not None else None - course_location = CourseDescriptor.id_to_location(course_id) - course = modulestore().get_item(course_location) - chapter = clean(chapter) section = clean(section) @@ -249,12 +246,6 @@ def jump_to(request, probname=None): @ensure_csrf_cookie def course_info(request, course_id): - csrf_token = csrf(request)['csrf_token'] + course = check_course(course_id) - try: - course_location = CourseDescriptor.id_to_location(course_id) - course = modulestore().get_item(course_location) - except KeyError: - raise Http404("Course not found") - - return render_to_response('info.html', {'csrf': csrf_token, 'course': course}) + return render_to_response('info.html', {'course': course}) diff --git a/lms/djangoapps/simplewiki/views.py b/lms/djangoapps/simplewiki/views.py index 81ce8abf76..a0f1463fe5 100644 --- a/lms/djangoapps/simplewiki/views.py +++ b/lms/djangoapps/simplewiki/views.py @@ -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.courses 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) - + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, article_path, course ) if err: return err @@ -72,7 +63,8 @@ def view(request, article_path, course_id=None): 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) + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, article_path, course ) if err: return err @@ -93,24 +85,26 @@ 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) + course = check_course(course_id, course_required=False) + + #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) - +def create(request, article_path, course_id=None): + course = check_course(course_id, course_required=False) + article_path_components = article_path.split('/') # Ensure the namespace exists @@ -169,7 +163,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) + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, article_path, course ) if err: return err @@ -215,7 +210,8 @@ def edit(request, article_path, course_id=None): return render_to_response('simplewiki/simplewiki_edit.html', d) def history(request, article_path, page=1, course_id=None): - course = get_course(course_id) + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, article_path, course ) if err: return err @@ -295,9 +291,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) + course = check_course(course_id, course_required=False) page_size = 10 @@ -329,6 +324,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): + course = check_course(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. # Adding some context to results (eg where matches were) would also be nice. @@ -339,9 +336,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 +372,9 @@ 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): + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, slug, namespace if namespace else course_id ) if err: return err @@ -409,6 +405,8 @@ def search_add_related(request, course_id, slug, namespace): return HttpResponse(json, mimetype='application/json') def add_related(request, course_id, slug, namespace): + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, slug, namespace if namespace else course_id ) if err: return err @@ -430,6 +428,8 @@ def add_related(request, course_id, slug, namespace): return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) def remove_related(request, course_id, namespace, slug, related_id): + course = check_course(course_id, course_required=False) + (article, err) = get_article(request, slug, namespace if namespace else course_id ) if err: @@ -449,8 +449,9 @@ 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) +def random_article(request, course_id=None): + course = check_course(course_id, course_required=False) + from random import randint num_arts = Article.objects.count() article = Article.objects.all()[randint(0, num_arts-1)] diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index 3bc62c2dbf..b7633dd1d3 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -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.courses 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) +def index(request, course_id, page=0): + course = check_course(course_id) 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) diff --git a/lms/envs/common.py b/lms/envs/common.py index ed84d9efb8..ea07b8030e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -124,6 +124,8 @@ COURSE_TITLE = "Circuits and Electronics" ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome) QUICKEDIT = False +WIKI_ENABLED = False + ### COURSE_DEFAULT = '6.002x_Fall_2012' diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 3f7eba922c..ff4e853c12 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -13,6 +13,8 @@ from .logsettings import get_logger_config DEBUG = True TEMPLATE_DEBUG = True +WIKI_ENABLED = True + LOGGING = get_logger_config(ENV_ROOT / "log", logging_env="dev", tracking_filename="tracking.log", diff --git a/lms/envs/devplus.py b/lms/envs/devplus.py index 32e0a7beb7..b15322c2c7 100644 --- a/lms/envs/devplus.py +++ b/lms/envs/devplus.py @@ -15,6 +15,8 @@ Dir structure: """ from .dev import * +WIKI_ENABLED = True + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -42,3 +44,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', +) diff --git a/lms/urls.py b/lms/urls.py index 26d9c0df35..2cc1b88971 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -98,9 +98,6 @@ if settings.COURSEWARE_ENABLED: url(r'^xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.xqueue_callback'), url(r'^change_setting$', 'student.views.change_setting'), url(r'^s/(?P