Commit 491f10d1 by David Ormsbee

Merge branch 'master' into feature/dave/email_bugfixes

parents 3385e063 4cadbcc5
......@@ -293,6 +293,8 @@ def add_openid_simple_registration(request, response, data):
sreg_data['email'] = data['email']
elif field == 'fullname' and 'fullname' in data:
sreg_data['fullname'] = data['fullname']
elif field == 'nickname' and 'nickname' in data:
sreg_data['nickname'] = data['nickname']
# construct sreg response
sreg_response = sreg.SRegResponse.extractResponse(sreg_request,
......@@ -486,13 +488,22 @@ def provider_login(request):
url = endpoint + urlquote(user.username)
response = openid_request.answer(True, None, url)
return provider_respond(server,
openid_request,
response,
{
'fullname': profile.name,
'email': user.email
})
# TODO: for CS50 we are forcibly returning the username
# instead of fullname. In the OpenID simple registration
# extension, we don't have to return any fields we don't
# want to, even if they were marked as required by the
# Consumer. The behavior of what to do when there are
# missing fields is up to the Consumer. The proper change
# should only return the username, however this will likely
# break the CS50 client. Temporarily we will be returning
# username filling in for fullname in addition to username
# as sreg nickname.
results = {
'nickname': user.username,
'email': user.email,
'fullname': user.username
}
return provider_respond(server, openid_request, response, results)
request.session['openid_error'] = True
msg = "Login failed - Account not active for user {0}".format(username)
......
......@@ -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
......@@ -53,7 +55,9 @@ entry_types = ['textline',
'radiogroup',
'checkboxgroup',
'filesubmission',
'javascriptinput',]
'javascriptinput',
'crystallography',
'chemicalequationinput',]
# extra things displayed after "show answers" is pressed
solution_types = ['solution']
......@@ -72,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"]
......@@ -436,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
......@@ -478,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 = ''
......@@ -496,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']
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 id="input_${id}_preview" class="equation">
</div>
<p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
<% doinline = "inline" if inline else "" %>
<section id="textinput_${id}" class="textinput ${doinline}" >
<div id="holder" style="width:${width};height:${height}"></div>
<div class="script_placeholder" data-src="/static/js/raphael.js"></div><div class="script_placeholder" data-src="/static/js/sylvester.js"></div><div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
% if state == 'unsubmitted':
<div class="unanswered ${doinline}" id="status_${id}">
% elif state == 'correct':
<div class="correct ${doinline}" id="status_${id}">
% elif state == 'incorrect':
<div class="incorrect ${doinline}" id="status_${id}">
% elif state == 'incomplete':
<div class="incorrect ${doinline}" id="status_${id}">
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
% if hidden:
style="display:none;"
% endif
/>
<p class="status">
% if state == 'unsubmitted':
unanswered
% elif state == 'correct':
correct
% elif state == 'incorrect':
incorrect
% elif state == 'incomplete':
incomplete
% endif
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${msg|n}</span>
% endif
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
</div>
% endif
</section>
......@@ -5,8 +5,6 @@
% endif
>${value|h}</textarea>
<span id="answer_${id}"></span>
<div class="grader-status">
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
......@@ -26,6 +24,8 @@
<p class="debug">${state}</p>
</div>
<span id="answer_${id}"></span>
<div class="external-grader-message">
${msg|n}
</div>
......
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
......@@ -619,12 +624,14 @@ class CapaModule(XModule):
if self.closed():
event_info['failure'] = 'closed'
self.system.track_function('reset_problem_fail', event_info)
return "Problem is closed"
return {'success': False,
'error': "Problem is closed"}
if not self.lcp.done:
event_info['failure'] = 'not_done'
self.system.track_function('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting."
return {'success': False,
'error': "Refresh the page and make an attempt before resetting."}
self.lcp.do_reset()
if self.rerandomize in ["always", "onreset"]:
......
......@@ -572,7 +572,7 @@ section.problem {
}
}
section {
> section {
padding: 9px;
}
}
......
......@@ -334,10 +334,13 @@ class @Problem
inputtypeShowAnswerMethods:
choicegroup: (element, display, answers) =>
element = $(element)
for key, value of answers
element.find('input').attr('disabled', 'disabled')
for choice in value
element.find("label[for='input_#{key}_#{choice}']").addClass 'choicegroup_correct'
element.find('input').attr('disabled', 'disabled')
input_id = element.attr('id').replace(/inputtype_/,'')
answer = answers[input_id]
for choice in answer
element.find("label[for='input_#{input_id}_#{choice}']").addClass 'choicegroup_correct'
javascriptinput: (element, display, answers) =>
answer_id = $(element).attr('id').split("_")[1...].join("_")
......
......@@ -11,11 +11,12 @@ 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
@toggleFull: (event) =>
event.preventDefault()
$(event.target).parent().siblings().slideToggle()
$(event.target).parent().parent().toggleClass('open')
if $(event.target).text() == 'See full output'
......
......@@ -41,20 +41,30 @@ class @JavascriptLoader
callbackCalled = true
callback() if callback?
# Keep a map of what sources we're loaded from, and don't do it twice.
loaded = {}
placeholders.each (index, placeholder) ->
# TODO: Check if the script already exists in DOM. If so, (1) copy it
# into memory; (2) delete the DOM script element; (3) reappend it.
# This would prevent memory bloat and save a network request.
s = document.createElement('script')
s.setAttribute('src', $(placeholder).attr("data-src"))
s.setAttribute('type', "text/javascript")
src = $(placeholder).attr("data-src")
if src not of loaded
loaded[src] = true
s = document.createElement('script')
s.setAttribute('src', src)
s.setAttribute('type', "text/javascript")
s.onload = completionHandlerGenerator(index)
s.onload = completionHandlerGenerator(index)
# s.onload does not fire in IE8; this does.
s.onreadystatechange = completionHandlerGeneratorIE(index)
# s.onload does not fire in IE8; this does.
s.onreadystatechange = completionHandlerGeneratorIE(index)
# Need to use the DOM elements directly or the scripts won't execute
# properly.
$('head')[0].appendChild(s)
# Need to use the DOM elements directly or the scripts won't execute
# properly.
$('head')[0].appendChild(s)
else
# just call the completion callback directly, without reloading the file
completionHandlerGenerator(index)()
$(placeholder).remove()
"""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 () {
update = function() {
function create_handler(saved_div) {
return (function(response) {
if (response.error) {
saved_div.html("<span class='error'>" + response.error + "</span>");
} else {
saved_div.html(response.preview);
}
});
}
prev_id = "#" + this.id + "_preview";
preview_div = $(prev_id)
$.get("/preview/chemcalc/", {"formula" : this.value}, create_handler(preview_div));
}
inputs = $('.chemicalequationinput input');
// update on load
inputs.each(update);
// and on every change
inputs.bind("input", update);
}).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))
......
......@@ -356,7 +356,7 @@ def static_tab(request, course_id, tab_slug):
tab = tabs.get_static_tab_by_slug(course, tab_slug)
if tab is None:
raise Http404
contents = tabs.get_static_tab_contents(course, tab)
if contents is None:
raise Http404
......@@ -415,6 +415,16 @@ def course_about(request, course_id):
@ensure_csrf_cookie
@cache_if_anonymous
def static_university_profile(request, org_id):
"""
Return the profile for the particular org_id that does not have any courses.
"""
template_file = "university_profile/{0}.html".format(org_id).lower()
context = dict(courses=[], org_id=org_id)
return render_to_response(template_file, context)
@ensure_csrf_cookie
@cache_if_anonymous
def university_profile(request, org_id):
"""
Return the profile for the particular org_id. 404 if it's not valid.
......@@ -485,7 +495,7 @@ def progress(request, course_id, student_id=None):
courseware_summary = grades.progress_summary(student, request, course,
student_module_cache)
grade_summary = grades.grade(student, request, course, student_module_cache)
if courseware_summary is None:
#This means the student didn't have access to the course (which the instructor requested)
raise Http404
......@@ -498,4 +508,3 @@ def progress(request, course_id, student_id=None):
context.update()
return render_to_response('courseware/progress.html', context)
......@@ -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
import os.path
from uuid import uuid4
from optparse import make_option
from django.utils.html import escape
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from licenses.models import CourseSoftware, UserLicense
class Command(BaseCommand):
help = """Generate random serial numbers for software used in a course.
Usage: generate_serial_numbers <course_id> <software_name> <count>
<count> is the number of numbers to generate.
Example:
import_serial_numbers MITx/6.002x/2012_Fall matlab 100
"""
args = "course_id software_id count"
def handle(self, *args, **options):
"""
"""
course_id, software_name, count = self._parse_arguments(args)
software, _ = CourseSoftware.objects.get_or_create(course_id=course_id,
name=software_name)
self._generate_serials(software, count)
def _parse_arguments(self, args):
if len(args) != 3:
raise CommandError("Incorrect number of arguments")
course_id = args[0]
courses = modulestore().get_courses()
known_course_ids = set(c.id for c in courses)
if course_id not in known_course_ids:
raise CommandError("Unknown course_id")
software_name = escape(args[1].lower())
try:
count = int(args[2])
except ValueError:
raise CommandError("Invalid <count> argument.")
return course_id, software_name, count
def _generate_serials(self, software, count):
print "Generating {0} serials".format(count)
# add serial numbers them to the database
for _ in xrange(count):
serial = str(uuid4())
license = UserLicense(software=software, serial=serial)
license.save()
print "{0} new serial numbers generated.".format(count)
import os.path
from optparse import make_option
from django.utils.html import escape
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from licenses.models import CourseSoftware, UserLicense
class Command(BaseCommand):
help = """Imports serial numbers for software used in a course.
Usage: import_serial_numbers <course_id> <software_name> <file>
<file> is a text file that list one available serial number per line.
Example:
import_serial_numbers MITx/6.002x/2012_Fall matlab serials.txt
"""
args = "course_id software_id serial_file"
def handle(self, *args, **options):
"""
"""
course_id, software_name, filename = self._parse_arguments(args)
software, _ = CourseSoftware.objects.get_or_create(course_id=course_id,
name=software_name)
self._import_serials(software, filename)
def _parse_arguments(self, args):
if len(args) != 3:
raise CommandError("Incorrect number of arguments")
course_id = args[0]
courses = modulestore().get_courses()
known_course_ids = set(c.id for c in courses)
if course_id not in known_course_ids:
raise CommandError("Unknown course_id")
software_name = escape(args[1].lower())
filename = os.path.abspath(args[2])
if not os.path.exists(filename):
raise CommandError("Cannot find filename {0}".format(filename))
return course_id, software_name, filename
def _import_serials(self, software, filename):
print "Importing serial numbers for {0}.".format(software)
serials = set(unicode(l.strip()) for l in open(filename))
# remove serial numbers we already have
licenses = UserLicense.objects.filter(software=software)
known_serials = set(l.serial for l in licenses)
if known_serials:
serials = serials.difference(known_serials)
# add serial numbers them to the database
for serial in serials:
license = UserLicense(software=software, serial=serial)
license.save()
print "{0} new serial numbers imported.".format(len(serials))
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CourseSoftware'
db.create_table('licenses_coursesoftware', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('full_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
('url', self.gf('django.db.models.fields.CharField')(max_length=255)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('licenses', ['CourseSoftware'])
# Adding model 'UserLicense'
db.create_table('licenses_userlicense', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('software', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['licenses.CourseSoftware'])),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True)),
('serial', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal('licenses', ['UserLicense'])
def backwards(self, orm):
# Deleting model 'CourseSoftware'
db.delete_table('licenses_coursesoftware')
# Deleting model 'UserLicense'
db.delete_table('licenses_userlicense')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'licenses.coursesoftware': {
'Meta': {'object_name': 'CourseSoftware'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'full_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'licenses.userlicense': {
'Meta': {'object_name': 'UserLicense'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'serial': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'software': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['licenses.CourseSoftware']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
}
}
complete_apps = ['licenses']
\ No newline at end of file
import logging
from django.db import models, transaction
from student.models import User
log = logging.getLogger("mitx.licenses")
class CourseSoftware(models.Model):
name = models.CharField(max_length=255)
full_name = models.CharField(max_length=255)
url = models.CharField(max_length=255)
course_id = models.CharField(max_length=255)
def __unicode__(self):
return u'{0} for {1}'.format(self.name, self.course_id)
class UserLicense(models.Model):
software = models.ForeignKey(CourseSoftware, db_index=True)
user = models.ForeignKey(User, null=True)
serial = models.CharField(max_length=255)
def get_courses_licenses(user, courses):
course_ids = set(course.id for course in courses)
all_software = CourseSoftware.objects.filter(course_id__in=course_ids)
assigned_licenses = UserLicense.objects.filter(software__in=all_software,
user=user)
licenses = dict.fromkeys(all_software, None)
for license in assigned_licenses:
licenses[license.software] = license
log.info(assigned_licenses)
log.info(licenses)
return licenses
def get_license(user, software):
try:
license = UserLicense.objects.get(user=user, software=software)
except UserLicense.DoesNotExist:
license = None
return license
def get_or_create_license(user, software):
license = get_license(user, software)
if license is None:
license = _create_license(user, software)
return license
def _create_license(user, software):
license = None
try:
# find one license that has not been assigned, locking the
# table/rows with select_for_update to prevent race conditions
with transaction.commit_on_success():
selected = UserLicense.objects.select_for_update()
license = selected.filter(user__isnull=True, software=software)[0]
license.user = user
license.save()
except IndexError:
# there are no free licenses
log.error('No serial numbers available for {0}', software)
license = None
# TODO [rocha]look if someone has unenrolled from the class
# and already has a serial number
return license
import logging
from uuid import uuid4
from random import shuffle
from tempfile import NamedTemporaryFile
from django.test import TestCase
from django.core.management import call_command
from models import CourseSoftware, UserLicense
COURSE_1 = 'MITx/6.002x/2012_Fall'
SOFTWARE_1 = 'matlab'
SOFTWARE_2 = 'stata'
log = logging.getLogger(__name__)
class CommandTest(TestCase):
def test_import_serial_numbers(self):
size = 20
log.debug('Adding one set of serials for {0}'.format(SOFTWARE_1))
with generate_serials_file(size) as temp_file:
args = [COURSE_1, SOFTWARE_1, temp_file.name]
call_command('import_serial_numbers', *args)
log.debug('Adding one set of serials for {0}'.format(SOFTWARE_2))
with generate_serials_file(size) as temp_file:
args = [COURSE_1, SOFTWARE_2, temp_file.name]
call_command('import_serial_numbers', *args)
log.debug('There should be only 2 course-software entries')
software_count = CourseSoftware.objects.all().count()
self.assertEqual(2, software_count)
log.debug('We added two sets of {0} serials'.format(size))
licenses_count = UserLicense.objects.all().count()
self.assertEqual(2 * size, licenses_count)
log.debug('Adding more serial numbers to {0}'.format(SOFTWARE_1))
with generate_serials_file(size) as temp_file:
args = [COURSE_1, SOFTWARE_1, temp_file.name]
call_command('import_serial_numbers', *args)
log.debug('There should be still only 2 course-software entries')
software_count = CourseSoftware.objects.all().count()
self.assertEqual(2, software_count)
log.debug('Now we should have 3 sets of 20 serials'.format(size))
licenses_count = UserLicense.objects.all().count()
self.assertEqual(3 * size, licenses_count)
cs = CourseSoftware.objects.get(pk=1)
lics = UserLicense.objects.filter(software=cs)[:size]
known_serials = list(l.serial for l in lics)
known_serials.extend(generate_serials(10))
shuffle(known_serials)
log.debug('Adding some new and old serials to {0}'.format(SOFTWARE_1))
with NamedTemporaryFile() as f:
f.write('\n'.join(known_serials))
f.flush()
args = [COURSE_1, SOFTWARE_1, f.name]
call_command('import_serial_numbers', *args)
log.debug('Check if we added only the new ones')
licenses_count = UserLicense.objects.filter(software=cs).count()
self.assertEqual((2 * size) + 10, licenses_count)
def generate_serials(size=20):
return [str(uuid4()) for _ in range(size)]
def generate_serials_file(size=20):
serials = generate_serials(size)
temp_file = NamedTemporaryFile()
temp_file.write('\n'.join(serials))
temp_file.flush()
return temp_file
import logging
import json
import re
from urlparse import urlparse
from collections import namedtuple, defaultdict
from mitxmako.shortcuts import render_to_string
from django.contrib.auth.models import User
from django.http import HttpResponse, Http404
from django.views.decorators.csrf import requires_csrf_token, csrf_protect
from models import CourseSoftware
from models import get_courses_licenses, get_or_create_license, get_license
log = logging.getLogger("mitx.licenses")
License = namedtuple('License', 'software serial')
def get_licenses_by_course(user, courses):
licenses = get_courses_licenses(user, courses)
licenses_by_course = defaultdict(list)
# create missing licenses and group by course_id
for software, license in licenses.iteritems():
if license is None:
licenses[software] = get_or_create_license(user, software)
course_id = software.course_id
serial = license.serial if license else None
licenses_by_course[course_id].append(License(software, serial))
# render elements
data_by_course = {}
for course_id, licenses in licenses_by_course.iteritems():
context = {'licenses': licenses}
template = 'licenses/serial_numbers.html'
data_by_course[course_id] = render_to_string(template, context)
return data_by_course
@requires_csrf_token
def user_software_license(request):
if request.method != 'POST' or not request.is_ajax():
raise Http404
# get the course id from the referer
url_path = urlparse(request.META.get('HTTP_REFERER', '')).path
pattern = re.compile('^/courses/(?P<id>[^/]+/[^/]+/[^/]+)/.*/?$')
match = re.match(pattern, url_path)
if not match:
raise Http404
course_id = match.groupdict().get('id', '')
user_id = request.session.get('_auth_user_id')
software_name = request.POST.get('software')
generate = request.POST.get('generate', False) == 'true'
try:
software = CourseSoftware.objects.get(name=software_name,
course_id=course_id)
print software
except CourseSoftware.DoesNotExist:
raise Http404
user = User.objects.get(id=user_id)
if generate:
license = get_or_create_license(user, software)
else:
license = get_license(user, software)
if license:
response = {'serial': license.serial}
else:
response = {'error': 'No serial number found'}
return HttpResponse(json.dumps(response), mimetype='application/json')
......@@ -626,6 +626,7 @@ INSTALLED_APPS = (
'certificates',
'instructor',
'psychometrics',
'licenses',
#For the wiki
'wiki', # The new django-wiki from benjaoming
......
......@@ -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
......
......@@ -6,16 +6,25 @@
<link type="text/html" rel="alternate" href="http://blog.edx.org/"/>
##<link type="application/atom+xml" rel="self" href="https://github.com/blog.atom"/>
<title>EdX Blog</title>
<updated>2012-07-16T14:08:12-07:00</updated>
<updated>2012-10-14T14:08:12-07:00</updated>
<entry>
<id>tag:www.edx.org,2012:Post/5</id>
<published>2012-09-25T14:00:00-07:00</published>
<updated>2012-09-25T14:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/elsevier-collaborates-with-edx')}"/>
<title>Elsevier collaborates with edX</title>
<content type="html">&lt;img src=&quot;${static.url('images/press/foundations-of-analog-109x84.jpg')}&quot; /&gt;
&lt;p&gt;Free course textbook made available to edX students&lt;/p&gt;</content>
<id>tag:www.edx.org,2012:Post/6</id>
<published>2012-10-15T14:00:00-07:00</published>
<updated>2012-10-14T14:00:00-07:00</updated>
<link type="text/html" rel="alternate" href="${reverse('press/ut-joins-edx')}"/>
<title>The University of Texas System joins edX</title>
<content type="html">&lt;img src=&quot;${static.url('images/press/uts-seal_109x84.jpg')}&quot; /&gt;
&lt;p&gt;Nine universities and six health institutions&lt;/p&gt;</content>
</entry>
<!-- <entry> -->
<!-- <id>tag:www.edx.org,2012:Post/5</id> -->
<!-- <published>2012-09-25T14:00:00-07:00</published> -->
<!-- <updated>2012-09-25T14:00:00-07:00</updated> -->
<!-- <link type="text/html" rel="alternate" href="${reverse('press/elsevier-collaborates-with-edx')}"/> -->
<!-- <title>Elsevier collaborates with edX</title> -->
<!-- <content type="html">&lt;img src=&quot;${static.url('images/press/foundations-of-analog-109x84.jpg')}&quot; /&gt; -->
<!-- &lt;p&gt;Free course textbook made available to edX students&lt;/p&gt;</content> -->
<!-- </entry> -->
<entry>
<id>tag:www.edx.org,2012:Post/4</id>
<published>2012-09-06T14:00:00-07:00</published>
......
......@@ -65,7 +65,7 @@
</div>
</a>
</li>
<li class="partner last">
<li class="partner">
<a href="${reverse('university_profile', args=['BerkeleyX'])}">
<img src="${static.url('images/university/berkeley/berkeley.png')}" />
<div class="name">
......@@ -73,6 +73,14 @@
</div>
</a>
</li>
<li class="partner last">
<a href="${reverse('university_profile', args=['UTx'])}">
<img src="${static.url('images/university/ut/ut-rollover_160x90.png')}" />
<div class="name">
<span>UTx</span>
</div>
</a>
</li>
</ol>
</section>
......
<dl>
% for license in licenses:
<dt> ${license.software.name}: </dt>
% if license.serial:
<dd> ${license.serial} </dd>
% else:
<dd> None Available </dd>
% endif
% endfor
</dl>
<%inherit file="base.html" />
<%namespace name='static' file='../static_content.html'/>
<%block name="title"><title>UTx</title></%block>
<%block name="university_header">
<header class="search" style="background: url('/static/images/university/ut/ut-cover_2025x550.jpg')">
<div class="inner-wrapper university-search">
<hgroup>
<div class="logo">
<img src="${static.url('images/university/ut/ut-standalone_187x80.png')}" />
</div>
<h1>UTx</h1>
</hgroup>
</div>
</header>
</%block>
<%block name="university_description">
<p>Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.</p>
</%block>
${parent.body()}
......@@ -52,6 +52,7 @@ urlpatterns = ('',
url(r'^heartbeat$', include('heartbeat.urls')),
url(r'^university_profile/UTx$', 'courseware.views.static_university_profile', name="static_university_profile", kwargs={'org_id':'UTx'}),
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile', name="university_profile"),
#Semi-static views (these need to be rendered and have the login bar, but don't change)
......@@ -88,6 +89,8 @@ urlpatterns = ('',
{'template': 'press_releases/edX_announces_proctored_exam_testing.html'}, name="press/edX-announces-proctored-exam-testing"),
url(r'^press/elsevier-collaborates-with-edx$', 'static_template_view.views.render',
{'template': 'press_releases/Elsevier_collaborates_with_edX.html'}, name="press/elsevier-collaborates-with-edx"),
url(r'^press/ut-joins-edx$', 'static_template_view.views.render',
{'template': 'press_releases/UT_joins_edX.html'}, name="press/ut-joins-edx"),
# Should this always update to point to the latest press release?
......@@ -141,6 +144,24 @@ 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'),
# Software Licenses
# TODO: for now, this is the endpoint of an ajax replay
# service that retrieve and assigns license numbers for
# software assigned to a course. The numbers have to be loaded
# into the database.
url(r'^software-licenses$', 'licenses.views.user_software_license', name="user_software_license"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.xqueue_callback',
name='xqueue_callback'),
......@@ -244,7 +265,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')
)
......
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