Commit 04b8dbe0 by bridger

Merge pull request #179 from MITx/course_start

Course start
parents 08ddd03b 2f4cf23b
......@@ -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'))
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('/')
......
......@@ -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
......
......@@ -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
......
from collections import namedtuple
import logging
import os
from path import path
import yaml
log = logging.getLogger('mitx.courseware.courses')
_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.
from functools import wraps
from django.http import Http404
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
def check_course(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.
If course_must_be_open is False, the course will be returned
without a 404 even if it is not open.
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.
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
......@@ -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 = check_course(course_id)
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,
......@@ -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']
try:
course_location = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_location)
except KeyError:
raise Http404("Course not found")
course = check_course(course_id)
return render_to_response('info.html', {'csrf': csrf_token, 'course': course})
return render_to_response('info.html', {'course': course})
......@@ -9,21 +9,13 @@ 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'}
if not 'course_id' in kwargs and course:
......@@ -55,9 +47,8 @@ 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:
......@@ -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,23 +85,25 @@ 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)
course = check_course(course_id, course_required=False)
article_path_components = article_path.split('/')
......@@ -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.
......@@ -340,8 +337,6 @@ def search_articles(request, namespace=None, course_id = None):
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)]
......
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)
......@@ -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'
......
......@@ -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",
......
......@@ -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',
)
......@@ -98,9 +98,6 @@ if settings.COURSEWARE_ENABLED:
url(r'^xqueue/(?P<username>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'),
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'),
......@@ -110,12 +107,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>[^/]*)/$',
......@@ -124,14 +130,10 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.profile', name="profile"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
'courseware.views.profile'),
# TODO (vshnayder): there is no student.views.course_info.
# Where should this point instead? same as the info view?
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
'student.views.course_info', name="about_course"),
)
# Multicourse wiki
if settings.WIKI_ENABLED:
urlpatterns += (
url(r'^wiki/', include('simplewiki.urls')),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/wiki/', include('simplewiki.urls')),
......
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