Merge branch 'feature/bk_forum_int'
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -134,6 +161,10 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
'all_descriptors' : all_descriptors,}
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
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,6 +272,27 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
return course_descriptor
|
||||
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
"""
|
||||
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
|
||||
|
||||
If no object is found at that location, raises
|
||||
xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
location = Location(location)
|
||||
try:
|
||||
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.
|
||||
@@ -271,11 +307,8 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
location = Location(location)
|
||||
try:
|
||||
return self.modules[location]
|
||||
except KeyError:
|
||||
raise ItemNotFoundError(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():
|
||||
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))
|
||||
# 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")
|
||||
|
||||
1
common/test/data/two_toys/README
Normal file
1
common/test/data/two_toys/README
Normal file
@@ -0,0 +1 @@
|
||||
A copy of the toy course, with different metadata. Used to test policy loading for course with identical org course fields and shared content.
|
||||
4
common/test/data/two_toys/chapter/Overview.xml
Normal file
4
common/test/data/two_toys/chapter/Overview.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<chapter>
|
||||
<videosequence url_name="Toy_Videos"/>
|
||||
<video url_name="Welcome"/>
|
||||
</chapter>
|
||||
1
common/test/data/two_toys/course.xml
Normal file
1
common/test/data/two_toys/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course url_name="TT_2012_Fall" org="edX" course="toy"/>
|
||||
3
common/test/data/two_toys/course/TT_2012_Fall.xml
Normal file
3
common/test/data/two_toys/course/TT_2012_Fall.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<course display_name="Toy Course" graceperiod="2 days 5 hours 59 minutes 59 seconds" start="2015-07-17T12:00">
|
||||
<chapter url_name="Overview"/>
|
||||
</course>
|
||||
23
common/test/data/two_toys/policies/TT_2012_Fall.json
Normal file
23
common/test/data/two_toys/policies/TT_2012_Fall.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
1
common/test/data/two_toys/video/Video_Resources.xml
Normal file
1
common/test/data/two_toys/video/Video_Resources.xml
Normal file
@@ -0,0 +1 @@
|
||||
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/>
|
||||
1
common/test/data/two_toys/video/Welcome.xml
Normal file
1
common/test/data/two_toys/video/Welcome.xml
Normal file
@@ -0,0 +1 @@
|
||||
<video youtube="1.0:p2Q6BrNhdh9" display_name="Welcome"/>
|
||||
3
common/test/data/two_toys/videosequence/Toy_Videos.xml
Normal file
3
common/test/data/two_toys/videosequence/Toy_Videos.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<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.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
span {
|
||||
background: #eee;
|
||||
color: #555;
|
||||
padding: 2px 5px;
|
||||
@include border-radius(2px);
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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>
|
||||
@@ -1,8 +1,13 @@
|
||||
<%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">
|
||||
@@ -1,7 +1,7 @@
|
||||
<%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">
|
||||
@@ -1,8 +1,8 @@
|
||||
<%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">
|
||||
@@ -1,6 +1,6 @@
|
||||
<%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
|
||||
@@ -1,11 +1,13 @@
|
||||
<%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
|
||||
%>
|
||||
@@ -1,12 +1,12 @@
|
||||
<%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">
|
||||
85
lms/templates/courseware/progress.html
Normal file
85
lms/templates/courseware/progress.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<%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">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<%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">
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
<%inherit file="main.html" />
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
</%block>
|
||||
|
||||
<%namespace name="profile_graphs" file="profile_graphs.js"/>
|
||||
|
||||
<%block name="title"><title>Profile - edX 6.002x</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>
|
||||
${profile_graphs.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var loc=true; // Activate on clicks? Not if already clicked.
|
||||
var lang=true;
|
||||
$(function() {
|
||||
$("#change_location").click(function() {
|
||||
$(this).hide();
|
||||
|
||||
log_event("profile", {"type":"location_show", "old":$("#location_sub").text()});
|
||||
|
||||
if(loc) {
|
||||
$("#description").html('<div>'+
|
||||
"Preferred format is city, state, country (so for us, "+
|
||||
""Cambridge, Massachusetts, USA"), but give "+
|
||||
"as much or as little detail as you want. </div>");
|
||||
|
||||
loc=false;
|
||||
|
||||
$("#location_sub").html('<form>'+'<input id="id_loc_text" type="text" name="loc_text" />'+
|
||||
'<input type="submit" id="change_loc_button" value="Save" />'+'</form>');
|
||||
|
||||
$("#change_loc_button").click(function() {
|
||||
$("#change_location").show();
|
||||
|
||||
postJSON('/change_setting', {'location':$("#id_loc_text").attr("value")}, function(json) {
|
||||
$("#location_sub").text(json.location);
|
||||
loc=true;
|
||||
$("#description").html("");
|
||||
log_event("profile", {"type":"location_change", "new":json.location});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#change_password').click(function(){
|
||||
$('.modal').trigger('click');
|
||||
log_event("profile", {"type":"password_show"});
|
||||
});
|
||||
|
||||
$('#pwd_reset_button').click(function() {
|
||||
$.postWithPrefix('/password_reset/',{ "csrfmiddlewaretoken" : "${ csrf }",
|
||||
"email" : $('#id_email').val()}, function(data){
|
||||
$("#password_reset_complete_link").click();
|
||||
log_event("profile", {"type":"password_send"});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#change_email_form").submit(function(){
|
||||
var new_email = $('#new_email_field').val();
|
||||
var new_password = $('#new_email_password').val();
|
||||
|
||||
postJSON('/change_email',{"new_email":new_email,
|
||||
"password":new_password},
|
||||
function(data){
|
||||
if(data.success){
|
||||
$("#change_email").html("<h1>Please verify your new email</h1><p>You'll receive a confirmation in your in-box. Please click the link in the email to confirm the email change.</p>");
|
||||
} else {
|
||||
$("#change_email_error").html(data.error);
|
||||
}
|
||||
});
|
||||
log_event("profile", {"type":"email_change_request",
|
||||
"old_email":"${email}",
|
||||
"new_email":new_email});
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#change_name_form").submit(function(){
|
||||
var new_name = $('#new_name_field').val();
|
||||
var rationale = $('#name_rationale_field').val();
|
||||
|
||||
postJSON('/change_name',{"new_name":new_name,
|
||||
"rationale":rationale},
|
||||
function(data){
|
||||
if(data.success){
|
||||
$("#apply_name_change").html("<h1>Your request has been submitted.</h1><p>We'll send you an e-mail when approve the change or need further information. Please allow for up to a week for us to process your request.</p>");
|
||||
} else {
|
||||
$("#change_name_error").html(data.error);
|
||||
}
|
||||
});
|
||||
log_event("profile", {"type":"name_change_request",
|
||||
"new_name":new_name,
|
||||
"rationale":rationale});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%include file="course_navigation.html" args="active_page='profile'" />
|
||||
|
||||
<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>
|
||||
|
||||
<section aria-label="Profile Navigation" class="user-info">
|
||||
|
||||
<header>
|
||||
<h1>Student Profile</h1>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Name: <strong>${name}</strong>
|
||||
%if True:
|
||||
<a href="#apply_name_change" rel="leanModal" class="name-edit" aria-label="Edit Name">Edit</a>
|
||||
%else:
|
||||
(Name change pending)
|
||||
%endif
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Forum name: <strong>${username}</strong>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
E-mail: <strong>${email}</strong> <a href="#change_email" rel="leanModal" class="edit-email" aria-label="Edit Email">Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
Location: <div id="location_sub">${location}</div><div id="description"></div> <a href="#" id="change_location" aria-label="Edit Location">Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
Password reset
|
||||
<input id="id_email" type="hidden" name="email" maxlength="75" value="${email}" />
|
||||
<input type="submit" id="pwd_reset_button" value="Reset" aria-label="Reset Password" />
|
||||
<p>We'll e-mail a password reset link to ${email}.</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="password_reset_complete" class="modal">
|
||||
<a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a>
|
||||
<h1>Password Reset Email Sent</h1>
|
||||
<p>
|
||||
An email has been sent to ${email}. Follow the link in the email to change your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="apply_name_change" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Apply to change your name</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<form id="change_name_form">
|
||||
<div id="change_name_error"> </div>
|
||||
<fieldset>
|
||||
<p>To uphold the credibility of <span class="edx">edX</span> certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
|
||||
<label>Enter your desired full name, as it will appear on the <span class="edx">edX</span> Certificate: </label>
|
||||
<input id="new_name_field" value="" type="text" />
|
||||
<label>Reason for name change:</label>
|
||||
<textarea id="name_rationale_field" value=""></textarea>
|
||||
<input type="submit" id="submit">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="change_email" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Change e-mail</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<div id="apply_name_change_error"></div>
|
||||
<form id="change_email_form">
|
||||
<div id="change_email_error"> </div>
|
||||
<fieldset>
|
||||
<label> Please enter your new email address: </label>
|
||||
<input id="new_email_field" type="email" value="" />
|
||||
<label> Please confirm your password: </label>
|
||||
<input id="new_email_password" value="" type="password" />
|
||||
<p>We will send a confirmation to both ${email} and your new e-mail as part of the process.</p>
|
||||
<input type="submit" id="submit_email_change" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="deactivate-account" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Deactivate <span class="edx">edX</span> Account</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<p>Once you deactivate you’re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
|
||||
<p>If you’d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
|
||||
|
||||
<form id="unenroll_form">
|
||||
<div id="unenroll_error"> </div>
|
||||
<fieldset>
|
||||
<input type="submit" id="" value="Yes, I don't want an edX account or hear about any new classes or updates to edX" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="unenroll" class="modal">
|
||||
<div class="inner-wrapper">
|
||||
<header>
|
||||
<h2>Unenroll from 6.002x</h2>
|
||||
<hr />
|
||||
</header>
|
||||
<p>Please note: you will still receive updates and new class announcements from ed<em>X</em>. If you don’t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
|
||||
|
||||
<form id="unenroll_form">
|
||||
<div id="unenroll_error"> </div>
|
||||
<fieldset>
|
||||
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to edX" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 }}">
|
||||
|
||||
18
lms/urls.py
18
lms/urls.py
@@ -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/?$',
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
-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
|
||||
|
||||
Reference in New Issue
Block a user