Commit b0b10aa5 by Tom Giannattasio

Merge branch 'feature/bridger/new_wiki' of github.com:MITx/mitx into feature/bridger/new_wiki

parents 490eb7a3 2de55d07
import functools
import json import json
import logging import logging
import random import random
...@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None): ...@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None):
log.debug('ExtAuth: doing signup for %s' % eamap.external_email) 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 # MIT SSL
...@@ -206,7 +207,7 @@ def edXauth_ssl_login(request): ...@@ -206,7 +207,7 @@ def edXauth_ssl_login(request):
pass pass
if not cert: if not cert:
# no certificate information - go onward to main index # 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) (user, email, fullname) = ssl_dn_extract_info(cert)
...@@ -216,4 +217,4 @@ def edXauth_ssl_login(request): ...@@ -216,4 +217,4 @@ def edXauth_ssl_login(request):
credentials=cert, credentials=cert,
email=email, email=email,
fullname=fullname, fullname=fullname,
retfun = student_views.main_index) retfun = functools.partial(student_views.main_index, request))
...@@ -68,9 +68,9 @@ def index(request): ...@@ -68,9 +68,9 @@ def index(request):
from external_auth.views import edXauth_ssl_login from external_auth.views import edXauth_ssl_login
return edXauth_ssl_login(request) 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. Render the edX main page.
...@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None): ...@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None):
entry.summary = soup.getText() entry.summary = soup.getText()
# The course selection work is done in courseware.courses. # 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 = {'universities': universities, 'entries': entries}
context.update(extra_context) context.update(extra_context)
return render_to_response('index.html', context) return render_to_response('index.html', context)
......
...@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template): ...@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template):
return _get_html 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): def replace_static_urls(get_html, prefix, module):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
......
...@@ -49,9 +49,9 @@ class ABTestModule(XModule): ...@@ -49,9 +49,9 @@ class ABTestModule(XModule):
return json.dumps({'group': self.group}) return json.dumps({'group': self.group})
def displayable_items(self): def displayable_items(self):
return filter(None, [self.system.get_module(child) child_locations = self.definition['data']['group_content'][self.group]
for child children = [self.system.get_module(loc) for loc in child_locations]
in self.definition['data']['group_content'][self.group]]) return [c for c in children if c is not None]
# TODO (cpennington): Use Groups should be a first class object, rather than being # TODO (cpennington): Use Groups should be a first class object, rather than being
......
...@@ -21,6 +21,7 @@ def process_includes(fn): ...@@ -21,6 +21,7 @@ def process_includes(fn):
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
next_include = xml_object.find('include') next_include = xml_object.find('include')
while next_include is not None: while next_include is not None:
system.error_tracker("WARNING: the <include> tag is deprecated, and will go away.")
file = next_include.get('file') file = next_include.get('file')
parent = next_include.getparent() parent = next_include.getparent()
...@@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
the child element the child element
""" """
xml_object = etree.fromstring(xml_data) 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: if len(xml_object) == 1:
for (key, val) in xml_object.items(): for (key, val) in xml_object.items():
...@@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor):
return system.process_xml(etree.tostring(xml_object[0])) return system.process_xml(etree.tostring(xml_object[0]))
else: else:
xml_object.tag = 'sequence' xml_object.tag = 'sequential'
return system.process_xml(etree.tostring(xml_object)) return system.process_xml(etree.tostring(xml_object))
...@@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor): ...@@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
""" """
Transforms the xml_data from <$custom_tag attr="" attr=""/> to 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) 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 tag = xml_object.tag
xml_object.tag = 'customtag' xml_object.tag = 'customtag'
xml_object.attrib['impl'] = tag xml_object.attrib['impl'] = tag
......
...@@ -237,7 +237,7 @@ class CapaModule(XModule): ...@@ -237,7 +237,7 @@ class CapaModule(XModule):
else: else:
raise raise
content = {'name': self.metadata['display_name'], content = {'name': self.display_name,
'html': html, 'html': html,
'weight': self.weight, 'weight': self.weight,
} }
...@@ -376,14 +376,17 @@ class CapaModule(XModule): ...@@ -376,14 +376,17 @@ class CapaModule(XModule):
''' '''
For the "show answer" button. For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js
Returns the answers: {'answers' : answers} 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(): if not self.answer_available():
raise NotFoundError('Answer is not available') raise NotFoundError('Answer is not available')
else: else:
answers = self.lcp.get_question_answers() 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} return {'answers': answers}
# Figure out if we should move these to capa_problem? # Figure out if we should move these to capa_problem?
...@@ -464,7 +467,7 @@ class CapaModule(XModule): ...@@ -464,7 +467,7 @@ class CapaModule(XModule):
return {'success': msg} return {'success': msg}
log.exception("Error in capa_module problem checking") log.exception("Error in capa_module problem checking")
raise Exception("error in capa_module") raise Exception("error in capa_module")
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done = True self.lcp.done = True
......
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
import time import time
import logging import logging
from lxml import etree
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy from xmodule.graders import load_grading_policy
...@@ -10,12 +11,28 @@ from xmodule.timeparse import parse_time, stringify_time ...@@ -10,12 +11,28 @@ from xmodule.timeparse import parse_time, stringify_time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule 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): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self.textbooks = self.definition['data']['textbooks']
msg = None msg = None
if self.start is None: if self.start is None:
...@@ -28,6 +45,16 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -28,6 +45,16 @@ class CourseDescriptor(SequenceDescriptor):
self.enrollment_start = self._try_parse_time("enrollment_start") self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end") 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): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
...@@ -53,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -53,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor):
return grading_policy return grading_policy
@lazyproperty @lazyproperty
def grading_context(self): def grading_context(self):
""" """
...@@ -140,7 +166,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -140,7 +166,7 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def title(self): def title(self):
return self.metadata['display_name'] return self.display_name
@property @property
def number(self): def number(self):
......
...@@ -4,7 +4,7 @@ nav.sequence-nav { ...@@ -4,7 +4,7 @@ nav.sequence-nav {
@extend .topbar; @extend .topbar;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
@include border-top-right-radius(4px); @include border-top-right-radius(4px);
margin: (-(lh())) (-(lh())) lh() (-(lh())); margin: 0 0 lh() (-(lh()));
position: relative; position: relative;
ol { ol {
......
...@@ -4,7 +4,7 @@ div.video { ...@@ -4,7 +4,7 @@ div.video {
border-bottom: 1px solid #e1e1e1; border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1;
display: block; display: block;
margin: 0 (-(lh())); margin: 0 0 0 (-(lh()));
padding: 6px lh(); padding: 6px lh();
article.video-wrapper { article.video-wrapper {
......
...@@ -112,8 +112,8 @@ class TestMongoModuleStore(object): ...@@ -112,8 +112,8 @@ class TestMongoModuleStore(object):
should_work = ( should_work = (
("i4x://edX/toy/video/Welcome", ("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)), ("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/html/toylab", ("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)), ("edX/toy/2012_Fall", "Overview", None, None)),
) )
for location, expected in should_work: for location, expected in should_work:
assert_equals(path_to_location(self.store, location), expected) assert_equals(path_to_location(self.store, location), expected)
......
import json
import logging import logging
import os import os
import re import re
...@@ -5,6 +6,7 @@ import re ...@@ -5,6 +6,7 @@ import re
from fs.osfs import OSFS from fs.osfs import OSFS
from importlib import import_module from importlib import import_module
from lxml import etree from lxml import etree
from lxml.html import HtmlComment
from path import path from path import path
from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
...@@ -15,9 +17,10 @@ from cStringIO import StringIO ...@@ -15,9 +17,10 @@ from cStringIO import StringIO
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
etree.set_default_parser( edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
etree.XMLParser(dtd_validation=False, load_dtd=False, remove_comments=True, remove_blank_text=True)
remove_comments=True, remove_blank_text=True))
etree.set_default_parser(edx_xml_parser)
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string): ...@@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string):
return xml_string return xml_string
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): 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 A class that handles loading from xml. Does some munging to ensure that
all elements have unique slugs. all elements have unique slugs.
...@@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
MakoDescriptorSystem.__init__(self, load_item, resources_fs, MakoDescriptorSystem.__init__(self, load_item, resources_fs,
error_tracker, render_template, **kwargs) error_tracker, render_template, **kwargs)
XMLParsingSystem.__init__(self, load_item, resources_fs, XMLParsingSystem.__init__(self, load_item, resources_fs,
error_tracker, process_xml, **kwargs) error_tracker, process_xml, policy, **kwargs)
class XMLModuleStore(ModuleStoreBase): class XMLModuleStore(ModuleStoreBase):
...@@ -149,7 +153,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -149,7 +153,7 @@ class XMLModuleStore(ModuleStoreBase):
for course_dir in course_dirs: for course_dir in course_dirs:
self.try_load_course(course_dir) 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. Load a course, keeping track of errors as we go along.
''' '''
...@@ -170,7 +174,28 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -170,7 +174,28 @@ class XMLModuleStore(ModuleStoreBase):
''' '''
String representation - for debugging 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): def load_course(self, course_dir, tracker):
""" """
...@@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase):
# been imported into the cms from xml # been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read())) 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') org = course_data.get('org')
...@@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase):
tracker(msg) tracker(msg)
course = course_dir 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)) course_descriptor = system.process_xml(etree.tostring(course_data))
# NOTE: The descriptors end up loading somewhat bottom up, which # NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead # breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass # (actually, in addition to, for now), we do a final inheritance pass
......
...@@ -76,7 +76,7 @@ class SequenceModule(XModule): ...@@ -76,7 +76,7 @@ class SequenceModule(XModule):
contents.append({ contents.append({
'content': child.get_html(), 'content': child.get_html(),
'title': "\n".join( 'title': "\n".join(
grand_child.metadata['display_name'].strip() grand_child.display_name.strip()
for grand_child in child.get_children() for grand_child in child.get_children()
if 'display_name' in grand_child.metadata if 'display_name' in grand_child.metadata
), ),
...@@ -107,7 +107,7 @@ class SequenceModule(XModule): ...@@ -107,7 +107,7 @@ class SequenceModule(XModule):
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is stores_state = True # For remembering where in the sequence the student is
@classmethod @classmethod
......
...@@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem): ...@@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem):
descriptor.get_children() descriptor.get_children()
return descriptor return descriptor
policy = {}
XMLParsingSystem.__init__(self, load_item, self.resources_fs, 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): def render_template(self, template, context):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
......
...@@ -219,11 +219,11 @@ class XModule(HTMLSnippet): ...@@ -219,11 +219,11 @@ class XModule(HTMLSnippet):
Return module instances for all the children of this module. Return module instances for all the children of this module.
''' '''
if self._loaded_children is None: 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 # get_module returns None if the current user doesn't have access
# to the location. # to the location.
self._loaded_children = filter(None, self._loaded_children = [c for c in children if c is not None]
[self.system.get_module(child)
for child in self.definition.get('children', [])])
return self._loaded_children return self._loaded_children
...@@ -298,6 +298,14 @@ class XModule(HTMLSnippet): ...@@ -298,6 +298,14 @@ class XModule(HTMLSnippet):
return "" 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): class XModuleDescriptor(Plugin, HTMLSnippet):
""" """
An XModuleDescriptor is a specification for an element of a course. This An XModuleDescriptor is a specification for an element of a course. This
...@@ -416,6 +424,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -416,6 +424,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
return dict((k,v) for k,v in self.metadata.items() return dict((k,v) for k,v in self.metadata.items()
if k not in self._inherited_metadata) if k not in self._inherited_metadata)
@staticmethod @staticmethod
def compute_inherited_metadata(node): def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata """Given a descriptor, traverse all of its descendants and do metadata
...@@ -671,16 +680,19 @@ class DescriptorSystem(object): ...@@ -671,16 +680,19 @@ class DescriptorSystem(object):
class XMLParsingSystem(DescriptorSystem): 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 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 process_xml: Takes an xml string, and returns a XModuleDescriptor
created from that xml created from that xml
""" """
DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker, DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker,
**kwargs) **kwargs)
self.process_xml = process_xml self.process_xml = process_xml
self.policy = policy
class ModuleSystem(object): class ModuleSystem(object):
......
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import json import json
...@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor):
Subclasses should not need to override this except in special Subclasses should not need to override this except in special
cases (e.g. html module)''' 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) # converted. (note: make sure html files still work once this goes away)
filename = xml_object.get('filename') filename = xml_object.get('filename')
if filename is None: if filename is None:
...@@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor):
log.debug('Error %s in loading metadata %s' % (err,dmdata)) log.debug('Error %s in loading metadata %s' % (err,dmdata))
metadata['definition_metadata_err'] = str(err) 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( return cls(
system, system,
definition, definition,
......
<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>
roots/2012_Fall.xml
\ No newline at end of file
<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>
{
"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"
}
}
<course org="edX" course="toy" url_name="2012_Fall"/>
\ No newline at end of file
#!/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: ...@@ -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 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 @@ ...@@ -31,7 +31,7 @@
{% spaceless %} {% spaceless %}
<span class="action-link"> <span class="action-link">
<a class="question-delete" id="answer-delete-link-{{answer.id}}"> <a class="question-delete" id="answer-delete-link-{{answer.id}}">
{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}&#10006;{% endif %}</a> {% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
</span> </span>
{% endspaceless %} {% endspaceless %}
{% endif %} {% endif %}
......
...@@ -40,6 +40,5 @@ ...@@ -40,6 +40,5 @@
{% endif %} {% endif %}
{% if request.user|can_delete_post(question) %}{{ pipe() }} {% 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 %}&#10006;{% endif %}</a> <a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
{% endif %} {% endif %}
...@@ -18,11 +18,14 @@ ...@@ -18,11 +18,14 @@
{% include "widgets/system_messages.html" %} {% include "widgets/system_messages.html" %}
{% include "debug_header.html" %} {% include "debug_header.html" %}
{% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #} {% 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 %} {% block body %}
{% endblock %} {% endblock %}
</section>
</section> </section>
{% if settings.FOOTER_MODE == 'default' %} {% if settings.FOOTER_MODE == 'default' %}
......
{% load extra_filters_jinja %} {% load extra_filters_jinja %}
<!--<link href="{{"/style/style.css"|media }}" rel="stylesheet" type="text/css" />-->
{{ 'application' | compressed_css }} {{ 'application' | compressed_css }}
{{ 'course' | compressed_css }} {{ 'course' | compressed_css }}
...@@ -65,9 +65,10 @@ def has_access(user, obj, action): ...@@ -65,9 +65,10 @@ def has_access(user, obj, action):
# Passing an unknown object here is a coding error, so rather than # Passing an unknown object here is a coding error, so rather than
# returning a default, complain. # 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))) .format(type(obj)))
# ================ Implementation helpers ================================ # ================ Implementation helpers ================================
def _has_access_course_desc(user, course, action): def _has_access_course_desc(user, course, action):
...@@ -83,8 +84,12 @@ 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. 'staff' -- staff access to course.
""" """
def can_load(): 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) return _has_access_descriptor(user, course, action)
def can_enroll(): def can_enroll():
...@@ -169,6 +174,12 @@ def _has_access_descriptor(user, descriptor, action): ...@@ -169,6 +174,12 @@ def _has_access_descriptor(user, descriptor, action):
has_access(), it will not do the right thing. has_access(), it will not do the right thing.
""" """
def can_load(): 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 start dates are off, can always load
if settings.MITX_FEATURES['DISABLE_START_DATES']: if settings.MITX_FEATURES['DISABLE_START_DATES']:
debug("Allow: DISABLE_START_DATES") debug("Allow: DISABLE_START_DATES")
...@@ -196,8 +207,6 @@ def _has_access_descriptor(user, descriptor, action): ...@@ -196,8 +207,6 @@ def _has_access_descriptor(user, descriptor, action):
return _dispatch(checkers, action, user, descriptor) return _dispatch(checkers, action, user, descriptor)
def _has_access_xmodule(user, xmodule, action): def _has_access_xmodule(user, xmodule, action):
""" """
Check if user has access to this xmodule. Check if user has access to this xmodule.
......
...@@ -2,8 +2,8 @@ from collections import defaultdict ...@@ -2,8 +2,8 @@ from collections import defaultdict
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
from functools import wraps from functools import wraps
import logging import logging
from path import path
from path import path
from django.conf import settings from django.conf import settings
from django.http import Http404 from django.http import Http404
...@@ -142,7 +142,8 @@ def get_course_info_section(course, section_key): ...@@ -142,7 +142,8 @@ def get_course_info_section(course, section_key):
raise KeyError("Invalid about key " + str(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). Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number. Courses are sorted by course.number.
...@@ -152,9 +153,21 @@ def get_courses_by_university(user): ...@@ -152,9 +153,21 @@ def get_courses_by_university(user):
courses = [c for c in modulestore().get_courses() courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)] if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number) 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) universities = defaultdict(list)
for course in courses: for course in courses:
if has_access(user, course, 'see_exists'): if not has_access(user, course, 'see_exists'):
universities[course.org].append(course) continue
if course.id not in visible_courses:
continue
universities[course.org].append(course)
return universities return universities
"""
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): ...@@ -77,7 +77,7 @@ class StudentModuleCache(object):
Arguments Arguments
user: The user for which to fetch maching StudentModules user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors. 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(): if user.is_authenticated():
module_ids = self._get_module_state_keys(descriptors) module_ids = self._get_module_state_keys(descriptors)
...@@ -110,7 +110,7 @@ class StudentModuleCache(object): ...@@ -110,7 +110,7 @@ class StudentModuleCache(object):
the supplied descriptor. If depth is None, load all descendent StudentModules 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 descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached 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): def get_child_descriptors(descriptor, depth, descriptor_filter):
......
...@@ -19,7 +19,7 @@ from xmodule.exceptions import NotFoundError ...@@ -19,7 +19,7 @@ from xmodule.exceptions import NotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem 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") log = logging.getLogger("mitx.courseware")
...@@ -48,7 +48,7 @@ def make_track_function(request): ...@@ -48,7 +48,7 @@ def make_track_function(request):
return f 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 Create a table of contents from the module store
...@@ -71,7 +71,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -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) 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() chapters = list()
for chapter in course.get_display_items(): for chapter in course.get_display_items():
...@@ -127,7 +127,7 @@ def get_section(course_module, chapter, section): ...@@ -127,7 +127,7 @@ def get_section(course_module, chapter, section):
return section_module 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, ''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none setting the state based on an existing StudentModule, or creating one if none
exists. exists.
...@@ -144,6 +144,14 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -144,6 +144,14 @@ def get_module(user, request, location, student_module_cache, position=None):
''' '''
descriptor = modulestore().get_item(location) 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 # Short circuit--if the user shouldn't have access, bail without doing any work
if not has_access(user, descriptor, 'load'): if not has_access(user, descriptor, 'load'):
...@@ -167,7 +175,7 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -167,7 +175,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# Setup system context for module instance # Setup system context for module instance
ajax_url = reverse('modx_dispatch', ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=descriptor.location.course_id, kwargs=dict(course_id=course_id,
id=descriptor.location.url(), id=descriptor.location.url(),
dispatch=''), dispatch=''),
) )
...@@ -175,7 +183,7 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -175,7 +183,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
xqueue_callback_url += reverse('xqueue_callback', xqueue_callback_url += reverse('xqueue_callback',
kwargs=dict(course_id=descriptor.location.course_id, kwargs=dict(course_id=course_id,
userid=str(user.id), userid=str(user.id),
id=descriptor.location.url(), id=descriptor.location.url(),
dispatch='score_update'), dispatch='score_update'),
...@@ -195,7 +203,7 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -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 Delegate to get_module. It does an access check, so may return None
""" """
return get_module(user, request, location, 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 # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # 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): ...@@ -225,6 +233,10 @@ def get_module(user, request, location, student_module_cache, position=None):
module.metadata['data_dir'], module 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 settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
if has_access(user, module, 'staff'): if has_access(user, module, 'staff'):
module.get_html = add_histogram(module.get_html, module, user) 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): ...@@ -370,7 +382,7 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
p[inputfile_id] = inputfile p[inputfile_id] = inputfile
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id)) 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: if instance is None:
# Either permissions just changed, or someone is trying to be clever # Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to. # and load something they shouldn't have access to.
......
...@@ -33,6 +33,7 @@ log = logging.getLogger("mitx.courseware") ...@@ -33,6 +33,7 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
def user_groups(user): def user_groups(user):
""" """
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately. TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
...@@ -63,11 +64,12 @@ def courses(request): ...@@ -63,11 +64,12 @@ def courses(request):
''' '''
Render "find courses" page. The course selection work is done in courseware.courses. 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}) 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 ''' Draws navigation bar. Takes current position in accordion as
parameter. parameter.
...@@ -78,7 +80,7 @@ def render_accordion(request, course, chapter, section): ...@@ -78,7 +80,7 @@ def render_accordion(request, course, chapter, section):
Returns the html string''' Returns the html string'''
# grab the table of contents # 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), context = dict([('toc', toc),
('course_id', course.id), ('course_id', course.id),
...@@ -110,6 +112,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -110,6 +112,7 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse - HTTPresponse
""" """
course = get_course_with_access(request.user, course_id, 'load') 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) registered = registered_for_course(course, request.user)
if not registered: if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course? # 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, ...@@ -119,11 +122,12 @@ def index(request, course_id, chapter=None, section=None,
try: try:
context = { context = {
'csrf': csrf(request)['csrf_token'], '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_TITLE': course.title,
'course': course, 'course': course,
'init': '', 'init': '',
'content': '' 'content': '',
'staff_access': staff_access,
} }
look_for_module = chapter is not None and section is not None 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, ...@@ -135,7 +139,7 @@ def index(request, course_id, chapter=None, section=None,
section_descriptor) section_descriptor)
module = get_module(request.user, request, module = get_module(request.user, request,
section_descriptor.location, section_descriptor.location,
student_module_cache) student_module_cache, course_id=course_id)
if module is None: if module is None:
# User is probably being clever and trying to access something # User is probably being clever and trying to access something
# they don't have access to. # they don't have access to.
...@@ -166,7 +170,8 @@ def index(request, course_id, chapter=None, section=None, ...@@ -166,7 +170,8 @@ def index(request, course_id, chapter=None, section=None,
position=position position=position
)) ))
try: try:
result = render_to_response('courseware-error.html', {}) result = render_to_response('courseware-error.html',
{'staff_access': staff_access})
except: except:
result = HttpResponse("There was an unrecoverable error") result = HttpResponse("There was an unrecoverable error")
...@@ -208,8 +213,10 @@ def course_info(request, course_id): ...@@ -208,8 +213,10 @@ def course_info(request, course_id):
Assumes the course_id is in a valid format. Assumes the course_id is in a valid format.
""" """
course = get_course_with_access(request.user, course_id, 'load') 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): def registered_for_course(course, user):
...@@ -241,7 +248,8 @@ def university_profile(request, org_id): ...@@ -241,7 +248,8 @@ def university_profile(request, org_id):
raise Http404("University Profile not found for {0}".format(org_id)) raise Http404("University Profile not found for {0}".format(org_id))
# Only grab courses for this org... # 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) context = dict(courses=courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower() template_file = "university_profile/{0}.html".format(org_id).lower()
...@@ -257,20 +265,21 @@ def profile(request, course_id, student_id=None): ...@@ -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 staff are allowed to see the profiles of students in their class.
""" """
course = get_course_with_access(request.user, course_id, 'load') 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: if student_id is None or student_id == request.user.id:
# always allowed to see your own profile # always allowed to see your own profile
student = request.user student = request.user
else: else:
# Requesting access to a different student's profile # Requesting access to a different student's profile
if not has_access(request.user, course, 'staff'): if not staff_access:
raise Http404 raise Http404
student = User.objects.get(id=int(student_id)) student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student) user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course) 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) courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
grade_summary = grades.grade(request.user, request, course, 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): ...@@ -282,8 +291,9 @@ def profile(request, course_id, student_id=None):
'email': student.email, 'email': student.email,
'course': course, 'course': course,
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'courseware_summary' : courseware_summary, 'courseware_summary': courseware_summary,
'grade_summary' : grade_summary 'grade_summary': grade_summary,
'staff_access': staff_access,
} }
context.update() context.update()
...@@ -316,7 +326,10 @@ def gradebook(request, course_id): ...@@ -316,7 +326,10 @@ def gradebook(request, course_id):
for student in enrolled_students] for student in enrolled_students]
return render_to_response('gradebook.html', {'students': student_info, 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) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
...@@ -325,7 +338,8 @@ def grade_summary(request, course_id): ...@@ -325,7 +338,8 @@ def grade_summary(request, course_id):
course = get_course_with_access(request.user, course_id, 'staff') course = get_course_with_access(request.user, course_id, 'staff')
# For now, just a static page # For now, just a static page
context = {'course': course } context = {'course': course,
'staff_access': True,}
return render_to_response('grade_summary.html', context) return render_to_response('grade_summary.html', context)
...@@ -335,6 +349,7 @@ def instructor_dashboard(request, course_id): ...@@ -335,6 +349,7 @@ def instructor_dashboard(request, course_id):
course = get_course_with_access(request.user, course_id, 'staff') course = get_course_with_access(request.user, course_id, 'staff')
# For now, just a static page # For now, just a static page
context = {'course': course } context = {'course': course,
'staff_access': True,}
return render_to_response('instructor_dashboard.html', context) return render_to_response('instructor_dashboard.html', context)
...@@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None):
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
if not ip: if not ip:
ip = request.META.get('REMOTE_ADDR','None') ip = request.META.get('REMOTE_ADDR','None')
if LOCAL_DEBUG: if LOCAL_DEBUG:
html += '<h3>IP address: %s ' % ip html += '<h3>IP address: %s ' % ip
html += '<h3>User: %s ' % request.user html += '<h3>User: %s ' % request.user
...@@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None):
html += 'Permission denied' html += 'Permission denied'
html += "</body></html>" html += "</body></html>"
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS) log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
return HttpResponse(html) return HttpResponse(html)
#---------------------------------------- #----------------------------------------
# reload course if specified # reload course if specified
...@@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None): ...@@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None):
#---------------------------------------- #----------------------------------------
dumpfields = ['definition','location','metadata'] dumpfields = ['definition','location','metadata']
for cdir, course in def_ms.courses.items(): for cdir, course in def_ms.courses.items():
html += '<hr width="100%"/>' 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: for field in dumpfields:
data = getattr(course,field) data = getattr(course,field)
...@@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None): ...@@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None):
html += '</ul>' html += '</ul>'
else: else:
html += '<ul><li>%s</li></ul>' % escape(data) html += '<ul><li>%s</li></ul>' % escape(data)
#---------------------------------------- #----------------------------------------
...@@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None): ...@@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None):
log.debug('def_ms=%s' % unicode(def_ms)) log.debug('def_ms=%s' % unicode(def_ms))
html += "</body></html>" html += "</body></html>"
return HttpResponse(html) return HttpResponse(html)
...@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.courses import get_opt_course_with_access from courseware.courses import get_opt_course_with_access
from courseware.access import has_access
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -49,6 +50,10 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No ...@@ -49,6 +50,10 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
if request: if request:
dictionary.update(csrf(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): def view(request, article_path, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load') course = get_opt_course_with_access(request.user, course_id, 'load')
......
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.access import has_access
from courseware.courses import get_course_with_access from courseware.courses import get_course_with_access
from lxml import etree from lxml import etree
@login_required @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') 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 staff_access = has_access(request.user, course, 'staff')
table_of_contents = etree.parse(raw_table_of_contents).getroot()
textbook = course.textbooks[int(book_index)]
table_of_contents = textbook.table_of_contents
return render_to_response('staticbook.html', return render_to_response('staticbook.html',
{'page': int(page), 'course': course, {'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): def index_shifted(request, course_id, page):
return index(request, course_id=course_id, page=int(page) + 24) return index(request, course_id=course_id, page=int(page) + 24)
...@@ -49,6 +49,11 @@ MITX_FEATURES = { ...@@ -49,6 +49,11 @@ MITX_FEATURES = {
## Doing so will cause all courses to be released on production ## 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 '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_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True, 'ENABLE_DISCUSSION' : True,
...@@ -61,6 +66,7 @@ MITX_FEATURES = { ...@@ -61,6 +66,7 @@ MITX_FEATURES = {
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False, 'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False, 'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False, 'AUTH_USE_MIT_CERTIFICATES' : False,
} }
# Used for A/B testing # Used for A/B testing
......
...@@ -54,7 +54,7 @@ CACHES = { ...@@ -54,7 +54,7 @@ CACHES = {
} }
XQUEUE_INTERFACE = { XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org", "url": "https://sandbox-xqueue.edx.org",
"django_auth": { "django_auth": {
"username": "lms", "username": "lms",
"password": "***REMOVED***" "password": "***REMOVED***"
......
"""
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" ...@@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data"
XQUEUE_INTERFACE = { XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org", "url": "http://sandbox-xqueue.edx.org",
"django_auth": { "django_auth": {
"username": "lms", "username": "lms",
"password": "***REMOVED***" "password": "***REMOVED***"
......
lms/static/images/askbot/vote-arrow-up.png

200 Bytes | W: | H:

lms/static/images/askbot/vote-arrow-up.png

1.11 KB | W: | H:

lms/static/images/askbot/vote-arrow-up.png
lms/static/images/askbot/vote-arrow-up.png
lms/static/images/askbot/vote-arrow-up.png
lms/static/images/askbot/vote-arrow-up.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -69,10 +69,15 @@ div.info-wrapper { ...@@ -69,10 +69,15 @@ div.info-wrapper {
section.handouts { section.handouts {
@extend .sidebar; @extend .sidebar;
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
@include border-radius(0 4px 4px 0);
border-right: 0; border-right: 0;
@include border-radius(0 4px 4px 0);
@include box-shadow(none); @include box-shadow(none);
&:after {
left: -1px;
right: auto;
}
h1 { h1 {
@extend .bottom-border; @extend .bottom-border;
margin-bottom: 0; margin-bottom: 0;
......
...@@ -8,6 +8,11 @@ div.profile-wrapper { ...@@ -8,6 +8,11 @@ div.profile-wrapper {
@include border-radius(0px 4px 4px 0); @include border-radius(0px 4px 4px 0);
border-right: 0; border-right: 0;
&:after {
left: -1px;
right: auto;
}
header { header {
@extend .bottom-border; @extend .bottom-border;
margin: 0; margin: 0;
......
...@@ -10,7 +10,6 @@ div.book-wrapper { ...@@ -10,7 +10,6 @@ div.book-wrapper {
font-size: em(14); font-size: em(14);
.chapter-number { .chapter-number {
} }
.chapter { .chapter {
...@@ -81,9 +80,8 @@ div.book-wrapper { ...@@ -81,9 +80,8 @@ div.book-wrapper {
section.book { section.book {
@extend .content; @extend .content;
padding-bottom: 0;
padding-right: 0; padding-right: 0;
padding-top: 0; padding-left: lh();
nav { nav {
@extend .clearfix; @extend .clearfix;
......
...@@ -2,7 +2,7 @@ body { ...@@ -2,7 +2,7 @@ body {
min-width: 980px; 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; text-align: left;
font-family: $sans-serif; font-family: $sans-serif;
} }
...@@ -27,6 +27,14 @@ form { ...@@ -27,6 +27,14 @@ form {
} }
} }
img {
max-width: 100%;
}
.container {
padding: em(40) 0;
}
::selection, ::-moz-selection, ::-webkit-selection { ::selection, ::-moz-selection, ::-webkit-selection {
background:#444; background:#444;
color:#fff; color:#fff;
......
...@@ -12,10 +12,13 @@ h1.top-header { ...@@ -12,10 +12,13 @@ h1.top-header {
@include box-shadow(inset 0 1px 0 #fff); @include box-shadow(inset 0 1px 0 #fff);
color: #666; color: #666;
cursor: pointer; 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%)); @include linear-gradient(#fff, lighten(#888, 40%));
padding: 4px 8px; padding: 4px 8px;
text-decoration: none; text-decoration: none;
text-shadow: none;
text-transform: none;
letter-spacing: 0;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
&:hover, &:focus { &:hover, &:focus {
...@@ -28,7 +31,7 @@ h1.top-header { ...@@ -28,7 +31,7 @@ h1.top-header {
.content { .content {
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
padding: lh(); padding-right: lh();
vertical-align: top; vertical-align: top;
width: flex-grid(9) + flex-gutter(); width: flex-grid(9) + flex-gutter();
...@@ -46,6 +49,18 @@ h1.top-header { ...@@ -46,6 +49,18 @@ h1.top-header {
vertical-align: top; vertical-align: top;
width: flex-grid(3); 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 { h1, h2 {
font-size: em(20); font-size: em(20);
font-weight: 100; font-weight: 100;
...@@ -134,7 +149,7 @@ h1.top-header { ...@@ -134,7 +149,7 @@ h1.top-header {
position: absolute; position: absolute;
right: -1px; right: -1px;
text-indent: -9999px; text-indent: -9999px;
top: 6px; top: 12px;
width: 16px; width: 16px;
z-index: 99; z-index: 99;
......
...@@ -12,7 +12,8 @@ div.course-wrapper { ...@@ -12,7 +12,8 @@ div.course-wrapper {
section.course-content { section.course-content {
@extend .content; @extend .content;
@include border-radius(0 4px 4px 0); padding-right: 0;
padding-left: lh();
h1 { h1 {
margin: 0 0 lh(); margin: 0 0 lh();
......
...@@ -7,9 +7,16 @@ div.answer-controls { ...@@ -7,9 +7,16 @@ div.answer-controls {
padding-left: flex-grid(1.1); padding-left: flex-grid(1.1);
width: 100%; width: 100%;
div.answer-count { div.answer-count {
display: inline-block; display: inline-block;
float: left; float: left;
h1 {
margin-bottom: 0;
font-size: em(24);
font-weight: 100;
}
} }
div.answer-sort { div.answer-sort {
...@@ -18,7 +25,7 @@ div.answer-controls { ...@@ -18,7 +25,7 @@ div.answer-controls {
nav { nav {
float: right; float: right;
margin-top: 34px; margin-top: 10px;
a { a {
&.on span{ &.on span{
...@@ -44,8 +51,9 @@ div.answer-block { ...@@ -44,8 +51,9 @@ div.answer-block {
width: 100%; width: 100%;
img.answer-img-accept { img.answer-img-accept {
margin: 10px 0px 10px 16px; margin: 10px 0px 10px 11px;
} }
div.answer-container { div.answer-container {
@extend div.question-container; @extend div.question-container;
...@@ -130,21 +138,19 @@ div.answer-own { ...@@ -130,21 +138,19 @@ div.answer-own {
div.answer-actions { div.answer-actions {
margin: 0; margin: 0;
padding:8px 8px 8px 0; padding:8px 0 8px 8px;
text-align: right; text-align: right;
border-top: 1px solid #efefef; border-top: 1px solid #efefef;
span.sep { span.sep {
color: #EDDFAA; color: $border-color;
} }
a { a {
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
@extend a:link;
&.question-delete { font-size: em(14);
color: $mit-red;
}
} }
} }
...@@ -22,6 +22,8 @@ div#award-list{ ...@@ -22,6 +22,8 @@ div#award-list{
} }
ul.badge-list { ul.badge-list {
padding-left: 0;
li.badge { li.badge {
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
@extend .clearfix; @extend .clearfix;
...@@ -70,12 +72,17 @@ ul.badge-list { ...@@ -70,12 +72,17 @@ ul.badge-list {
.bronze, .badge3 { .bronze, .badge3 {
color: #cc9933; color: #cc9933;
} }
div.badge-desc {
> div { div.discussion-wrapper aside {
margin-bottom: 20px; div.badge-desc {
span { border-top: 0;
font-size: 18px;
@include border-radius(10px); > div {
} margin-bottom: 20px;
span {
font-size: 18px;
@include border-radius(10px);
}
}
} }
} }
...@@ -8,7 +8,7 @@ body.askbot { ...@@ -8,7 +8,7 @@ body.askbot {
@include box-sizing(border-box); @include box-sizing(border-box);
display: table-cell; display: table-cell;
min-width: 650px; min-width: 650px;
padding: lh(); padding-right: lh();
vertical-align: top; vertical-align: top;
width: flex-grid(9) + flex-gutter(); width: flex-grid(9) + flex-gutter();
......
...@@ -5,6 +5,11 @@ form.answer-form { ...@@ -5,6 +5,11 @@ form.answer-form {
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
overflow: hidden; overflow: hidden;
padding-left: flex-grid(1.1); padding-left: flex-grid(1.1);
padding-top: lh();
p {
margin-bottom: lh();
}
textarea { textarea {
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -121,7 +126,6 @@ form.question-form { ...@@ -121,7 +126,6 @@ form.question-form {
border: none; border: none;
padding: 15px 0 0 0; padding: 15px 0 0 0;
input[type="text"] { input[type="text"] {
@include box-sizing(border-box); @include box-sizing(border-box);
width: flex-grid(6); width: flex-grid(6);
...@@ -131,6 +135,11 @@ form.question-form { ...@@ -131,6 +135,11 @@ form.question-form {
margin-top: 10px; margin-top: 10px;
} }
input[value="Cancel"] {
@extend .light-button;
float: right;
}
div#question-list { div#question-list {
background-color: rgba(255,255,255,0.95); background-color: rgba(255,255,255,0.95);
@include box-sizing(border-box); @include box-sizing(border-box);
......
// Style for modal boxes that pop up to notify the user of various events // Style for modal boxes that pop up to notify the user of various events
.vote-notification { .vote-notification {
background-color: darken($mit-red, 7%); background-color: darken($mit-red, 7%);
@include border-radius(4px); @include border-radius(4px);
......
...@@ -9,9 +9,9 @@ body.user-profile-page { ...@@ -9,9 +9,9 @@ body.user-profile-page {
} }
ul.sub-info { ul.sub-info {
// border-top: 1px solid #ddd;
margin-top: lh(); margin-top: lh();
list-style: none; list-style: none;
padding: 0;
> li { > li {
display: table-cell; display: table-cell;
...@@ -57,6 +57,7 @@ body.user-profile-page { ...@@ -57,6 +57,7 @@ body.user-profile-page {
ul { ul {
list-style: none; list-style: none;
padding: 0;
&.user-stats-table { &.user-stats-table {
list-style: none; list-style: none;
...@@ -72,37 +73,28 @@ body.user-profile-page { ...@@ -72,37 +73,28 @@ body.user-profile-page {
margin-bottom: 30px; margin-bottom: 30px;
li { li {
background-position: 10px center; background-position: 10px -10px;
background-repeat: no-repeat; background-repeat: no-repeat;
@include border-radius(4px);
display: inline-block; display: inline-block;
height: 20px; padding: 2px 10px 2px 40px;
padding: 10px 10px 10px 40px; margin-bottom: lh(.5);
border: 1px solid lighten($border-color, 10%);
&.up { &.up {
background-color:#d1e3a8; background-image: url(../images/askbot/vote-arrow-up.png);
background-image: url(../images/askbot/vote-arrow-up-activate.png);
margin-right: 6px; margin-right: 6px;
span.vote-count {
color: #3f6c3e;
}
} }
&.down { &.down {
background-image: url(../images/askbot/vote-arrow-down-activate.png); background-image: url(../images/askbot/vote-arrow-down.png);
background-color:#eac6ad;
span.vote-count {
color: $mit-red;
}
} }
} }
} }
&.badges { &.badges {
@include inline-block(); @include inline-block();
padding: 0;
margin: 0;
a { a {
background-color: #e3e3e3; background-color: #e3e3e3;
......
// Styles for the single question view // Styles for the single question view
div.question-header { div.question-header {
@include clearfix();
div.official-stamp { div.official-stamp {
background: $mit-red; background: $mit-red;
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
margin-left: -1px;
margin-top: 10px; margin-top: 10px;
padding: 2px 5px; padding: 2px 5px;
text-align: center; text-align: center;
margin-left: -1px;
} }
div.vote-buttons { div.vote-buttons {
...@@ -19,40 +20,40 @@ div.question-header { ...@@ -19,40 +20,40 @@ div.question-header {
width: flex-grid(0.7,9); width: flex-grid(0.7,9);
ul { ul {
padding: 0;
margin: 0;
li { li {
background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
cursor: pointer; color: #999;
font-size: em(20);
font-weight: bold; font-weight: bold;
height: 20px;
list-style: none; list-style: none;
padding: 10px;
text-align: center; text-align: center;
width: 70%;
&.post-vote {
@include border-radius(4px);
@include box-shadow(inset 0 1px 0px #fff);
}
&.question-img-upvote, &.answer-img-upvote { &.question-img-upvote, &.answer-img-upvote {
background-image: url(../images/askbot/vote-arrow-up.png); 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 { &:hover, &.on {
background-color:#d1e3a8; background-image: url(../images/askbot/vote-arrow-up.png);
border-color: darken(#D1E3A8, 20%); background-position: center -22px;
background-image: url(../images/askbot/vote-arrow-up-activate.png);
} }
} }
&.question-img-downvote, &.answer-img-downvote { &.question-img-downvote, &.answer-img-downvote {
cursor: pointer;
background-image: url(../images/askbot/vote-arrow-down.png); background-image: url(../images/askbot/vote-arrow-down.png);
background-position: center 0;
height: 12px;
margin-top: lh(.5);
&:hover, &.on { &:hover, &.on {
background-color:#EAC6AD; background-image: url(../images/askbot/vote-arrow-down.png);
border-color: darken(#EAC6AD, 20%); background-position: center -22px;
background-image: url(../images/askbot/vote-arrow-down-activate.png);
} }
} }
} }
...@@ -66,12 +67,19 @@ div.question-header { ...@@ -66,12 +67,19 @@ div.question-header {
h1 { h1 {
margin-top: 0; margin-top: 0;
font-weight: 100;
line-height: 1.1em;
a {
font-weight: 100;
line-height: 1.1em;
}
} }
div.meta-bar { div.meta-bar {
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
display: block; display: block;
margin: 10px 0; margin: lh(.5) 0 lh();
overflow: hidden; overflow: hidden;
padding: 5px 0 10px; padding: 5px 0 10px;
...@@ -89,11 +97,8 @@ div.question-header { ...@@ -89,11 +97,8 @@ div.question-header {
width: flex-grid(4,8); width: flex-grid(4,8);
a { a {
&.question-delete { @extend a:link;
color: $mit-red; cursor: pointer;
text-decoration: none;
cursor: pointer;
}
} }
span.sep { span.sep {
...@@ -155,7 +160,7 @@ div.question-header { ...@@ -155,7 +160,7 @@ div.question-header {
} }
div.change-date { div.change-date {
font-size: 12px; font-size: em(14);
margin-bottom: 2px; margin-bottom: 2px;
} }
...@@ -179,13 +184,13 @@ div.question-header { ...@@ -179,13 +184,13 @@ div.question-header {
display: inline-block; display: inline-block;
padding: 0 0 3% 0; padding: 0 0 3% 0;
width: 100%; width: 100%;
margin-top: lh(2);
div.comments-content { div.comments-content {
font-size: 13px; border-top: 1px solid lighten($border-color, 10%);
background: #efefef;
.block { .block {
border-top: 1px solid #ddd; border-top: 1px solid lighten($border-color, 10%);
padding: 15px; padding: 15px;
display: block; display: block;
...@@ -197,10 +202,10 @@ div.question-header { ...@@ -197,10 +202,10 @@ div.question-header {
padding-top: 10px; padding-top: 10px;
span.official-comment { span.official-comment {
background: $mit-red; background: $pink;
color: #fff; color: #fff;
display: block; display: block;
font-size: 12px; font-size: em(12);
margin: 0 0 10px -5%; margin: 0 0 10px -5%;
padding:2px 5px 2px 5%; padding:2px 5px 2px 5%;
text-align: left; text-align: left;
...@@ -212,6 +217,10 @@ div.question-header { ...@@ -212,6 +217,10 @@ div.question-header {
form.post-comments { form.post-comments {
padding: 15px; padding: 15px;
button {
color: #fff;
}
button:last-child { button:last-child {
margin-left: 10px; margin-left: 10px;
@extend .light-button; @extend .light-button;
...@@ -232,7 +241,6 @@ div.question-header { ...@@ -232,7 +241,6 @@ div.question-header {
border: none; border: none;
@include box-shadow(none); @include box-shadow(none);
display: inline-block; display: inline-block;
margin-top: -8px;
padding:0 2% 0 0; padding:0 2% 0 0;
text-align: center; text-align: center;
width: 5%; width: 5%;
...@@ -278,16 +286,14 @@ div.question-header { ...@@ -278,16 +286,14 @@ div.question-header {
} }
div.comment-delete { div.comment-delete {
// display: inline; @extend a:link;
color: $mit-red;
cursor: pointer; cursor: pointer;
font-size: 15px;
} }
div.comment-edit { div.comment-edit {
@include transform(rotate(50deg)); @include transform(rotate(50deg));
cursor: pointer; cursor: pointer;
font-size: 16px;
a.edit-icon { a.edit-icon {
color: #555; color: #555;
text-decoration: none; text-decoration: none;
...@@ -305,13 +311,13 @@ div.question-header { ...@@ -305,13 +311,13 @@ div.question-header {
div.comment-meta { div.comment-meta {
text-align: right; text-align: right;
margin-top: lh(.5);
a.author { a.author {
font-weight: bold; font-weight: bold;
} }
a.edit { a.edit {
font-size: 12px;
padding: 2px 10px; padding: 2px 10px;
} }
} }
...@@ -334,12 +340,10 @@ div.question-header { ...@@ -334,12 +340,10 @@ div.question-header {
} }
div.controls { div.controls {
border-top: 1px solid #efefef;
text-align: right; text-align: right;
a { a {
display: inline-block; display: inline-block;
font-size: 12px;
margin: 10px 10px 10px 0; margin: 10px 10px 10px 0;
} }
} }
......
// Styles for the default question list view // Styles for the default question list view
div.question-list-header { div.question-list-header {
@extend h1.top-header;
display: block; display: block;
margin-bottom: 0px; margin-bottom: 0px;
padding-bottom: lh(.5);
overflow: hidden; overflow: hidden;
width: flex-grid(9,9); width: flex-grid(9,9);
@extend h1.top-header;
h1 { h1 {
margin: 0; margin: 0;
font-size: 1em;
font-weight: 100;
padding-bottom: lh(.5);
> a.light-button { > a.light-button {
float: right; float: right;
font-size: em(14, 24);
letter-spacing: 0;
font-weight: 400;
} }
} }
...@@ -49,8 +56,11 @@ div.question-list-header { ...@@ -49,8 +56,11 @@ div.question-list-header {
nav { nav {
@extend .action-link; @extend .action-link;
float: right; float: right;
font-size: em(16, 24);
a { a {
font-size: 1em;
&.on span{ &.on span{
font-weight: bold; font-weight: bold;
} }
...@@ -82,6 +92,7 @@ div.question-list-header { ...@@ -82,6 +92,7 @@ div.question-list-header {
a { a {
color: #555; color: #555;
font-size: em(14, 24);
} }
} }
...@@ -90,12 +101,10 @@ div.question-list-header { ...@@ -90,12 +101,10 @@ div.question-list-header {
} }
ul.tags { ul.tags {
li { span, div {
background: #fff; line-height: 1em;
margin-left: 6px;
&:before { cursor: pointer;
border-color: transparent #fff transparent transparent;
}
} }
} }
} }
...@@ -103,26 +112,15 @@ div.question-list-header { ...@@ -103,26 +112,15 @@ div.question-list-header {
ul.question-list, div#question-list { ul.question-list, div#question-list {
width: flex-grid(9,9); width: flex-grid(9,9);
padding-left: 0;
margin: 0;
li.single-question { li.single-question {
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
list-style: none; list-style: none;
padding: 10px lh(); padding: lh() 0;
margin-left: (-(lh()));
width: 100%; width: 100%;
&:hover {
background: #F3F3F3;
ul.tags li {
background: #ddd;
&:before {
border-color: transparent #ddd transparent transparent;
}
}
}
&:first-child { &:first-child {
border-top: 0; border-top: 0;
} }
...@@ -133,14 +131,19 @@ ul.question-list, div#question-list { ...@@ -133,14 +131,19 @@ ul.question-list, div#question-list {
&.question-body { &.question-body {
@include box-sizing(border-box); @include box-sizing(border-box);
margin-right: flex-gutter(); margin-right: flex-gutter();
width: flex-grid(5.5,9); width: flex-grid(5,9);
h2 { h2 {
font-size: 16px; font-size: em(20);
font-weight: bold; font-weight: bold;
letter-spacing: 0; letter-spacing: 0;
margin: 0px 0 15px 0; margin: 0 0 lh() 0;
text-transform: none; text-transform: none;
line-height: lh();
a {
line-height: lh();
}
} }
p.excerpt { p.excerpt {
...@@ -151,40 +154,41 @@ ul.question-list, div#question-list { ...@@ -151,40 +154,41 @@ ul.question-list, div#question-list {
div.user-info { div.user-info {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
margin-bottom: 10px; margin: lh() 0 0 0;
line-height: lh();
span.relative-time { span.relative-time {
font-weight: normal; font-weight: normal;
} line-height: lh();
a {
color: $mit-red;
} }
} }
ul.tags { ul.tags {
display: inline-block; display: inline-block;
margin: lh() 0 0 0;
padding: 0;
} }
} }
&.question-meta { &.question-meta {
float: right; float: right;
margin-top: 10px; width: flex-grid(3,9);
width: flex-grid(3.5,9);
ul { ul {
text-align: right; @include clearfix;
margin: 0;
padding: 0;
list-style: none;
li { li {
border: 1px solid #ddd; border: 1px solid lighten($border-color, 10%);
@include box-sizing(border-box);
@include box-shadow(0 1px 0 #fff); @include box-shadow(0 1px 0 #fff);
display: inline-block;
height:60px; height:60px;
@include linear-gradient(#fff, #f5f5f5); float: left;
margin-right: 10px; margin-right: flex-gutter(3);
width: 60px; width: flex-grid(1,3);
&:last-child { &:last-child {
margin-right: 0px; margin-right: 0px;
...@@ -196,31 +200,22 @@ ul.question-list, div#question-list { ...@@ -196,31 +200,22 @@ ul.question-list, div#question-list {
} }
} }
&.views {
}
&.answers { &.answers {
&.accepted { &.accepted {
border-color: lighten($border-color, 10%);
@include linear-gradient(#fff, lighten( #c4dfbe, 12% ));
border-color: #c4dfbe;
span, div { span, div {
color: darken(#c4dfbe, 35%); color: darken(#c4dfbe, 35%);
} }
} }
&.no-answers {
&.no-answers {
span, div { span, div {
color: lighten($mit-red, 20%); color: $pink;
} }
} }
} }
&.votes {
}
span, div { span, div {
@include box-sizing(border-box); @include box-sizing(border-box);
color: #888; color: #888;
......
...@@ -2,25 +2,31 @@ ...@@ -2,25 +2,31 @@
div.discussion-wrapper aside { div.discussion-wrapper aside {
@extend .sidebar; @extend .sidebar;
border-left: 1px solid #d3d3d3; border-left: 1px solid $border-color;
@include border-radius(0 4px 4px 0); border-right: 0;
border-right: 1px solid #f6f6f6;
@include box-shadow(inset 1px 0 0 #f6f6f6);
padding: lh();
width: flex-grid(3); width: flex-grid(3);
&:after {
left: -1px;
right: auto;
}
&.main-sidebar { &.main-sidebar {
min-width:200px; min-width:200px;
} }
h1 { h1 {
@extend .bottom-border; @extend .bottom-border;
margin: (-(lh())) (-(lh())) 0;
padding: lh(.5) lh(); padding: lh(.5) lh();
margin-bottom: em(16, 20);
} }
h2 { h2 {
color: #4D4D4D; color: #3C3C3C;
font-size: 1em;
font-style: normal;
font-weight: bold;
margin-bottom: 1em;
&.first { &.first {
margin-top: 0px; margin-top: 0px;
...@@ -36,6 +42,9 @@ div.discussion-wrapper aside { ...@@ -36,6 +42,9 @@ div.discussion-wrapper aside {
input[type="submit"] { input[type="submit"] {
width: 27%; width: 27%;
float: right; float: right;
text-align: center;
padding-left: 0;
padding-right: 0;
} }
input[type="text"] { input[type="text"] {
...@@ -45,24 +54,30 @@ div.discussion-wrapper aside { ...@@ -45,24 +54,30 @@ div.discussion-wrapper aside {
div.box { div.box {
display: block; display: block;
margin: lh(.5) 0; padding: lh(.5) lh();
border-top: 1px solid lighten($border-color, 10%);
&:last-child { &:first-child {
@include box-shadow(none); border-top: 0;
border: 0;
} }
h2 { ul#related-tags {
text-transform: uppercase; position: relative;
font-weight: bold; left: -10px;
font-size: 14px;
letter-spacing: 1px;
&:not(.first) { li {
@include box-shadow(inset 0 1px 0 #eee); border-bottom: 0;
border-top: 1px solid #d3d3d3; background: #eee;
margin: 0 (-(lh())) 0; padding: 6px 10px 6px 5px;
padding: lh(.5) lh();
a {
padding: 0;
line-height: 12px;
&:hover {
background: transparent;
}
}
} }
} }
...@@ -85,9 +100,6 @@ div.discussion-wrapper aside { ...@@ -85,9 +100,6 @@ div.discussion-wrapper aside {
} }
} }
img.gravatar {
@include border-radius(3px);
}
} }
&.tag-selector { &.tag-selector {
...@@ -100,17 +112,19 @@ div.discussion-wrapper aside { ...@@ -100,17 +112,19 @@ div.discussion-wrapper aside {
div.search-box { div.search-box {
margin-top: lh(.5); margin-top: lh(.5);
input { input {
@include box-sizing(border-box); @include box-sizing(border-box);
display: inline; display: inline;
} }
input[type='submit'] { input[type='submit'] {
@include box-shadow(none);
opacity: 0.5;
background: url(../images/askbot/search-icon.png) no-repeat center; background: url(../images/askbot/search-icon.png) no-repeat center;
border: 0; border: 0;
@include box-shadow(none);
margin-left: 3px; margin-left: 3px;
opacity: 0.5;
padding: 6px 0 0;
position: absolute; position: absolute;
text-indent: -9999px; text-indent: -9999px;
width: 24px; width: 24px;
...@@ -131,30 +145,26 @@ div.discussion-wrapper aside { ...@@ -131,30 +145,26 @@ div.discussion-wrapper aside {
} }
input#clear { input#clear {
@include box-shadow(none); background: none;
@include border-radius(15px);
border: none; border: none;
background: #bbb; @include border-radius(0);
color: #fff; @include box-shadow(none);
color: #999;
display: inline; display: inline;
font-size: 10px; font-size: 12px;
margin-left: -25px; font-weight: bold;
height: 19px;
line-height: 1em;
margin: {
left: -25px;
top: 8px;
}
padding: 2px 5px; padding: 2px 5px;
text-shadow: none;
} }
} }
div#tagSelector { 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 { ul {
margin: 0; margin: 0;
} }
...@@ -167,11 +177,17 @@ div.discussion-wrapper aside { ...@@ -167,11 +177,17 @@ div.discussion-wrapper aside {
p.choice { p.choice {
@include inline-block(); @include inline-block();
margin-right: lh(.5); margin-right: lh(.5);
margin-top: 0;
} }
} }
label {
font-style: normal;
font-weight: 400;
}
} }
// Question view sopecific // Question view specific
div.follow-buttons { div.follow-buttons {
margin-top: 20px; margin-top: 20px;
...@@ -187,12 +203,15 @@ div.discussion-wrapper aside { ...@@ -187,12 +203,15 @@ div.discussion-wrapper aside {
div.question-stats { div.question-stats {
border-top: 0;
ul { ul {
color: #777; color: #777;
list-style: none; list-style: none;
li { li {
padding: 7px 0 0; padding: 7px 0 0;
border: 0;
&:last-child { &:last-child {
@include box-shadow(none); @include box-shadow(none);
...@@ -216,19 +235,20 @@ div.discussion-wrapper aside { ...@@ -216,19 +235,20 @@ div.discussion-wrapper aside {
} }
div.karma { div.karma {
background: #eee; border: 1px solid $border-color;
border: 1px solid #D3D3D3;
@include border-radius(3px);
@include box-sizing(border-box); @include box-sizing(border-box);
@include box-shadow(inset 0 0 0 1px #fff, 0 1px 0 #fff);
padding: lh(.4) 0; padding: lh(.4) 0;
text-align: center; text-align: center;
width: flex-grid(1, 3); width: flex-grid(1, 3);
float: right; float: right;
strong { p {
display: block; text-align: center;
font-style: 20px;
strong {
display: block;
font-style: 20px;
}
} }
} }
...@@ -255,8 +275,6 @@ div.discussion-wrapper aside { ...@@ -255,8 +275,6 @@ div.discussion-wrapper aside {
overflow: visible; overflow: visible;
ul { ul {
font-size: 14px;
h2 { h2 {
margin:0 (-(lh())) 5px (-(lh())); margin:0 (-(lh())) 5px (-(lh()));
padding: lh(.5) lh(); padding: lh(.5) lh();
...@@ -265,40 +283,29 @@ div.discussion-wrapper aside { ...@@ -265,40 +283,29 @@ div.discussion-wrapper aside {
} }
div.question-tips, div.markdown { div.question-tips, div.markdown {
ul { ul,
margin-left: 8%;
}
ol { ol {
margin-left: 8%;
}
}
div.markdown ul li {
margin: 20px 0;
&:first-child {
margin: 0; margin: 0;
} padding: 0;
ol li { li {
margin: 0; border-bottom: 0;
line-height: lh();
margin-bottom: em(8);
}
} }
} }
div.view-profile { div.view-profile {
h2 { border-top: 0;
border-top: 0;
@include box-shadow(none);
}
a { a {
width: 100%; @extend .light-button;
@include box-sizing(border-box); @include box-sizing(border-box);
text-align: center;
padding: 10px;
display: block; display: block;
margin-top: 10px; text-align: center;
@extend .light-button; width: 100%;
margin-top: lh(.5);
&:first-child { &:first-child {
margin-top: 0; margin-top: 0;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
ul.tags { ul.tags {
list-style: none; list-style: none;
display: inline; display: inline;
padding: 0;
li, a { li, a {
position: relative; position: relative;
...@@ -10,19 +11,17 @@ ul.tags { ...@@ -10,19 +11,17 @@ ul.tags {
li { li {
background: #eee; background: #eee;
@include border-radius(4px);
@include box-shadow(0px 1px 0px #ccc);
color: #555; color: #555;
display: inline-block; display: inline-block;
font-size: 12px; font-size: 12px;
margin-bottom: 5px; margin-bottom: 5px;
margin-left: 15px; margin-left: 15px;
padding: 3px 10px 5px 5px; padding: 6px 10px 6px 5px;
&:before { &:before {
border-color:transparent #eee transparent transparent; border-color:transparent #eee transparent transparent;
border-style:solid; border-style:solid;
border-width:12px 12px 12px 0; border-width:12px 10px 12px 0;
content:""; content:"";
height:0; height:0;
left:-10px; left:-10px;
...@@ -31,25 +30,6 @@ ul.tags { ...@@ -31,25 +30,6 @@ ul.tags {
width:0; 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 { a {
color: #555; color: #555;
text-decoration: none; text-decoration: none;
...@@ -61,11 +41,4 @@ ul.tags { ...@@ -61,11 +41,4 @@ ul.tags {
span.tag-number { span.tag-number {
display: none; 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 { ...@@ -42,6 +42,7 @@ textarea {
input[type="submit"], input[type="submit"],
input[type="button"], input[type="button"],
button,
.button { .button {
@include border-radius(3px); @include border-radius(3px);
@include button(shiny, $blue); @include button(shiny, $blue);
......
...@@ -21,8 +21,10 @@ def url_class(url): ...@@ -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> <li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
% if user.is_authenticated(): % if user.is_authenticated():
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'): % if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li> % for index, textbook in enumerate(course.textbooks):
% endif <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'): % if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li> <li class="discussion"><a href="${reverse('questions')}">Discussion</a></li>
% endif % endif
...@@ -33,7 +35,7 @@ def url_class(url): ...@@ -33,7 +35,7 @@ def url_class(url):
% if user.is_authenticated(): % 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('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
% endif % 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> <li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif % endif
......
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<h2 class="problem-header"> <h2 class="problem-header">
${ problem['name'] } ${ problem['name'] }
% if problem['weight'] != 1: % if problem['weight'] != 1 and problem['weight'] != None:
: ${ problem['weight'] } points : ${ problem['weight'] } points
% endif % endif
</h2> </h2>
......
...@@ -126,9 +126,9 @@ if settings.COURSEWARE_ENABLED: ...@@ -126,9 +126,9 @@ if settings.COURSEWARE_ENABLED:
#Inside the course #Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
'courseware.views.course_info', name="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"), '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'), 'staticbook.views.index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
'staticbook.views.index_shifted'), 'staticbook.views.index_shifted'),
......
...@@ -44,5 +44,5 @@ django-ses ...@@ -44,5 +44,5 @@ django-ses
django-storages django-storages
django-threaded-multihost django-threaded-multihost
django-sekizai<0.7 django-sekizai<0.7
git+git://github.com/benjaoming/django-wiki.git@484ff1ce49 git+git://github.com/benjaoming/django-wiki.git@473fd5e
-r repo-requirements.txt -r repo-requirements.txt
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment