Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None):
|
||||
|
||||
log.debug('ExtAuth: doing signup for %s' % eamap.external_email)
|
||||
|
||||
return student_views.main_index(extra_context=context)
|
||||
return student_views.main_index(request, extra_context=context)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# MIT SSL
|
||||
@@ -206,7 +207,7 @@ def edXauth_ssl_login(request):
|
||||
pass
|
||||
if not cert:
|
||||
# no certificate information - go onward to main index
|
||||
return student_views.main_index()
|
||||
return student_views.main_index(request)
|
||||
|
||||
(user, email, fullname) = ssl_dn_extract_info(cert)
|
||||
|
||||
@@ -216,4 +217,4 @@ def edXauth_ssl_login(request):
|
||||
credentials=cert,
|
||||
email=email,
|
||||
fullname=fullname,
|
||||
retfun = student_views.main_index)
|
||||
retfun = functools.partial(student_views.main_index, request))
|
||||
|
||||
@@ -68,9 +68,9 @@ def index(request):
|
||||
from external_auth.views import edXauth_ssl_login
|
||||
return edXauth_ssl_login(request)
|
||||
|
||||
return main_index(user=request.user)
|
||||
return main_index(request, user=request.user)
|
||||
|
||||
def main_index(extra_context = {}, user=None):
|
||||
def main_index(request, extra_context={}, user=None):
|
||||
'''
|
||||
Render the edX main page.
|
||||
|
||||
@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None):
|
||||
entry.summary = soup.getText()
|
||||
|
||||
# The course selection work is done in courseware.courses.
|
||||
universities = get_courses_by_university(None)
|
||||
universities = get_courses_by_university(None,
|
||||
domain=request.META.get('HTTP_HOST'))
|
||||
context = {'universities': universities, 'entries': entries}
|
||||
context.update(extra_context)
|
||||
return render_to_response('index.html', context)
|
||||
|
||||
@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template):
|
||||
return _get_html
|
||||
|
||||
|
||||
def replace_course_urls(get_html, course_id, module):
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
the old get_html function and substitutes urls of the form /course/...
|
||||
with urls that are /courses/<course_id>/...
|
||||
"""
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
|
||||
return _get_html
|
||||
|
||||
def replace_static_urls(get_html, prefix, module):
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
|
||||
@@ -49,9 +49,9 @@ class ABTestModule(XModule):
|
||||
return json.dumps({'group': self.group})
|
||||
|
||||
def displayable_items(self):
|
||||
return filter(None, [self.system.get_module(child)
|
||||
for child
|
||||
in self.definition['data']['group_content'][self.group]])
|
||||
child_locations = self.definition['data']['group_content'][self.group]
|
||||
children = [self.system.get_module(loc) for loc in child_locations]
|
||||
return [c for c in children if c is not None]
|
||||
|
||||
|
||||
# TODO (cpennington): Use Groups should be a first class object, rather than being
|
||||
|
||||
@@ -21,6 +21,7 @@ def process_includes(fn):
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
next_include = xml_object.find('include')
|
||||
while next_include is not None:
|
||||
system.error_tracker("WARNING: the <include> tag is deprecated, and will go away.")
|
||||
file = next_include.get('file')
|
||||
parent = next_include.getparent()
|
||||
|
||||
@@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
|
||||
the child element
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
system.error_tracker("WARNING: the <{}> tag is deprecated. Please do not use in new content."
|
||||
.format(xml_object.tag))
|
||||
|
||||
if len(xml_object) == 1:
|
||||
for (key, val) in xml_object.items():
|
||||
@@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor):
|
||||
|
||||
return system.process_xml(etree.tostring(xml_object[0]))
|
||||
else:
|
||||
xml_object.tag = 'sequence'
|
||||
xml_object.tag = 'sequential'
|
||||
return system.process_xml(etree.tostring(xml_object))
|
||||
|
||||
|
||||
@@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Transforms the xml_data from <$custom_tag attr="" attr=""/> to
|
||||
<customtag attr="" attr=""><impl>$custom_tag</impl></customtag>
|
||||
<customtag attr="" attr="" impl="$custom_tag"/>
|
||||
"""
|
||||
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
system.error_tracker('WARNING: the <{tag}> tag is deprecated. '
|
||||
'Instead, use <customtag impl="{tag}" attr1="..." attr2="..."/>. '
|
||||
.format(tag=xml_object.tag))
|
||||
|
||||
tag = xml_object.tag
|
||||
xml_object.tag = 'customtag'
|
||||
xml_object.attrib['impl'] = tag
|
||||
|
||||
@@ -237,7 +237,7 @@ class CapaModule(XModule):
|
||||
else:
|
||||
raise
|
||||
|
||||
content = {'name': self.metadata['display_name'],
|
||||
content = {'name': self.display_name,
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
}
|
||||
@@ -376,14 +376,17 @@ class CapaModule(XModule):
|
||||
'''
|
||||
For the "show answer" button.
|
||||
|
||||
TODO: show answer events should be logged here, not just in the problem.js
|
||||
|
||||
Returns the answers: {'answers' : answers}
|
||||
'''
|
||||
event_info = dict()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
self.system.track_function('show_answer', event_info)
|
||||
if not self.answer_available():
|
||||
raise NotFoundError('Answer is not available')
|
||||
else:
|
||||
answers = self.lcp.get_question_answers()
|
||||
# answers (eg <solution>) may have embedded images
|
||||
answers = dict( (k,self.system.replace_urls(answers[k], self.metadata['data_dir'])) for k in answers )
|
||||
return {'answers': answers}
|
||||
|
||||
# Figure out if we should move these to capa_problem?
|
||||
@@ -464,7 +467,7 @@ class CapaModule(XModule):
|
||||
return {'success': msg}
|
||||
log.exception("Error in capa_module problem checking")
|
||||
raise Exception("error in capa_module")
|
||||
|
||||
|
||||
self.attempts = self.attempts + 1
|
||||
self.lcp.done = True
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import time
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.util.decorators import lazyproperty
|
||||
from xmodule.graders import load_grading_policy
|
||||
@@ -10,12 +11,28 @@ from xmodule.timeparse import parse_time, stringify_time
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
|
||||
class Textbook:
|
||||
def __init__(self, title, table_of_contents_url):
|
||||
self.title = title
|
||||
self.table_of_contents_url = table_of_contents_url
|
||||
|
||||
@classmethod
|
||||
def from_xml_object(cls, xml_object):
|
||||
return cls(xml_object.get('title'), xml_object.get('table_of_contents_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 table_of_contents
|
||||
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
self.textbooks = self.definition['data']['textbooks']
|
||||
|
||||
msg = None
|
||||
if self.start is None:
|
||||
@@ -28,6 +45,16 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
self.enrollment_start = self._try_parse_time("enrollment_start")
|
||||
self.enrollment_end = self._try_parse_time("enrollment_end")
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
textbooks = []
|
||||
for textbook in xml_object.findall("textbook"):
|
||||
textbooks.append(cls.Textbook.from_xml_object(textbook))
|
||||
xml_object.remove(textbook)
|
||||
definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
|
||||
definition.setdefault('data', {})['textbooks'] = textbooks
|
||||
return definition
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
|
||||
@@ -53,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
return grading_policy
|
||||
|
||||
|
||||
@lazyproperty
|
||||
def grading_context(self):
|
||||
"""
|
||||
@@ -140,7 +166,7 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.metadata['display_name']
|
||||
return self.display_name
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
|
||||
@@ -4,7 +4,7 @@ nav.sequence-nav {
|
||||
@extend .topbar;
|
||||
border-bottom: 1px solid $border-color;
|
||||
@include border-top-right-radius(4px);
|
||||
margin: (-(lh())) (-(lh())) lh() (-(lh()));
|
||||
margin: 0 0 lh() (-(lh()));
|
||||
position: relative;
|
||||
|
||||
ol {
|
||||
|
||||
@@ -4,7 +4,7 @@ div.video {
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
display: block;
|
||||
margin: 0 (-(lh()));
|
||||
margin: 0 0 0 (-(lh()));
|
||||
padding: 6px lh();
|
||||
|
||||
article.video-wrapper {
|
||||
|
||||
@@ -112,8 +112,8 @@ class TestMongoModuleStore(object):
|
||||
should_work = (
|
||||
("i4x://edX/toy/video/Welcome",
|
||||
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
|
||||
("i4x://edX/toy/html/toylab",
|
||||
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
|
||||
("i4x://edX/toy/chapter/Overview",
|
||||
("edX/toy/2012_Fall", "Overview", None, None)),
|
||||
)
|
||||
for location, expected in should_work:
|
||||
assert_equals(path_to_location(self.store, location), expected)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -5,6 +6,7 @@ import re
|
||||
from fs.osfs import OSFS
|
||||
from importlib import import_module
|
||||
from lxml import etree
|
||||
from lxml.html import HtmlComment
|
||||
from path import path
|
||||
from xmodule.errortracker import ErrorLog, make_error_tracker
|
||||
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
|
||||
@@ -15,9 +17,10 @@ from cStringIO import StringIO
|
||||
from . import ModuleStoreBase, Location
|
||||
from .exceptions import ItemNotFoundError
|
||||
|
||||
etree.set_default_parser(
|
||||
etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True))
|
||||
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True)
|
||||
|
||||
etree.set_default_parser(edx_xml_parser)
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string):
|
||||
return xml_string
|
||||
|
||||
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
def __init__(self, xmlstore, org, course, course_dir, error_tracker, **kwargs):
|
||||
def __init__(self, xmlstore, org, course, course_dir,
|
||||
policy, error_tracker, **kwargs):
|
||||
"""
|
||||
A class that handles loading from xml. Does some munging to ensure that
|
||||
all elements have unique slugs.
|
||||
@@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
MakoDescriptorSystem.__init__(self, load_item, resources_fs,
|
||||
error_tracker, render_template, **kwargs)
|
||||
XMLParsingSystem.__init__(self, load_item, resources_fs,
|
||||
error_tracker, process_xml, **kwargs)
|
||||
error_tracker, process_xml, policy, **kwargs)
|
||||
|
||||
|
||||
class XMLModuleStore(ModuleStoreBase):
|
||||
@@ -149,7 +153,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
for course_dir in course_dirs:
|
||||
self.try_load_course(course_dir)
|
||||
|
||||
def try_load_course(self,course_dir):
|
||||
def try_load_course(self, course_dir):
|
||||
'''
|
||||
Load a course, keeping track of errors as we go along.
|
||||
'''
|
||||
@@ -170,7 +174,28 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
'''
|
||||
String representation - for debugging
|
||||
'''
|
||||
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (self.data_dir,len(self.courses),len(self.modules))
|
||||
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (
|
||||
self.data_dir, len(self.courses), len(self.modules))
|
||||
|
||||
def load_policy(self, policy_path, tracker):
|
||||
"""
|
||||
Attempt to read a course policy from policy_path. If the file
|
||||
exists, but is invalid, log an error and return {}.
|
||||
|
||||
If the policy loads correctly, returns the deserialized version.
|
||||
"""
|
||||
if not os.path.exists(policy_path):
|
||||
return {}
|
||||
try:
|
||||
log.debug("Loading policy from {}".format(policy_path))
|
||||
with open(policy_path) as f:
|
||||
return json.load(f)
|
||||
except (IOError, ValueError) as err:
|
||||
msg = "Error loading course policy from {}".format(policy_path)
|
||||
tracker(msg)
|
||||
log.warning(msg + " " + str(err))
|
||||
return {}
|
||||
|
||||
|
||||
def load_course(self, course_dir, tracker):
|
||||
"""
|
||||
@@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
# been imported into the cms from xml
|
||||
course_file = StringIO(clean_out_mako_templating(course_file.read()))
|
||||
|
||||
course_data = etree.parse(course_file).getroot()
|
||||
course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
|
||||
|
||||
org = course_data.get('org')
|
||||
|
||||
@@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
tracker(msg)
|
||||
course = course_dir
|
||||
|
||||
system = ImportSystem(self, org, course, course_dir, tracker)
|
||||
url_name = course_data.get('url_name')
|
||||
if url_name:
|
||||
policy_path = self.data_dir / course_dir / 'policies' / '{}.json'.format(url_name)
|
||||
policy = self.load_policy(policy_path, tracker)
|
||||
else:
|
||||
policy = {}
|
||||
|
||||
system = ImportSystem(self, org, course, course_dir, policy, tracker)
|
||||
|
||||
course_descriptor = system.process_xml(etree.tostring(course_data))
|
||||
|
||||
# NOTE: The descriptors end up loading somewhat bottom up, which
|
||||
# breaks metadata inheritance via get_children(). Instead
|
||||
# (actually, in addition to, for now), we do a final inheritance pass
|
||||
|
||||
@@ -76,7 +76,7 @@ class SequenceModule(XModule):
|
||||
contents.append({
|
||||
'content': child.get_html(),
|
||||
'title': "\n".join(
|
||||
grand_child.metadata['display_name'].strip()
|
||||
grand_child.display_name.strip()
|
||||
for grand_child in child.get_children()
|
||||
if 'display_name' in grand_child.metadata
|
||||
),
|
||||
@@ -107,7 +107,7 @@ class SequenceModule(XModule):
|
||||
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
mako_template = 'widgets/sequence-edit.html'
|
||||
module_class = SequenceModule
|
||||
|
||||
|
||||
stores_state = True # For remembering where in the sequence the student is
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem):
|
||||
descriptor.get_children()
|
||||
return descriptor
|
||||
|
||||
|
||||
policy = {}
|
||||
XMLParsingSystem.__init__(self, load_item, self.resources_fs,
|
||||
self.errorlog.tracker, process_xml)
|
||||
self.errorlog.tracker, process_xml, policy)
|
||||
|
||||
def render_template(self, template, context):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
@@ -219,11 +219,11 @@ class XModule(HTMLSnippet):
|
||||
Return module instances for all the children of this module.
|
||||
'''
|
||||
if self._loaded_children is None:
|
||||
child_locations = self.definition.get('children', [])
|
||||
children = [self.system.get_module(loc) for loc in child_locations]
|
||||
# get_module returns None if the current user doesn't have access
|
||||
# to the location.
|
||||
self._loaded_children = filter(None,
|
||||
[self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])])
|
||||
self._loaded_children = [c for c in children if c is not None]
|
||||
|
||||
return self._loaded_children
|
||||
|
||||
@@ -298,6 +298,14 @@ class XModule(HTMLSnippet):
|
||||
return ""
|
||||
|
||||
|
||||
def policy_key(location):
|
||||
"""
|
||||
Get the key for a location in a policy file. (Since the policy file is
|
||||
specific to a course, it doesn't need the full location url).
|
||||
"""
|
||||
return '{cat}/{name}'.format(cat=location.category, name=location.name)
|
||||
|
||||
|
||||
class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
"""
|
||||
An XModuleDescriptor is a specification for an element of a course. This
|
||||
@@ -416,6 +424,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
return dict((k,v) for k,v in self.metadata.items()
|
||||
if k not in self._inherited_metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def compute_inherited_metadata(node):
|
||||
"""Given a descriptor, traverse all of its descendants and do metadata
|
||||
@@ -671,16 +680,19 @@ class DescriptorSystem(object):
|
||||
|
||||
|
||||
class XMLParsingSystem(DescriptorSystem):
|
||||
def __init__(self, load_item, resources_fs, error_tracker, process_xml, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_tracker, process_xml, policy, **kwargs):
|
||||
"""
|
||||
load_item, resources_fs, error_tracker: see DescriptorSystem
|
||||
|
||||
policy: a policy dictionary for overriding xml metadata
|
||||
|
||||
process_xml: Takes an xml string, and returns a XModuleDescriptor
|
||||
created from that xml
|
||||
"""
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker,
|
||||
**kwargs)
|
||||
self.process_xml = process_xml
|
||||
self.policy = policy
|
||||
|
||||
|
||||
class ModuleSystem(object):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.x_module import (XModuleDescriptor, policy_key)
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
import json
|
||||
@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
Subclasses should not need to override this except in special
|
||||
cases (e.g. html module)'''
|
||||
|
||||
# VS[compat] -- the filename tag should go away once everything is
|
||||
# VS[compat] -- the filename attr should go away once everything is
|
||||
# converted. (note: make sure html files still work once this goes away)
|
||||
filename = xml_object.get('filename')
|
||||
if filename is None:
|
||||
@@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
log.debug('Error %s in loading metadata %s' % (err,dmdata))
|
||||
metadata['definition_metadata_err'] = str(err)
|
||||
|
||||
# Set/override any metadata specified by policy
|
||||
k = policy_key(location)
|
||||
if k in system.policy:
|
||||
metadata.update(system.policy[k])
|
||||
|
||||
return cls(
|
||||
system,
|
||||
definition,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
|
||||
<chapter name="Overview">
|
||||
<videosequence format="Lecture Sequence" name="Toy Videos">
|
||||
<html name="toylab" filename="toylab"/>
|
||||
<video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
</videosequence>
|
||||
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
</chapter>
|
||||
</course>
|
||||
1
common/test/data/toy/course.xml
Symbolic link
1
common/test/data/toy/course.xml
Symbolic link
@@ -0,0 +1 @@
|
||||
roots/2012_Fall.xml
|
||||
9
common/test/data/toy/course/2012_Fall.xml
Normal file
9
common/test/data/toy/course/2012_Fall.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<course>
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
</chapter>
|
||||
</course>
|
||||
23
common/test/data/toy/policies/2012_Fall.json
Normal file
23
common/test/data/toy/policies/2012_Fall.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"course/2012_Fall": {
|
||||
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
|
||||
"start": "2015-07-17T12:00",
|
||||
"display_name": "Toy Course"
|
||||
},
|
||||
"chapter/Overview": {
|
||||
"display_name": "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/toy/roots/2012_Fall.xml
Normal file
1
common/test/data/toy/roots/2012_Fall.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="toy" url_name="2012_Fall"/>
|
||||
112
common/xml_cleanup.py
Executable file
112
common/xml_cleanup.py
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Victor's xml cleanup script. A big pile of useful hacks. Do not use
|
||||
without carefully reading the code and deciding that this is what you want.
|
||||
|
||||
In particular, the remove-meta option is only intended to be used after pulling out a policy
|
||||
using the metadata_to_json management command.
|
||||
"""
|
||||
|
||||
import os, fnmatch, re, sys
|
||||
from lxml import etree
|
||||
from collections import defaultdict
|
||||
|
||||
INVALID_CHARS = re.compile(r"[^\w.-]")
|
||||
|
||||
def clean(value):
|
||||
"""
|
||||
Return value, made into a form legal for locations
|
||||
"""
|
||||
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
|
||||
|
||||
|
||||
# category -> set of url_names for that category that we've already seen
|
||||
used_names = defaultdict(set)
|
||||
|
||||
def clean_unique(category, name):
|
||||
cleaned = clean(name)
|
||||
if cleaned not in used_names[category]:
|
||||
used_names[category].add(cleaned)
|
||||
return cleaned
|
||||
x = 1
|
||||
while cleaned + str(x) in used_names[category]:
|
||||
x += 1
|
||||
|
||||
# Found one!
|
||||
cleaned = cleaned + str(x)
|
||||
used_names[category].add(cleaned)
|
||||
return cleaned
|
||||
|
||||
def cleanup(filepath, remove_meta):
|
||||
# Keys that are exported to the policy file, and so
|
||||
# can be removed from the xml afterward
|
||||
to_remove = ('format', 'display_name',
|
||||
'graceperiod', 'showanswer', 'rerandomize',
|
||||
'start', 'due', 'graded', 'hide_from_toc',
|
||||
'ispublic', 'xqa_key')
|
||||
|
||||
try:
|
||||
print "Cleaning {}".format(filepath)
|
||||
with open(filepath) as f:
|
||||
parser = etree.XMLParser(remove_comments=False)
|
||||
xml = etree.parse(filepath, parser=parser)
|
||||
except:
|
||||
print "Error parsing file {}".format(filepath)
|
||||
return
|
||||
|
||||
for node in xml.iter(tag=etree.Element):
|
||||
attrs = node.attrib
|
||||
if 'url_name' in attrs:
|
||||
used_names[node.tag].add(attrs['url_name'])
|
||||
if 'name' in attrs:
|
||||
# Replace name with an identical display_name, and a unique url_name
|
||||
name = attrs['name']
|
||||
attrs['display_name'] = name
|
||||
attrs['url_name'] = clean_unique(node.tag, name)
|
||||
del attrs['name']
|
||||
|
||||
if 'url_name' in attrs and 'slug' in attrs:
|
||||
print "WARNING: {} has both slug and url_name"
|
||||
|
||||
if ('url_name' in attrs and 'filename' in attrs and
|
||||
len(attrs)==2 and attrs['url_name'] == attrs['filename']):
|
||||
# This is a pointer tag in disguise. Get rid of the filename.
|
||||
print 'turning {}.{} into a pointer tag'.format(node.tag, attrs['url_name'])
|
||||
del attrs['filename']
|
||||
|
||||
if remove_meta:
|
||||
for attr in to_remove:
|
||||
if attr in attrs:
|
||||
del attrs[attr]
|
||||
|
||||
|
||||
with open(filepath, "w") as f:
|
||||
f.write(etree.tostring(xml))
|
||||
|
||||
|
||||
def find_replace(directory, filePattern, remove_meta):
|
||||
for path, dirs, files in os.walk(os.path.abspath(directory)):
|
||||
for filename in fnmatch.filter(files, filePattern):
|
||||
filepath = os.path.join(path, filename)
|
||||
cleanup(filepath, remove_meta)
|
||||
|
||||
|
||||
def main(args):
|
||||
usage = "xml_cleanup [dir] [remove-meta]"
|
||||
n = len(args)
|
||||
if n < 1 or n > 2 or (n == 2 and args[1] != 'remove-meta'):
|
||||
print usage
|
||||
return
|
||||
|
||||
remove_meta = False
|
||||
if n == 2:
|
||||
remove_meta = True
|
||||
|
||||
find_replace(args[0], '*.xml', remove_meta)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
@@ -66,3 +66,9 @@ To run a single nose test:
|
||||
|
||||
|
||||
Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
|
||||
|
||||
## Content development
|
||||
|
||||
If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore.
|
||||
|
||||
Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course.
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
{% spaceless %}
|
||||
<span class="action-link">
|
||||
<a class="question-delete" id="answer-delete-link-{{answer.id}}">
|
||||
{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}✖{% endif %}</a>
|
||||
{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
|
||||
</span>
|
||||
{% endspaceless %}
|
||||
{% endif %}
|
||||
|
||||
@@ -40,6 +40,5 @@
|
||||
{% endif %}
|
||||
|
||||
{% if request.user|can_delete_post(question) %}{{ pipe() }}
|
||||
<a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}✖{% endif %}</a>
|
||||
|
||||
<a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -18,11 +18,14 @@
|
||||
{% include "widgets/system_messages.html" %}
|
||||
{% include "debug_header.html" %}
|
||||
{% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #}
|
||||
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
|
||||
|
||||
<section class="container">
|
||||
<section class="content-wrapper">
|
||||
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
|
||||
|
||||
<section class="container">
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{% if settings.FOOTER_MODE == 'default' %}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{% load extra_filters_jinja %}
|
||||
<!--<link href="{{"/style/style.css"|media }}" rel="stylesheet" type="text/css" />-->
|
||||
{{ 'application' | compressed_css }}
|
||||
{{ 'course' | compressed_css }}
|
||||
|
||||
@@ -65,9 +65,10 @@ def has_access(user, obj, action):
|
||||
|
||||
# Passing an unknown object here is a coding error, so rather than
|
||||
# returning a default, complain.
|
||||
raise TypeError("Unknown object type in has_access(). Object type: '{}'"
|
||||
raise TypeError("Unknown object type in has_access(): '{}'"
|
||||
.format(type(obj)))
|
||||
|
||||
|
||||
# ================ Implementation helpers ================================
|
||||
|
||||
def _has_access_course_desc(user, course, action):
|
||||
@@ -83,8 +84,12 @@ def _has_access_course_desc(user, course, action):
|
||||
'staff' -- staff access to course.
|
||||
"""
|
||||
def can_load():
|
||||
"Can this user load this course?"
|
||||
# delegate to generic descriptor check
|
||||
"""
|
||||
Can this user load this course?
|
||||
|
||||
NOTE: this is not checking whether user is actually enrolled in the course.
|
||||
"""
|
||||
# delegate to generic descriptor check to check start dates
|
||||
return _has_access_descriptor(user, course, action)
|
||||
|
||||
def can_enroll():
|
||||
@@ -169,6 +174,12 @@ def _has_access_descriptor(user, descriptor, action):
|
||||
has_access(), it will not do the right thing.
|
||||
"""
|
||||
def can_load():
|
||||
"""
|
||||
NOTE: This does not check that the student is enrolled in the course
|
||||
that contains this module. We may or may not want to allow non-enrolled
|
||||
students to see modules. If not, views should check the course, so we
|
||||
don't have to hit the enrollments table on every module load.
|
||||
"""
|
||||
# If start dates are off, can always load
|
||||
if settings.MITX_FEATURES['DISABLE_START_DATES']:
|
||||
debug("Allow: DISABLE_START_DATES")
|
||||
@@ -196,8 +207,6 @@ def _has_access_descriptor(user, descriptor, action):
|
||||
return _dispatch(checkers, action, user, descriptor)
|
||||
|
||||
|
||||
|
||||
|
||||
def _has_access_xmodule(user, xmodule, action):
|
||||
"""
|
||||
Check if user has access to this xmodule.
|
||||
|
||||
@@ -2,8 +2,8 @@ from collections import defaultdict
|
||||
from fs.errors import ResourceNotFoundError
|
||||
from functools import wraps
|
||||
import logging
|
||||
from path import path
|
||||
|
||||
from path import path
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
|
||||
@@ -142,7 +142,8 @@ def get_course_info_section(course, section_key):
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
def get_courses_by_university(user):
|
||||
|
||||
def get_courses_by_university(user, domain=None):
|
||||
'''
|
||||
Returns dict of lists of courses available, keyed by course.org (ie university).
|
||||
Courses are sorted by course.number.
|
||||
@@ -152,9 +153,21 @@ def get_courses_by_university(user):
|
||||
courses = [c for c in modulestore().get_courses()
|
||||
if isinstance(c, CourseDescriptor)]
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
|
||||
if domain and settings.MITX_FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'):
|
||||
subdomain = domain.split(".")[0]
|
||||
if subdomain not in settings.COURSE_LISTINGS:
|
||||
subdomain = 'default'
|
||||
visible_courses = frozenset(settings.COURSE_LISTINGS[subdomain])
|
||||
else:
|
||||
visible_courses = frozenset(c.id for c in courses)
|
||||
|
||||
universities = defaultdict(list)
|
||||
for course in courses:
|
||||
if has_access(user, course, 'see_exists'):
|
||||
universities[course.org].append(course)
|
||||
if not has_access(user, course, 'see_exists'):
|
||||
continue
|
||||
if course.id not in visible_courses:
|
||||
continue
|
||||
universities[course.org].append(course)
|
||||
return universities
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
A script to walk a course xml tree, generate a dictionary of all the metadata,
|
||||
and print it out as a json dict.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
from collections import OrderedDict
|
||||
from path import path
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.x_module import policy_key
|
||||
|
||||
def import_course(course_dir, verbose=True):
|
||||
course_dir = path(course_dir)
|
||||
data_dir = course_dir.dirname()
|
||||
course_dirs = [course_dir.basename()]
|
||||
|
||||
# No default class--want to complain if it doesn't find plugins for any
|
||||
# module.
|
||||
modulestore = XMLModuleStore(data_dir,
|
||||
default_class=None,
|
||||
eager=True,
|
||||
course_dirs=course_dirs)
|
||||
|
||||
def str_of_err(tpl):
|
||||
(msg, exc_str) = tpl
|
||||
return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
|
||||
|
||||
courses = modulestore.get_courses()
|
||||
|
||||
n = len(courses)
|
||||
if n != 1:
|
||||
sys.stderr.write('ERROR: Expect exactly 1 course. Loaded {n}: {lst}\n'.format(
|
||||
n=n, lst=courses))
|
||||
return None
|
||||
|
||||
course = courses[0]
|
||||
errors = modulestore.get_item_errors(course.location)
|
||||
if len(errors) != 0:
|
||||
sys.stderr.write('ERRORs during import: {}\n'.format('\n'.join(map(str_of_err, errors))))
|
||||
|
||||
return course
|
||||
|
||||
def node_metadata(node):
|
||||
# make a copy
|
||||
to_export = ('format', 'display_name',
|
||||
'graceperiod', 'showanswer', 'rerandomize',
|
||||
'start', 'due', 'graded', 'hide_from_toc',
|
||||
'ispublic', 'xqa_key')
|
||||
|
||||
orig = node.own_metadata
|
||||
d = {k: orig[k] for k in to_export if k in orig}
|
||||
return d
|
||||
|
||||
def get_metadata(course):
|
||||
d = OrderedDict({})
|
||||
queue = [course]
|
||||
while len(queue) > 0:
|
||||
node = queue.pop()
|
||||
d[policy_key(node.location)] = node_metadata(node)
|
||||
# want to print first children first, so put them at the end
|
||||
# (we're popping from the end)
|
||||
queue.extend(reversed(node.get_children()))
|
||||
return d
|
||||
|
||||
|
||||
def print_metadata(course_dir, output):
|
||||
course = import_course(course_dir)
|
||||
if course:
|
||||
meta = get_metadata(course)
|
||||
result = json.dumps(meta, indent=4)
|
||||
if output:
|
||||
with file(output, 'w') as f:
|
||||
f.write(result)
|
||||
else:
|
||||
print result
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Imports specified course.xml and prints its
|
||||
metadata as a json dict.
|
||||
|
||||
Usage: metadata_to_json PATH-TO-COURSE-DIR OUTPUT-PATH
|
||||
|
||||
if OUTPUT-PATH isn't given, print to stdout.
|
||||
"""
|
||||
def handle(self, *args, **options):
|
||||
n = len(args)
|
||||
if n < 1 or n > 2:
|
||||
print Command.help
|
||||
return
|
||||
|
||||
output_path = args[1] if n > 1 else None
|
||||
print_metadata(args[0], output_path)
|
||||
@@ -77,7 +77,7 @@ class StudentModuleCache(object):
|
||||
Arguments
|
||||
user: The user for which to fetch maching StudentModules
|
||||
descriptors: An array of XModuleDescriptors.
|
||||
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||
select_for_update: Flag indicating whether the rows should be locked until end of transaction
|
||||
'''
|
||||
if user.is_authenticated():
|
||||
module_ids = self._get_module_state_keys(descriptors)
|
||||
@@ -110,7 +110,7 @@ class StudentModuleCache(object):
|
||||
the supplied descriptor. If depth is None, load all descendent StudentModules
|
||||
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
|
||||
should be cached
|
||||
select_for_update: Flag indicating whether the row should be locked until end of transaction
|
||||
select_for_update: Flag indicating whether the rows should be locked until end of transaction
|
||||
"""
|
||||
|
||||
def get_child_descriptors(descriptor, depth, descriptor_filter):
|
||||
|
||||
@@ -19,7 +19,7 @@ from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import ModuleSystem
|
||||
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
|
||||
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -48,7 +48,7 @@ def make_track_function(request):
|
||||
return f
|
||||
|
||||
|
||||
def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
def toc_for_course(user, request, course, active_chapter, active_section, course_id=None):
|
||||
'''
|
||||
Create a table of contents from the module store
|
||||
|
||||
@@ -71,7 +71,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
'''
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
|
||||
course = get_module(user, request, course.location, student_module_cache)
|
||||
course = get_module(user, request, course.location, student_module_cache, course_id=course_id)
|
||||
|
||||
chapters = list()
|
||||
for chapter in course.get_display_items():
|
||||
@@ -127,7 +127,7 @@ def get_section(course_module, chapter, section):
|
||||
return section_module
|
||||
|
||||
|
||||
def get_module(user, request, location, student_module_cache, position=None):
|
||||
def get_module(user, request, location, student_module_cache, position=None, course_id=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.
|
||||
@@ -144,6 +144,14 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
|
||||
'''
|
||||
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
|
||||
|
||||
# Short circuit--if the user shouldn't have access, bail without doing any work
|
||||
if not has_access(user, descriptor, 'load'):
|
||||
@@ -167,7 +175,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
|
||||
# Setup system context for module instance
|
||||
ajax_url = reverse('modx_dispatch',
|
||||
kwargs=dict(course_id=descriptor.location.course_id,
|
||||
kwargs=dict(course_id=course_id,
|
||||
id=descriptor.location.url(),
|
||||
dispatch=''),
|
||||
)
|
||||
@@ -175,7 +183,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# Fully qualified callback URL for external queueing system
|
||||
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
|
||||
xqueue_callback_url += reverse('xqueue_callback',
|
||||
kwargs=dict(course_id=descriptor.location.course_id,
|
||||
kwargs=dict(course_id=course_id,
|
||||
userid=str(user.id),
|
||||
id=descriptor.location.url(),
|
||||
dispatch='score_update'),
|
||||
@@ -195,7 +203,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
"""
|
||||
return get_module(user, request, location,
|
||||
student_module_cache, position)
|
||||
student_module_cache, position, course_id=course_id)
|
||||
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
# prefix is going to have to be specific to the module, not the directory
|
||||
@@ -225,6 +233,10 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
module.metadata['data_dir'], module
|
||||
)
|
||||
|
||||
# Allow URLs of the form '/course/' refer to the root of multicourse directory
|
||||
# hierarchy of this course
|
||||
module.get_html = replace_course_urls(module.get_html, course_id, module)
|
||||
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
|
||||
if has_access(user, module, 'staff'):
|
||||
module.get_html = add_histogram(module.get_html, module, user)
|
||||
@@ -370,7 +382,7 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
|
||||
p[inputfile_id] = inputfile
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
|
||||
instance = get_module(request.user, request, id, student_module_cache)
|
||||
instance = get_module(request.user, request, id, student_module_cache, course_id=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.
|
||||
|
||||
@@ -33,6 +33,7 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
|
||||
def user_groups(user):
|
||||
"""
|
||||
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
|
||||
@@ -63,11 +64,12 @@ 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})
|
||||
|
||||
|
||||
def render_accordion(request, course, chapter, section):
|
||||
def render_accordion(request, course, chapter, section, course_id=None):
|
||||
''' Draws navigation bar. Takes current position in accordion as
|
||||
parameter.
|
||||
|
||||
@@ -78,7 +80,7 @@ def render_accordion(request, course, chapter, section):
|
||||
Returns the html string'''
|
||||
|
||||
# grab the table of contents
|
||||
toc = toc_for_course(request.user, request, course, chapter, section)
|
||||
toc = toc_for_course(request.user, request, course, chapter, section, course_id=course_id)
|
||||
|
||||
context = dict([('toc', toc),
|
||||
('course_id', course.id),
|
||||
@@ -110,6 +112,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
- HTTPresponse
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
registered = registered_for_course(course, request.user)
|
||||
if not registered:
|
||||
# TODO (vshnayder): do course instructors need to be registered to see course?
|
||||
@@ -119,11 +122,12 @@ def index(request, course_id, chapter=None, section=None,
|
||||
try:
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'accordion': render_accordion(request, course, chapter, section),
|
||||
'accordion': render_accordion(request, course, chapter, section, course_id=course_id),
|
||||
'COURSE_TITLE': course.title,
|
||||
'course': course,
|
||||
'init': '',
|
||||
'content': ''
|
||||
'content': '',
|
||||
'staff_access': staff_access,
|
||||
}
|
||||
|
||||
look_for_module = chapter is not None and section is not None
|
||||
@@ -135,7 +139,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
section_descriptor)
|
||||
module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
student_module_cache, course_id=course_id)
|
||||
if module is None:
|
||||
# User is probably being clever and trying to access something
|
||||
# they don't have access to.
|
||||
@@ -166,7 +170,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
position=position
|
||||
))
|
||||
try:
|
||||
result = render_to_response('courseware-error.html', {})
|
||||
result = render_to_response('courseware-error.html',
|
||||
{'staff_access': staff_access})
|
||||
except:
|
||||
result = HttpResponse("There was an unrecoverable error")
|
||||
|
||||
@@ -208,8 +213,10 @@ def course_info(request, course_id):
|
||||
Assumes the course_id is in a valid format.
|
||||
"""
|
||||
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('info.html', {'course': course,
|
||||
'staff_access': staff_access,})
|
||||
|
||||
|
||||
def registered_for_course(course, user):
|
||||
@@ -241,7 +248,8 @@ def university_profile(request, org_id):
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
|
||||
# Only grab courses for this org...
|
||||
courses = get_courses_by_university(request.user)[org_id]
|
||||
courses = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
@@ -257,20 +265,21 @@ def profile(request, course_id, student_id=None):
|
||||
Course staff are allowed to see the profiles of students in their class.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
|
||||
if student_id is None or student_id == request.user.id:
|
||||
# always allowed to see your own profile
|
||||
student = request.user
|
||||
else:
|
||||
# Requesting access to a different student's profile
|
||||
if not has_access(request.user, course, 'staff'):
|
||||
if not staff_access:
|
||||
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_module = get_module(request.user, request, course.location, student_module_cache, course_id=course_id)
|
||||
|
||||
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
|
||||
grade_summary = grades.grade(request.user, request, course, student_module_cache)
|
||||
@@ -282,8 +291,9 @@ def profile(request, course_id, student_id=None):
|
||||
'email': student.email,
|
||||
'course': course,
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'courseware_summary' : courseware_summary,
|
||||
'grade_summary' : grade_summary
|
||||
'courseware_summary': courseware_summary,
|
||||
'grade_summary': grade_summary,
|
||||
'staff_access': staff_access,
|
||||
}
|
||||
context.update()
|
||||
|
||||
@@ -316,7 +326,10 @@ def gradebook(request, course_id):
|
||||
for student in enrolled_students]
|
||||
|
||||
return render_to_response('gradebook.html', {'students': student_info,
|
||||
'course': course, 'course_id': course_id})
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
# Checked above
|
||||
'staff_access': True,})
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@@ -325,7 +338,8 @@ def grade_summary(request, course_id):
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
context = {'course': course,
|
||||
'staff_access': True,}
|
||||
return render_to_response('grade_summary.html', context)
|
||||
|
||||
|
||||
@@ -335,6 +349,7 @@ def instructor_dashboard(request, course_id):
|
||||
course = get_course_with_access(request.user, course_id, 'staff')
|
||||
|
||||
# For now, just a static page
|
||||
context = {'course': course }
|
||||
context = {'course': course,
|
||||
'staff_access': True,}
|
||||
return render_to_response('instructor_dashboard.html', context)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None):
|
||||
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
|
||||
if not ip:
|
||||
ip = request.META.get('REMOTE_ADDR','None')
|
||||
|
||||
|
||||
if LOCAL_DEBUG:
|
||||
html += '<h3>IP address: %s ' % ip
|
||||
html += '<h3>User: %s ' % request.user
|
||||
@@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None):
|
||||
html += 'Permission denied'
|
||||
html += "</body></html>"
|
||||
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
|
||||
return HttpResponse(html)
|
||||
return HttpResponse(html)
|
||||
|
||||
#----------------------------------------
|
||||
# reload course if specified
|
||||
@@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None):
|
||||
#----------------------------------------
|
||||
|
||||
dumpfields = ['definition','location','metadata']
|
||||
|
||||
|
||||
for cdir, course in def_ms.courses.items():
|
||||
html += '<hr width="100%"/>'
|
||||
html += '<h2>Course: %s (%s)</h2>' % (course.metadata['display_name'],cdir)
|
||||
html += '<h2>Course: %s (%s)</h2>' % (course.display_name,cdir)
|
||||
|
||||
for field in dumpfields:
|
||||
data = getattr(course,field)
|
||||
@@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None):
|
||||
html += '</ul>'
|
||||
else:
|
||||
html += '<ul><li>%s</li></ul>' % escape(data)
|
||||
|
||||
|
||||
|
||||
#----------------------------------------
|
||||
|
||||
@@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None):
|
||||
log.debug('def_ms=%s' % unicode(def_ms))
|
||||
|
||||
html += "</body></html>"
|
||||
return HttpResponse(html)
|
||||
return HttpResponse(html)
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.courses import get_opt_course_with_access
|
||||
from courseware.access import has_access
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -49,6 +50,10 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
|
||||
if request:
|
||||
dictionary.update(csrf(request))
|
||||
|
||||
if request and course:
|
||||
dictionary['staff_access'] = has_access(request.user, course, 'staff')
|
||||
else:
|
||||
dictionary['staff_access'] = False
|
||||
|
||||
def view(request, article_path, course_id=None):
|
||||
course = get_opt_course_with_access(request.user, course_id, 'load')
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import get_course_with_access
|
||||
from lxml import etree
|
||||
|
||||
@login_required
|
||||
def index(request, course_id, page=0):
|
||||
def index(request, course_id, book_index, page=0):
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
|
||||
table_of_contents = etree.parse(raw_table_of_contents).getroot()
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
|
||||
textbook = course.textbooks[int(book_index)]
|
||||
table_of_contents = textbook.table_of_contents
|
||||
|
||||
return render_to_response('staticbook.html',
|
||||
{'page': int(page), 'course': course,
|
||||
'table_of_contents': table_of_contents})
|
||||
|
||||
'table_of_contents': table_of_contents,
|
||||
'staff_access': staff_access})
|
||||
|
||||
def index_shifted(request, course_id, page):
|
||||
return index(request, course_id=course_id, page=int(page) + 24)
|
||||
|
||||
@@ -49,6 +49,11 @@ MITX_FEATURES = {
|
||||
## Doing so will cause all courses to be released on production
|
||||
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
|
||||
|
||||
# When True, will only publicly list courses by the subdomain. Expects you
|
||||
# to define COURSE_LISTINGS, a dictionary mapping subdomains to lists of
|
||||
# course_ids (see dev_int.py for an example)
|
||||
'SUBDOMAIN_COURSE_LISTINGS' : False,
|
||||
|
||||
'ENABLE_TEXTBOOK' : True,
|
||||
'ENABLE_DISCUSSION' : True,
|
||||
|
||||
@@ -61,6 +66,7 @@ MITX_FEATURES = {
|
||||
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
|
||||
'AUTH_USE_OPENID': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES' : False,
|
||||
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
|
||||
@@ -54,7 +54,7 @@ CACHES = {
|
||||
}
|
||||
|
||||
XQUEUE_INTERFACE = {
|
||||
"url": "http://xqueue.sandbox.edx.org",
|
||||
"url": "https://sandbox-xqueue.edx.org",
|
||||
"django_auth": {
|
||||
"username": "lms",
|
||||
"password": "***REMOVED***"
|
||||
|
||||
32
lms/envs/dev_int.py
Normal file
32
lms/envs/dev_int.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
This enables use of course listings by subdomain. To see it in action, point the
|
||||
following domains to 127.0.0.1 in your /etc/hosts file:
|
||||
|
||||
berkeley.dev
|
||||
harvard.dev
|
||||
mit.dev
|
||||
|
||||
Note that OS X has a bug where using *.local domains is excruciatingly slow, so
|
||||
use *.dev domains instead for local testing.
|
||||
"""
|
||||
from .dev import *
|
||||
|
||||
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
|
||||
|
||||
COURSE_LISTINGS = {
|
||||
'default' : ['BerkeleyX/CS169.1x/2012_Fall',
|
||||
'BerkeleyX/CS188.1x/2012_Fall',
|
||||
'HarvardX/CS50x/2012',
|
||||
'HarvardX/PH207x/2012_Fall',
|
||||
'MITx/3.091x/2012_Fall',
|
||||
'MITx/6.002x/2012_Fall',
|
||||
'MITx/6.00x/2012_Fall'],
|
||||
|
||||
'berkeley': ['BerkeleyX/CS169.1x/2012_Fall',
|
||||
'BerkeleyX/CS188.1x/2012_Fall'],
|
||||
|
||||
'harvard' : ['HarvardX/CS50x/2012'],
|
||||
|
||||
'mit' : ['MITx/3.091x/2012_Fall',
|
||||
'MITx/6.00x/2012_Fall']
|
||||
}
|
||||
@@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||
|
||||
|
||||
XQUEUE_INTERFACE = {
|
||||
"url": "http://xqueue.sandbox.edx.org",
|
||||
"url": "http://sandbox-xqueue.edx.org",
|
||||
"django_auth": {
|
||||
"username": "lms",
|
||||
"password": "***REMOVED***"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 214 B |
Binary file not shown.
|
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 211 B |
Binary file not shown.
|
Before Width: | Height: | Size: 200 B After Width: | Height: | Size: 1.1 KiB |
@@ -69,10 +69,15 @@ div.info-wrapper {
|
||||
section.handouts {
|
||||
@extend .sidebar;
|
||||
border-left: 1px solid $border-color;
|
||||
@include border-radius(0 4px 4px 0);
|
||||
border-right: 0;
|
||||
@include border-radius(0 4px 4px 0);
|
||||
@include box-shadow(none);
|
||||
|
||||
&:after {
|
||||
left: -1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@extend .bottom-border;
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -8,6 +8,11 @@ div.profile-wrapper {
|
||||
@include border-radius(0px 4px 4px 0);
|
||||
border-right: 0;
|
||||
|
||||
&:after {
|
||||
left: -1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
header {
|
||||
@extend .bottom-border;
|
||||
margin: 0;
|
||||
|
||||
@@ -10,7 +10,6 @@ div.book-wrapper {
|
||||
font-size: em(14);
|
||||
|
||||
.chapter-number {
|
||||
|
||||
}
|
||||
|
||||
.chapter {
|
||||
@@ -81,9 +80,8 @@ div.book-wrapper {
|
||||
|
||||
section.book {
|
||||
@extend .content;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-left: lh();
|
||||
|
||||
nav {
|
||||
@extend .clearfix;
|
||||
|
||||
@@ -2,7 +2,7 @@ body {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
|
||||
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a, label {
|
||||
text-align: left;
|
||||
font-family: $sans-serif;
|
||||
}
|
||||
@@ -27,6 +27,14 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: em(40) 0;
|
||||
}
|
||||
|
||||
::selection, ::-moz-selection, ::-webkit-selection {
|
||||
background:#444;
|
||||
color:#fff;
|
||||
|
||||
@@ -12,10 +12,13 @@ h1.top-header {
|
||||
@include box-shadow(inset 0 1px 0 #fff);
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
font: normal $body-font-size $body-font-family;
|
||||
font: 400 $body-font-size $body-font-family;
|
||||
@include linear-gradient(#fff, lighten(#888, 40%));
|
||||
padding: 4px 8px;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:hover, &:focus {
|
||||
@@ -28,7 +31,7 @@ h1.top-header {
|
||||
.content {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
padding: lh();
|
||||
padding-right: lh();
|
||||
vertical-align: top;
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
|
||||
@@ -46,6 +49,18 @@ h1.top-header {
|
||||
vertical-align: top;
|
||||
width: flex-grid(3);
|
||||
|
||||
&:after {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
@include position(absolute, 0px -1px 0px 0);
|
||||
content: "";
|
||||
@include background-image(linear-gradient(top, #fff, rgba(#fff, 0)), linear-gradient(top, rgba(#fff, 0), #fff));
|
||||
background-position: top, bottom;
|
||||
@include background-size(1px 20px);
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-size: em(20);
|
||||
font-weight: 100;
|
||||
@@ -134,7 +149,7 @@ h1.top-header {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
text-indent: -9999px;
|
||||
top: 6px;
|
||||
top: 12px;
|
||||
width: 16px;
|
||||
z-index: 99;
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ div.course-wrapper {
|
||||
|
||||
section.course-content {
|
||||
@extend .content;
|
||||
@include border-radius(0 4px 4px 0);
|
||||
padding-right: 0;
|
||||
padding-left: lh();
|
||||
|
||||
h1 {
|
||||
margin: 0 0 lh();
|
||||
|
||||
@@ -7,9 +7,16 @@ div.answer-controls {
|
||||
padding-left: flex-grid(1.1);
|
||||
width: 100%;
|
||||
|
||||
|
||||
div.answer-count {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
|
||||
h1 {
|
||||
margin-bottom: 0;
|
||||
font-size: em(24);
|
||||
font-weight: 100;
|
||||
}
|
||||
}
|
||||
|
||||
div.answer-sort {
|
||||
@@ -18,7 +25,7 @@ div.answer-controls {
|
||||
|
||||
nav {
|
||||
float: right;
|
||||
margin-top: 34px;
|
||||
margin-top: 10px;
|
||||
|
||||
a {
|
||||
&.on span{
|
||||
@@ -44,8 +51,9 @@ div.answer-block {
|
||||
width: 100%;
|
||||
|
||||
img.answer-img-accept {
|
||||
margin: 10px 0px 10px 16px;
|
||||
margin: 10px 0px 10px 11px;
|
||||
}
|
||||
|
||||
div.answer-container {
|
||||
@extend div.question-container;
|
||||
|
||||
@@ -130,21 +138,19 @@ div.answer-own {
|
||||
|
||||
div.answer-actions {
|
||||
margin: 0;
|
||||
padding:8px 8px 8px 0;
|
||||
padding:8px 0 8px 8px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #efefef;
|
||||
|
||||
span.sep {
|
||||
color: #EDDFAA;
|
||||
color: $border-color;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&.question-delete {
|
||||
color: $mit-red;
|
||||
}
|
||||
@extend a:link;
|
||||
font-size: em(14);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ div#award-list{
|
||||
}
|
||||
|
||||
ul.badge-list {
|
||||
padding-left: 0;
|
||||
|
||||
li.badge {
|
||||
border-bottom: 1px solid #eee;
|
||||
@extend .clearfix;
|
||||
@@ -70,12 +72,17 @@ ul.badge-list {
|
||||
.bronze, .badge3 {
|
||||
color: #cc9933;
|
||||
}
|
||||
div.badge-desc {
|
||||
> div {
|
||||
margin-bottom: 20px;
|
||||
span {
|
||||
font-size: 18px;
|
||||
@include border-radius(10px);
|
||||
}
|
||||
|
||||
div.discussion-wrapper aside {
|
||||
div.badge-desc {
|
||||
border-top: 0;
|
||||
|
||||
> div {
|
||||
margin-bottom: 20px;
|
||||
span {
|
||||
font-size: 18px;
|
||||
@include border-radius(10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ body.askbot {
|
||||
@include box-sizing(border-box);
|
||||
display: table-cell;
|
||||
min-width: 650px;
|
||||
padding: lh();
|
||||
padding-right: lh();
|
||||
vertical-align: top;
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@ form.answer-form {
|
||||
border-top: 1px solid #ddd;
|
||||
overflow: hidden;
|
||||
padding-left: flex-grid(1.1);
|
||||
padding-top: lh();
|
||||
|
||||
p {
|
||||
margin-bottom: lh();
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include box-sizing(border-box);
|
||||
@@ -121,7 +126,6 @@ form.question-form {
|
||||
border: none;
|
||||
padding: 15px 0 0 0;
|
||||
|
||||
|
||||
input[type="text"] {
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(6);
|
||||
@@ -131,6 +135,11 @@ form.question-form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
input[value="Cancel"] {
|
||||
@extend .light-button;
|
||||
float: right;
|
||||
}
|
||||
|
||||
div#question-list {
|
||||
background-color: rgba(255,255,255,0.95);
|
||||
@include box-sizing(border-box);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Style for modal boxes that pop up to notify the user of various events
|
||||
|
||||
.vote-notification {
|
||||
background-color: darken($mit-red, 7%);
|
||||
@include border-radius(4px);
|
||||
|
||||
@@ -9,9 +9,9 @@ body.user-profile-page {
|
||||
}
|
||||
|
||||
ul.sub-info {
|
||||
// border-top: 1px solid #ddd;
|
||||
margin-top: lh();
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
display: table-cell;
|
||||
@@ -57,6 +57,7 @@ body.user-profile-page {
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
&.user-stats-table {
|
||||
list-style: none;
|
||||
@@ -72,37 +73,28 @@ body.user-profile-page {
|
||||
margin-bottom: 30px;
|
||||
|
||||
li {
|
||||
background-position: 10px center;
|
||||
background-position: 10px -10px;
|
||||
background-repeat: no-repeat;
|
||||
@include border-radius(4px);
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
padding: 10px 10px 10px 40px;
|
||||
padding: 2px 10px 2px 40px;
|
||||
margin-bottom: lh(.5);
|
||||
border: 1px solid lighten($border-color, 10%);
|
||||
|
||||
&.up {
|
||||
background-color:#d1e3a8;
|
||||
background-image: url(../images/askbot/vote-arrow-up-activate.png);
|
||||
background-image: url(../images/askbot/vote-arrow-up.png);
|
||||
margin-right: 6px;
|
||||
|
||||
span.vote-count {
|
||||
color: #3f6c3e;
|
||||
}
|
||||
}
|
||||
|
||||
&.down {
|
||||
background-image: url(../images/askbot/vote-arrow-down-activate.png);
|
||||
background-color:#eac6ad;
|
||||
|
||||
span.vote-count {
|
||||
color: $mit-red;
|
||||
}
|
||||
|
||||
background-image: url(../images/askbot/vote-arrow-down.png);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.badges {
|
||||
@include inline-block();
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
background-color: #e3e3e3;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// Styles for the single question view
|
||||
|
||||
div.question-header {
|
||||
@include clearfix();
|
||||
|
||||
div.official-stamp {
|
||||
background: $mit-red;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
margin-left: -1px;
|
||||
margin-top: 10px;
|
||||
padding: 2px 5px;
|
||||
text-align: center;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
div.vote-buttons {
|
||||
@@ -19,40 +20,40 @@ div.question-header {
|
||||
width: flex-grid(0.7,9);
|
||||
|
||||
ul {
|
||||
li {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
list-style: none;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
width: 70%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&.post-vote {
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(inset 0 1px 0px #fff);
|
||||
}
|
||||
li {
|
||||
background-repeat: no-repeat;
|
||||
color: #999;
|
||||
font-size: em(20);
|
||||
font-weight: bold;
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
|
||||
&.question-img-upvote, &.answer-img-upvote {
|
||||
background-image: url(../images/askbot/vote-arrow-up.png);
|
||||
@include box-shadow(inset 0 1px 0px rgba(255, 255, 255, 0.5));
|
||||
background-position: center 0;
|
||||
cursor: pointer;
|
||||
height: 12px;
|
||||
margin-bottom: lh(.5);
|
||||
|
||||
&:hover, &.on {
|
||||
background-color:#d1e3a8;
|
||||
border-color: darken(#D1E3A8, 20%);
|
||||
background-image: url(../images/askbot/vote-arrow-up-activate.png);
|
||||
background-image: url(../images/askbot/vote-arrow-up.png);
|
||||
background-position: center -22px;
|
||||
}
|
||||
}
|
||||
|
||||
&.question-img-downvote, &.answer-img-downvote {
|
||||
cursor: pointer;
|
||||
background-image: url(../images/askbot/vote-arrow-down.png);
|
||||
background-position: center 0;
|
||||
height: 12px;
|
||||
margin-top: lh(.5);
|
||||
|
||||
&:hover, &.on {
|
||||
background-color:#EAC6AD;
|
||||
border-color: darken(#EAC6AD, 20%);
|
||||
background-image: url(../images/askbot/vote-arrow-down-activate.png);
|
||||
background-image: url(../images/askbot/vote-arrow-down.png);
|
||||
background-position: center -22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,12 +67,19 @@ div.question-header {
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-weight: 100;
|
||||
line-height: 1.1em;
|
||||
|
||||
a {
|
||||
font-weight: 100;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
div.meta-bar {
|
||||
border-bottom: 1px solid #eee;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
margin: lh(.5) 0 lh();
|
||||
overflow: hidden;
|
||||
padding: 5px 0 10px;
|
||||
|
||||
@@ -89,11 +97,8 @@ div.question-header {
|
||||
width: flex-grid(4,8);
|
||||
|
||||
a {
|
||||
&.question-delete {
|
||||
color: $mit-red;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
@extend a:link;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.sep {
|
||||
@@ -155,7 +160,7 @@ div.question-header {
|
||||
}
|
||||
|
||||
div.change-date {
|
||||
font-size: 12px;
|
||||
font-size: em(14);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
@@ -179,13 +184,13 @@ div.question-header {
|
||||
display: inline-block;
|
||||
padding: 0 0 3% 0;
|
||||
width: 100%;
|
||||
margin-top: lh(2);
|
||||
|
||||
div.comments-content {
|
||||
font-size: 13px;
|
||||
background: #efefef;
|
||||
border-top: 1px solid lighten($border-color, 10%);
|
||||
|
||||
.block {
|
||||
border-top: 1px solid #ddd;
|
||||
border-top: 1px solid lighten($border-color, 10%);
|
||||
padding: 15px;
|
||||
display: block;
|
||||
|
||||
@@ -197,10 +202,10 @@ div.question-header {
|
||||
padding-top: 10px;
|
||||
|
||||
span.official-comment {
|
||||
background: $mit-red;
|
||||
background: $pink;
|
||||
color: #fff;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-size: em(12);
|
||||
margin: 0 0 10px -5%;
|
||||
padding:2px 5px 2px 5%;
|
||||
text-align: left;
|
||||
@@ -212,6 +217,10 @@ div.question-header {
|
||||
form.post-comments {
|
||||
padding: 15px;
|
||||
|
||||
button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-left: 10px;
|
||||
@extend .light-button;
|
||||
@@ -232,7 +241,6 @@ div.question-header {
|
||||
border: none;
|
||||
@include box-shadow(none);
|
||||
display: inline-block;
|
||||
margin-top: -8px;
|
||||
padding:0 2% 0 0;
|
||||
text-align: center;
|
||||
width: 5%;
|
||||
@@ -278,16 +286,14 @@ div.question-header {
|
||||
}
|
||||
|
||||
div.comment-delete {
|
||||
// display: inline;
|
||||
color: $mit-red;
|
||||
@extend a:link;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
div.comment-edit {
|
||||
@include transform(rotate(50deg));
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
|
||||
a.edit-icon {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
@@ -305,13 +311,13 @@ div.question-header {
|
||||
|
||||
div.comment-meta {
|
||||
text-align: right;
|
||||
margin-top: lh(.5);
|
||||
|
||||
a.author {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a.edit {
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
}
|
||||
@@ -334,12 +340,10 @@ div.question-header {
|
||||
}
|
||||
|
||||
div.controls {
|
||||
border-top: 1px solid #efefef;
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
margin: 10px 10px 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
// Styles for the default question list view
|
||||
|
||||
div.question-list-header {
|
||||
@extend h1.top-header;
|
||||
display: block;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: lh(.5);
|
||||
overflow: hidden;
|
||||
width: flex-grid(9,9);
|
||||
@extend h1.top-header;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 100;
|
||||
padding-bottom: lh(.5);
|
||||
|
||||
> a.light-button {
|
||||
float: right;
|
||||
font-size: em(14, 24);
|
||||
letter-spacing: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +56,11 @@ div.question-list-header {
|
||||
nav {
|
||||
@extend .action-link;
|
||||
float: right;
|
||||
font-size: em(16, 24);
|
||||
|
||||
a {
|
||||
font-size: 1em;
|
||||
|
||||
&.on span{
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -82,6 +92,7 @@ div.question-list-header {
|
||||
|
||||
a {
|
||||
color: #555;
|
||||
font-size: em(14, 24);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +101,10 @@ div.question-list-header {
|
||||
}
|
||||
|
||||
ul.tags {
|
||||
li {
|
||||
background: #fff;
|
||||
|
||||
&:before {
|
||||
border-color: transparent #fff transparent transparent;
|
||||
}
|
||||
span, div {
|
||||
line-height: 1em;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,26 +112,15 @@ div.question-list-header {
|
||||
|
||||
ul.question-list, div#question-list {
|
||||
width: flex-grid(9,9);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
|
||||
li.single-question {
|
||||
border-bottom: 1px solid #eee;
|
||||
list-style: none;
|
||||
padding: 10px lh();
|
||||
margin-left: (-(lh()));
|
||||
padding: lh() 0;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: #F3F3F3;
|
||||
|
||||
ul.tags li {
|
||||
background: #ddd;
|
||||
|
||||
&:before {
|
||||
border-color: transparent #ddd transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
@@ -133,14 +131,19 @@ ul.question-list, div#question-list {
|
||||
&.question-body {
|
||||
@include box-sizing(border-box);
|
||||
margin-right: flex-gutter();
|
||||
width: flex-grid(5.5,9);
|
||||
width: flex-grid(5,9);
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-size: em(20);
|
||||
font-weight: bold;
|
||||
letter-spacing: 0;
|
||||
margin: 0px 0 15px 0;
|
||||
margin: 0 0 lh() 0;
|
||||
text-transform: none;
|
||||
line-height: lh();
|
||||
|
||||
a {
|
||||
line-height: lh();
|
||||
}
|
||||
}
|
||||
|
||||
p.excerpt {
|
||||
@@ -151,40 +154,41 @@ ul.question-list, div#question-list {
|
||||
div.user-info {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: 10px;
|
||||
margin: lh() 0 0 0;
|
||||
line-height: lh();
|
||||
|
||||
span.relative-time {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $mit-red;
|
||||
line-height: lh();
|
||||
}
|
||||
}
|
||||
|
||||
ul.tags {
|
||||
display: inline-block;
|
||||
margin: lh() 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.question-meta {
|
||||
float: right;
|
||||
margin-top: 10px;
|
||||
width: flex-grid(3.5,9);
|
||||
|
||||
width: flex-grid(3,9);
|
||||
|
||||
ul {
|
||||
text-align: right;
|
||||
@include clearfix;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid lighten($border-color, 10%);
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(0 1px 0 #fff);
|
||||
display: inline-block;
|
||||
height:60px;
|
||||
@include linear-gradient(#fff, #f5f5f5);
|
||||
margin-right: 10px;
|
||||
width: 60px;
|
||||
float: left;
|
||||
margin-right: flex-gutter(3);
|
||||
width: flex-grid(1,3);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
@@ -196,31 +200,22 @@ ul.question-list, div#question-list {
|
||||
}
|
||||
}
|
||||
|
||||
&.views {
|
||||
}
|
||||
|
||||
&.answers {
|
||||
&.accepted {
|
||||
|
||||
@include linear-gradient(#fff, lighten( #c4dfbe, 12% ));
|
||||
border-color: #c4dfbe;
|
||||
border-color: lighten($border-color, 10%);
|
||||
|
||||
span, div {
|
||||
color: darken(#c4dfbe, 35%);
|
||||
}
|
||||
}
|
||||
|
||||
&.no-answers {
|
||||
|
||||
|
||||
span, div {
|
||||
color: lighten($mit-red, 20%);
|
||||
color: $pink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.votes {
|
||||
}
|
||||
|
||||
span, div {
|
||||
@include box-sizing(border-box);
|
||||
color: #888;
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
|
||||
div.discussion-wrapper aside {
|
||||
@extend .sidebar;
|
||||
border-left: 1px solid #d3d3d3;
|
||||
@include border-radius(0 4px 4px 0);
|
||||
border-right: 1px solid #f6f6f6;
|
||||
@include box-shadow(inset 1px 0 0 #f6f6f6);
|
||||
padding: lh();
|
||||
border-left: 1px solid $border-color;
|
||||
border-right: 0;
|
||||
width: flex-grid(3);
|
||||
|
||||
&:after {
|
||||
left: -1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
&.main-sidebar {
|
||||
min-width:200px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@extend .bottom-border;
|
||||
margin: (-(lh())) (-(lh())) 0;
|
||||
padding: lh(.5) lh();
|
||||
margin-bottom: em(16, 20);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #4D4D4D;
|
||||
color: #3C3C3C;
|
||||
font-size: 1em;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.first {
|
||||
margin-top: 0px;
|
||||
@@ -36,6 +42,9 @@ div.discussion-wrapper aside {
|
||||
input[type="submit"] {
|
||||
width: 27%;
|
||||
float: right;
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
@@ -45,24 +54,30 @@ div.discussion-wrapper aside {
|
||||
|
||||
div.box {
|
||||
display: block;
|
||||
margin: lh(.5) 0;
|
||||
padding: lh(.5) lh();
|
||||
border-top: 1px solid lighten($border-color, 10%);
|
||||
|
||||
&:last-child {
|
||||
@include box-shadow(none);
|
||||
border: 0;
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
ul#related-tags {
|
||||
position: relative;
|
||||
left: -10px;
|
||||
|
||||
&:not(.first) {
|
||||
@include box-shadow(inset 0 1px 0 #eee);
|
||||
border-top: 1px solid #d3d3d3;
|
||||
margin: 0 (-(lh())) 0;
|
||||
padding: lh(.5) lh();
|
||||
li {
|
||||
border-bottom: 0;
|
||||
background: #eee;
|
||||
padding: 6px 10px 6px 5px;
|
||||
|
||||
a {
|
||||
padding: 0;
|
||||
line-height: 12px;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +100,6 @@ div.discussion-wrapper aside {
|
||||
}
|
||||
|
||||
}
|
||||
img.gravatar {
|
||||
@include border-radius(3px);
|
||||
}
|
||||
}
|
||||
|
||||
&.tag-selector {
|
||||
@@ -100,17 +112,19 @@ div.discussion-wrapper aside {
|
||||
|
||||
div.search-box {
|
||||
margin-top: lh(.5);
|
||||
|
||||
input {
|
||||
@include box-sizing(border-box);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
input[type='submit'] {
|
||||
@include box-shadow(none);
|
||||
opacity: 0.5;
|
||||
background: url(../images/askbot/search-icon.png) no-repeat center;
|
||||
border: 0;
|
||||
@include box-shadow(none);
|
||||
margin-left: 3px;
|
||||
opacity: 0.5;
|
||||
padding: 6px 0 0;
|
||||
position: absolute;
|
||||
text-indent: -9999px;
|
||||
width: 24px;
|
||||
@@ -131,30 +145,26 @@ div.discussion-wrapper aside {
|
||||
}
|
||||
|
||||
input#clear {
|
||||
@include box-shadow(none);
|
||||
@include border-radius(15px);
|
||||
background: none;
|
||||
border: none;
|
||||
background: #bbb;
|
||||
color: #fff;
|
||||
@include border-radius(0);
|
||||
@include box-shadow(none);
|
||||
color: #999;
|
||||
display: inline;
|
||||
font-size: 10px;
|
||||
margin-left: -25px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
height: 19px;
|
||||
line-height: 1em;
|
||||
margin: {
|
||||
left: -25px;
|
||||
top: 8px;
|
||||
}
|
||||
padding: 2px 5px;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
div#tagSelector {
|
||||
h2 {
|
||||
@include box-shadow(inset 0 1px 0 #eee);
|
||||
border-top: 1px solid #d3d3d3;
|
||||
margin: 0 (-(lh())) 0;
|
||||
padding: lh(.5) lh();
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -167,11 +177,17 @@ div.discussion-wrapper aside {
|
||||
p.choice {
|
||||
@include inline-block();
|
||||
margin-right: lh(.5);
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
// Question view sopecific
|
||||
// Question view specific
|
||||
|
||||
div.follow-buttons {
|
||||
margin-top: 20px;
|
||||
@@ -187,12 +203,15 @@ div.discussion-wrapper aside {
|
||||
|
||||
|
||||
div.question-stats {
|
||||
border-top: 0;
|
||||
|
||||
ul {
|
||||
color: #777;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
padding: 7px 0 0;
|
||||
border: 0;
|
||||
|
||||
&:last-child {
|
||||
@include box-shadow(none);
|
||||
@@ -216,19 +235,20 @@ div.discussion-wrapper aside {
|
||||
}
|
||||
|
||||
div.karma {
|
||||
background: #eee;
|
||||
border: 1px solid #D3D3D3;
|
||||
@include border-radius(3px);
|
||||
border: 1px solid $border-color;
|
||||
@include box-sizing(border-box);
|
||||
@include box-shadow(inset 0 0 0 1px #fff, 0 1px 0 #fff);
|
||||
padding: lh(.4) 0;
|
||||
text-align: center;
|
||||
width: flex-grid(1, 3);
|
||||
float: right;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-style: 20px;
|
||||
p {
|
||||
text-align: center;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-style: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +275,6 @@ div.discussion-wrapper aside {
|
||||
overflow: visible;
|
||||
|
||||
ul {
|
||||
font-size: 14px;
|
||||
|
||||
h2 {
|
||||
margin:0 (-(lh())) 5px (-(lh()));
|
||||
padding: lh(.5) lh();
|
||||
@@ -265,40 +283,29 @@ div.discussion-wrapper aside {
|
||||
}
|
||||
|
||||
div.question-tips, div.markdown {
|
||||
ul {
|
||||
margin-left: 8%;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 8%;
|
||||
}
|
||||
}
|
||||
div.markdown ul li {
|
||||
margin: 20px 0;
|
||||
|
||||
&:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
padding: 0;
|
||||
|
||||
ol li {
|
||||
margin: 0;
|
||||
li {
|
||||
border-bottom: 0;
|
||||
line-height: lh();
|
||||
margin-bottom: em(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.view-profile {
|
||||
h2 {
|
||||
border-top: 0;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
border-top: 0;
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
@extend .light-button;
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: lh(.5);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
ul.tags {
|
||||
list-style: none;
|
||||
display: inline;
|
||||
padding: 0;
|
||||
|
||||
li, a {
|
||||
position: relative;
|
||||
@@ -10,19 +11,17 @@ ul.tags {
|
||||
|
||||
li {
|
||||
background: #eee;
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(0px 1px 0px #ccc);
|
||||
color: #555;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 15px;
|
||||
padding: 3px 10px 5px 5px;
|
||||
padding: 6px 10px 6px 5px;
|
||||
|
||||
&:before {
|
||||
border-color:transparent #eee transparent transparent;
|
||||
border-style:solid;
|
||||
border-width:12px 12px 12px 0;
|
||||
border-width:12px 10px 12px 0;
|
||||
content:"";
|
||||
height:0;
|
||||
left:-10px;
|
||||
@@ -31,25 +30,6 @@ ul.tags {
|
||||
width:0;
|
||||
}
|
||||
|
||||
span.delete-icon, div.delete-icon {
|
||||
background: #555;
|
||||
@include border-radius(0 4px 4px 0);
|
||||
clear: none;
|
||||
color: #eee;
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
float: none;
|
||||
left: 10px;
|
||||
opacity: 0.5;
|
||||
padding: 4px 6px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
@@ -61,11 +41,4 @@ ul.tags {
|
||||
|
||||
span.tag-number {
|
||||
display: none;
|
||||
// @include border-radius(3px);
|
||||
// background: #555;
|
||||
// font-size: 10px;
|
||||
// margin: 0 3px;
|
||||
// padding: 2px 5px;
|
||||
// color: #eee;
|
||||
// opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ textarea {
|
||||
|
||||
input[type="submit"],
|
||||
input[type="button"],
|
||||
button,
|
||||
.button {
|
||||
@include border-radius(3px);
|
||||
@include button(shiny, $blue);
|
||||
|
||||
@@ -21,8 +21,10 @@ def url_class(url):
|
||||
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
|
||||
% if user.is_authenticated():
|
||||
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
|
||||
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li>
|
||||
% endif
|
||||
% for index, textbook in enumerate(course.textbooks):
|
||||
<li class="book"><a href="${reverse('book', args=[course.id, index])}" class="${url_class('book')}">${textbook.title}</a></li>
|
||||
% endfor
|
||||
% endif
|
||||
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
|
||||
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li>
|
||||
% endif
|
||||
@@ -33,7 +35,7 @@ def url_class(url):
|
||||
% if user.is_authenticated():
|
||||
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
|
||||
% endif
|
||||
% if has_access(user, course, 'staff'):
|
||||
% if staff_access:
|
||||
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
|
||||
% endif
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<h2 class="problem-header">
|
||||
${ problem['name'] }
|
||||
% if problem['weight'] != 1:
|
||||
% if problem['weight'] != 1 and problem['weight'] != None:
|
||||
: ${ problem['weight'] } points
|
||||
% endif
|
||||
</h2>
|
||||
|
||||
@@ -126,9 +126,9 @@ if settings.COURSEWARE_ENABLED:
|
||||
#Inside the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
|
||||
'courseware.views.course_info', name="info"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/$',
|
||||
'staticbook.views.index', name="book"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/(?P<page>[^/]*)$',
|
||||
'staticbook.views.index'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
|
||||
'staticbook.views.index_shifted'),
|
||||
|
||||
Reference in New Issue
Block a user