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