Merge remote-tracking branch 'origin/master' into feature/bridger/new_wiki

This commit is contained in:
Bridger Maxwell
2012-08-16 13:02:00 -04:00
62 changed files with 818 additions and 354 deletions

View File

@@ -1,3 +1,4 @@
import functools
import json
import logging
import random
@@ -156,7 +157,7 @@ def edXauth_signup(request, eamap=None):
log.debug('ExtAuth: doing signup for %s' % eamap.external_email)
return student_views.main_index(extra_context=context)
return student_views.main_index(request, extra_context=context)
#-----------------------------------------------------------------------------
# MIT SSL
@@ -206,7 +207,7 @@ def edXauth_ssl_login(request):
pass
if not cert:
# no certificate information - go onward to main index
return student_views.main_index()
return student_views.main_index(request)
(user, email, fullname) = ssl_dn_extract_info(cert)
@@ -216,4 +217,4 @@ def edXauth_ssl_login(request):
credentials=cert,
email=email,
fullname=fullname,
retfun = student_views.main_index)
retfun = functools.partial(student_views.main_index, request))

View File

@@ -68,9 +68,9 @@ def index(request):
from external_auth.views import edXauth_ssl_login
return edXauth_ssl_login(request)
return main_index(user=request.user)
return main_index(request, user=request.user)
def main_index(extra_context = {}, user=None):
def main_index(request, extra_context={}, user=None):
'''
Render the edX main page.
@@ -93,7 +93,8 @@ def main_index(extra_context = {}, user=None):
entry.summary = soup.getText()
# The course selection work is done in courseware.courses.
universities = get_courses_by_university(None)
universities = get_courses_by_university(None,
domain=request.META.get('HTTP_HOST'))
context = {'universities': universities, 'entries': entries}
context.update(extra_context)
return render_to_response('index.html', context)

View File

@@ -34,6 +34,17 @@ def wrap_xmodule(get_html, module, template):
return _get_html
def replace_course_urls(get_html, course_id, module):
"""
Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /course/...
with urls that are /courses/<course_id>/...
"""
@wraps(get_html)
def _get_html():
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
return _get_html
def replace_static_urls(get_html, prefix, module):
"""
Updates the supplied module with a new get_html function that wraps

View File

@@ -49,9 +49,9 @@ class ABTestModule(XModule):
return json.dumps({'group': self.group})
def displayable_items(self):
return filter(None, [self.system.get_module(child)
for child
in self.definition['data']['group_content'][self.group]])
child_locations = self.definition['data']['group_content'][self.group]
children = [self.system.get_module(loc) for loc in child_locations]
return [c for c in children if c is not None]
# TODO (cpennington): Use Groups should be a first class object, rather than being

View File

@@ -21,6 +21,7 @@ def process_includes(fn):
xml_object = etree.fromstring(xml_data)
next_include = xml_object.find('include')
while next_include is not None:
system.error_tracker("WARNING: the <include> tag is deprecated, and will go away.")
file = next_include.get('file')
parent = next_include.getparent()
@@ -67,6 +68,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
the child element
"""
xml_object = etree.fromstring(xml_data)
system.error_tracker("WARNING: the <{}> tag is deprecated. Please do not use in new content."
.format(xml_object.tag))
if len(xml_object) == 1:
for (key, val) in xml_object.items():
@@ -74,7 +77,7 @@ class SemanticSectionDescriptor(XModuleDescriptor):
return system.process_xml(etree.tostring(xml_object[0]))
else:
xml_object.tag = 'sequence'
xml_object.tag = 'sequential'
return system.process_xml(etree.tostring(xml_object))
@@ -83,10 +86,14 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Transforms the xml_data from <$custom_tag attr="" attr=""/> to
<customtag attr="" attr=""><impl>$custom_tag</impl></customtag>
<customtag attr="" attr="" impl="$custom_tag"/>
"""
xml_object = etree.fromstring(xml_data)
system.error_tracker('WARNING: the <{tag}> tag is deprecated. '
'Instead, use <customtag impl="{tag}" attr1="..." attr2="..."/>. '
.format(tag=xml_object.tag))
tag = xml_object.tag
xml_object.tag = 'customtag'
xml_object.attrib['impl'] = tag

View File

@@ -237,7 +237,7 @@ class CapaModule(XModule):
else:
raise
content = {'name': self.metadata['display_name'],
content = {'name': self.display_name,
'html': html,
'weight': self.weight,
}
@@ -376,14 +376,17 @@ class CapaModule(XModule):
'''
For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js
Returns the answers: {'answers' : answers}
'''
event_info = dict()
event_info['problem_id'] = self.location.url()
self.system.track_function('show_answer', event_info)
if not self.answer_available():
raise NotFoundError('Answer is not available')
else:
answers = self.lcp.get_question_answers()
# answers (eg <solution>) may have embedded images
answers = dict( (k,self.system.replace_urls(answers[k], self.metadata['data_dir'])) for k in answers )
return {'answers': answers}
# Figure out if we should move these to capa_problem?
@@ -464,7 +467,7 @@ class CapaModule(XModule):
return {'success': msg}
log.exception("Error in capa_module problem checking")
raise Exception("error in capa_module")
self.attempts = self.attempts + 1
self.lcp.done = True

View File

@@ -1,6 +1,7 @@
from fs.errors import ResourceNotFoundError
import time
import logging
from lxml import etree
from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy
@@ -10,12 +11,28 @@ from xmodule.timeparse import parse_time, stringify_time
log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
class Textbook:
def __init__(self, title, table_of_contents_url):
self.title = title
self.table_of_contents_url = table_of_contents_url
@classmethod
def from_xml_object(cls, xml_object):
return cls(xml_object.get('title'), xml_object.get('table_of_contents_url'))
@property
def table_of_contents(self):
raw_table_of_contents = open(self.table_of_contents_url, 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
return table_of_contents
def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self.textbooks = self.definition['data']['textbooks']
msg = None
if self.start is None:
@@ -28,6 +45,16 @@ class CourseDescriptor(SequenceDescriptor):
self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end")
@classmethod
def definition_from_xml(cls, xml_object, system):
textbooks = []
for textbook in xml_object.findall("textbook"):
textbooks.append(cls.Textbook.from_xml_object(textbook))
xml_object.remove(textbook)
definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
definition.setdefault('data', {})['textbooks'] = textbooks
return definition
def has_started(self):
return time.gmtime() > self.start
@@ -53,7 +80,6 @@ class CourseDescriptor(SequenceDescriptor):
return grading_policy
@lazyproperty
def grading_context(self):
"""
@@ -140,7 +166,7 @@ class CourseDescriptor(SequenceDescriptor):
@property
def title(self):
return self.metadata['display_name']
return self.display_name
@property
def number(self):

View File

@@ -4,7 +4,7 @@ nav.sequence-nav {
@extend .topbar;
border-bottom: 1px solid $border-color;
@include border-top-right-radius(4px);
margin: (-(lh())) (-(lh())) lh() (-(lh()));
margin: 0 0 lh() (-(lh()));
position: relative;
ol {

View File

@@ -4,7 +4,7 @@ div.video {
border-bottom: 1px solid #e1e1e1;
border-top: 1px solid #e1e1e1;
display: block;
margin: 0 (-(lh()));
margin: 0 0 0 (-(lh()));
padding: 6px lh();
article.video-wrapper {

View File

@@ -112,8 +112,8 @@ class TestMongoModuleStore(object):
should_work = (
("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/html/toylab",
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", None, None)),
)
for location, expected in should_work:
assert_equals(path_to_location(self.store, location), expected)

View File

@@ -1,3 +1,4 @@
import json
import logging
import os
import re
@@ -5,6 +6,7 @@ import re
from fs.osfs import OSFS
from importlib import import_module
from lxml import etree
from lxml.html import HtmlComment
from path import path
from xmodule.errortracker import ErrorLog, make_error_tracker
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
@@ -15,9 +17,10 @@ from cStringIO import StringIO
from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError
etree.set_default_parser(
etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True))
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True)
etree.set_default_parser(edx_xml_parser)
log = logging.getLogger('mitx.' + __name__)
@@ -30,7 +33,8 @@ def clean_out_mako_templating(xml_string):
return xml_string
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, org, course, course_dir, error_tracker, **kwargs):
def __init__(self, xmlstore, org, course, course_dir,
policy, error_tracker, **kwargs):
"""
A class that handles loading from xml. Does some munging to ensure that
all elements have unique slugs.
@@ -96,7 +100,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
MakoDescriptorSystem.__init__(self, load_item, resources_fs,
error_tracker, render_template, **kwargs)
XMLParsingSystem.__init__(self, load_item, resources_fs,
error_tracker, process_xml, **kwargs)
error_tracker, process_xml, policy, **kwargs)
class XMLModuleStore(ModuleStoreBase):
@@ -149,7 +153,7 @@ class XMLModuleStore(ModuleStoreBase):
for course_dir in course_dirs:
self.try_load_course(course_dir)
def try_load_course(self,course_dir):
def try_load_course(self, course_dir):
'''
Load a course, keeping track of errors as we go along.
'''
@@ -170,7 +174,28 @@ class XMLModuleStore(ModuleStoreBase):
'''
String representation - for debugging
'''
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (self.data_dir,len(self.courses),len(self.modules))
return '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (
self.data_dir, len(self.courses), len(self.modules))
def load_policy(self, policy_path, tracker):
"""
Attempt to read a course policy from policy_path. If the file
exists, but is invalid, log an error and return {}.
If the policy loads correctly, returns the deserialized version.
"""
if not os.path.exists(policy_path):
return {}
try:
log.debug("Loading policy from {}".format(policy_path))
with open(policy_path) as f:
return json.load(f)
except (IOError, ValueError) as err:
msg = "Error loading course policy from {}".format(policy_path)
tracker(msg)
log.warning(msg + " " + str(err))
return {}
def load_course(self, course_dir, tracker):
"""
@@ -188,7 +213,7 @@ class XMLModuleStore(ModuleStoreBase):
# been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file).getroot()
course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
org = course_data.get('org')
@@ -211,9 +236,17 @@ class XMLModuleStore(ModuleStoreBase):
tracker(msg)
course = course_dir
system = ImportSystem(self, org, course, course_dir, tracker)
url_name = course_data.get('url_name')
if url_name:
policy_path = self.data_dir / course_dir / 'policies' / '{}.json'.format(url_name)
policy = self.load_policy(policy_path, tracker)
else:
policy = {}
system = ImportSystem(self, org, course, course_dir, policy, tracker)
course_descriptor = system.process_xml(etree.tostring(course_data))
# NOTE: The descriptors end up loading somewhat bottom up, which
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass

View File

@@ -76,7 +76,7 @@ class SequenceModule(XModule):
contents.append({
'content': child.get_html(),
'title': "\n".join(
grand_child.metadata['display_name'].strip()
grand_child.display_name.strip()
for grand_child in child.get_children()
if 'display_name' in grand_child.metadata
),
@@ -107,7 +107,7 @@ class SequenceModule(XModule):
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is
@classmethod

View File

@@ -42,9 +42,9 @@ class DummySystem(XMLParsingSystem):
descriptor.get_children()
return descriptor
policy = {}
XMLParsingSystem.__init__(self, load_item, self.resources_fs,
self.errorlog.tracker, process_xml)
self.errorlog.tracker, process_xml, policy)
def render_template(self, template, context):
raise Exception("Shouldn't be called")

View File

@@ -219,11 +219,11 @@ class XModule(HTMLSnippet):
Return module instances for all the children of this module.
'''
if self._loaded_children is None:
child_locations = self.definition.get('children', [])
children = [self.system.get_module(loc) for loc in child_locations]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = filter(None,
[self.system.get_module(child)
for child in self.definition.get('children', [])])
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
@@ -298,6 +298,14 @@ class XModule(HTMLSnippet):
return ""
def policy_key(location):
"""
Get the key for a location in a policy file. (Since the policy file is
specific to a course, it doesn't need the full location url).
"""
return '{cat}/{name}'.format(cat=location.category, name=location.name)
class XModuleDescriptor(Plugin, HTMLSnippet):
"""
An XModuleDescriptor is a specification for an element of a course. This
@@ -416,6 +424,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
return dict((k,v) for k,v in self.metadata.items()
if k not in self._inherited_metadata)
@staticmethod
def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata
@@ -671,16 +680,19 @@ class DescriptorSystem(object):
class XMLParsingSystem(DescriptorSystem):
def __init__(self, load_item, resources_fs, error_tracker, process_xml, **kwargs):
def __init__(self, load_item, resources_fs, error_tracker, process_xml, policy, **kwargs):
"""
load_item, resources_fs, error_tracker: see DescriptorSystem
policy: a policy dictionary for overriding xml metadata
process_xml: Takes an xml string, and returns a XModuleDescriptor
created from that xml
"""
DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker,
**kwargs)
self.process_xml = process_xml
self.policy = policy
class ModuleSystem(object):

View File

@@ -1,4 +1,4 @@
from xmodule.x_module import XModuleDescriptor
from xmodule.x_module import (XModuleDescriptor, policy_key)
from xmodule.modulestore import Location
from lxml import etree
import json
@@ -166,7 +166,7 @@ class XmlDescriptor(XModuleDescriptor):
Subclasses should not need to override this except in special
cases (e.g. html module)'''
# VS[compat] -- the filename tag should go away once everything is
# VS[compat] -- the filename attr should go away once everything is
# converted. (note: make sure html files still work once this goes away)
filename = xml_object.get('filename')
if filename is None:
@@ -270,6 +270,11 @@ class XmlDescriptor(XModuleDescriptor):
log.debug('Error %s in loading metadata %s' % (err,dmdata))
metadata['definition_metadata_err'] = str(err)
# Set/override any metadata specified by policy
k = policy_key(location)
if k in system.policy:
metadata.update(system.policy[k])
return cls(
system,
definition,

View File

@@ -1,9 +0,0 @@
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
<chapter name="Overview">
<videosequence format="Lecture Sequence" name="Toy Videos">
<html name="toylab" filename="toylab"/>
<video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
</course>

View File

@@ -0,0 +1 @@
roots/2012_Fall.xml

View File

@@ -0,0 +1,9 @@
<course>
<chapter url_name="Overview">
<videosequence url_name="Toy_Videos">
<html url_name="toylab"/>
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
</course>

View File

@@ -0,0 +1,23 @@
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Toy Course"
},
"chapter/Overview": {
"display_name": "Overview"
},
"videosequence/Toy_Videos": {
"display_name": "Toy Videos",
"format": "Lecture Sequence"
},
"html/toylab": {
"display_name": "Toy lab"
},
"video/Video_Resources": {
"display_name": "Video Resources"
},
"video/Welcome": {
"display_name": "Welcome"
}
}

View File

@@ -0,0 +1 @@
<course org="edX" course="toy" url_name="2012_Fall"/>

112
common/xml_cleanup.py Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python
"""
Victor's xml cleanup script. A big pile of useful hacks. Do not use
without carefully reading the code and deciding that this is what you want.
In particular, the remove-meta option is only intended to be used after pulling out a policy
using the metadata_to_json management command.
"""
import os, fnmatch, re, sys
from lxml import etree
from collections import defaultdict
INVALID_CHARS = re.compile(r"[^\w.-]")
def clean(value):
"""
Return value, made into a form legal for locations
"""
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
# category -> set of url_names for that category that we've already seen
used_names = defaultdict(set)
def clean_unique(category, name):
cleaned = clean(name)
if cleaned not in used_names[category]:
used_names[category].add(cleaned)
return cleaned
x = 1
while cleaned + str(x) in used_names[category]:
x += 1
# Found one!
cleaned = cleaned + str(x)
used_names[category].add(cleaned)
return cleaned
def cleanup(filepath, remove_meta):
# Keys that are exported to the policy file, and so
# can be removed from the xml afterward
to_remove = ('format', 'display_name',
'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'hide_from_toc',
'ispublic', 'xqa_key')
try:
print "Cleaning {}".format(filepath)
with open(filepath) as f:
parser = etree.XMLParser(remove_comments=False)
xml = etree.parse(filepath, parser=parser)
except:
print "Error parsing file {}".format(filepath)
return
for node in xml.iter(tag=etree.Element):
attrs = node.attrib
if 'url_name' in attrs:
used_names[node.tag].add(attrs['url_name'])
if 'name' in attrs:
# Replace name with an identical display_name, and a unique url_name
name = attrs['name']
attrs['display_name'] = name
attrs['url_name'] = clean_unique(node.tag, name)
del attrs['name']
if 'url_name' in attrs and 'slug' in attrs:
print "WARNING: {} has both slug and url_name"
if ('url_name' in attrs and 'filename' in attrs and
len(attrs)==2 and attrs['url_name'] == attrs['filename']):
# This is a pointer tag in disguise. Get rid of the filename.
print 'turning {}.{} into a pointer tag'.format(node.tag, attrs['url_name'])
del attrs['filename']
if remove_meta:
for attr in to_remove:
if attr in attrs:
del attrs[attr]
with open(filepath, "w") as f:
f.write(etree.tostring(xml))
def find_replace(directory, filePattern, remove_meta):
for path, dirs, files in os.walk(os.path.abspath(directory)):
for filename in fnmatch.filter(files, filePattern):
filepath = os.path.join(path, filename)
cleanup(filepath, remove_meta)
def main(args):
usage = "xml_cleanup [dir] [remove-meta]"
n = len(args)
if n < 1 or n > 2 or (n == 2 and args[1] != 'remove-meta'):
print usage
return
remove_meta = False
if n == 2:
remove_meta = True
find_replace(args[0], '*.xml', remove_meta)
if __name__ == '__main__':
main(sys.argv[1:])

View File

@@ -66,3 +66,9 @@ To run a single nose test:
Very handy: if you uncomment the `--pdb` argument in `NOSE_ARGS` in `lms/envs/test.py`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out http://docs.python.org/library/pdb.html
## Content development
If you change course content, while running the LMS in dev mode, it is unnecessary to restart to refresh the modulestore.
Instead, hit /migrate/modules to see a list of all modules loaded, and click on links (eg /migrate/reload/edx4edx) to reload a course.

View File

@@ -31,7 +31,7 @@
{% spaceless %}
<span class="action-link">
<a class="question-delete" id="answer-delete-link-{{answer.id}}">
{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}&#10006;{% endif %}</a>
{% if answer.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
</span>
{% endspaceless %}
{% endif %}

View File

@@ -40,6 +40,5 @@
{% endif %}
{% if request.user|can_delete_post(question) %}{{ pipe() }}
<a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}&#10006;{% endif %}</a>
<a class="question-delete" id="question-delete-link-{{question.id}}">{% if question.deleted %}{% trans %}undelete{% endtrans %}{% else %}delete{% endif %}</a>
{% endif %}

View File

@@ -18,11 +18,14 @@
{% include "widgets/system_messages.html" %}
{% include "debug_header.html" %}
{% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #}
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
<section class="container">
<section class="content-wrapper">
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
<section class="container">
{% block body %}
{% endblock %}
</section>
</section>
{% if settings.FOOTER_MODE == 'default' %}

View File

@@ -1,4 +1,3 @@
{% load extra_filters_jinja %}
<!--<link href="{{"/style/style.css"|media }}" rel="stylesheet" type="text/css" />-->
{{ 'application' | compressed_css }}
{{ 'course' | compressed_css }}

View File

@@ -65,9 +65,10 @@ def has_access(user, obj, action):
# Passing an unknown object here is a coding error, so rather than
# returning a default, complain.
raise TypeError("Unknown object type in has_access(). Object type: '{}'"
raise TypeError("Unknown object type in has_access(): '{}'"
.format(type(obj)))
# ================ Implementation helpers ================================
def _has_access_course_desc(user, course, action):
@@ -83,8 +84,12 @@ def _has_access_course_desc(user, course, action):
'staff' -- staff access to course.
"""
def can_load():
"Can this user load this course?"
# delegate to generic descriptor check
"""
Can this user load this course?
NOTE: this is not checking whether user is actually enrolled in the course.
"""
# delegate to generic descriptor check to check start dates
return _has_access_descriptor(user, course, action)
def can_enroll():
@@ -169,6 +174,12 @@ def _has_access_descriptor(user, descriptor, action):
has_access(), it will not do the right thing.
"""
def can_load():
"""
NOTE: This does not check that the student is enrolled in the course
that contains this module. We may or may not want to allow non-enrolled
students to see modules. If not, views should check the course, so we
don't have to hit the enrollments table on every module load.
"""
# If start dates are off, can always load
if settings.MITX_FEATURES['DISABLE_START_DATES']:
debug("Allow: DISABLE_START_DATES")
@@ -196,8 +207,6 @@ def _has_access_descriptor(user, descriptor, action):
return _dispatch(checkers, action, user, descriptor)
def _has_access_xmodule(user, xmodule, action):
"""
Check if user has access to this xmodule.

View File

@@ -2,8 +2,8 @@ from collections import defaultdict
from fs.errors import ResourceNotFoundError
from functools import wraps
import logging
from path import path
from path import path
from django.conf import settings
from django.http import Http404
@@ -142,7 +142,8 @@ def get_course_info_section(course, section_key):
raise KeyError("Invalid about key " + str(section_key))
def get_courses_by_university(user):
def get_courses_by_university(user, domain=None):
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number.
@@ -152,9 +153,21 @@ def get_courses_by_university(user):
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
if domain and settings.MITX_FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'):
subdomain = domain.split(".")[0]
if subdomain not in settings.COURSE_LISTINGS:
subdomain = 'default'
visible_courses = frozenset(settings.COURSE_LISTINGS[subdomain])
else:
visible_courses = frozenset(c.id for c in courses)
universities = defaultdict(list)
for course in courses:
if has_access(user, course, 'see_exists'):
universities[course.org].append(course)
if not has_access(user, course, 'see_exists'):
continue
if course.id not in visible_courses:
continue
universities[course.org].append(course)
return universities

View File

@@ -0,0 +1,98 @@
"""
A script to walk a course xml tree, generate a dictionary of all the metadata,
and print it out as a json dict.
"""
import os
import sys
import json
from collections import OrderedDict
from path import path
from django.core.management.base import BaseCommand
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.x_module import policy_key
def import_course(course_dir, verbose=True):
course_dir = path(course_dir)
data_dir = course_dir.dirname()
course_dirs = [course_dir.basename()]
# No default class--want to complain if it doesn't find plugins for any
# module.
modulestore = XMLModuleStore(data_dir,
default_class=None,
eager=True,
course_dirs=course_dirs)
def str_of_err(tpl):
(msg, exc_str) = tpl
return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
courses = modulestore.get_courses()
n = len(courses)
if n != 1:
sys.stderr.write('ERROR: Expect exactly 1 course. Loaded {n}: {lst}\n'.format(
n=n, lst=courses))
return None
course = courses[0]
errors = modulestore.get_item_errors(course.location)
if len(errors) != 0:
sys.stderr.write('ERRORs during import: {}\n'.format('\n'.join(map(str_of_err, errors))))
return course
def node_metadata(node):
# make a copy
to_export = ('format', 'display_name',
'graceperiod', 'showanswer', 'rerandomize',
'start', 'due', 'graded', 'hide_from_toc',
'ispublic', 'xqa_key')
orig = node.own_metadata
d = {k: orig[k] for k in to_export if k in orig}
return d
def get_metadata(course):
d = OrderedDict({})
queue = [course]
while len(queue) > 0:
node = queue.pop()
d[policy_key(node.location)] = node_metadata(node)
# want to print first children first, so put them at the end
# (we're popping from the end)
queue.extend(reversed(node.get_children()))
return d
def print_metadata(course_dir, output):
course = import_course(course_dir)
if course:
meta = get_metadata(course)
result = json.dumps(meta, indent=4)
if output:
with file(output, 'w') as f:
f.write(result)
else:
print result
class Command(BaseCommand):
help = """Imports specified course.xml and prints its
metadata as a json dict.
Usage: metadata_to_json PATH-TO-COURSE-DIR OUTPUT-PATH
if OUTPUT-PATH isn't given, print to stdout.
"""
def handle(self, *args, **options):
n = len(args)
if n < 1 or n > 2:
print Command.help
return
output_path = args[1] if n > 1 else None
print_metadata(args[0], output_path)

View File

@@ -77,7 +77,7 @@ class StudentModuleCache(object):
Arguments
user: The user for which to fetch maching StudentModules
descriptors: An array of XModuleDescriptors.
select_for_update: Flag indicating whether the row should be locked until end of transaction
select_for_update: Flag indicating whether the rows should be locked until end of transaction
'''
if user.is_authenticated():
module_ids = self._get_module_state_keys(descriptors)
@@ -110,7 +110,7 @@ class StudentModuleCache(object):
the supplied descriptor. If depth is None, load all descendent StudentModules
descriptor_filter is a function that accepts a descriptor and return wether the StudentModule
should be cached
select_for_update: Flag indicating whether the row should be locked until end of transaction
select_for_update: Flag indicating whether the rows should be locked until end of transaction
"""
def get_child_descriptors(descriptor, depth, descriptor_filter):

View File

@@ -19,7 +19,7 @@ from xmodule.exceptions import NotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
log = logging.getLogger("mitx.courseware")
@@ -48,7 +48,7 @@ def make_track_function(request):
return f
def toc_for_course(user, request, course, active_chapter, active_section):
def toc_for_course(user, request, course, active_chapter, active_section, course_id=None):
'''
Create a table of contents from the module store
@@ -71,7 +71,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
'''
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2)
course = get_module(user, request, course.location, student_module_cache)
course = get_module(user, request, course.location, student_module_cache, course_id=course_id)
chapters = list()
for chapter in course.get_display_items():
@@ -127,7 +127,7 @@ def get_section(course_module, chapter, section):
return section_module
def get_module(user, request, location, student_module_cache, position=None):
def get_module(user, request, location, student_module_cache, position=None, course_id=None):
''' Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
exists.
@@ -144,6 +144,14 @@ def get_module(user, request, location, student_module_cache, position=None):
'''
descriptor = modulestore().get_item(location)
# NOTE:
# A 'course_id' is understood to be the triplet (org, course, run), for example
# (MITx, 6.002x, 2012_Spring).
# At the moment generic XModule does not contain enough information to replicate
# the triplet (it is missing 'run'), so we must pass down course_id
if course_id is None:
course_id = descriptor.location.course_id # Will NOT produce (org, course, run) for non-CourseModule's
# Short circuit--if the user shouldn't have access, bail without doing any work
if not has_access(user, descriptor, 'load'):
@@ -167,7 +175,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# Setup system context for module instance
ajax_url = reverse('modx_dispatch',
kwargs=dict(course_id=descriptor.location.course_id,
kwargs=dict(course_id=course_id,
id=descriptor.location.url(),
dispatch=''),
)
@@ -175,7 +183,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# Fully qualified callback URL for external queueing system
xqueue_callback_url = request.build_absolute_uri('/')[:-1] # Trailing slash provided by reverse
xqueue_callback_url += reverse('xqueue_callback',
kwargs=dict(course_id=descriptor.location.course_id,
kwargs=dict(course_id=course_id,
userid=str(user.id),
id=descriptor.location.url(),
dispatch='score_update'),
@@ -195,7 +203,7 @@ def get_module(user, request, location, student_module_cache, position=None):
Delegate to get_module. It does an access check, so may return None
"""
return get_module(user, request, location,
student_module_cache, position)
student_module_cache, position, course_id=course_id)
# TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory
@@ -225,6 +233,10 @@ def get_module(user, request, location, student_module_cache, position=None):
module.metadata['data_dir'], module
)
# Allow URLs of the form '/course/' refer to the root of multicourse directory
# hierarchy of this course
module.get_html = replace_course_urls(module.get_html, course_id, module)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
if has_access(user, module, 'staff'):
module.get_html = add_histogram(module.get_html, module, user)
@@ -370,7 +382,7 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
p[inputfile_id] = inputfile
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id))
instance = get_module(request.user, request, id, student_module_cache)
instance = get_module(request.user, request, id, student_module_cache, course_id=course_id)
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.

View File

@@ -33,6 +33,7 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib}
def user_groups(user):
"""
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
@@ -63,11 +64,12 @@ def courses(request):
'''
Render "find courses" page. The course selection work is done in courseware.courses.
'''
universities = get_courses_by_university(request.user)
universities = get_courses_by_university(request.user,
domain=request.META.get('HTTP_HOST'))
return render_to_response("courses.html", {'universities': universities})
def render_accordion(request, course, chapter, section):
def render_accordion(request, course, chapter, section, course_id=None):
''' Draws navigation bar. Takes current position in accordion as
parameter.
@@ -78,7 +80,7 @@ def render_accordion(request, course, chapter, section):
Returns the html string'''
# grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section)
toc = toc_for_course(request.user, request, course, chapter, section, course_id=course_id)
context = dict([('toc', toc),
('course_id', course.id),
@@ -110,6 +112,7 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
registered = registered_for_course(course, request.user)
if not registered:
# TODO (vshnayder): do course instructors need to be registered to see course?
@@ -119,11 +122,12 @@ def index(request, course_id, chapter=None, section=None,
try:
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'accordion': render_accordion(request, course, chapter, section, course_id=course_id),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': ''
'content': '',
'staff_access': staff_access,
}
look_for_module = chapter is not None and section is not None
@@ -135,7 +139,7 @@ def index(request, course_id, chapter=None, section=None,
section_descriptor)
module = get_module(request.user, request,
section_descriptor.location,
student_module_cache)
student_module_cache, course_id=course_id)
if module is None:
# User is probably being clever and trying to access something
# they don't have access to.
@@ -166,7 +170,8 @@ def index(request, course_id, chapter=None, section=None,
position=position
))
try:
result = render_to_response('courseware-error.html', {})
result = render_to_response('courseware-error.html',
{'staff_access': staff_access})
except:
result = HttpResponse("There was an unrecoverable error")
@@ -208,8 +213,10 @@ def course_info(request, course_id):
Assumes the course_id is in a valid format.
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
return render_to_response('info.html', {'course': course})
return render_to_response('info.html', {'course': course,
'staff_access': staff_access,})
def registered_for_course(course, user):
@@ -241,7 +248,8 @@ def university_profile(request, org_id):
raise Http404("University Profile not found for {0}".format(org_id))
# Only grab courses for this org...
courses = get_courses_by_university(request.user)[org_id]
courses = get_courses_by_university(request.user,
domain=request.META.get('HTTP_HOST'))[org_id]
context = dict(courses=courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower()
@@ -257,20 +265,21 @@ def profile(request, course_id, student_id=None):
Course staff are allowed to see the profiles of students in their class.
"""
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
if student_id is None or student_id == request.user.id:
# always allowed to see your own profile
student = request.user
else:
# Requesting access to a different student's profile
if not has_access(request.user, course, 'staff'):
if not staff_access:
raise Http404
student = User.objects.get(id=int(student_id))
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, course)
course_module = get_module(request.user, request, course.location, student_module_cache)
course_module = get_module(request.user, request, course.location, student_module_cache, course_id=course_id)
courseware_summary = grades.progress_summary(student, course_module, course.grader, student_module_cache)
grade_summary = grades.grade(request.user, request, course, student_module_cache)
@@ -282,8 +291,9 @@ def profile(request, course_id, student_id=None):
'email': student.email,
'course': course,
'csrf': csrf(request)['csrf_token'],
'courseware_summary' : courseware_summary,
'grade_summary' : grade_summary
'courseware_summary': courseware_summary,
'grade_summary': grade_summary,
'staff_access': staff_access,
}
context.update()
@@ -316,7 +326,10 @@ def gradebook(request, course_id):
for student in enrolled_students]
return render_to_response('gradebook.html', {'students': student_info,
'course': course, 'course_id': course_id})
'course': course,
'course_id': course_id,
# Checked above
'staff_access': True,})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@@ -325,7 +338,8 @@ def grade_summary(request, course_id):
course = get_course_with_access(request.user, course_id, 'staff')
# For now, just a static page
context = {'course': course }
context = {'course': course,
'staff_access': True,}
return render_to_response('grade_summary.html', context)
@@ -335,6 +349,7 @@ def instructor_dashboard(request, course_id):
course = get_course_with_access(request.user, course_id, 'staff')
# For now, just a static page
context = {'course': course }
context = {'course': course,
'staff_access': True,}
return render_to_response('instructor_dashboard.html', context)

View File

@@ -35,7 +35,7 @@ def manage_modulestores(request,reload_dir=None):
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
if not ip:
ip = request.META.get('REMOTE_ADDR','None')
if LOCAL_DEBUG:
html += '<h3>IP address: %s ' % ip
html += '<h3>User: %s ' % request.user
@@ -48,7 +48,7 @@ def manage_modulestores(request,reload_dir=None):
html += 'Permission denied'
html += "</body></html>"
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
return HttpResponse(html)
return HttpResponse(html)
#----------------------------------------
# reload course if specified
@@ -74,10 +74,10 @@ def manage_modulestores(request,reload_dir=None):
#----------------------------------------
dumpfields = ['definition','location','metadata']
for cdir, course in def_ms.courses.items():
html += '<hr width="100%"/>'
html += '<h2>Course: %s (%s)</h2>' % (course.metadata['display_name'],cdir)
html += '<h2>Course: %s (%s)</h2>' % (course.display_name,cdir)
for field in dumpfields:
data = getattr(course,field)
@@ -89,7 +89,7 @@ def manage_modulestores(request,reload_dir=None):
html += '</ul>'
else:
html += '<ul><li>%s</li></ul>' % escape(data)
#----------------------------------------
@@ -107,4 +107,4 @@ def manage_modulestores(request,reload_dir=None):
log.debug('def_ms=%s' % unicode(def_ms))
html += "</body></html>"
return HttpResponse(html)
return HttpResponse(html)

View File

@@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response
from courseware.courses import get_opt_course_with_access
from courseware.access import has_access
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.django import modulestore
@@ -49,6 +50,10 @@ def update_template_dictionary(dictionary, request=None, course=None, article=No
if request:
dictionary.update(csrf(request))
if request and course:
dictionary['staff_access'] = has_access(request.user, course, 'staff')
else:
dictionary['staff_access'] = False
def view(request, article_path, course_id=None):
course = get_opt_course_with_access(request.user, course_id, 'load')

View File

@@ -1,18 +1,22 @@
from django.contrib.auth.decorators import login_required
from mitxmako.shortcuts import render_to_response
from courseware.access import has_access
from courseware.courses import get_course_with_access
from lxml import etree
@login_required
def index(request, course_id, page=0):
def index(request, course_id, book_index, page=0):
course = get_course_with_access(request.user, course_id, 'load')
raw_table_of_contents = open('lms/templates/book_toc.xml', 'r') # TODO: This will need to come from S3
table_of_contents = etree.parse(raw_table_of_contents).getroot()
staff_access = has_access(request.user, course, 'staff')
textbook = course.textbooks[int(book_index)]
table_of_contents = textbook.table_of_contents
return render_to_response('staticbook.html',
{'page': int(page), 'course': course,
'table_of_contents': table_of_contents})
'table_of_contents': table_of_contents,
'staff_access': staff_access})
def index_shifted(request, course_id, page):
return index(request, course_id=course_id, page=int(page) + 24)

View File

@@ -49,6 +49,11 @@ MITX_FEATURES = {
## Doing so will cause all courses to be released on production
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
# When True, will only publicly list courses by the subdomain. Expects you
# to define COURSE_LISTINGS, a dictionary mapping subdomains to lists of
# course_ids (see dev_int.py for an example)
'SUBDOMAIN_COURSE_LISTINGS' : False,
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True,
@@ -61,6 +66,7 @@ MITX_FEATURES = {
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False,
}
# Used for A/B testing

View File

@@ -54,7 +54,7 @@ CACHES = {
}
XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org",
"url": "https://sandbox-xqueue.edx.org",
"django_auth": {
"username": "lms",
"password": "***REMOVED***"

32
lms/envs/dev_int.py Normal file
View File

@@ -0,0 +1,32 @@
"""
This enables use of course listings by subdomain. To see it in action, point the
following domains to 127.0.0.1 in your /etc/hosts file:
berkeley.dev
harvard.dev
mit.dev
Note that OS X has a bug where using *.local domains is excruciatingly slow, so
use *.dev domains instead for local testing.
"""
from .dev import *
MITX_FEATURES['SUBDOMAIN_COURSE_LISTINGS'] = True
COURSE_LISTINGS = {
'default' : ['BerkeleyX/CS169.1x/2012_Fall',
'BerkeleyX/CS188.1x/2012_Fall',
'HarvardX/CS50x/2012',
'HarvardX/PH207x/2012_Fall',
'MITx/3.091x/2012_Fall',
'MITx/6.002x/2012_Fall',
'MITx/6.00x/2012_Fall'],
'berkeley': ['BerkeleyX/CS169.1x/2012_Fall',
'BerkeleyX/CS188.1x/2012_Fall'],
'harvard' : ['HarvardX/CS50x/2012'],
'mit' : ['MITx/3.091x/2012_Fall',
'MITx/6.00x/2012_Fall']
}

View File

@@ -51,7 +51,7 @@ GITHUB_REPO_ROOT = ENV_ROOT / "data"
XQUEUE_INTERFACE = {
"url": "http://xqueue.sandbox.edx.org",
"url": "http://sandbox-xqueue.edx.org",
"django_auth": {
"username": "lms",
"password": "***REMOVED***"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -69,10 +69,15 @@ div.info-wrapper {
section.handouts {
@extend .sidebar;
border-left: 1px solid $border-color;
@include border-radius(0 4px 4px 0);
border-right: 0;
@include border-radius(0 4px 4px 0);
@include box-shadow(none);
&:after {
left: -1px;
right: auto;
}
h1 {
@extend .bottom-border;
margin-bottom: 0;

View File

@@ -8,6 +8,11 @@ div.profile-wrapper {
@include border-radius(0px 4px 4px 0);
border-right: 0;
&:after {
left: -1px;
right: auto;
}
header {
@extend .bottom-border;
margin: 0;

View File

@@ -10,7 +10,6 @@ div.book-wrapper {
font-size: em(14);
.chapter-number {
}
.chapter {
@@ -81,9 +80,8 @@ div.book-wrapper {
section.book {
@extend .content;
padding-bottom: 0;
padding-right: 0;
padding-top: 0;
padding-left: lh();
nav {
@extend .clearfix;

View File

@@ -2,7 +2,7 @@ body {
min-width: 980px;
}
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a {
body, h1, h2, h3, h4, h5, h6, p, p a:link, p a:visited, a, label {
text-align: left;
font-family: $sans-serif;
}
@@ -27,6 +27,14 @@ form {
}
}
img {
max-width: 100%;
}
.container {
padding: em(40) 0;
}
::selection, ::-moz-selection, ::-webkit-selection {
background:#444;
color:#fff;

View File

@@ -12,10 +12,13 @@ h1.top-header {
@include box-shadow(inset 0 1px 0 #fff);
color: #666;
cursor: pointer;
font: normal $body-font-size $body-font-family;
font: 400 $body-font-size $body-font-family;
@include linear-gradient(#fff, lighten(#888, 40%));
padding: 4px 8px;
text-decoration: none;
text-shadow: none;
text-transform: none;
letter-spacing: 0;
-webkit-font-smoothing: antialiased;
&:hover, &:focus {
@@ -28,7 +31,7 @@ h1.top-header {
.content {
@include box-sizing(border-box);
display: table-cell;
padding: lh();
padding-right: lh();
vertical-align: top;
width: flex-grid(9) + flex-gutter();
@@ -46,6 +49,18 @@ h1.top-header {
vertical-align: top;
width: flex-grid(3);
&:after {
width: 1px;
height: 100%;
@include position(absolute, 0px -1px 0px 0);
content: "";
@include background-image(linear-gradient(top, #fff, rgba(#fff, 0)), linear-gradient(top, rgba(#fff, 0), #fff));
background-position: top, bottom;
@include background-size(1px 20px);
background-repeat: no-repeat;
display: block;
}
h1, h2 {
font-size: em(20);
font-weight: 100;
@@ -134,7 +149,7 @@ h1.top-header {
position: absolute;
right: -1px;
text-indent: -9999px;
top: 6px;
top: 12px;
width: 16px;
z-index: 99;

View File

@@ -12,7 +12,8 @@ div.course-wrapper {
section.course-content {
@extend .content;
@include border-radius(0 4px 4px 0);
padding-right: 0;
padding-left: lh();
h1 {
margin: 0 0 lh();

View File

@@ -7,9 +7,16 @@ div.answer-controls {
padding-left: flex-grid(1.1);
width: 100%;
div.answer-count {
display: inline-block;
float: left;
h1 {
margin-bottom: 0;
font-size: em(24);
font-weight: 100;
}
}
div.answer-sort {
@@ -18,7 +25,7 @@ div.answer-controls {
nav {
float: right;
margin-top: 34px;
margin-top: 10px;
a {
&.on span{
@@ -44,8 +51,9 @@ div.answer-block {
width: 100%;
img.answer-img-accept {
margin: 10px 0px 10px 16px;
margin: 10px 0px 10px 11px;
}
div.answer-container {
@extend div.question-container;
@@ -130,21 +138,19 @@ div.answer-own {
div.answer-actions {
margin: 0;
padding:8px 8px 8px 0;
padding:8px 0 8px 8px;
text-align: right;
border-top: 1px solid #efefef;
span.sep {
color: #EDDFAA;
color: $border-color;
}
a {
cursor: pointer;
text-decoration: none;
&.question-delete {
color: $mit-red;
}
@extend a:link;
font-size: em(14);
}
}

View File

@@ -22,6 +22,8 @@ div#award-list{
}
ul.badge-list {
padding-left: 0;
li.badge {
border-bottom: 1px solid #eee;
@extend .clearfix;
@@ -70,12 +72,17 @@ ul.badge-list {
.bronze, .badge3 {
color: #cc9933;
}
div.badge-desc {
> div {
margin-bottom: 20px;
span {
font-size: 18px;
@include border-radius(10px);
}
div.discussion-wrapper aside {
div.badge-desc {
border-top: 0;
> div {
margin-bottom: 20px;
span {
font-size: 18px;
@include border-radius(10px);
}
}
}
}

View File

@@ -8,7 +8,7 @@ body.askbot {
@include box-sizing(border-box);
display: table-cell;
min-width: 650px;
padding: lh();
padding-right: lh();
vertical-align: top;
width: flex-grid(9) + flex-gutter();

View File

@@ -5,6 +5,11 @@ form.answer-form {
border-top: 1px solid #ddd;
overflow: hidden;
padding-left: flex-grid(1.1);
padding-top: lh();
p {
margin-bottom: lh();
}
textarea {
@include box-sizing(border-box);
@@ -121,7 +126,6 @@ form.question-form {
border: none;
padding: 15px 0 0 0;
input[type="text"] {
@include box-sizing(border-box);
width: flex-grid(6);
@@ -131,6 +135,11 @@ form.question-form {
margin-top: 10px;
}
input[value="Cancel"] {
@extend .light-button;
float: right;
}
div#question-list {
background-color: rgba(255,255,255,0.95);
@include box-sizing(border-box);

View File

@@ -1,5 +1,4 @@
// Style for modal boxes that pop up to notify the user of various events
.vote-notification {
background-color: darken($mit-red, 7%);
@include border-radius(4px);

View File

@@ -9,9 +9,9 @@ body.user-profile-page {
}
ul.sub-info {
// border-top: 1px solid #ddd;
margin-top: lh();
list-style: none;
padding: 0;
> li {
display: table-cell;
@@ -57,6 +57,7 @@ body.user-profile-page {
ul {
list-style: none;
padding: 0;
&.user-stats-table {
list-style: none;
@@ -72,37 +73,28 @@ body.user-profile-page {
margin-bottom: 30px;
li {
background-position: 10px center;
background-position: 10px -10px;
background-repeat: no-repeat;
@include border-radius(4px);
display: inline-block;
height: 20px;
padding: 10px 10px 10px 40px;
padding: 2px 10px 2px 40px;
margin-bottom: lh(.5);
border: 1px solid lighten($border-color, 10%);
&.up {
background-color:#d1e3a8;
background-image: url(../images/askbot/vote-arrow-up-activate.png);
background-image: url(../images/askbot/vote-arrow-up.png);
margin-right: 6px;
span.vote-count {
color: #3f6c3e;
}
}
&.down {
background-image: url(../images/askbot/vote-arrow-down-activate.png);
background-color:#eac6ad;
span.vote-count {
color: $mit-red;
}
background-image: url(../images/askbot/vote-arrow-down.png);
}
}
}
&.badges {
@include inline-block();
padding: 0;
margin: 0;
a {
background-color: #e3e3e3;

View File

@@ -1,15 +1,16 @@
// Styles for the single question view
div.question-header {
@include clearfix();
div.official-stamp {
background: $mit-red;
color: #fff;
font-size: 12px;
margin-left: -1px;
margin-top: 10px;
padding: 2px 5px;
text-align: center;
margin-left: -1px;
}
div.vote-buttons {
@@ -19,40 +20,40 @@ div.question-header {
width: flex-grid(0.7,9);
ul {
li {
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
font-weight: bold;
height: 20px;
list-style: none;
padding: 10px;
text-align: center;
width: 70%;
padding: 0;
margin: 0;
&.post-vote {
@include border-radius(4px);
@include box-shadow(inset 0 1px 0px #fff);
}
li {
background-repeat: no-repeat;
color: #999;
font-size: em(20);
font-weight: bold;
list-style: none;
text-align: center;
&.question-img-upvote, &.answer-img-upvote {
background-image: url(../images/askbot/vote-arrow-up.png);
@include box-shadow(inset 0 1px 0px rgba(255, 255, 255, 0.5));
background-position: center 0;
cursor: pointer;
height: 12px;
margin-bottom: lh(.5);
&:hover, &.on {
background-color:#d1e3a8;
border-color: darken(#D1E3A8, 20%);
background-image: url(../images/askbot/vote-arrow-up-activate.png);
background-image: url(../images/askbot/vote-arrow-up.png);
background-position: center -22px;
}
}
&.question-img-downvote, &.answer-img-downvote {
cursor: pointer;
background-image: url(../images/askbot/vote-arrow-down.png);
background-position: center 0;
height: 12px;
margin-top: lh(.5);
&:hover, &.on {
background-color:#EAC6AD;
border-color: darken(#EAC6AD, 20%);
background-image: url(../images/askbot/vote-arrow-down-activate.png);
background-image: url(../images/askbot/vote-arrow-down.png);
background-position: center -22px;
}
}
}
@@ -66,12 +67,19 @@ div.question-header {
h1 {
margin-top: 0;
font-weight: 100;
line-height: 1.1em;
a {
font-weight: 100;
line-height: 1.1em;
}
}
div.meta-bar {
border-bottom: 1px solid #eee;
display: block;
margin: 10px 0;
margin: lh(.5) 0 lh();
overflow: hidden;
padding: 5px 0 10px;
@@ -89,11 +97,8 @@ div.question-header {
width: flex-grid(4,8);
a {
&.question-delete {
color: $mit-red;
text-decoration: none;
cursor: pointer;
}
@extend a:link;
cursor: pointer;
}
span.sep {
@@ -155,7 +160,7 @@ div.question-header {
}
div.change-date {
font-size: 12px;
font-size: em(14);
margin-bottom: 2px;
}
@@ -179,13 +184,13 @@ div.question-header {
display: inline-block;
padding: 0 0 3% 0;
width: 100%;
margin-top: lh(2);
div.comments-content {
font-size: 13px;
background: #efefef;
border-top: 1px solid lighten($border-color, 10%);
.block {
border-top: 1px solid #ddd;
border-top: 1px solid lighten($border-color, 10%);
padding: 15px;
display: block;
@@ -197,10 +202,10 @@ div.question-header {
padding-top: 10px;
span.official-comment {
background: $mit-red;
background: $pink;
color: #fff;
display: block;
font-size: 12px;
font-size: em(12);
margin: 0 0 10px -5%;
padding:2px 5px 2px 5%;
text-align: left;
@@ -212,6 +217,10 @@ div.question-header {
form.post-comments {
padding: 15px;
button {
color: #fff;
}
button:last-child {
margin-left: 10px;
@extend .light-button;
@@ -232,7 +241,6 @@ div.question-header {
border: none;
@include box-shadow(none);
display: inline-block;
margin-top: -8px;
padding:0 2% 0 0;
text-align: center;
width: 5%;
@@ -278,16 +286,14 @@ div.question-header {
}
div.comment-delete {
// display: inline;
color: $mit-red;
@extend a:link;
cursor: pointer;
font-size: 15px;
}
div.comment-edit {
@include transform(rotate(50deg));
cursor: pointer;
font-size: 16px;
a.edit-icon {
color: #555;
text-decoration: none;
@@ -305,13 +311,13 @@ div.question-header {
div.comment-meta {
text-align: right;
margin-top: lh(.5);
a.author {
font-weight: bold;
}
a.edit {
font-size: 12px;
padding: 2px 10px;
}
}
@@ -334,12 +340,10 @@ div.question-header {
}
div.controls {
border-top: 1px solid #efefef;
text-align: right;
a {
display: inline-block;
font-size: 12px;
margin: 10px 10px 10px 0;
}
}

View File

@@ -1,17 +1,24 @@
// Styles for the default question list view
div.question-list-header {
@extend h1.top-header;
display: block;
margin-bottom: 0px;
padding-bottom: lh(.5);
overflow: hidden;
width: flex-grid(9,9);
@extend h1.top-header;
h1 {
margin: 0;
font-size: 1em;
font-weight: 100;
padding-bottom: lh(.5);
> a.light-button {
float: right;
font-size: em(14, 24);
letter-spacing: 0;
font-weight: 400;
}
}
@@ -49,8 +56,11 @@ div.question-list-header {
nav {
@extend .action-link;
float: right;
font-size: em(16, 24);
a {
font-size: 1em;
&.on span{
font-weight: bold;
}
@@ -82,6 +92,7 @@ div.question-list-header {
a {
color: #555;
font-size: em(14, 24);
}
}
@@ -90,12 +101,10 @@ div.question-list-header {
}
ul.tags {
li {
background: #fff;
&:before {
border-color: transparent #fff transparent transparent;
}
span, div {
line-height: 1em;
margin-left: 6px;
cursor: pointer;
}
}
}
@@ -103,26 +112,15 @@ div.question-list-header {
ul.question-list, div#question-list {
width: flex-grid(9,9);
padding-left: 0;
margin: 0;
li.single-question {
border-bottom: 1px solid #eee;
list-style: none;
padding: 10px lh();
margin-left: (-(lh()));
padding: lh() 0;
width: 100%;
&:hover {
background: #F3F3F3;
ul.tags li {
background: #ddd;
&:before {
border-color: transparent #ddd transparent transparent;
}
}
}
&:first-child {
border-top: 0;
}
@@ -133,14 +131,19 @@ ul.question-list, div#question-list {
&.question-body {
@include box-sizing(border-box);
margin-right: flex-gutter();
width: flex-grid(5.5,9);
width: flex-grid(5,9);
h2 {
font-size: 16px;
font-size: em(20);
font-weight: bold;
letter-spacing: 0;
margin: 0px 0 15px 0;
margin: 0 0 lh() 0;
text-transform: none;
line-height: lh();
a {
line-height: lh();
}
}
p.excerpt {
@@ -151,40 +154,41 @@ ul.question-list, div#question-list {
div.user-info {
display: inline-block;
vertical-align: top;
margin-bottom: 10px;
margin: lh() 0 0 0;
line-height: lh();
span.relative-time {
font-weight: normal;
}
a {
color: $mit-red;
line-height: lh();
}
}
ul.tags {
display: inline-block;
margin: lh() 0 0 0;
padding: 0;
}
}
&.question-meta {
float: right;
margin-top: 10px;
width: flex-grid(3.5,9);
width: flex-grid(3,9);
ul {
text-align: right;
@include clearfix;
margin: 0;
padding: 0;
list-style: none;
li {
border: 1px solid #ddd;
border: 1px solid lighten($border-color, 10%);
@include box-sizing(border-box);
@include box-shadow(0 1px 0 #fff);
display: inline-block;
height:60px;
@include linear-gradient(#fff, #f5f5f5);
margin-right: 10px;
width: 60px;
float: left;
margin-right: flex-gutter(3);
width: flex-grid(1,3);
&:last-child {
margin-right: 0px;
@@ -196,31 +200,22 @@ ul.question-list, div#question-list {
}
}
&.views {
}
&.answers {
&.accepted {
@include linear-gradient(#fff, lighten( #c4dfbe, 12% ));
border-color: #c4dfbe;
border-color: lighten($border-color, 10%);
span, div {
color: darken(#c4dfbe, 35%);
}
}
&.no-answers {
span, div {
color: lighten($mit-red, 20%);
color: $pink;
}
}
}
&.votes {
}
span, div {
@include box-sizing(border-box);
color: #888;

View File

@@ -2,25 +2,31 @@
div.discussion-wrapper aside {
@extend .sidebar;
border-left: 1px solid #d3d3d3;
@include border-radius(0 4px 4px 0);
border-right: 1px solid #f6f6f6;
@include box-shadow(inset 1px 0 0 #f6f6f6);
padding: lh();
border-left: 1px solid $border-color;
border-right: 0;
width: flex-grid(3);
&:after {
left: -1px;
right: auto;
}
&.main-sidebar {
min-width:200px;
}
h1 {
@extend .bottom-border;
margin: (-(lh())) (-(lh())) 0;
padding: lh(.5) lh();
margin-bottom: em(16, 20);
}
h2 {
color: #4D4D4D;
color: #3C3C3C;
font-size: 1em;
font-style: normal;
font-weight: bold;
margin-bottom: 1em;
&.first {
margin-top: 0px;
@@ -36,6 +42,9 @@ div.discussion-wrapper aside {
input[type="submit"] {
width: 27%;
float: right;
text-align: center;
padding-left: 0;
padding-right: 0;
}
input[type="text"] {
@@ -45,24 +54,30 @@ div.discussion-wrapper aside {
div.box {
display: block;
margin: lh(.5) 0;
padding: lh(.5) lh();
border-top: 1px solid lighten($border-color, 10%);
&:last-child {
@include box-shadow(none);
border: 0;
&:first-child {
border-top: 0;
}
h2 {
text-transform: uppercase;
font-weight: bold;
font-size: 14px;
letter-spacing: 1px;
ul#related-tags {
position: relative;
left: -10px;
&:not(.first) {
@include box-shadow(inset 0 1px 0 #eee);
border-top: 1px solid #d3d3d3;
margin: 0 (-(lh())) 0;
padding: lh(.5) lh();
li {
border-bottom: 0;
background: #eee;
padding: 6px 10px 6px 5px;
a {
padding: 0;
line-height: 12px;
&:hover {
background: transparent;
}
}
}
}
@@ -85,9 +100,6 @@ div.discussion-wrapper aside {
}
}
img.gravatar {
@include border-radius(3px);
}
}
&.tag-selector {
@@ -100,17 +112,19 @@ div.discussion-wrapper aside {
div.search-box {
margin-top: lh(.5);
input {
@include box-sizing(border-box);
display: inline;
}
input[type='submit'] {
@include box-shadow(none);
opacity: 0.5;
background: url(../images/askbot/search-icon.png) no-repeat center;
border: 0;
@include box-shadow(none);
margin-left: 3px;
opacity: 0.5;
padding: 6px 0 0;
position: absolute;
text-indent: -9999px;
width: 24px;
@@ -131,30 +145,26 @@ div.discussion-wrapper aside {
}
input#clear {
@include box-shadow(none);
@include border-radius(15px);
background: none;
border: none;
background: #bbb;
color: #fff;
@include border-radius(0);
@include box-shadow(none);
color: #999;
display: inline;
font-size: 10px;
margin-left: -25px;
font-size: 12px;
font-weight: bold;
height: 19px;
line-height: 1em;
margin: {
left: -25px;
top: 8px;
}
padding: 2px 5px;
text-shadow: none;
}
}
div#tagSelector {
h2 {
@include box-shadow(inset 0 1px 0 #eee);
border-top: 1px solid #d3d3d3;
margin: 0 (-(lh())) 0;
padding: lh(.5) lh();
text-transform: uppercase;
font-weight: bold;
font-size: 14px;
letter-spacing: 1px;
}
ul {
margin: 0;
}
@@ -167,11 +177,17 @@ div.discussion-wrapper aside {
p.choice {
@include inline-block();
margin-right: lh(.5);
margin-top: 0;
}
}
label {
font-style: normal;
font-weight: 400;
}
}
// Question view sopecific
// Question view specific
div.follow-buttons {
margin-top: 20px;
@@ -187,12 +203,15 @@ div.discussion-wrapper aside {
div.question-stats {
border-top: 0;
ul {
color: #777;
list-style: none;
li {
padding: 7px 0 0;
border: 0;
&:last-child {
@include box-shadow(none);
@@ -216,19 +235,20 @@ div.discussion-wrapper aside {
}
div.karma {
background: #eee;
border: 1px solid #D3D3D3;
@include border-radius(3px);
border: 1px solid $border-color;
@include box-sizing(border-box);
@include box-shadow(inset 0 0 0 1px #fff, 0 1px 0 #fff);
padding: lh(.4) 0;
text-align: center;
width: flex-grid(1, 3);
float: right;
strong {
display: block;
font-style: 20px;
p {
text-align: center;
strong {
display: block;
font-style: 20px;
}
}
}
@@ -255,8 +275,6 @@ div.discussion-wrapper aside {
overflow: visible;
ul {
font-size: 14px;
h2 {
margin:0 (-(lh())) 5px (-(lh()));
padding: lh(.5) lh();
@@ -265,40 +283,29 @@ div.discussion-wrapper aside {
}
div.question-tips, div.markdown {
ul {
margin-left: 8%;
}
ul,
ol {
margin-left: 8%;
}
}
div.markdown ul li {
margin: 20px 0;
&:first-child {
margin: 0;
}
padding: 0;
ol li {
margin: 0;
li {
border-bottom: 0;
line-height: lh();
margin-bottom: em(8);
}
}
}
div.view-profile {
h2 {
border-top: 0;
@include box-shadow(none);
}
border-top: 0;
a {
width: 100%;
@include box-sizing(border-box);
text-align: center;
padding: 10px;
display: block;
margin-top: 10px;
@extend .light-button;
@include box-sizing(border-box);
display: block;
text-align: center;
width: 100%;
margin-top: lh(.5);
&:first-child {
margin-top: 0;

View File

@@ -3,6 +3,7 @@
ul.tags {
list-style: none;
display: inline;
padding: 0;
li, a {
position: relative;
@@ -10,19 +11,17 @@ ul.tags {
li {
background: #eee;
@include border-radius(4px);
@include box-shadow(0px 1px 0px #ccc);
color: #555;
display: inline-block;
font-size: 12px;
margin-bottom: 5px;
margin-left: 15px;
padding: 3px 10px 5px 5px;
padding: 6px 10px 6px 5px;
&:before {
border-color:transparent #eee transparent transparent;
border-style:solid;
border-width:12px 12px 12px 0;
border-width:12px 10px 12px 0;
content:"";
height:0;
left:-10px;
@@ -31,25 +30,6 @@ ul.tags {
width:0;
}
span.delete-icon, div.delete-icon {
background: #555;
@include border-radius(0 4px 4px 0);
clear: none;
color: #eee;
cursor: pointer;
display: inline;
float: none;
left: 10px;
opacity: 0.5;
padding: 4px 6px;
position: relative;
top: 1px;
&:hover {
opacity: 1;
}
}
a {
color: #555;
text-decoration: none;
@@ -61,11 +41,4 @@ ul.tags {
span.tag-number {
display: none;
// @include border-radius(3px);
// background: #555;
// font-size: 10px;
// margin: 0 3px;
// padding: 2px 5px;
// color: #eee;
// opacity: 0.5;
}

View File

@@ -42,6 +42,7 @@ textarea {
input[type="submit"],
input[type="button"],
button,
.button {
@include border-radius(3px);
@include button(shiny, $blue);

View File

@@ -21,8 +21,10 @@ def url_class(url):
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
% if user.is_authenticated():
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li>
% endif
% for index, textbook in enumerate(course.textbooks):
<li class="book"><a href="${reverse('book', args=[course.id, index])}" class="${url_class('book')}">${textbook.title}</a></li>
% endfor
% endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li>
% endif
@@ -33,7 +35,7 @@ def url_class(url):
% if user.is_authenticated():
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
% endif
% if has_access(user, course, 'staff'):
% if staff_access:
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif

View File

@@ -1,7 +1,7 @@
<%namespace name='static' file='static_content.html'/>
<h2 class="problem-header">
${ problem['name'] }
% if problem['weight'] != 1:
% if problem['weight'] != 1 and problem['weight'] != None:
: ${ problem['weight'] } points
% endif
</h2>

View File

@@ -126,9 +126,9 @@ if settings.COURSEWARE_ENABLED:
#Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
'courseware.views.course_info', name="info"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$',
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/$',
'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$',
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<book_index>[^/]*)/(?P<page>[^/]*)$',
'staticbook.views.index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
'staticbook.views.index_shifted'),