Commit cf28dbea by Rocky Duan

Merge branch 'feature/bk_forum_int'

parents f82c2e00 bca6699b
......@@ -6,3 +6,4 @@ gfortran
python
yuicompressor
node
graphviz
......@@ -12,7 +12,6 @@ $bright-blue: #3c8ebf;
$orange: #f96e5b;
$yellow: #fff8af;
$cream: #F6EFD4;
$mit-red: #933;
$border-color: #ddd;
@mixin hide-text {
......
......@@ -102,7 +102,7 @@ def main_index(request, extra_context={}, user=None):
def course_from_id(course_id):
"""Return the CourseDescriptor corresponding to this course_id"""
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_item(course_loc)
return modulestore().get_instance(course_id, course_loc)
def press(request):
......
......@@ -83,6 +83,9 @@ class XQueueInterface(object):
if error and (msg == 'login_required'): # Log in, then try again
self._login()
if files_to_upload is not None:
for f in files_to_upload: # Need to rewind file pointers
f.seek(0)
(error, msg) = self._send_to_queue(header, body, files_to_upload)
return (error, msg)
......
from fs.errors import ResourceNotFoundError
import time
import logging
import requests
from lxml import etree
from xmodule.util.decorators import lazyproperty
......@@ -15,18 +16,44 @@ class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
class Textbook:
def __init__(self, title, table_of_contents_url):
def __init__(self, title, book_url):
self.title = title
self.table_of_contents_url = table_of_contents_url
self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3()
@classmethod
def from_xml_object(cls, xml_object):
return cls(xml_object.get('title'), xml_object.get('table_of_contents_url'))
return cls(xml_object.get('title'), xml_object.get('book_url'))
@property
def table_of_contents(self):
raw_table_of_contents = open(self.table_of_contents_url, 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
return self.table_of_contents
def _get_toc_from_s3(self):
'''
Accesses the textbook's table of contents (default name "toc.xml") at the URL self.book_url
Returns XML tree representation of the table of contents
'''
toc_url = self.book_url + 'toc.xml'
# Get the table of contents from S3
log.info("Retrieving textbook table of contents from %s" % toc_url)
try:
r = requests.get(toc_url)
except Exception as err:
msg = 'Error %s: Unable to retrieve textbook table of contents at %s' % (err, toc_url)
log.error(msg)
raise Exception(msg)
# TOC is XML. Parse it
try:
table_of_contents = etree.fromstring(r.text)
except Exception as err:
msg = 'Error %s: Unable to parse XML for textbook table of contents at %s' % (err, toc_url)
log.error(msg)
raise Exception(msg)
return table_of_contents
......@@ -135,6 +162,10 @@ class CourseDescriptor(SequenceDescriptor):
@staticmethod
def make_id(org, course, url_name):
return '/'.join([org, course, url_name])
@staticmethod
def id_to_location(course_id):
'''Convert the given course_id (org/course/name) to a location object.
Throws ValueError if course_id is of the wrong format.
......
h2 {
margin-top: 0;
margin-bottom: 15px;
width: flex-grid(2, 9);
padding-right: flex-gutter(9);
border-right: 1px dashed #ddd;
@include box-sizing(border-box);
display: table-cell;
vertical-align: top;
&.problem-header {
section.staff {
......@@ -15,12 +9,6 @@ h2 {
}
}
@media screen and (max-width:1120px) {
display: block;
width: auto;
border-right: 0;
}
@media print {
display: block;
width: auto;
......@@ -29,16 +17,6 @@ h2 {
}
section.problem {
display: table-cell;
width: flex-grid(7, 9);
padding-left: flex-gutter(9);
@media screen and (max-width:1120px) {
display: block;
width: auto;
padding: 0;
}
@media print {
display: block;
width: auto;
......@@ -292,4 +270,10 @@ section.problem {
border: 1px solid #ccc;
padding: lh();
}
section.action {
input.save {
@extend .blue-button;
}
}
}
......@@ -305,11 +305,11 @@ div.video {
@include box-shadow(0 1px 0 #333);
a.ui-slider-handle {
background: $mit-red url(../images/slider-handle.png) center center no-repeat;
background: $pink url(../images/slider-handle.png) center center no-repeat;
@include background-size(50%);
border: 1px solid darken($mit-red, 20%);
border: 1px solid darken($pink, 20%);
@include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
cursor: pointer;
height: 15px;
left: -6px;
......@@ -408,6 +408,7 @@ div.video {
cursor: pointer;
margin-bottom: 8px;
padding: 0;
line-height: lh();
&.current {
color: #333;
......@@ -415,7 +416,7 @@ div.video {
}
&:hover {
color: $mit-red;
color: $blue;
}
&:empty {
......
......@@ -223,6 +223,13 @@ class ModuleStore(object):
"""
raise NotImplementedError
def get_instance(self, course_id, location):
"""
Get an instance of this location, with policy for course_id applied.
TODO (vshnayder): this may want to live outside the modulestore eventually
"""
raise NotImplementedError
def get_item_errors(self, location):
"""
Return a list of (msg, exception-or-None) errors that the modulestore
......@@ -331,7 +338,8 @@ class ModuleStoreBase(ModuleStore):
and datastores.
"""
# check that item is present and raise the promised exceptions if needed
self.get_item(location)
# TODO (vshnayder): post-launch, make errors properties of items
#self.get_item(location)
errorlog = self._get_errorlog(location)
return errorlog.errors
......@@ -217,6 +217,13 @@ class MongoModuleStore(ModuleStoreBase):
item = self._find_one(location)
return self._load_items([item], depth)[0]
def get_instance(self, course_id, location):
"""
TODO (vshnayder): implement policy tracking in mongo.
For now, just delegate to get_item and ignore policy.
"""
return self.get_item(location)
def get_items(self, location, depth=0):
items = self.collection.find(
location_to_query(location),
......
......@@ -3,6 +3,7 @@ import logging
import os
import re
from collections import defaultdict
from fs.osfs import OSFS
from importlib import import_module
from lxml import etree
......@@ -33,7 +34,7 @@ def clean_out_mako_templating(xml_string):
return xml_string
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, org, course, course_dir,
def __init__(self, xmlstore, course_id, course_dir,
policy, error_tracker, **kwargs):
"""
A class that handles loading from xml. Does some munging to ensure that
......@@ -43,6 +44,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
"""
self.unnamed_modules = 0
self.used_slugs = set()
self.org, self.course, self.url_name = course_id.split('/')
def process_xml(xml):
"""Takes an xml string, and returns a XModuleDescriptor created from
......@@ -80,21 +82,24 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xml_data.set('url_name', slug)
descriptor = XModuleDescriptor.load_from_xml(
etree.tostring(xml_data), self, org,
course, xmlstore.default_class)
etree.tostring(xml_data), self, self.org,
self.course, xmlstore.default_class)
#log.debug('==> importing descriptor location %s' %
# repr(descriptor.location))
descriptor.metadata['data_dir'] = course_dir
xmlstore.modules[descriptor.location] = descriptor
xmlstore.modules[course_id][descriptor.location] = descriptor
if xmlstore.eager:
descriptor.get_children()
return descriptor
render_template = lambda: ''
load_item = xmlstore.get_item
# TODO (vshnayder): we are somewhat architecturally confused in the loading code:
# load_item should actually be get_instance, because it expects the course-specific
# policy to be loaded. For now, just add the course_id here...
load_item = lambda location: xmlstore.get_instance(course_id, location)
resources_fs = OSFS(xmlstore.data_dir / course_dir)
MakoDescriptorSystem.__init__(self, load_item, resources_fs,
......@@ -127,7 +132,7 @@ class XMLModuleStore(ModuleStoreBase):
self.eager = eager
self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor
self.modules = defaultdict(dict) # course_id -> dict(location -> XModuleDescriptor)
self.courses = {} # course_dir -> XModuleDescriptor for the course
if default_class is None:
......@@ -236,14 +241,24 @@ class XMLModuleStore(ModuleStoreBase):
tracker(msg)
course = course_dir
url_name = course_data.get('url_name')
url_name = course_data.get('url_name', course_data.get('slug'))
if url_name:
policy_path = self.data_dir / course_dir / 'policies' / '{0}.json'.format(url_name)
policy = self.load_policy(policy_path, tracker)
else:
policy = {}
# VS[compat] : 'name' is deprecated, but support it for now...
if course_data.get('name'):
url_name = Location.clean(course_data.get('name'))
tracker("'name' is deprecated for module xml. Please use "
"display_name and url_name.")
else:
raise ValueError("Can't load a course without a 'url_name' "
"(or 'name') set. Set url_name.")
system = ImportSystem(self, org, course, course_dir, policy, tracker)
course_id = CourseDescriptor.make_id(org, course, url_name)
system = ImportSystem(self, course_id, course_dir, policy, tracker)
course_descriptor = system.process_xml(etree.tostring(course_data))
......@@ -257,11 +272,12 @@ class XMLModuleStore(ModuleStoreBase):
return course_descriptor
def get_item(self, location, depth=0):
def get_instance(self, course_id, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the most item with the most
recent revision
Returns an XModuleDescriptor instance for the item at
location, with the policy for course_id. (In case two xml
dirs have different content at the same location, return the
one for this course_id.)
If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError
......@@ -273,10 +289,27 @@ class XMLModuleStore(ModuleStoreBase):
"""
location = Location(location)
try:
return self.modules[location]
return self.modules[course_id][location]
except KeyError:
raise ItemNotFoundError(location)
def get_item(self, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the most item with the most
recent revision
If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError
If no object is found at that location, raises
xmodule.modulestore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
"""
raise NotImplementedError("XMLModuleStores can't guarantee that definitions"
" are unique. Use get_instance.")
def get_courses(self, depth=0):
"""
......
......@@ -22,21 +22,22 @@ def import_from_xml(store, data_dir, course_dirs=None, eager=True,
eager=eager,
course_dirs=course_dirs
)
for module in module_store.modules.itervalues():
# TODO (cpennington): This forces import to overrite the same items.
# This should in the future create new revisions of the items on import
try:
store.create_item(module.location)
except DuplicateItemError:
log.exception('Item already exists at %s' % module.location.url())
pass
if 'data' in module.definition:
store.update_item(module.location, module.definition['data'])
if 'children' in module.definition:
store.update_children(module.location, module.definition['children'])
# NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere.
store.update_metadata(module.location, dict(module.own_metadata))
for course_id in module_store.modules.keys():
for module in module_store.modules[course_id].itervalues():
# TODO (cpennington): This forces import to overrite the same items.
# This should in the future create new revisions of the items on import
try:
store.create_item(module.location)
except DuplicateItemError:
log.exception('Item already exists at %s' % module.location.url())
pass
if 'data' in module.definition:
store.update_item(module.location, module.definition['data'])
if 'children' in module.definition:
store.update_children(module.location, module.definition['children'])
# NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere.
store.update_metadata(module.location, dict(module.own_metadata))
return module_store
......@@ -8,6 +8,7 @@
import unittest
import os
import fs
import fs.osfs
import json
import json
......
......@@ -84,20 +84,22 @@ class RoundTripTestCase(unittest.TestCase):
strip_filenames(exported_course)
self.assertEquals(initial_course, exported_course)
self.assertEquals(initial_course.id, exported_course.id)
course_id = initial_course.id
print "Checking key equality"
self.assertEquals(sorted(initial_import.modules.keys()),
sorted(second_import.modules.keys()))
self.assertEquals(sorted(initial_import.modules[course_id].keys()),
sorted(second_import.modules[course_id].keys()))
print "Checking module equality"
for location in initial_import.modules.keys():
for location in initial_import.modules[course_id].keys():
print "Checking", location
if location.category == 'html':
print ("Skipping html modules--they can't import in"
" final form without writing files...")
continue
self.assertEquals(initial_import.modules[location],
second_import.modules[location])
self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[course_id][location])
def setUp(self):
......
......@@ -207,3 +207,48 @@ class ImportTestCase(unittest.TestCase):
check_for_key(key, c)
check_for_key('graceperiod', course)
def test_policy_loading(self):
"""Make sure that when two courses share content with the same
org and course names, policy applies to the right one."""
def get_course(name):
print "Importing {0}".format(name)
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=[name])
courses = modulestore.get_courses()
self.assertEquals(len(courses), 1)
return courses[0]
toy = get_course('toy')
two_toys = get_course('two_toys')
self.assertEqual(toy.url_name, "2012_Fall")
self.assertEqual(two_toys.url_name, "TT_2012_Fall")
toy_ch = toy.get_children()[0]
two_toys_ch = two_toys.get_children()[0]
self.assertEqual(toy_ch.display_name, "Overview")
self.assertEqual(two_toys_ch.display_name, "Two Toy Overview")
def test_definition_loading(self):
"""When two courses share the same org and course name and
both have a module with the same url_name, the definitions shouldn't clash.
TODO (vshnayder): once we have a CMS, this shouldn't
happen--locations should uniquely name definitions. But in
our imperfect XML world, it can (and likely will) happen."""
modulestore = XMLModuleStore(DATA_DIR, eager=True, course_dirs=['toy', 'two_toys'])
toy_id = "edX/toy/2012_Fall"
two_toy_id = "edX/toy/TT_2012_Fall"
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh8")
self.assertEqual(two_toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh9")
A copy of the toy course, with different metadata. Used to test policy loading for course with identical org course fields and shared content.
<chapter>
<videosequence url_name="Toy_Videos"/>
<video url_name="Welcome"/>
</chapter>
<course url_name="TT_2012_Fall" org="edX" course="toy"/>
<course display_name="Toy Course" graceperiod="2 days 5 hours 59 minutes 59 seconds" start="2015-07-17T12:00">
<chapter url_name="Overview"/>
</course>
{
"course/TT_2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Two Toys Course"
},
"chapter/Overview": {
"display_name": "Two Toy Overview"
},
"videosequence/Toy_Videos": {
"display_name": "Toy Videos",
"format": "Lecture Sequence"
},
"html/toylab": {
"display_name": "Toy lab"
},
"video/Video_Resources": {
"display_name": "Video Resources"
},
"video/Welcome": {
"display_name": "Welcome"
}
}
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/>
<video youtube="1.0:p2Q6BrNhdh9" display_name="Welcome"/>
<videosequence display_name="Toy Videos" format="Lecture Sequence">
<video url_name="Video_Resources"/>
</videosequence>
......@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt"
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log"
APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript"
APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz"
if [[ $EUID -eq 0 ]]; then
error "This script should not be run using sudo or as the root user"
......
......@@ -74,5 +74,4 @@ There is also a script "create-dev-env.sh" that automates these steps.
$ django-admin.py syncdb --settings=envs.dev --pythonpath=.
$ django-admin.py migrate --settings=envs.dev --pythonpath=.
$ django-admin.py runserver --settings=envs.dev --pythonpath=.
......@@ -36,7 +36,7 @@ class WikiRedirectTestCase(PageLoader):
"""
Test that requesting wiki URLs redirect properly to or out of classes.
An enrolled in student going from /courses/edX/toy/2012_Fall/profile
An enrolled in student going from /courses/edX/toy/2012_Fall/progress
to /wiki/some/fake/wiki/page/ will redirect to
/courses/edX/toy/2012_Fall/wiki/some/fake/wiki/page/
......@@ -48,10 +48,10 @@ class WikiRedirectTestCase(PageLoader):
self.enroll(self.toy)
referer = reverse("profile", kwargs={ 'course_id' : self.toy.id })
referer = reverse("progress", kwargs={ 'course_id' : self.toy.id })
destination = reverse("wiki:get", kwargs={'path': 'some/fake/wiki/page/'})
redirected_to = referer.replace("profile", "wiki/some/fake/wiki/page/")
redirected_to = referer.replace("progress", "wiki/some/fake/wiki/page/")
resp = self.client.get( destination, HTTP_REFERER=referer)
self.assertEqual(resp.status_code, 302 )
......@@ -77,11 +77,11 @@ class WikiRedirectTestCase(PageLoader):
"""
course_wiki_home = reverse('course_wiki', kwargs={'course_id' : course.id})
referer = reverse("profile", kwargs={ 'course_id' : self.toy.id })
referer = reverse("progress", kwargs={ 'course_id' : self.toy.id })
resp = self.client.get(course_wiki_home, follow=True, HTTP_REFERER=referer)
course_wiki_page = referer.replace('profile', 'wiki/' + self.toy.wiki_slug + "/")
course_wiki_page = referer.replace('progress', 'wiki/' + self.toy.wiki_slug + "/")
ending_location = resp.redirect_chain[-1][0]
ending_status = resp.redirect_chain[-1][1]
......
......@@ -25,7 +25,7 @@ def get_course_by_id(course_id):
"""
try:
course_loc = CourseDescriptor.id_to_location(course_id)
return modulestore().get_item(course_loc)
return modulestore().get_instance(course_id, course_loc)
except (KeyError, ItemNotFoundError):
raise Http404("Course not found.")
......
......@@ -9,6 +9,7 @@ from django.conf import settings
from models import StudentModuleCache
from module_render import get_module, get_instance_module
from xmodule import graders
from xmodule.course_module import CourseDescriptor
from xmodule.graders import Score
from models import StudentModule
......@@ -63,8 +64,10 @@ def grade(student, request, course, student_module_cache=None):
scores = []
# TODO: We need the request to pass into here. If we could forgo that, our arguments
# would be simpler
course_id = CourseDescriptor.location_to_id(course.location)
section_module = get_module(student, request,
section_descriptor.location, student_module_cache)
section_descriptor.location, student_module_cache,
course_id)
if section_module is None:
# student doesn't have access to this module, or something else
# went wrong.
......
import os.path
# THIS COMMAND IS OUT OF DATE
from lxml import etree
from django.core.management.base import BaseCommand
......@@ -72,13 +74,17 @@ class Command(BaseCommand):
# TODO: use args as list of files to check. Fix loading to work for other files.
print "This command needs updating before use"
return
"""
sample_user = User.objects.all()[0]
print "Attempting to load courseware"
# TODO (cpennington): Get coursename in a legitimate way
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(sample_user, modulestore().get_item(course_location))
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
sample_user, modulestore().get_item(course_location))
course = get_module(sample_user, None, course_location, student_module_cache)
to_run = [
......@@ -96,3 +102,4 @@ class Command(BaseCommand):
print 'Courseware passes all checks!'
else:
print "Courseware fails some checks"
"""
......@@ -124,9 +124,11 @@ def check_roundtrip(course_dir):
print "======== ideally there is no diff above this ======="
def clean_xml(course_dir, export_dir):
def clean_xml(course_dir, export_dir, force):
(ok, course) = import_with_checks(course_dir)
if ok:
if ok or force:
if not ok:
print "WARNING: Exporting despite errors"
export(course, export_dir)
check_roundtrip(export_dir)
else:
......@@ -138,11 +140,18 @@ class Command(BaseCommand):
help = """Imports specified course.xml, validate it, then exports in
a canonical format.
Usage: clean_xml PATH-TO-COURSE-DIR PATH-TO-OUTPUT-DIR
Usage: clean_xml PATH-TO-COURSE-DIR PATH-TO-OUTPUT-DIR [force]
If 'force' is specified as the last argument, exports even if there
were import errors.
"""
def handle(self, *args, **options):
if len(args) != 2:
n = len(args)
if n < 2 or n > 3:
print Command.help
return
clean_xml(args[0], args[1])
force = False
if n == 3 and args[2] == 'force':
force = True
clean_xml(args[0], args[1], force)
......@@ -71,13 +71,13 @@ def toc_for_course(user, request, course, active_chapter, active_section, course
'''
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
course = get_module(user, request, course.location, student_module_cache, course_id=course_id)
course = get_module(user, request, course.location, student_module_cache, course_id)
chapters = list()
for chapter in course.get_display_items():
hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
if hide_from_toc:
continue
continue
sections = list()
for section in chapter.get_display_items():
......@@ -131,7 +131,7 @@ def get_section(course_module, chapter, section):
return section_module
def get_module(user, request, location, student_module_cache, position=None, course_id=None):
def get_module(user, request, location, student_module_cache, course_id, position=None):
''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
exists.
......@@ -141,21 +141,14 @@ def get_module(user, request, location, student_module_cache, position=None, cou
- request : current django HTTPrequest
- location : A Location-like object identifying the module to load
- student_module_cache : a StudentModuleCache
- course_id : the course_id in the context of which to load module
- position : extra information from URL for user-specified
position within module
Returns: xmodule instance
'''
descriptor = modulestore().get_item(location)
# NOTE:
# A 'course_id' is understood to be the triplet (org, course, run), for example
# (MITx, 6.002x, 2012_Spring).
# At the moment generic XModule does not contain enough information to replicate
# the triplet (it is missing 'run'), so we must pass down course_id
if course_id is None:
course_id = descriptor.location.course_id # Will NOT produce (org, course, run) for non-CourseModule's
descriptor = modulestore().get_instance(course_id, location)
# Short circuit--if the user shouldn't have access, bail without doing any work
if not has_access(user, descriptor, 'load'):
......@@ -181,7 +174,7 @@ def get_module(user, request, location, student_module_cache, position=None, cou
# Setup system context for module instance
ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=course_id,
id=descriptor.location.url(),
location=descriptor.location.url(),
dispatch=''),
)
......@@ -208,7 +201,7 @@ def get_module(user, request, location, student_module_cache, position=None, cou
Delegate to get_module. It does an access check, so may return None
"""
return get_module(user, request, location,
student_module_cache, position, course_id=course_id)
student_module_cache, course_id, position)
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
......@@ -276,7 +269,7 @@ def get_instance_module(user, module, student_module_cache):
else:
return None
def get_shared_instance_module(user, module, student_module_cache):
def get_shared_instance_module(course_id, user, module, student_module_cache):
"""
Return shared_module is a StudentModule specific to all modules with the same
'shared_state_key' attribute, or None if the module does not elect to
......@@ -284,7 +277,7 @@ def get_shared_instance_module(user, module, student_module_cache):
"""
if user.is_authenticated():
# To get the shared_state_key, we need to descriptor
descriptor = modulestore().get_item(module.location)
descriptor = modulestore().get_instance(course_id, module.location)
shared_state_key = getattr(module, 'shared_state_key', None)
if shared_state_key is not None:
......@@ -325,8 +318,8 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
user = User.objects.get(id=userid)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
user, modulestore().get_item(id), depth=0, select_for_update=True)
instance = get_module(user, request, id, student_module_cache)
user, modulestore().get_instance(course_id, id), depth=0, select_for_update=True)
instance = get_module(user, request, id, student_module_cache, course_id)
if instance is None:
log.debug("No module {0} for user {1}--access denied?".format(id, user))
raise Http404
......@@ -362,7 +355,7 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
return HttpResponse("")
def modx_dispatch(request, dispatch=None, id=None, course_id=None):
def modx_dispatch(request, dispatch, location, course_id):
''' Generic view for extensions. This is where AJAX calls go.
Arguments:
......@@ -371,7 +364,8 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
- dispatch -- the command string to pass through to the module's handle_ajax call
(e.g. 'problem_reset'). If this string contains '?', only pass
through the part before the first '?'.
- id -- the module id. Used to look up the XModule instance
- location -- the module location. Used to look up the XModule instance
- course_id -- defines the course context for this request.
'''
# ''' (fix emacs broken parsing)
......@@ -380,6 +374,12 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
if request.FILES:
for fileinput_id in request.FILES.keys():
inputfiles = request.FILES.getlist(fileinput_id)
if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT:
too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' %\
settings.MAX_FILEUPLOADS_PER_INPUT
return HttpResponse(json.dumps({'success': too_many_files_msg}))
for inputfile in inputfiles:
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\
......@@ -387,16 +387,18 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
return HttpResponse(json.dumps({'success': file_too_big_msg}))
p[fileinput_id] = inputfiles
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
instance = get_module(request.user, request, id, student_module_cache, course_id=course_id)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
request.user, modulestore().get_instance(course_id, location))
instance = get_module(request.user, request, location, student_module_cache, course_id)
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
log.debug("No module {0} for user {1}--access denied?".format(id, user))
log.debug("No module {0} for user {1}--access denied?".format(location, user))
raise Http404
instance_module = get_instance_module(request.user, instance, student_module_cache)
shared_module = get_shared_instance_module(request.user, instance, student_module_cache)
shared_module = get_shared_instance_module(course_id, request.user, instance, student_module_cache)
# Don't track state for anonymous users (who don't have student modules)
if instance_module is not None:
......
......@@ -303,7 +303,7 @@ class TestViewAuth(PageLoader):
'instructor_dashboard',
'gradebook',
'grade_summary',)]
urls.append(reverse('student_profile', kwargs={'course_id': course.id,
urls.append(reverse('student_progress', kwargs={'course_id': course.id,
'student_id': user(self.student).id}))
return urls
......@@ -388,7 +388,7 @@ class TestViewAuth(PageLoader):
list of urls that students should be able to see only
after launch, but staff should see before
"""
urls = reverse_urls(['info', 'courseware', 'profile'], course)
urls = reverse_urls(['info', 'courseware', 'progress'], course)
urls.extend([
reverse('book', kwargs={'course_id': course.id, 'book_index': book.title})
for book in course.textbooks
......@@ -411,7 +411,7 @@ class TestViewAuth(PageLoader):
"""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,
urls.append(reverse('student_progress', kwargs={'course_id': course.id,
'student_id': user(self.student).id}))
return urls
......
......@@ -78,7 +78,7 @@ def courses(request):
'''
Render "find courses" page. The course selection work is done in courseware.courses.
'''
universities = get_courses_by_university(request.user,
universities = get_courses_by_university(request.user,
domain=request.META.get('HTTP_HOST'))
return render_to_response("courses.html", {'universities': universities})
......@@ -153,7 +153,7 @@ def index(request, course_id, chapter=None, section=None,
section_descriptor)
module = get_module(request.user, request,
section_descriptor.location,
student_module_cache, course_id=course_id)
student_module_cache, course_id)
if module is None:
# User is probably being clever and trying to access something
# they don't have access to.
......@@ -168,7 +168,7 @@ def index(request, course_id, chapter=None, section=None,
# Add a list of all the errors...
context['course_errors'] = modulestore().get_item_errors(course.location)
result = render_to_response('courseware.html', context)
result = render_to_response('courseware/courseware.html', context)
except:
# In production, don't want to let a 500 out for any reason
if settings.DEBUG:
......@@ -184,8 +184,9 @@ def index(request, course_id, chapter=None, section=None,
position=position
))
try:
result = render_to_response('courseware-error.html',
{'staff_access': staff_access})
result = render_to_response('courseware/courseware-error.html',
{'staff_access': staff_access,
'course' : course})
except:
result = HttpResponse("There was an unrecoverable error")
......@@ -229,7 +230,7 @@ def course_info(request, course_id):
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
return render_to_response('info.html', {'course': course,
return render_to_response('courseware/info.html', {'course': course,
'staff_access': staff_access,})
......@@ -292,11 +293,10 @@ def news(request, course_id):
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
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.
def progress(request, course_id, student_id=None):
""" User progress. We show the grade bar and every problem score.
Course staff are allowed to see the profiles of students in their class.
Course staff are allowed to see the progress of students in their class.
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
......@@ -310,28 +310,22 @@ def profile(request, course_id, student_id=None):
raise Http404
student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module = get_module(request.user, request, course.location, student_module_cache, course_id=course_id)
course_module = get_module(request.user, request, course.location,
student_module_cache, course_id)
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
courseware_summary = grades.progress_summary(student, course_module,
course.grader, student_module_cache)
grade_summary = grades.grade(request.user, request, course, student_module_cache)
context = {'name': user_info.name,
'username': student.username,
'location': user_info.location,
'language': user_info.language,
'email': student.email,
'course': course,
'csrf': csrf(request)['csrf_token'],
context = {'course': course,
'courseware_summary': courseware_summary,
'grade_summary': grade_summary,
'staff_access': staff_access,
}
context.update()
return render_to_response('profile.html', context)
return render_to_response('courseware/progress.html', context)
......@@ -359,7 +353,7 @@ def gradebook(request, course_id):
}
for student in enrolled_students]
return render_to_response('gradebook.html', {'students': student_info,
return render_to_response('courseware/gradebook.html', {'students': student_info,
'course': course,
'course_id': course_id,
# Checked above
......@@ -374,7 +368,7 @@ def grade_summary(request, course_id):
# For now, just a static page
context = {'course': course,
'staff_access': True,}
return render_to_response('grade_summary.html', context)
return render_to_response('courseware/grade_summary.html', context)
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
......@@ -385,4 +379,4 @@ def instructor_dashboard(request, course_id):
# For now, just a static page
context = {'course': course,
'staff_access': True,}
return render_to_response('instructor_dashboard.html', context)
return render_to_response('courseware/instructor_dashboard.html', context)
......@@ -3,13 +3,16 @@ from django_comment_client.models import Permission, Role
class Command(BaseCommand):
args = ''
args = 'course_id'
help = 'Seed default permisssions and roles'
def handle(self, *args, **options):
administrator_role = Role.objects.get_or_create(name="Administrator", course_id="MITx/6.002x/2012_Fall")[0]
moderator_role = Role.objects.get_or_create(name="Moderator", course_id="MITx/6.002x/2012_Fall")[0]
student_role = Role.objects.get_or_create(name="Student", course_id="MITx/6.002x/2012_Fall")[0]
if len(args) != 1:
raise CommandError("The number of arguments does not match. ")
course_id = args[0]
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
"update_comment", "create_sub_comment", "unvote" , "create_thread",
......
from django.conf import settings
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
......@@ -14,7 +15,7 @@ def index(request, course_id, book_index, page=0):
table_of_contents = textbook.table_of_contents
return render_to_response('staticbook.html',
{'page': int(page), 'course': course,
{'page': int(page), 'course': course, 'book_url': textbook.book_url,
'table_of_contents': table_of_contents,
'staff_access': staff_access})
......
......@@ -19,6 +19,11 @@ EMAIL_BACKEND = 'django_ses.SESBackend'
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
# Disable askbot, enable Berkeley forums
MITX_FEATURES['ENABLE_DISCUSSION'] = False
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
########################### NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc.
with open(ENV_ROOT / "env.json") as env_file:
......@@ -60,3 +65,5 @@ XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
if 'COURSE_ID' in ENV_TOKENS:
ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
COMMENTS_SERVICE_URL = ENV_TOKENS["COMMENTS_SERVICE_URL"]
......@@ -146,6 +146,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
)
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
MAX_FILEUPLOADS_PER_INPUT = 10
# FIXME:
# We should have separate S3 staged URLs in case we need to make changes to
......
SERVICE_HOST = 'http://localhost:4567'
from django.conf import settings
if hasattr(settings, "COMMENTS_SERVICE_URL"):
SERVICE_HOST = settings.COMMENTS_SERVICE_URL
else:
SERVICE_HOST = 'http://localhost:4567'
PREFIX = SERVICE_HOST + '/api/v1'
......@@ -27,5 +27,4 @@ $border-color: #C8C8C8;
$light-gray: #ddd;
$dark-gray: #333;
$mit-red: #993333;
$text-color: $dark-gray;
......@@ -53,6 +53,23 @@ input[type="password"] {
}
}
label {
font-weight: normal;
font-style: normal;
}
input[type="reset"],
input[type="submit"],
input[type="button"],
button,
.button {
@extend .gray-button;
form & {
@extend .gray-button;
}
}
img {
max-width: 100%;
......
......@@ -6,28 +6,34 @@ h1.top-header {
padding-bottom: lh();
}
.light-button, a.light-button {
border: 1px solid #ccc;
@include border-radius(3px);
@include box-shadow(inset 0 1px 0 #fff);
color: #666;
cursor: pointer;
font: 400 $body-font-size $body-font-family;
@include linear-gradient(#fff, lighten(#888, 40%));
padding: 4px 8px;
text-decoration: none;
text-shadow: none;
.button-reset {
text-transform: none;
letter-spacing: 0;
-webkit-font-smoothing: antialiased;
&:hover, &:focus {
border: 1px solid #ccc;
@include linear-gradient(#fff, lighten(#888, 37%));
&:hover {
text-decoration: none;
}
}
.light-button, a.light-button, // only used in askbot as classes
.gray-button {
@include button(simple, #eee);
@extend .button-reset;
font-size: em(13);
}
.blue-button {
@include button(simple, $blue);
@extend .button-reset;
font-size: em(13);
}
.pink-button {
@include button(simple, $pink);
@extend .button-reset;
font-size: em(13);
}
.content {
@include box-sizing(border-box);
display: table-cell;
......
......@@ -159,7 +159,7 @@ div.course-wrapper {
a.ui-slider-handle {
@include box-shadow(inset 0 1px 0 lighten($pink, 10%));
background: $mit-red url(../images/slider-bars.png) center center no-repeat;
background: $pink url(../images/slider-bars.png) center center no-repeat;
border: 1px solid darken($pink, 20%);
cursor: pointer;
......
......@@ -91,7 +91,7 @@ div.answer-block {
div.deleted {
p {
color: $mit-red;
color: $pink;
}
}
......@@ -113,7 +113,7 @@ div.paginator {
&.curr {
background: none;
color: $mit-red;
color: $pink;
font-weight: bold;
}
......
......@@ -76,6 +76,6 @@ body.askbot {
}
.acSelect {
background-color: $mit-red;
background-color: $pink;
color: #fff;
}
......@@ -16,6 +16,18 @@ form.answer-form {
margin-top: 15px;
resize: vertical;
width: 99%;
&#editor {
min-height: em(120);
}
}
div.checkbox {
margin-bottom: lh();
label {
display: inline;
}
}
div.form-item {
......@@ -97,7 +109,7 @@ form.answer-form {
margin-left: 2.5%;
padding-left: 1.5%;
border-left: 1px dashed #ddd;
color: $mit-red;
color: $pink;
}
ul, ol, pre {
......@@ -141,32 +153,32 @@ form.question-form {
}
div#question-list {
background-color: rgba(255,255,255,0.95);
@include box-sizing(border-box);
margin-top: -15px;
max-width: 505px;
min-width: 300px;
overflow: hidden;
padding-left: 5px;
position: absolute;
width: 35%;
z-index: 9999;
h2 {
text-transform: none;
padding: 8px 0;
border-bottom: 1px solid #eee;
margin: 0;
span {
background: #eee;
color: #555;
padding: 2px 5px;
@include border-radius(2px);
margin-right: 5px;
background-color: rgba(255,255,255,0.95);
@include box-sizing(border-box);
margin-top: -15px;
max-width: 505px;
min-width: 300px;
overflow: hidden;
padding-left: 5px;
position: absolute;
width: 35%;
z-index: 9999;
h2 {
text-transform: none;
padding: 8px 0;
border-bottom: 1px solid #eee;
margin: 0;
span {
background: #eee;
color: #555;
padding: 2px 5px;
@include border-radius(2px);
margin-right: 5px;
}
}
}
}
}
// Style for modal boxes that pop up to notify the user of various events
.vote-notification {
background-color: darken($mit-red, 7%);
background-color: darken(#666, 7%);
@include border-radius(4px);
@include box-shadow(0px 2px 9px #aaa);
color: white;
......@@ -14,12 +14,12 @@
z-index: 1;
h3 {
background: $mit-red;
background: #666;
padding: 10px 10px 10px 10px;
font-size: 13px;
margin-bottom: 5px;
border-bottom: darken(#8e0000, 10%) 1px solid;
@include box-shadow(0 1px 0 lighten($mit-red, 10%));
border-bottom: darken(#666, 10%) 1px solid;
@include box-shadow(0 1px 0 lighten(#666, 10%));
color: #fff;
font-weight: normal;
@include border-radius(4px 4px 0 0);
......
......@@ -4,7 +4,7 @@ div.question-header {
@include clearfix();
div.official-stamp {
background: $mit-red;
background: $pink;
color: #fff;
font-size: 12px;
margin-left: -1px;
......@@ -120,7 +120,7 @@ div.question-header {
margin-left: 2.5%;
padding-left: 1.5%;
border-left: 1px dashed #ddd;
color: $mit-red;;
color: $pink;
}
ul, ol, pre {
......@@ -217,13 +217,13 @@ div.question-header {
form.post-comments {
padding: 15px;
button {
color: #fff;
button:first-of-type {
@extend .blue-button;
}
button:last-child {
margin-left: 10px;
@extend .light-button;
float: right;
}
}
......@@ -352,7 +352,7 @@ div.question-header {
}
div.question-status {
background: $mit-red;
background: $pink;
clear:both;
color: #fff;
display: block;
......
......@@ -43,8 +43,8 @@ div.discussion-wrapper aside {
width: 27%;
float: right;
text-align: center;
padding-left: 0;
padding-right: 0;
padding: 4px 0;
text-transform: capitalize;
}
input[type="text"] {
......@@ -300,7 +300,7 @@ div.discussion-wrapper aside {
border-top: 0;
a {
@extend .light-button;
@extend .gray-button;
@include box-sizing(border-box);
display: block;
text-align: center;
......
......@@ -38,7 +38,7 @@ def url_class(url):
<li class="wiki"><a href="${reverse('course_wiki', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
% endif
% if user.is_authenticated():
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
<li class="profile"><a href="${reverse('progress', args=[course.id])}" class="${url_class('progress')}">Progress</a></li>
% endif
% if staff_access:
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
......
<%inherit file="main.html" />
<%inherit file="/main.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="bodyclass">courseware</%block>
<%block name="title"><title>Courseware – edX</title></%block>
<%include file="course_navigation.html" args="active_page='courseware'" />
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
<section class="container">
<section class="outside-app">
......
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">courseware</%block>
<%block name="title"><title>Courseware – MITx 6.002x</title></%block>
<%block name="title"><title>${course.number} Courseware</title></%block>
<%block name="headextra">
<%static:css group='course'/>
......@@ -22,7 +22,7 @@
<%static:js group='courseware'/>
<%include file="discussion/_js_dependencies.html" />
<%include file="/mathjax_include.html" />
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript">
......@@ -35,7 +35,7 @@
</script>
</%block>
<%include file="course_navigation.html" args="active_page='courseware'" />
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
<section class="container">
<div class="course-wrapper">
......
<%inherit file="main.html" />
<%inherit file="/main.html" />
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/>
<%namespace name='static' file='/static_content.html'/>
<%include file="course_navigation.html" args="active_page=''" />
<%include file="/courseware/course_navigation.html" args="active_page=''" />
<section class="container">
<div class="gradebook-summary-wrapper">
......
<%inherit file="main.html" />
<%inherit file="/main.html" />
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/>
<%namespace name='static' file='/static_content.html'/>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
......@@ -28,7 +28,7 @@
</%block>
<%include file="course_navigation.html" args="active_page=''" />
<%include file="/courseware/course_navigation.html" args="active_page=''" />
<section class="container">
<div class="gradebook-wrapper">
......@@ -49,7 +49,7 @@
%for student in students:
<tr>
<td>
<a href="${reverse('student_profile', kwargs=dict(course_id=course_id, student_id=student['id']))}">${student['username']}</a>
<a href="${reverse('student_progress', kwargs=dict(course_id=course_id, student_id=student['id']))}">${student['username']}</a>
</td>
</tr>
%endfor
......
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%include file="course_navigation.html" args="active_page='info'" />
<%block name="title"><title>${course.number} Course Info</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='info'" />
<%!
from courseware.courses import get_course_info_section
%>
......
<%inherit file="main.html" />
<%inherit file="/main.html" />
<%! from django.core.urlresolvers import reverse %>
<%namespace name='static' file='static_content.html'/>
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%include file="course_navigation.html" args="active_page='instructor'" />
<%include file="/courseware/course_navigation.html" args="active_page='instructor'" />
<section class="container">
<div class="instructor-dashboard-wrapper">
......
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%namespace name="progress_graph" file="/courseware/progress_graph.js"/>
<%block name="title"><title>${course.number} Progress</title></%block>
<%!
from django.core.urlresolvers import reverse
%>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
<script>
${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")}
</script>
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='progress'" />
<section class="container">
<div class="profile-wrapper">
<section class="course-info">
<header>
<h1>Course Progress</h1>
</header>
<div id="grade-detail-graph"></div>
<ol class="chapters">
%for chapter in courseware_summary:
%if not chapter['display_name'] == "hidden":
<li>
<h2>${ chapter['display_name'] }</h2>
<ol class="sections">
%for section in chapter['sections']:
<li>
<%
earned = section['section_total'].earned
total = section['section_total'].possible
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%>
<h3><a href="${reverse('courseware_section', kwargs=dict(course_id=course.id, chapter=chapter['url_name'], section=section['url_name']))}">
${ section['display_name'] }</a><span> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</span></h3>
<p>
${section['format']}
%if 'due' in section and section['due']!="":
<em>
due ${section['due']}
</em>
%endif
</p>
%if len(section['scores']) > 0:
<section class="scores">
<h3> ${ "Problem Scores: " if section['graded'] else "Practice Scores: "} </h3>
<ol>
%for score in section['scores']:
<li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
%endfor
</ol>
</section>
%endif
</li> <!--End section-->
%endfor
</ol> <!--End sections-->
</li> <!--End chapter-->
%endif
%endfor
</ol> <!--End chapters-->
</section>
</div>
</section>
......@@ -11,7 +11,7 @@
<%include file="_js_dependencies.html" />
</%block>
<%include file="../course_navigation.html" args="active_page='discussion'" />
<%include file="/courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container">
<div class="course-wrapper">
......
......@@ -14,7 +14,7 @@
<%include file="_js_dependencies.html" />
</%block>
<%include file="../course_navigation.html" args="active_page='discussion'" />
<%include file="/courseware/course_navigation.html" args="active_page='discussion'" />
<section class="container">
<div class="course-wrapper">
......
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%namespace name="profile_graphs" file="profile_graphs.js"/>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.stack.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.symbol.js')}"></script>
% for s in students:
<script>
${profile_graphs.body(s['grade_info']['grade_summary'], "grade-detail-graph-" + str(s['id']))}
</script>
%endfor
</%block>
<%include file="navigation.html" args="active_page=''" />
<section class="container">
<div class="gradebook-wrapper">
<section class="gradebook-content">
<h1>Gradebook</h1>
<ol>
% for s in students:
<li>
<h2><a href=/profile/${s['id']}>${s['username']}</a></h2>
<div id="grade-detail-graph-${s['id']}" style="width:1000px;height:300px;"></div>
</li>
% endfor
</ol>
</section>
</div>
</section>
......@@ -10,7 +10,7 @@
<%block name="js_extra">
</%block>
<%include file="course_navigation.html" args="active_page='news'" />
<%include file="/courseware/course_navigation.html" args="active_page='news'" />
<section class="container">
<div class="course-wrapper">
......
......@@ -79,7 +79,7 @@
<%block name="bodyextra">
%if course:
<%include file="../course_navigation.html" args="active_page='wiki'" />
<%include file="/courseware/course_navigation.html" args="active_page='wiki'" />
%endif
<section class="container">
......
......@@ -32,7 +32,7 @@ function goto_page(n) {
if(n<10) {
prefix="00";
}
$("#bookpage").attr("src","${ settings.BOOK_URL }p"+prefix+n+".png");
$("#bookpage").attr("src","${ book_url }p"+prefix+n+".png");
$.cookie("book_page", n, {'expires':3650, 'path':'/'});
};
......@@ -61,7 +61,7 @@ $("#open_close_accordion a").click(function(){
</script>
</%block>
<%include file="course_navigation.html" args="active_page='book'" />
<%include file="/courseware/course_navigation.html" args="active_page='book'" />
<section class="container">
<div class="book-wrapper">
......@@ -113,7 +113,7 @@ $("#open_close_accordion a").click(function(){
</ul>
</nav>
<img id="bookpage" src="${ settings.BOOK_URL }p${ "%03i"%(page) }.png">
<img id="bookpage" src="${ book_url }p${ "%03i"%(page) }.png">
</section>
</section>
</div>
......
......@@ -47,7 +47,7 @@
{% block body %}
{% if course %}
{% include "course_navigation.html" with active_page_context="wiki" %}
{% include "courseware/course_navigation.html" with active_page_context="wiki" %}
{% endif %}
<section class="container wiki {{ selected_tab }}">
......
......@@ -100,7 +100,7 @@ if settings.COURSEWARE_ENABLED:
url(r'^masquerade/', include('masquerade.urls')),
url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.modx_dispatch',
name='modx_dispatch'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
......@@ -138,11 +138,11 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.index', name="courseware_chapter"),
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>[^/]+/[^/]+/[^/]+)/progress$',
'courseware.views.progress', name="progress"),
# Takes optional student_id for instructor use--shows profile as that student sees it.
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
'courseware.views.profile', name="student_profile"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/progress/(?P<student_id>[^/]*)/$',
'courseware.views.progress', name="student_progress"),
# For the instructor
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor$',
......@@ -169,18 +169,18 @@ if settings.COURSEWARE_ENABLED:
if settings.WIKI_ENABLED:
from wiki.urls import get_pattern as wiki_pattern
from django_notify.urls import get_pattern as notify_pattern
# Note that some of these urls are repeated in course_wiki.course_nav. Make sure to update
# them together.
urlpatterns += (
urlpatterns += (
# First we include views from course_wiki that we use to override the default views.
# They come first in the urlpatterns so they get resolved first
url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'),
url(r'^wiki/', include(wiki_pattern())),
url(r'^notify/', include(notify_pattern())),
# These urls are for viewing the wiki in the context of a course. They should
# never be returned by a reverse() so they come after the other url patterns
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/course_wiki/?$',
......
-e git://github.com/MITx/django-staticfiles.git@6d2504e5c8#egg=django-staticfiles
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
-e git://github.com/benjaoming/django-wiki.git@c145596#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git#egg=pystache_custom
-e common/lib/capa
-e common/lib/xmodule
......@@ -11,8 +11,6 @@ python-memcached
python-openid
path.py
django_debug_toolbar
-e git://github.com/MITx/django-pipeline.git#egg=django-pipeline
django-staticfiles>=1.2.1
fs
beautifulsoup
beautifulsoup4
......@@ -44,6 +42,5 @@ django-ses
django-storages
django-threaded-multihost
django-sekizai<0.7
-e git://github.com/benjaoming/django-wiki.git@c145596#egg=django-wiki
-e git://github.com/dementrock/pystache_custom.git#egg=pystache_custom
networkx
-r repo-requirements.txt
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