Commit c768975e by Victor Shnayder

merge

parents 8ae1f207 e89a1ba7
......@@ -3,27 +3,41 @@ A tiny app that checks for a status message.
"""
from django.conf import settings
import json
import logging
import os
import sys
log = logging.getLogger(__name__)
def get_site_status_msg():
def get_site_status_msg(course_id):
"""
Look for a file settings.STATUS_MESSAGE_PATH. If found, return the
contents. Otherwise, return None.
Look for a file settings.STATUS_MESSAGE_PATH. If found, read it,
parse as json, and do the following:
* if there is a key 'global', include that in the result list.
* if course is not None, and there is a key for course.id, add that to the result list.
* return "<br/>".join(result)
Otherwise, return None.
If something goes wrong, returns None. ("is there a status msg?" logic is
not allowed to break the entire site).
"""
try:
content = None
if os.path.isfile(settings.STATUS_MESSAGE_PATH):
with open(settings.STATUS_MESSAGE_PATH) as f:
content = f.read()
else:
return None
status_dict = json.loads(content)
msg = status_dict.get('global', None)
if course_id in status_dict:
msg = msg + "<br>" if msg else ''
msg += status_dict[course_id]
return content
return msg
except:
log.exception("Error while getting a status message.")
return None
from django.conf import settings
from django.test import TestCase
import os
from override_settings import override_settings
from tempfile import NamedTemporaryFile
from status import get_site_status_msg
# Get a name where we can put test files
TMP_FILE = NamedTemporaryFile(delete=False)
TMP_NAME = TMP_FILE.name
# Close it--we just want the path.
TMP_FILE.close()
@override_settings(STATUS_MESSAGE_PATH=TMP_NAME)
class TestStatus(TestCase):
"""Test that the get_site_status_msg function does the right thing"""
no_file = None
invalid_json = """{
"global" : "Hello, Globe",
}"""
global_only = """{
"global" : "Hello, Globe"
}"""
toy_only = """{
"edX/toy/2012_Fall" : "A toy story"
}"""
global_and_toy = """{
"global" : "Hello, Globe",
"edX/toy/2012_Fall" : "A toy story"
}"""
# json to use, expected results for course=None (e.g. homepage),
# for toy course, for full course. Note that get_site_status_msg
# is supposed to return global message even if course=None. The
# template just happens to not display it outside the courseware
# at the moment...
checks = [
(no_file, None, None, None),
(invalid_json, None, None, None),
(global_only, "Hello, Globe", "Hello, Globe", "Hello, Globe"),
(toy_only, None, "A toy story", None),
(global_and_toy, "Hello, Globe", "Hello, Globe<br>A toy story", "Hello, Globe"),
]
def setUp(self):
"""
Fake course ids, since we don't have to have full django
settings (common tests run without the lms settings imported)
"""
self.full_id = 'edX/full/2012_Fall'
self.toy_id = 'edX/toy/2012_Fall'
def create_status_file(self, contents):
"""
Write contents to settings.STATUS_MESSAGE_PATH.
"""
with open(settings.STATUS_MESSAGE_PATH, 'w') as f:
f.write(contents)
def remove_status_file(self):
"""Delete the status file if it exists"""
if os.path.exists(settings.STATUS_MESSAGE_PATH):
os.remove(settings.STATUS_MESSAGE_PATH)
def tearDown(self):
self.remove_status_file()
def test_get_site_status_msg(self):
"""run the tests"""
for (json_str, exp_none, exp_toy, exp_full) in self.checks:
self.remove_status_file()
if json_str:
self.create_status_file(json_str)
print "checking results for {0}".format(json_str)
print "course=None:"
self.assertEqual(get_site_status_msg(None), exp_none)
print "course=toy:"
self.assertEqual(get_site_status_msg(self.toy_id), exp_toy)
print "course=full:"
self.assertEqual(get_site_status_msg(self.full_id), exp_full)
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
import re
class Command(BaseCommand):
args = '<user/email user/email ...>'
help = """
This command will set isstaff to true for one or more users.
Lookup by username or email address, assumes usernames
do not look like email addresses.
"""
def handle(self, *args, **kwargs):
if len(args) < 1:
print Command.help
return
for user in args:
if re.match('[^@]+@[^@]+\.[^@]+', user):
try:
v = User.objects.get(email=user)
except:
raise CommandError("User {0} does not exist".format(
user))
else:
try:
v = User.objects.get(username=user)
except:
raise CommandError("User {0} does not exist".format(
user))
v.is_staff = True
v.save()
......@@ -30,6 +30,8 @@ import sys
from lxml import etree
from xml.sax.saxutils import unescape
import chem
import chem.chemcalc
import calc
from correctmap import CorrectMap
import eia
......@@ -54,7 +56,8 @@ entry_types = ['textline',
'checkboxgroup',
'filesubmission',
'javascriptinput',
'crystallography',]
'crystallography',
'chemicalequationinput',]
# extra things displayed after "show answers" is pressed
solution_types = ['solution']
......@@ -73,7 +76,8 @@ global_context = {'random': random,
'math': math,
'scipy': scipy,
'calc': calc,
'eia': eia}
'eia': eia,
'chemcalc': chem.chemcalc}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
......@@ -437,7 +441,7 @@ class LoncapaProblem(object):
sys.path = original_path + self._extract_system_path(script)
stype = script.get('type')
if stype:
if 'javascript' in stype:
continue # skip javascript
......@@ -479,8 +483,8 @@ class LoncapaProblem(object):
problemid = problemtree.get('id') # my ID
if problemtree.tag in inputtypes.get_input_xml_tags():
if problemtree.tag in inputtypes.registered_input_tags():
# If this is an inputtype subtree, let it render itself.
status = "unsubmitted"
msg = ''
hint = ''
......@@ -497,20 +501,17 @@ class LoncapaProblem(object):
value = self.student_answers[problemid]
# do the rendering
render_object = inputtypes.SimpleInput(system=self.system,
xml=problemtree,
state={'value': value,
'status': status,
'id': problemtree.get('id'),
'feedback': {'message': msg,
'hint': hint,
'hintmode': hintmode,
}
},
use='capa_input')
# function(problemtree, value, status, msg)
# render the special response (textline, schematic,...)
return render_object.get_html()
state = {'value': value,
'status': status,
'id': problemtree.get('id'),
'feedback': {'message': msg,
'hint': hint,
'hintmode': hintmode,}}
input_type_cls = inputtypes.get_class_for_tag(problemtree.tag)
the_input = input_type_cls(self.system, problemtree, state)
return the_input.get_html()
# let each Response render itself
if problemtree in self.responders:
......
......@@ -746,7 +746,7 @@ class NumericalResponse(LoncapaResponse):
id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
except Exception:
self.tolerance = 0
self.tolerance = '0'
try:
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
......@@ -756,15 +756,26 @@ class NumericalResponse(LoncapaResponse):
def get_score(self, student_answers):
'''Grade a numeric response '''
student_answer = student_answers[self.answer_id]
try:
correct_ans = complex(self.correct_answer)
except ValueError:
log.debug("Content error--answer '{0}' is not a valid complex number".format(self.correct_answer))
raise StudentInputError("There was a problem with the staff answer to this problem")
try:
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer),
complex(self.correct_answer), self.tolerance)
correct_ans, self.tolerance)
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
except:
raise StudentInputError("Invalid input: could not interpret '%s' as a number" %
cgi.escape(student_answer))
# Use the traceback-preserving version of re-raising with a different type
import sys
type, value, traceback = sys.exc_info()
raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" %
cgi.escape(student_answer)), traceback
if correct:
return CorrectMap(self.answer_id, 'correct')
......@@ -856,7 +867,7 @@ def sympy_check2():
</customresponse>"""}]
response_tag = 'customresponse'
allowed_inputfields = ['textline', 'textbox', 'crystallography']
allowed_inputfields = ['textline', 'textbox', 'crystallography', 'chemicalequationinput']
def setup_response(self):
xml = self.xml
......
<section id="chemicalequationinput_${id}" class="chemicalequationinput">
<div class="script_placeholder" data-src="${previewer}"/>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if size:
size="${size}"
% endif
/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<div class="equation">
</div>
<p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
import fs
import fs.osfs
import os
from mock import Mock
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=Mock(),
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id = 'student'
)
"""
Tests of input types (and actually responsetypes too)
"""
from datetime import datetime
import json
from mock import Mock
from nose.plugins.skip import SkipTest
import os
import unittest
from . import test_system
from capa import inputtypes
from lxml import etree
def tst_render_template(template, context):
"""
A test version of render to template. Renders to the repr of the context, completely ignoring the template name.
"""
return repr(context)
system = Mock(render_template=tst_render_template)
class OptionInputTest(unittest.TestCase):
'''
Make sure option inputs work
'''
def test_rendering_new(self):
xml = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
element = etree.fromstring(xml)
value = 'Down'
status = 'answered'
context = inputtypes._optioninput(element, value, status, test_system.render_template)
print 'context: ', context
expected = {'value': 'Down',
'options': [('Up', 'Up'), ('Down', 'Down')],
'state': 'answered',
'msg': '',
'inline': '',
'id': 'sky_input'}
self.assertEqual(context, expected)
def test_rendering(self):
xml_str = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
element = etree.fromstring(xml_str)
state = {'value': 'Down',
'id': 'sky_input',
'status': 'answered'}
option_input = inputtypes.OptionInput(system, element, state)
context = option_input._get_render_context()
expected = {'value': 'Down',
'options': [('Up', 'Up'), ('Down', 'Down')],
'state': 'answered',
'msg': '',
'inline': '',
'id': 'sky_input'}
self.assertEqual(context, expected)
......@@ -11,7 +11,7 @@ def compare_with_tolerance(v1, v2, tol):
- v1 : student result (number)
- v2 : instructor result (number)
- tol : tolerance (string or number)
- tol : tolerance (string representing a number)
'''
relative = tol.endswith('%')
......
......@@ -133,6 +133,11 @@ class CapaModule(XModule):
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
# TODO: This line is badly broken:
# (1) We're passing student ID to xmodule.
# (2) There aren't bins of students. -- we only want 10 or 20 randomizations, and want to assign students
# to these bins, and may not want cohorts. So e.g. hash(your-id, problem_id) % num_bins.
# - analytics really needs small number of bins.
self.seed = system.id
else:
self.seed = None
......
......@@ -572,7 +572,7 @@ section.problem {
}
}
section {
> section {
padding: 9px;
}
}
......
......@@ -11,7 +11,7 @@ class @Collapsible
###
el.find('.longform').hide()
el.find('.shortform').append('<a href="#" class="full">See full output</a>')
el.find('.collapsible section').hide()
el.find('.collapsible header + section').hide()
el.find('.full').click @toggleFull
el.find('.collapsible header a').click @toggleHint
......
......@@ -15,7 +15,7 @@ class @JavascriptLoader
placeholders = el.find(".script_placeholder")
if placeholders.length == 0
callback()
callback() if callback?
return
# TODO: Verify the execution order of multiple placeholders
......
"""Module progress tests"""
import unittest
from xmodule.progress import Progress
from xmodule import x_module
from . import i4xs
class ProgressTest(unittest.TestCase):
''' Test that basic Progress objects work. A Progress represents a
fraction between 0 and 1.
'''
not_started = Progress(0, 17)
part_done = Progress(2, 6)
half_done = Progress(3, 6)
also_half_done = Progress(1, 2)
done = Progress(7, 7)
def test_create_object(self):
# These should work:
p = Progress(0, 2)
p = Progress(1, 2)
p = Progress(2, 2)
p = Progress(2.5, 5.0)
p = Progress(3.7, 12.3333)
# These shouldn't
self.assertRaises(ValueError, Progress, 0, 0)
self.assertRaises(ValueError, Progress, 2, 0)
self.assertRaises(ValueError, Progress, 1, -2)
self.assertRaises(TypeError, Progress, 0, "all")
# check complex numbers just for the heck of it :)
self.assertRaises(TypeError, Progress, 2j, 3)
def test_clamp(self):
self.assertEqual((2, 2), Progress(3, 2).frac())
self.assertEqual((0, 2), Progress(-2, 2).frac())
def test_frac(self):
p = Progress(1, 2)
(a, b) = p.frac()
self.assertEqual(a, 1)
self.assertEqual(b, 2)
def test_percent(self):
self.assertEqual(self.not_started.percent(), 0)
self.assertAlmostEqual(self.part_done.percent(), 33.33333333333333)
self.assertEqual(self.half_done.percent(), 50)
self.assertEqual(self.done.percent(), 100)
self.assertEqual(self.half_done.percent(), self.also_half_done.percent())
def test_started(self):
self.assertFalse(self.not_started.started())
self.assertTrue(self.part_done.started())
self.assertTrue(self.half_done.started())
self.assertTrue(self.done.started())
def test_inprogress(self):
# only true if working on it
self.assertFalse(self.done.inprogress())
self.assertFalse(self.not_started.inprogress())
self.assertTrue(self.part_done.inprogress())
self.assertTrue(self.half_done.inprogress())
def test_done(self):
self.assertTrue(self.done.done())
self.assertFalse(self.half_done.done())
self.assertFalse(self.not_started.done())
def test_str(self):
self.assertEqual(str(self.not_started), "0/17")
self.assertEqual(str(self.part_done), "2/6")
self.assertEqual(str(self.done), "7/7")
def test_ternary_str(self):
self.assertEqual(self.not_started.ternary_str(), "none")
self.assertEqual(self.half_done.ternary_str(), "in_progress")
self.assertEqual(self.done.ternary_str(), "done")
def test_to_js_status(self):
'''Test the Progress.to_js_status_str() method'''
self.assertEqual(Progress.to_js_status_str(self.not_started), "none")
self.assertEqual(Progress.to_js_status_str(self.half_done), "in_progress")
self.assertEqual(Progress.to_js_status_str(self.done), "done")
self.assertEqual(Progress.to_js_status_str(None), "NA")
def test_to_js_detail_str(self):
'''Test the Progress.to_js_detail_str() method'''
f = Progress.to_js_detail_str
for p in (self.not_started, self.half_done, self.done):
self.assertEqual(f(p), str(p))
# But None should be encoded as NA
self.assertEqual(f(None), "NA")
def test_add(self):
'''Test the Progress.add_counts() method'''
p = Progress(0, 2)
p2 = Progress(1, 3)
p3 = Progress(2, 5)
pNone = None
add = lambda a, b: Progress.add_counts(a, b).frac()
self.assertEqual(add(p, p), (0, 4))
self.assertEqual(add(p, p2), (1, 5))
self.assertEqual(add(p2, p3), (3, 8))
self.assertEqual(add(p2, pNone), p2.frac())
self.assertEqual(add(pNone, p2), p2.frac())
def test_equality(self):
'''Test that comparing Progress objects for equality
works correctly.'''
p = Progress(1, 2)
p2 = Progress(2, 4)
p3 = Progress(1, 2)
self.assertTrue(p == p3)
self.assertFalse(p == p2)
# Check != while we're at it
self.assertTrue(p != p2)
self.assertFalse(p != p3)
class ModuleProgressTest(unittest.TestCase):
''' Test that get_progress() does the right thing for the different modules
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(i4xs, 'a://b/c/d/e', None, {})
p = xm.get_progress()
self.assertEqual(p, None)
These files really should be in the capa module, but we don't have a way to load js from there at the moment. (TODO)
(function () {
var preview_div = $('.chemicalequationinput .equation');
$('.chemicalequationinput input').bind("input", function(eventObject) {
$.get("/preview/chemcalc/", {"formula" : this.value}, function(response) {
if (response.error) {
preview_div.html("<span class='error'>" + response.error + "</span>");
} else {
preview_div.html(response.preview);
}
});
});
}).call(this);
......@@ -329,9 +329,15 @@ def progress_summary(student, request, course, student_module_cache):
def get_score(course_id, user, problem_descriptor, module_creator, student_module_cache):
"""
Return the score for a user on a problem, as a tuple (correct, total).
e.g. (5,7) if you got 5 out of 7 points.
If this problem doesn't have a score, or we couldn't load it, returns (None,
None).
user: a Student object
problem: an XModule
problem_descriptor: an XModuleDescriptor
module_creator: a function that takes a descriptor, and returns the corresponding XModule for this user.
Can return None if user doesn't have access, or if something else went wrong.
cache: A StudentModuleCache
"""
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
......@@ -339,14 +345,16 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
return (None, None)
correct = 0.0
instance_module = student_module_cache.lookup(
course_id, problem_descriptor.category, problem_descriptor.location.url())
if not instance_module:
# If the problem was not in the cache, we need to instantiate the problem.
# Otherwise, the max score (cached in instance_module) won't be available
# Otherwise, the max score (cached in instance_module) won't be available
problem = module_creator(problem_descriptor)
if problem is None:
return (None, None)
instance_module = get_instance_module(course_id, user, problem, student_module_cache)
# If this problem is ungraded/ungradable, bail
......@@ -361,7 +369,7 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
weight = getattr(problem_descriptor, 'weight', None)
if weight is not None:
if total == 0:
log.exception("Cannot reweight a problem with zero weight. Problem: " + str(instance_module))
log.exception("Cannot reweight a problem with zero total points. Problem: " + str(instance_module))
return (correct, total)
correct = correct * weight / total
total = weight
......
import hashlib
import json
import logging
import pyparsing
import sys
from django.conf import settings
......@@ -13,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface
from capa.chem import chemcalc
from courseware.access import has_access
from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache
......@@ -471,3 +473,42 @@ def modx_dispatch(request, dispatch, location, course_id):
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
def preview_chemcalc(request):
"""
Render an html preview of a chemical formula or equation. The fact that
this is here is a bit of hack. See the note in lms/urls.py about why it's
here. (Victor is to blame.)
request should be a GET, with a key 'formula' and value 'some formula string'.
Returns a json dictionary:
{
'preview' : 'the-preview-html' or ''
'error' : 'the-error' or ''
}
"""
if request.method != "GET":
raise Http404
result = {'preview': '',
'error': '' }
formula = request.GET.get('formula')
if formula is None:
result['error'] = "No formula specified."
return HttpResponse(json.dumps(result))
try:
result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p:
result['error'] = "Couldn't parse formula: {0}".format(p)
except Exception:
# this is unexpected, so log
log.warning("Error while previewing chemical formula", exc_info=True)
result['error'] = "Error while rendering preview"
return HttpResponse(json.dumps(result))
......@@ -15,6 +15,8 @@ import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from fs.errors import ResourceNotFoundError
from courseware.access import has_access
from static_replace import replace_urls
......@@ -266,7 +268,8 @@ def get_static_tab_contents(course, tab):
try:
with fs.open(p) as tabfile:
# TODO: redundant with module_render.py. Want to be helper methods in static_replace or something.
contents = replace_urls(tabfile.read(), course.metadata['data_dir'])
text = tabfile.read().decode('utf-8')
contents = replace_urls(text, course.metadata['data_dir'])
return replace_urls(contents, staticfiles_prefix='/courses/'+course.id, replace_prefix='/course/')
except (ResourceNotFoundError) as err:
log.exception("Couldn't load tab contents from '{0}': {1}".format(p, err))
......
......@@ -14,6 +14,7 @@ class Command(BaseCommand):
course_id = args[0]
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
......@@ -30,4 +31,7 @@ class Command(BaseCommand):
moderator_role.inherit_permissions(student_role)
# For now, Community TA == Moderator, except for the styling.
community_ta_role.inherit_permissions(moderator_role)
administrator_role.inherit_permissions(moderator_role)
#!/usr/bin/python
#
# django management command: dump grades to csv files
# for use by batch processes
import os, sys, string
import datetime
import json
from instructor.views import *
from courseware.courses import get_course_by_id
from xmodule.modulestore.django import modulestore
from django.conf import settings
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "dump grades to CSV file. Usage: dump_grades course_id_or_dir filename dump_type\n"
help += " course_id_or_dir: either course_id or course_dir\n"
help += " filename: where the output CSV is to be stored\n"
# help += " start_date: end date as M/D/Y H:M (defaults to end of available data)"
help += " dump_type: 'all' or 'raw' (see instructor dashboard)"
def handle(self, *args, **options):
# current grading logic and data schema doesn't handle dates
# datetime.strptime("21/11/06 16:30", "%m/%d/%y %H:%M")
print "args = ", args
course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section'
fn = "grades.csv"
get_raw_scores = False
if len(args)>0:
course_id = args[0]
if len(args)>1:
fn = args[1]
if len(args)>2:
get_raw_scores = args[2].lower()=='raw'
request = self.DummyRequest()
try:
course = get_course_by_id(course_id)
except Exception as err:
if course_id in modulestore().courses:
course = modulestore().courses[course_id]
else:
print "-----------------------------------------------------------------------------"
print "Sorry, cannot find course %s" % course_id
print "Please provide a course ID or course data directory name, eg content-mit-801rq"
return
print "-----------------------------------------------------------------------------"
print "Dumping grades from %s to file %s (get_raw_scores=%s)" % (course.id, fn, get_raw_scores)
datatable = get_student_grade_summary_data(request, course, course.id, get_raw_scores=get_raw_scores)
fp = open(fn,'w')
writer = csv.writer(fp, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
writer.writerow(datatable['header'])
for datarow in datatable['data']:
encoded_row = [unicode(s).encode('utf-8') for s in datarow]
writer.writerow(encoded_row)
fp.close()
print "Done: %d records dumped" % len(datatable['data'])
class DummyRequest(object):
META = {}
def __init__(self):
return
def get_host(self):
return 'edx.mit.edu'
def is_secure(self):
return False
......@@ -129,7 +129,7 @@ NODE_PATH = ':'.join(node_paths)
# Where to look for a status message
STATUS_MESSAGE_PATH = ENV_ROOT / "status_message.html"
STATUS_MESSAGE_PATH = ENV_ROOT / "status_message.json"
############################ OpenID Provider ##################################
OPENID_PROVIDER_TRUSTED_ROOTS = ['cs50.net', '*.cs50.net']
......
......@@ -40,6 +40,8 @@ TEST_ROOT = path("test_root")
# Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles"
STATUS_MESSAGE_PATH = TEST_ROOT / "status_message.json"
COURSES_ROOT = TEST_ROOT / "data"
DATA_DIR = COURSES_ROOT
......@@ -77,6 +79,19 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
]
# point tests at the test courses by default
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': COMMON_TEST_DATA_ROOT,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
}
}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
......
......@@ -25,6 +25,10 @@ class @DiscussionUtil
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
_.include(staff, parseInt(user_id))
@isTA: (user_id) ->
ta = _.union(@roleIds['Community TA'])
_.include(ta, parseInt(user_id))
@bulkUpdateContentInfo: (infos) ->
for id, info of infos
Content.getContent(id).updateInfo(info)
......@@ -157,7 +161,7 @@ class @DiscussionUtil
@makeWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
placeholder = elem.data('placeholder')
id = elem.data("id")
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = @urlFor('upload')
_processor = (_this) ->
......@@ -170,12 +174,12 @@ class @DiscussionUtil
@getWmdEditor: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = elem.data("id")
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
@wmdEditors["#{cls_identifier}-#{id}"]
@getWmdInput: ($content, $local, cls_identifier) ->
elem = $local(".#{cls_identifier}")
id = elem.data("id")
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
$local("#wmd-input-#{cls_identifier}-#{id}")
@getWmdContent: ($content, $local, cls_identifier) ->
......
......@@ -156,7 +156,11 @@ if Backbone?
@$(".post-list").append(view.el)
threadSelected: (e) =>
thread_id = $(e.target).closest("a").data("id")
# Use .attr('data-id') rather than .data('id') because .data does type
# coercion. Usually, this is fine, but when Mongo gives an object id with
# no letters, it casts it to a Number.
thread_id = $(e.target).closest("a").attr("data-id")
@setActiveThread(thread_id)
@trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above...
false
......
......@@ -32,3 +32,5 @@ if Backbone?
markAsStaff: ->
if DiscussionUtil.isStaff(@model.get("user_id"))
@$el.find("a.profile-link").after('<span class="staff-label">staff</span>')
else if DiscussionUtil.isTA(@model.get("user_id"))
@$el.find("a.profile-link").after('<span class="community-ta-label">Community&nbsp;&nbsp;TA</span>')
......@@ -37,6 +37,9 @@ if Backbone?
if DiscussionUtil.isStaff(@model.get("user_id"))
@$el.addClass("staff")
@$el.prepend('<div class="staff-banner">staff</div>')
else if DiscussionUtil.isTA(@model.get("user_id"))
@$el.addClass("community-ta")
@$el.prepend('<div class="community-ta-banner">Community TA</div>')
toggleVote: (event) ->
event.preventDefault()
......
......@@ -1376,6 +1376,11 @@ body.discussion {
border-color: #009fe2;
}
&.community-ta{
padding-top: 38px;
border-color: #449944;
}
.staff-banner {
position: absolute;
top: 0;
......@@ -1392,6 +1397,23 @@ body.discussion {
text-transform: uppercase;
}
.community-ta-banner{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 14px;
padding: 1px 5px;
@include box-sizing(border-box);
border-radius: 2px 2px 0 0;
background: #449944;
font-size: 9px;
font-weight: 700;
color: #fff;
text-transform: uppercase;
}
&.loading {
height: 0;
margin: 0;
......@@ -1556,11 +1578,11 @@ body.discussion {
}
}
.moderator-label {
.community-ta-label{
margin-left: 2px;
padding: 0 4px;
border-radius: 2px;
background: #55dc9e;
background: #449944;
font-size: 9px;
font-weight: 700;
font-style: normal;
......
......@@ -65,16 +65,19 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph")
%endif
</p>
%if len(section['scores']) > 0:
<section class="scores">
<section class="scores">
%if len(section['scores']) > 0:
<h3> ${ "Problem Scores: " if section['graded'] else "Practice Scores: "} </h3>
<ol>
%for score in section['scores']:
<li>${"{0:.3n}/{1:.3n}".format(float(score.earned),float(score.possible))}</li>
%endfor
</ol>
</section>
%endif
%else:
<h3 class="no-scores"> No problem scores in this section </h3>
%endif
</section>
</li> <!--End section-->
%endfor
......
......@@ -14,7 +14,12 @@ from status.status import get_site_status_msg
<%block cached="False">
<%
site_status_msg = get_site_status_msg()
try:
course_id = course.id
except:
# can't figure out a better way to get at a possibly-defined course var
course_id = None
site_status_msg = get_site_status_msg(course_id)
%>
% if site_status_msg:
<div class="site-status">
......
......@@ -141,6 +141,16 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.modx_dispatch',
name='modx_dispatch'),
# TODO (vshnayder): This is a hack. It creates a direct connection from
# the LMS to capa functionality, and really wants to go through the
# input types system so that previews can be context-specific.
# Unfortunately, we don't have time to think through the right way to do
# that (and implement it), and it's not a terrible thing to provide a
# generic chemican-equation rendering service.
url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc',
name='preview_chemcalc'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.xqueue_callback',
name='xqueue_callback'),
......@@ -244,7 +254,7 @@ if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
if settings.MITX_FEATURES.get('AUTH_USE_OPENID_PROVIDER'):
urlpatterns += (
url(r'^openid/provider/login/$', 'external_auth.views.provider_login', name='openid-provider-login'),
url(r'^openid/provider/login/(?:[\w%\. ]+)$', 'external_auth.views.provider_identity', name='openid-provider-login-identity'),
url(r'^openid/provider/login/(?:.+)$', 'external_auth.views.provider_identity', name='openid-provider-login-identity'),
url(r'^openid/provider/identity/$', 'external_auth.views.provider_identity', name='openid-provider-identity'),
url(r'^openid/provider/xrds/$', 'external_auth.views.provider_xrds', name='openid-provider-xrds')
)
......
......@@ -171,6 +171,12 @@ task "django-admin", [:action, :system, :env, :options] => [:predjango] do |t, a
sh(django_admin(args.system, args.env, args.action, args.options))
end
desc "Set the staff bit for a user"
task :set_staff, [:user, :system, :env] do |t, args|
args.with_defaults(:env => 'dev', :system => 'lms', :options => '')
sh(django_admin(args.system, args.env, 'set_staff', args.user))
end
task :package do
FileUtils.mkdir_p(BUILD_DIR)
......
......@@ -49,3 +49,4 @@ networkx
pygraphviz
-r repo-requirements.txt
pil
nltk
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