Cleaning up pep8 violations
This commit is contained in:
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@ def user(email):
|
||||
'''look up a user by email'''
|
||||
return User.objects.get(email=email)
|
||||
|
||||
|
||||
def registration(email):
|
||||
'''look up registration object by email'''
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
|
||||
class AuthTestCase(TestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
@@ -36,7 +38,7 @@ class AuthTestCase(TestCase):
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, expected)
|
||||
return resp
|
||||
|
||||
|
||||
def test_public_pages_load(self):
|
||||
"""Make sure pages that don't require login load without error."""
|
||||
pages = (
|
||||
@@ -60,11 +62,11 @@ class AuthTestCase(TestCase):
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': pw,
|
||||
'location' : 'home',
|
||||
'language' : 'Franglish',
|
||||
'name' : 'Fred Weasley',
|
||||
'terms_of_service' : 'true',
|
||||
'honor_code' : 'true',
|
||||
'location': 'home',
|
||||
'language': 'Franglish',
|
||||
'name': 'Fred Weasley',
|
||||
'terms_of_service': 'true',
|
||||
'honor_code': 'true',
|
||||
})
|
||||
return resp
|
||||
|
||||
@@ -99,7 +101,6 @@ class AuthTestCase(TestCase):
|
||||
self.create_account(self.username, self.email, self.pw)
|
||||
self.activate_user(self.email)
|
||||
|
||||
|
||||
def _login(self, email, pw):
|
||||
'''Login. View should always return 200. The success/fail is in the
|
||||
returned json'''
|
||||
@@ -108,7 +109,6 @@ class AuthTestCase(TestCase):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp
|
||||
|
||||
|
||||
def login(self, email, pw):
|
||||
'''Login, check that it worked.'''
|
||||
resp = self._login(self.email, self.pw)
|
||||
@@ -162,7 +162,6 @@ class AuthTestCase(TestCase):
|
||||
for page in simple_auth_pages:
|
||||
print "Checking '{0}'".format(page)
|
||||
self.check_page_get(page, expected=200)
|
||||
|
||||
|
||||
def test_index_auth(self):
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ def export_to_github(course, commit_message, author_str=None):
|
||||
git_repo.git.commit(m=commit_message, author=author_str)
|
||||
else:
|
||||
git_repo.git.commit(m=commit_message)
|
||||
|
||||
|
||||
origin = git_repo.remotes.origin
|
||||
if settings.MITX_FEATURES['GITHUB_PUSH']:
|
||||
push_infos = origin.push()
|
||||
|
||||
@@ -50,4 +50,3 @@ class PostReceiveTestCase(TestCase):
|
||||
import_from_github.assert_called_with(settings.REPOS['repo'])
|
||||
mock_revision, mock_course = import_from_github.return_value
|
||||
export_to_github.assert_called_with(mock_course, 'path', "Changes from cms import of revision %s" % mock_revision)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
||||
def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
"""
|
||||
Returns the ``model`` instance with a primary key of ``instance_or_pk``.
|
||||
@@ -87,6 +88,7 @@ def get_instance(model, instance_or_pk, timeout=None, using=None):
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
def delete_instance(model, *instance_or_pk):
|
||||
"""
|
||||
Purges the cache keys for the instances of this model.
|
||||
@@ -94,6 +96,7 @@ def delete_instance(model, *instance_or_pk):
|
||||
|
||||
cache.delete_many([instance_key(model, x) for x in instance_or_pk])
|
||||
|
||||
|
||||
def instance_key(model, instance_or_pk):
|
||||
"""
|
||||
Returns the cache key for this (model, instance) pair.
|
||||
|
||||
@@ -84,6 +84,7 @@ from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
|
||||
from .model import cache_model
|
||||
|
||||
|
||||
class CacheBackedAuthenticationMiddleware(AuthenticationMiddleware):
|
||||
def __init__(self):
|
||||
cache_model(User)
|
||||
|
||||
@@ -58,6 +58,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from .core import get_instance, delete_instance
|
||||
|
||||
|
||||
def cache_model(model, timeout=None):
|
||||
if hasattr(model, 'get_cached'):
|
||||
# Already patched
|
||||
|
||||
@@ -74,6 +74,7 @@ from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from .core import get_instance, delete_instance
|
||||
|
||||
|
||||
def cache_relation(descriptor, timeout=None):
|
||||
rel = descriptor.related
|
||||
related_name = '%s_cache' % rel.field.related_query_name()
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.template import resolve_variable
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class CacheNode(Node):
|
||||
def __init__(self, nodelist, expire_time, key):
|
||||
self.nodelist = nodelist
|
||||
@@ -21,6 +22,7 @@ class CacheNode(Node):
|
||||
cache.set(key, value, expire_time)
|
||||
return value
|
||||
|
||||
|
||||
@register.tag
|
||||
def cachedeterministic(parser, token):
|
||||
"""
|
||||
@@ -42,6 +44,7 @@ def cachedeterministic(parser, token):
|
||||
raise TemplateSyntaxError(u"'%r' tag requires 2 arguments." % tokens[0])
|
||||
return CacheNode(nodelist, tokens[1], tokens[2])
|
||||
|
||||
|
||||
class ShowIfCachedNode(Node):
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
@@ -50,6 +53,7 @@ class ShowIfCachedNode(Node):
|
||||
key = resolve_variable(self.key, context)
|
||||
return cache.get(key) or ''
|
||||
|
||||
|
||||
@register.tag
|
||||
def showifcached(parser, token):
|
||||
"""
|
||||
|
||||
@@ -60,6 +60,7 @@ def csrf_response_exempt(view_func):
|
||||
PendingDeprecationWarning)
|
||||
return view_func
|
||||
|
||||
|
||||
def csrf_view_exempt(view_func):
|
||||
"""
|
||||
Marks a view function as being exempt from CSRF view protection.
|
||||
@@ -68,6 +69,7 @@ def csrf_view_exempt(view_func):
|
||||
PendingDeprecationWarning)
|
||||
return csrf_exempt(view_func)
|
||||
|
||||
|
||||
def csrf_exempt(view_func):
|
||||
"""
|
||||
Marks a view function as being exempt from the CSRF view protection.
|
||||
|
||||
@@ -6,6 +6,7 @@ from pipeline.conf import settings
|
||||
from pipeline.packager import Packager
|
||||
from pipeline.utils import guess_type
|
||||
|
||||
|
||||
def compressed_css(package_name):
|
||||
package = settings.PIPELINE_CSS.get(package_name, {})
|
||||
if package:
|
||||
@@ -20,6 +21,7 @@ def compressed_css(package_name):
|
||||
paths = packager.compile(package.paths)
|
||||
return render_individual_css(package, paths)
|
||||
|
||||
|
||||
def render_css(package, path):
|
||||
template_name = package.template_name or "mako/css.html"
|
||||
context = package.extra_context
|
||||
@@ -29,6 +31,7 @@ def render_css(package, path):
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
def render_individual_css(package, paths):
|
||||
tags = [render_css(package, path) for path in paths]
|
||||
return '\n'.join(tags)
|
||||
@@ -49,6 +52,7 @@ def compressed_js(package_name):
|
||||
templates = packager.pack_templates(package)
|
||||
return render_individual_js(package, paths, templates)
|
||||
|
||||
|
||||
def render_js(package, path):
|
||||
template_name = package.template_name or "mako/js.html"
|
||||
context = package.extra_context
|
||||
@@ -58,6 +62,7 @@ def render_js(package, path):
|
||||
})
|
||||
return render_to_string(template_name, context)
|
||||
|
||||
|
||||
def render_inline_js(package, js):
|
||||
context = package.extra_context
|
||||
context.update({
|
||||
@@ -65,6 +70,7 @@ def render_inline_js(package, js):
|
||||
})
|
||||
return render_to_string("mako/inline_js.html", context)
|
||||
|
||||
|
||||
def render_individual_js(package, paths, templates=None):
|
||||
tags = [render_js(package, js) for js in paths]
|
||||
if templates:
|
||||
|
||||
@@ -15,4 +15,3 @@ admin.site.register(CourseEnrollment)
|
||||
admin.site.register(Registration)
|
||||
|
||||
admin.site.register(PendingNameChange)
|
||||
|
||||
|
||||
@@ -25,27 +25,29 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Exports all users and user profiles.
|
||||
'''Exports all users and user profiles.
|
||||
Caveat: Should be looked over before any run
|
||||
for schema changes.
|
||||
for schema changes.
|
||||
|
||||
Current version grabs user_keys from
|
||||
Current version grabs user_keys from
|
||||
django.contrib.auth.models.User and up_keys
|
||||
from student.userprofile. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
users = list(User.objects.all())
|
||||
user_profiles = list(UserProfile.objects.all())
|
||||
user_profile_dict = dict([(up.user_id, up) for up in user_profiles])
|
||||
|
||||
|
||||
user_tuples = [(user_profile_dict[u.id], u) for u in users if u.id in user_profile_dict]
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
'password']
|
||||
up_keys = ['language', 'location','meta','name', 'id','user_id']
|
||||
|
||||
up_keys = ['language', 'location', 'meta', 'name', 'id', 'user_id']
|
||||
|
||||
def extract_dict(keys, object):
|
||||
d = {}
|
||||
for key in keys:
|
||||
|
||||
@@ -22,6 +22,7 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def import_user(u):
|
||||
user_info = u['u']
|
||||
up_info = u['up']
|
||||
@@ -30,11 +31,10 @@ def import_user(u):
|
||||
user_info['last_login'] = dateutil.parser.parse(user_info['last_login'])
|
||||
user_info['date_joined'] = dateutil.parser.parse(user_info['date_joined'])
|
||||
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
user_keys = ['id', 'username', 'email', 'password', 'is_staff',
|
||||
'is_active', 'is_superuser', 'last_login', 'date_joined',
|
||||
'password']
|
||||
up_keys = ['language', 'location','meta','name', 'id','user_id']
|
||||
|
||||
up_keys = ['language', 'location', 'meta', 'name', 'id', 'user_id']
|
||||
|
||||
u = User()
|
||||
for key in user_keys:
|
||||
@@ -47,20 +47,22 @@ def import_user(u):
|
||||
up.__setattr__(key, up_info[key])
|
||||
up.save()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Exports all users and user profiles.
|
||||
'''Exports all users and user profiles.
|
||||
Caveat: Should be looked over before any run
|
||||
for schema changes.
|
||||
for schema changes.
|
||||
|
||||
Current version grabs user_keys from
|
||||
Current version grabs user_keys from
|
||||
django.contrib.auth.models.User and up_keys
|
||||
from student.userprofile. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
extracted = json.load(open('transfer_users.txt'))
|
||||
n=0
|
||||
n = 0
|
||||
for u in extracted:
|
||||
import_user(u)
|
||||
if n%100 == 0:
|
||||
if n % 100 == 0:
|
||||
print n
|
||||
n = n+1
|
||||
n = n + 1
|
||||
|
||||
@@ -17,29 +17,32 @@ import json
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def group_from_value(groups, v):
|
||||
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
|
||||
in [0,1], return the associated group (in the above case, return
|
||||
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
|
||||
'''
|
||||
sum = 0
|
||||
for (g,p) in groups:
|
||||
for (g, p) in groups:
|
||||
sum = sum + p
|
||||
if sum > v:
|
||||
return g
|
||||
return g # For round-off errors
|
||||
return g # For round-off errors
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Assign users to test groups. Takes a list
|
||||
of groups:
|
||||
of groups:
|
||||
a:0.3,b:0.4,c:0.3 file.txt "Testing something"
|
||||
Will assign each user to group a, b, or c with
|
||||
probability 0.3, 0.4, 0.3. Probabilities must
|
||||
add up to 1.
|
||||
probability 0.3, 0.4, 0.3. Probabilities must
|
||||
add up to 1.
|
||||
|
||||
Will log what happened to file.txt.
|
||||
'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 3:
|
||||
print "Invalid number of options"
|
||||
@@ -47,13 +50,13 @@ Will log what happened to file.txt.
|
||||
|
||||
# Extract groups from string
|
||||
group_strs = [x.split(':') for x in args[0].split(',')]
|
||||
groups = [(group,float(value)) for group,value in group_strs]
|
||||
groups = [(group, float(value)) for group, value in group_strs]
|
||||
print "Groups", groups
|
||||
|
||||
## Confirm group probabilities add up to 1
|
||||
total = sum(zip(*groups)[1])
|
||||
print "Total:", total
|
||||
if abs(total-1)>0.01:
|
||||
if abs(total - 1) > 0.01:
|
||||
print "Total not 1"
|
||||
sys.exit(-1)
|
||||
|
||||
@@ -65,15 +68,15 @@ Will log what happened to file.txt.
|
||||
|
||||
group_objects = {}
|
||||
|
||||
f = open(args[1],"a+")
|
||||
f = open(args[1], "a+")
|
||||
|
||||
## Create groups
|
||||
for group in dict(groups):
|
||||
utg = UserTestGroup()
|
||||
utg.name=group
|
||||
utg.description = json.dumps({"description":args[2]},
|
||||
{"time":datetime.datetime.utcnow().isoformat()})
|
||||
group_objects[group]=utg
|
||||
utg.name = group
|
||||
utg.description = json.dumps({"description": args[2]},
|
||||
{"time": datetime.datetime.utcnow().isoformat()})
|
||||
group_objects[group] = utg
|
||||
group_objects[group].save()
|
||||
|
||||
## Assign groups
|
||||
@@ -83,11 +86,11 @@ Will log what happened to file.txt.
|
||||
if count % 1000 == 0:
|
||||
print count
|
||||
count = count + 1
|
||||
v = random.uniform(0,1)
|
||||
group = group_from_value(groups,v)
|
||||
v = random.uniform(0, 1)
|
||||
group = group_from_value(groups, v)
|
||||
group_objects[group].users.add(user)
|
||||
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
|
||||
id=user.id,
|
||||
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
|
||||
id=user.id,
|
||||
group=group))
|
||||
|
||||
## Save groups
|
||||
|
||||
@@ -10,9 +10,11 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Extract an e-mail list of all active students. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
|
||||
@@ -10,18 +10,20 @@ import mitxmako.middleware as middleware
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Sends an e-mail to all users. Takes a single
|
||||
'''Sends an e-mail to all users. Takes a single
|
||||
parameter -- name of e-mail template -- located
|
||||
in templates/email. Adds a .txt for the message
|
||||
body, and an _subject.txt for the subject. '''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
users = User.objects.all()
|
||||
text = middleware.lookup['main'].get_template('email/'+args[0]+".txt").render()
|
||||
subject = middleware.lookup['main'].get_template('email/'+args[0]+"_subject.txt").render().strip()
|
||||
text = middleware.lookup['main'].get_template('email/' + args[0] + ".txt").render()
|
||||
subject = middleware.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
|
||||
for user in users:
|
||||
if user.is_active:
|
||||
user.email_user(subject, text)
|
||||
|
||||
@@ -16,16 +16,18 @@ import datetime
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
""" Yield successive n-sized chunks from l.
|
||||
"""
|
||||
for i in xrange(0, len(l), n):
|
||||
yield l[i:i+n]
|
||||
yield l[i:i + n]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Sends an e-mail to all users in a text file.
|
||||
E.g.
|
||||
'''Sends an e-mail to all users in a text file.
|
||||
E.g.
|
||||
manage.py userlist.txt message logfile.txt rate
|
||||
userlist.txt -- list of all users
|
||||
message -- prefix for template with message
|
||||
@@ -35,28 +37,28 @@ rate -- messages per second
|
||||
log_file = None
|
||||
|
||||
def hard_log(self, text):
|
||||
self.log_file.write(datetime.datetime.utcnow().isoformat()+' -- '+text+'\n')
|
||||
self.log_file.write(datetime.datetime.utcnow().isoformat() + ' -- ' + text + '\n')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
global log_file
|
||||
(user_file, message_base, logfilename, ratestr) = args
|
||||
|
||||
|
||||
users = [u.strip() for u in open(user_file).readlines()]
|
||||
|
||||
message = middleware.lookup['main'].get_template('emails/'+message_base+"_body.txt").render()
|
||||
subject = middleware.lookup['main'].get_template('emails/'+message_base+"_subject.txt").render().strip()
|
||||
message = middleware.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
|
||||
subject = middleware.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
|
||||
rate = int(ratestr)
|
||||
|
||||
self.log_file = open(logfilename, "a+", buffering = 0)
|
||||
|
||||
i=0
|
||||
self.log_file = open(logfilename, "a+", buffering=0)
|
||||
|
||||
i = 0
|
||||
for users in chunks(users, rate):
|
||||
emails = [ (subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users ]
|
||||
emails = [(subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users]
|
||||
self.hard_log(" ".join(users))
|
||||
send_mass_mail( emails, fail_silently = False )
|
||||
send_mass_mail(emails, fail_silently=False)
|
||||
time.sleep(1)
|
||||
print datetime.datetime.utcnow().isoformat(), i
|
||||
i = i+len(users)
|
||||
i = i + len(users)
|
||||
# Emergency interruptor
|
||||
if os.path.exists("/tmp/stopemails.txt"):
|
||||
self.log_file.close()
|
||||
|
||||
@@ -13,26 +13,28 @@ from student.models import UserProfile
|
||||
|
||||
middleware.MakoMiddleware()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
''' Extract full user information into a JSON file.
|
||||
''' Extract full user information into a JSON file.
|
||||
Pass a single filename.'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
f = open(args[0],'w')
|
||||
f = open(args[0], 'w')
|
||||
#text = open(args[0]).read()
|
||||
#subject = open(args[1]).read()
|
||||
users = User.objects.all()
|
||||
|
||||
l = []
|
||||
for user in users:
|
||||
up = UserProfile.objects.get(user = user)
|
||||
d = { 'username':user.username,
|
||||
'email':user.email,
|
||||
'is_active':user.is_active,
|
||||
'joined':user.date_joined.isoformat(),
|
||||
'name':up.name,
|
||||
'language':up.language,
|
||||
'location':up.location}
|
||||
up = UserProfile.objects.get(user=user)
|
||||
d = {'username': user.username,
|
||||
'email': user.email,
|
||||
'is_active': user.is_active,
|
||||
'joined': user.date_joined.isoformat(),
|
||||
'name': up.name,
|
||||
'language': up.language,
|
||||
'location': up.location}
|
||||
l.append(d)
|
||||
json.dump(l,f)
|
||||
json.dump(l, f)
|
||||
f.close()
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'UserProfile'
|
||||
db.create_table('auth_userprofile', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -28,16 +29,14 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['Registration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'UserProfile'
|
||||
db.delete_table('auth_userprofile')
|
||||
|
||||
# Deleting model 'Registration'
|
||||
db.delete_table('auth_registration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.name'
|
||||
db.alter_column('auth_userprofile', 'name', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
@@ -32,9 +33,8 @@ class Migration(SchemaMigration):
|
||||
# Adding index on 'UserProfile', fields ['location']
|
||||
db.create_index('auth_userprofile', ['location'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Removing index on 'UserProfile', fields ['location']
|
||||
db.delete_index('auth_userprofile', ['location'])
|
||||
|
||||
@@ -59,7 +59,6 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.location'
|
||||
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'UserTestGroup'
|
||||
db.create_table('student_usertestgroup', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -24,16 +25,14 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.create_unique('student_usertestgroup_users', ['usertestgroup_id', 'user_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'UserTestGroup'
|
||||
db.delete_table('student_usertestgroup')
|
||||
|
||||
# Removing M2M table for field users on 'UserTestGroup'
|
||||
db.delete_table('student_usertestgroup_users')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,18 +4,17 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
db.execute("create unique index email on auth_user (email)")
|
||||
pass
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
db.execute("drop index email on auth_user")
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'PendingEmailChange'
|
||||
db.create_table('student_pendingemailchange', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -29,9 +30,8 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.user'
|
||||
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(unique=True, to=orm['auth.User']))
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'PendingEmailChange'
|
||||
db.delete_table('student_pendingemailchange')
|
||||
|
||||
@@ -41,7 +41,6 @@ class Migration(SchemaMigration):
|
||||
# Changing field 'UserProfile.user'
|
||||
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,20 +4,19 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.TextField')())
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'UserProfile.meta'
|
||||
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.CharField')(max_length=255))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,6 +4,7 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
@@ -16,12 +17,10 @@ class Migration(SchemaMigration):
|
||||
ALTER TABLE student_usertestgroup_users CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
""")
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Although this migration can't be undone, it is okay for it to be run backwards because it doesn't add/remove any fields
|
||||
pass
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -16,12 +16,10 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseRegistration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'CourseRegistration'
|
||||
db.delete_table('student_courseregistration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -129,4 +127,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -19,7 +19,6 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'CourseRegistration'
|
||||
db.create_table('student_courseregistration', (
|
||||
@@ -32,7 +31,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -140,4 +138,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -124,4 +124,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -13,8 +13,8 @@ class Migration(SchemaMigration):
|
||||
pass
|
||||
# # Removing unique constraint on 'CourseEnrollment', fields ['user']
|
||||
# db.delete_unique('student_courseenrollment', ['user_id'])
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
# # Changing field 'CourseEnrollment.user'
|
||||
# db.alter_column('student_courseenrollment', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User']))
|
||||
|
||||
@@ -25,7 +25,6 @@ class Migration(SchemaMigration):
|
||||
# # Adding unique constraint on 'CourseEnrollment', fields ['user']
|
||||
# db.create_unique('student_courseenrollment', ['user_id'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -133,4 +132,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -38,7 +38,6 @@ class Migration(SchemaMigration):
|
||||
self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'UserProfile.gender'
|
||||
db.delete_column('auth_userprofile', 'gender')
|
||||
@@ -58,7 +57,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting field 'UserProfile.occupation'
|
||||
db.delete_column('auth_userprofile', 'occupation')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -172,4 +170,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -130,4 +130,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -11,7 +11,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Adding model 'CourseEnrollment'
|
||||
db.create_table('student_courseenrollment', (
|
||||
@@ -21,7 +20,6 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('student', ['CourseEnrollment'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -129,4 +127,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -19,7 +19,6 @@ class Migration(SchemaMigration):
|
||||
# Adding unique constraint on 'CourseEnrollment', fields ['user', 'course_id']
|
||||
db.create_unique('student_courseenrollment', ['user_id', 'course_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'CourseEnrollment', fields ['user', 'course_id']
|
||||
db.delete_unique('student_courseenrollment', ['user_id', 'course_id'])
|
||||
@@ -27,7 +26,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting model 'CourseEnrollment'
|
||||
db.delete_table('student_courseenrollment')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -141,4 +139,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -13,7 +13,6 @@ class Migration(SchemaMigration):
|
||||
self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, blank=True),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django_countries.fields.CountryField')(max_length=2, null=True))
|
||||
|
||||
@@ -21,7 +20,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting field 'CourseEnrollment.date'
|
||||
db.delete_column('student_courseenrollment', 'date')
|
||||
|
||||
|
||||
# Changing field 'UserProfile.country'
|
||||
db.alter_column('auth_userprofile', 'country', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
|
||||
|
||||
@@ -139,4 +137,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -15,7 +15,6 @@ class Migration(SchemaMigration):
|
||||
# Rename 'created' field to 'date'
|
||||
db.rename_column('student_courseenrollment', 'created', 'date')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -130,4 +129,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -11,12 +11,10 @@ class Migration(SchemaMigration):
|
||||
# Adding index on 'CourseEnrollment', fields ['created']
|
||||
db.create_index('student_courseenrollment', ['created'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'CourseEnrollment', fields ['created']
|
||||
db.delete_index('student_courseenrollment', ['created'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -131,4 +129,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -38,7 +38,6 @@ class Migration(SchemaMigration):
|
||||
# Adding index on 'UserProfile', fields ['gender']
|
||||
db.create_index('auth_userprofile', ['gender'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing index on 'UserProfile', fields ['gender']
|
||||
db.delete_index('auth_userprofile', ['gender'])
|
||||
@@ -72,7 +71,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting field 'UserProfile.goals'
|
||||
db.delete_column('auth_userprofile', 'goals')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
@@ -186,4 +184,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['student']
|
||||
complete_apps = ['student']
|
||||
|
||||
@@ -18,6 +18,7 @@ from django_countries import CountryField
|
||||
|
||||
#from cache_toolbox import cache_model, cache_relation
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
class Meta:
|
||||
db_table = "auth_userprofile"
|
||||
@@ -28,7 +29,7 @@ class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile')
|
||||
name = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
|
||||
meta = models.TextField(blank=True) # JSON dictionary for future expansion
|
||||
meta = models.TextField(blank=True) # JSON dictionary for future expansion
|
||||
courseware = models.CharField(blank=True, max_length=255, default='course.xml')
|
||||
|
||||
# Location is no longer used, but is held here for backwards compatibility
|
||||
@@ -59,7 +60,6 @@ class UserProfile(models.Model):
|
||||
mailing_address = models.TextField(blank=True, null=True)
|
||||
goals = models.TextField(blank=True, null=True)
|
||||
|
||||
|
||||
def get_meta(self):
|
||||
js_str = self.meta
|
||||
if not js_str:
|
||||
@@ -69,9 +69,10 @@ class UserProfile(models.Model):
|
||||
|
||||
return js_str
|
||||
|
||||
def set_meta(self,js):
|
||||
def set_meta(self, js):
|
||||
self.meta = json.dumps(js)
|
||||
|
||||
|
||||
## TODO: Should be renamed to generic UserGroup, and possibly
|
||||
# Given an optional field for type of group
|
||||
class UserTestGroup(models.Model):
|
||||
@@ -79,6 +80,7 @@ class UserTestGroup(models.Model):
|
||||
name = models.CharField(blank=False, max_length=32, db_index=True)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
|
||||
class Registration(models.Model):
|
||||
''' Allows us to wait for e-mail before user is registered. A
|
||||
registration profile is created when the user creates an
|
||||
@@ -92,8 +94,8 @@ class Registration(models.Model):
|
||||
|
||||
def register(self, user):
|
||||
# MINOR TODO: Switch to crypto-secure key
|
||||
self.activation_key=uuid.uuid4().hex
|
||||
self.user=user
|
||||
self.activation_key = uuid.uuid4().hex
|
||||
self.user = user
|
||||
self.save()
|
||||
|
||||
def activate(self):
|
||||
@@ -101,22 +103,25 @@ class Registration(models.Model):
|
||||
self.user.save()
|
||||
#self.delete()
|
||||
|
||||
|
||||
class PendingNameChange(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True)
|
||||
new_name = models.CharField(blank=True, max_length=255)
|
||||
rationale = models.CharField(blank=True, max_length=1024)
|
||||
|
||||
|
||||
class PendingEmailChange(models.Model):
|
||||
user = models.OneToOneField(User, unique=True, db_index=True)
|
||||
new_email = models.CharField(blank=True, max_length=255, db_index=True)
|
||||
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
|
||||
|
||||
|
||||
class CourseEnrollment(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
course_id = models.CharField(max_length=255, db_index=True)
|
||||
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = (('user', 'course_id'), )
|
||||
|
||||
@@ -124,38 +129,45 @@ class CourseEnrollment(models.Model):
|
||||
|
||||
#### Helper methods for use from python manage.py shell.
|
||||
|
||||
|
||||
def get_user(email):
|
||||
u = User.objects.get(email = email)
|
||||
up = UserProfile.objects.get(user = u)
|
||||
return u,up
|
||||
u = User.objects.get(email=email)
|
||||
up = UserProfile.objects.get(user=u)
|
||||
return u, up
|
||||
|
||||
|
||||
def user_info(email):
|
||||
u,up = get_user(email)
|
||||
u, up = get_user(email)
|
||||
print "User id", u.id
|
||||
print "Username", u.username
|
||||
print "E-mail", u.email
|
||||
print "Name", up.name
|
||||
print "Location", up.location
|
||||
print "Language", up.language
|
||||
return u,up
|
||||
return u, up
|
||||
|
||||
|
||||
def change_email(old_email, new_email):
|
||||
u = User.objects.get(email = old_email)
|
||||
u = User.objects.get(email=old_email)
|
||||
u.email = new_email
|
||||
u.save()
|
||||
|
||||
|
||||
def change_name(email, new_name):
|
||||
u,up = get_user(email)
|
||||
u, up = get_user(email)
|
||||
up.name = new_name
|
||||
up.save()
|
||||
|
||||
|
||||
def user_count():
|
||||
print "All users", User.objects.all().count()
|
||||
print "Active users", User.objects.filter(is_active = True).count()
|
||||
print "Active users", User.objects.filter(is_active=True).count()
|
||||
return User.objects.all().count()
|
||||
|
||||
|
||||
def active_user_count():
|
||||
return User.objects.filter(is_active = True).count()
|
||||
return User.objects.filter(is_active=True).count()
|
||||
|
||||
|
||||
def create_group(name, description):
|
||||
utg = UserTestGroup()
|
||||
@@ -163,29 +175,31 @@ def create_group(name, description):
|
||||
utg.description = description
|
||||
utg.save()
|
||||
|
||||
|
||||
def add_user_to_group(user, group):
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg.users.add(User.objects.get(username = user))
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
utg.users.add(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
|
||||
def remove_user_from_group(user, group):
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg.users.remove(User.objects.get(username = user))
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
utg.users.remove(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
default_groups = {'email_future_courses' : 'Receive e-mails about future MITx courses',
|
||||
'email_helpers' : 'Receive e-mails about how to help with MITx',
|
||||
'mitx_unenroll' : 'Fully unenrolled -- no further communications',
|
||||
'6002x_unenroll' : 'Took and dropped 6002x'}
|
||||
default_groups = {'email_future_courses': 'Receive e-mails about future MITx courses',
|
||||
'email_helpers': 'Receive e-mails about how to help with MITx',
|
||||
'mitx_unenroll': 'Fully unenrolled -- no further communications',
|
||||
'6002x_unenroll': 'Took and dropped 6002x'}
|
||||
|
||||
|
||||
def add_user_to_default_group(user, group):
|
||||
try:
|
||||
utg = UserTestGroup.objects.get(name = group)
|
||||
utg = UserTestGroup.objects.get(name=group)
|
||||
except UserTestGroup.DoesNotExist:
|
||||
utg = UserTestGroup()
|
||||
utg.name = group
|
||||
utg.description = default_groups[group]
|
||||
utg.save()
|
||||
utg.users.add(User.objects.get(username = user))
|
||||
utg.users.add(User.objects.get(username=user))
|
||||
utg.save()
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ from django.core.cache import cache
|
||||
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from util.cache import cache_if_anonymous
|
||||
from util.cache import cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -80,10 +80,12 @@ def index(request):
|
||||
|
||||
return render_to_response('index.html', {'universities': universities, 'entries': entries})
|
||||
|
||||
|
||||
def course_from_id(id):
|
||||
course_loc = CourseDescriptor.id_to_location(id)
|
||||
return modulestore().get_item(course_loc)
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def dashboard(request):
|
||||
@@ -100,19 +102,18 @@ def dashboard(request):
|
||||
except ItemNotFoundError:
|
||||
log.error("User {0} enrolled in non-existant course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
|
||||
|
||||
|
||||
message = ""
|
||||
if not user.is_active:
|
||||
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
|
||||
|
||||
context = {'courses': courses, 'message' : message}
|
||||
context = {'courses': courses, 'message': message}
|
||||
return render_to_response('dashboard.html', context)
|
||||
|
||||
|
||||
def try_change_enrollment(request):
|
||||
"""
|
||||
This method calls change_enrollment if the necessary POST
|
||||
This method calls change_enrollment if the necessary POST
|
||||
parameters are present, but does not return anything. It
|
||||
simply logs the result or exception. This is usually
|
||||
called after a registration or login, as secondary action.
|
||||
@@ -126,22 +127,23 @@ def try_change_enrollment(request):
|
||||
log.info("Attempted to automatically enroll after login. Results: {0}".format(enrollment_output))
|
||||
except Exception, e:
|
||||
log.exception("Exception automatically enrolling after login: {0}".format(str(e)))
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def change_enrollment_view(request):
|
||||
return HttpResponse(json.dumps(change_enrollment(request)))
|
||||
|
||||
|
||||
def change_enrollment(request):
|
||||
if request.method != "POST":
|
||||
raise Http404
|
||||
|
||||
action = request.POST.get("enrollment_action" , "")
|
||||
|
||||
action = request.POST.get("enrollment_action", "")
|
||||
user = request.user
|
||||
course_id = request.POST.get("course_id", None)
|
||||
if course_id == None:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'There was an error receiving the course id.'}))
|
||||
|
||||
|
||||
if action == "enroll":
|
||||
# Make sure the course exists
|
||||
# We don't do this check on unenroll, or a bad course id can't be unenrolled from
|
||||
@@ -151,22 +153,23 @@ def change_enrollment(request):
|
||||
log.error("User {0} tried to enroll in non-existant course {1}"
|
||||
.format(user.username, enrollment.course_id))
|
||||
return {'success': False, 'error': 'The course requested does not exist.'}
|
||||
|
||||
|
||||
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
|
||||
return {'success': True}
|
||||
|
||||
|
||||
elif action == "unenroll":
|
||||
try:
|
||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
|
||||
enrollment.delete()
|
||||
return {'success': True}
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
return {'success': False, 'error': 'You are not enrolled for this course.'}
|
||||
else:
|
||||
return {'success': False, 'error': 'Invalid enrollment_action.'}
|
||||
|
||||
|
||||
return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'}
|
||||
|
||||
|
||||
# Need different levels of logging
|
||||
@ensure_csrf_cookie
|
||||
def login_user(request, error=""):
|
||||
@@ -195,7 +198,7 @@ def login_user(request, error=""):
|
||||
try:
|
||||
login(request, user)
|
||||
if request.POST.get('remember') == 'true':
|
||||
request.session.set_expiry(None) # or change to 604800 for 7 days
|
||||
request.session.set_expiry(None) # or change to 604800 for 7 days
|
||||
log.debug("Setting user session to never expire")
|
||||
else:
|
||||
request.session.set_expiry(0)
|
||||
@@ -204,38 +207,41 @@ def login_user(request, error=""):
|
||||
log.exception(e)
|
||||
|
||||
log.info("Login success - {0} ({1})".format(username, email))
|
||||
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
log.warning("Login failed - Account not active for user {0}".format(username))
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'value': 'This account has not been activated. Please check your e-mail for the activation instructions.'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def logout_user(request):
|
||||
''' HTTP request to log out the user. Redirects to marketing page'''
|
||||
logout(request)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def change_setting(request):
|
||||
''' JSON call to change a profile setting: Right now, location
|
||||
'''
|
||||
up = UserProfile.objects.get(user=request.user) #request.user.profile_cache
|
||||
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
|
||||
if 'location' in request.POST:
|
||||
up.location=request.POST['location']
|
||||
up.location = request.POST['location']
|
||||
up.save()
|
||||
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
'location':up.location,}))
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'location': up.location, }))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def create_account(request, post_override=None):
|
||||
''' JSON call to enroll in the course. '''
|
||||
js={'success':False}
|
||||
js = {'success': False}
|
||||
|
||||
post_vars = post_override if post_override else request.POST
|
||||
|
||||
@@ -246,12 +252,11 @@ def create_account(request, post_override=None):
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
if post_vars.get('honor_code', 'false') != u'true':
|
||||
js['value']="To enroll, you must follow the honor code.".format(field=a)
|
||||
js['value'] = "To enroll, you must follow the honor code.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
|
||||
if post_vars.get('terms_of_service', 'false') != u'true':
|
||||
js['value']="You must accept the terms of service.".format(field=a)
|
||||
js['value'] = "You must accept the terms of service.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
# Confirm appropriate fields are there.
|
||||
@@ -261,25 +266,25 @@ def create_account(request, post_override=None):
|
||||
# TODO: Check password is sane
|
||||
for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']:
|
||||
if len(post_vars[a]) < 2:
|
||||
error_str = {'username' : 'Username of length 2 or greater',
|
||||
'email' : 'Properly formatted e-mail',
|
||||
'name' : 'Your legal name ',
|
||||
error_str = {'username': 'Username of length 2 or greater',
|
||||
'email': 'Properly formatted e-mail',
|
||||
'name': 'Your legal name ',
|
||||
'password': 'Valid password ',
|
||||
'terms_of_service': 'Accepting Terms of Service',
|
||||
'honor_code': 'Agreeing to the Honor Code'}
|
||||
js['value']="{field} is required.".format(field=error_str[a])
|
||||
js['value'] = "{field} is required.".format(field=error_str[a])
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
try:
|
||||
validate_email(post_vars['email'])
|
||||
except ValidationError:
|
||||
js['value']="Valid e-mail is required.".format(field=a)
|
||||
js['value'] = "Valid e-mail is required.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
try:
|
||||
validate_slug(post_vars['username'])
|
||||
except ValidationError:
|
||||
js['value']="Username should only consist of A-Z and 0-9.".format(field=a)
|
||||
js['value'] = "Username should only consist of A-Z and 0-9.".format(field=a)
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
u = User(username=post_vars['username'],
|
||||
@@ -311,17 +316,17 @@ def create_account(request, post_override=None):
|
||||
up.gender = post_vars.get('gender')
|
||||
up.mailing_address = post_vars.get('mailing_address')
|
||||
up.goals = post_vars.get('goals')
|
||||
|
||||
|
||||
try:
|
||||
up.year_of_birth = int(post_vars['year_of_birth'])
|
||||
except (ValueError, KeyError):
|
||||
up.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
up.year_of_birth = None # If they give us garbage, just ignore it instead
|
||||
# of asking them to put an integer.
|
||||
try:
|
||||
up.save()
|
||||
except Exception:
|
||||
log.exception("UserProfile creation failed for user {0}.".format(u.id))
|
||||
|
||||
|
||||
d = {'name': post_vars['name'],
|
||||
'key': r.activation_key,
|
||||
}
|
||||
@@ -334,7 +339,7 @@ def create_account(request, post_override=None):
|
||||
try:
|
||||
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
|
||||
dest_addr = settings.MITX_FEATURES['REROUTE_ACTIVATION_EMAIL']
|
||||
message = "Activation for %s (%s): %s\n" % (u,u.email,up.name) + '-' * 80 + '\n\n' + message
|
||||
message = "Activation for %s (%s): %s\n" % (u, u.email, up.name) + '-' * 80 + '\n\n' + message
|
||||
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [dest_addr], fail_silently=False)
|
||||
elif not settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
@@ -342,57 +347,60 @@ def create_account(request, post_override=None):
|
||||
log.exception(sys.exc_info())
|
||||
js['value'] = 'Could not send activation e-mail.'
|
||||
return HttpResponse(json.dumps(js))
|
||||
|
||||
|
||||
# Immediately after a user creates an account, we log them in. They are only
|
||||
# logged in until they close the browser. They can't log in again until they click
|
||||
# the activation link from the email.
|
||||
login_user = authenticate(username=post_vars['username'], password = post_vars['password'] )
|
||||
login_user = authenticate(username=post_vars['username'], password=post_vars['password'])
|
||||
login(request, login_user)
|
||||
request.session.set_expiry(0)
|
||||
|
||||
request.session.set_expiry(0)
|
||||
|
||||
try_change_enrollment(request)
|
||||
|
||||
js={'success': True}
|
||||
|
||||
js = {'success': True}
|
||||
return HttpResponse(json.dumps(js), mimetype="application/json")
|
||||
|
||||
|
||||
def create_random_account(create_account_function):
|
||||
|
||||
def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def inner_create_random_account(request):
|
||||
post_override= {'username' : "random_" + id_generator(),
|
||||
'email' : id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
|
||||
'password' : id_generator(),
|
||||
'location' : id_generator(size=5, chars=string.ascii_uppercase),
|
||||
'name' : id_generator(size=5, chars=string.ascii_lowercase) + " " + id_generator(size=7, chars=string.ascii_lowercase),
|
||||
'honor_code' : u'true',
|
||||
'terms_of_service' : u'true',}
|
||||
post_override = {'username': "random_" + id_generator(),
|
||||
'email': id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
|
||||
'password': id_generator(),
|
||||
'location': id_generator(size=5, chars=string.ascii_uppercase),
|
||||
'name': id_generator(size=5, chars=string.ascii_lowercase) + " " + id_generator(size=7, chars=string.ascii_lowercase),
|
||||
'honor_code': u'true',
|
||||
'terms_of_service': u'true', }
|
||||
|
||||
return create_account_function(request, post_override = post_override)
|
||||
return create_account_function(request, post_override=post_override)
|
||||
|
||||
return inner_create_random_account
|
||||
|
||||
if settings.GENERATE_RANDOM_USER_CREDENTIALS:
|
||||
create_account = create_random_account(create_account)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def activate_account(request, key):
|
||||
''' When link in activation e-mail is clicked
|
||||
'''
|
||||
r=Registration.objects.filter(activation_key=key)
|
||||
if len(r)==1:
|
||||
r = Registration.objects.filter(activation_key=key)
|
||||
if len(r) == 1:
|
||||
user_logged_in = request.user.is_authenticated()
|
||||
already_active = True
|
||||
if not r[0].user.is_active:
|
||||
r[0].activate()
|
||||
already_active = False
|
||||
resp = render_to_response("registration/activation_complete.html",{'user_logged_in':user_logged_in, 'already_active' : already_active})
|
||||
resp = render_to_response("registration/activation_complete.html", {'user_logged_in': user_logged_in, 'already_active': already_active})
|
||||
return resp
|
||||
if len(r)==0:
|
||||
return render_to_response("registration/activation_invalid.html",{'csrf':csrf(request)['csrf_token']})
|
||||
if len(r) == 0:
|
||||
return render_to_response("registration/activation_invalid.html", {'csrf': csrf(request)['csrf_token']})
|
||||
return HttpResponse("Unknown error. Please e-mail us to let us know how it happened.")
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def password_reset(request):
|
||||
''' Attempts to send a password reset e-mail. '''
|
||||
@@ -400,43 +408,44 @@ def password_reset(request):
|
||||
raise Http404
|
||||
form = PasswordResetForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save( use_https = request.is_secure(),
|
||||
from_email = settings.DEFAULT_FROM_EMAIL,
|
||||
request = request )
|
||||
return HttpResponse(json.dumps({'success':True,
|
||||
form.save(use_https=request.is_secure(),
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
request=request)
|
||||
return HttpResponse(json.dumps({'success': True,
|
||||
'value': render_to_string('registration/password_reset_done.html', {})}))
|
||||
else:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid e-mail'}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def reactivation_email(request):
|
||||
''' Send an e-mail to reactivate a deactivated account, or to
|
||||
resend an activation e-mail. Untested. '''
|
||||
email = request.POST['email']
|
||||
try:
|
||||
user = User.objects.get(email = 'email')
|
||||
user = User.objects.get(email='email')
|
||||
except User.DoesNotExist:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'No inactive user with this e-mail exists'}))
|
||||
|
||||
if user.is_active:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'User is already active'}))
|
||||
|
||||
reg = Registration.objects.get(user = user)
|
||||
reg = Registration.objects.get(user=user)
|
||||
reg.register(user)
|
||||
|
||||
d={'name':UserProfile.get(user = user).name,
|
||||
'key':r.activation_key}
|
||||
d = {'name': UserProfile.get(user=user).name,
|
||||
'key': r.activation_key}
|
||||
|
||||
subject = render_to_string('reactivation_email_subject.txt',d)
|
||||
subject = render_to_string('reactivation_email_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('reactivation_email.txt',d)
|
||||
message = render_to_string('reactivation_email.txt', d)
|
||||
|
||||
res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
res = u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -450,26 +459,26 @@ def change_email_request(request):
|
||||
user = request.user
|
||||
|
||||
if not user.check_password(request.POST['password']):
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Invalid password'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Invalid password'}))
|
||||
|
||||
new_email = request.POST['new_email']
|
||||
try:
|
||||
validate_email(new_email)
|
||||
except ValidationError:
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Valid e-mail address required.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Valid e-mail address required.'}))
|
||||
|
||||
if len(User.objects.filter(email = new_email)) != 0:
|
||||
if len(User.objects.filter(email=new_email)) != 0:
|
||||
## CRITICAL TODO: Handle case sensitivity for e-mails
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'An account with this e-mail already exists.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'An account with this e-mail already exists.'}))
|
||||
|
||||
pec_list = PendingEmailChange.objects.filter(user = request.user)
|
||||
pec_list = PendingEmailChange.objects.filter(user=request.user)
|
||||
if len(pec_list) == 0:
|
||||
pec = PendingEmailChange()
|
||||
pec.user = user
|
||||
else :
|
||||
else:
|
||||
pec = pec_list[0]
|
||||
|
||||
pec.new_email = request.POST['new_email']
|
||||
@@ -478,20 +487,21 @@ def change_email_request(request):
|
||||
|
||||
if pec.new_email == user.email:
|
||||
pec.delete()
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error':'Old email is the same as the new email.'}))
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': 'Old email is the same as the new email.'}))
|
||||
|
||||
d = {'key':pec.activation_key,
|
||||
'old_email' : user.email,
|
||||
'new_email' : pec.new_email}
|
||||
d = {'key': pec.activation_key,
|
||||
'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt',d)
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/email_change.txt',d)
|
||||
message = render_to_string('emails/email_change.txt', d)
|
||||
|
||||
res=send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email])
|
||||
res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_email])
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def confirm_email_change(request, key):
|
||||
@@ -499,22 +509,21 @@ def confirm_email_change(request, key):
|
||||
link is clicked. We confirm with the old e-mail, and update
|
||||
'''
|
||||
try:
|
||||
pec=PendingEmailChange.objects.get(activation_key=key)
|
||||
pec = PendingEmailChange.objects.get(activation_key=key)
|
||||
except PendingEmailChange.DoesNotExist:
|
||||
return render_to_response("invalid_email_key.html", {})
|
||||
|
||||
user = pec.user
|
||||
d = {'old_email' : user.email,
|
||||
'new_email' : pec.new_email}
|
||||
d = {'old_email': user.email,
|
||||
'new_email': pec.new_email}
|
||||
|
||||
if len(User.objects.filter(email = pec.new_email)) != 0:
|
||||
if len(User.objects.filter(email=pec.new_email)) != 0:
|
||||
return render_to_response("email_exists.html", d)
|
||||
|
||||
|
||||
subject = render_to_string('emails/email_change_subject.txt',d)
|
||||
subject = render_to_string('emails/email_change_subject.txt', d)
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/confirm_email_change.txt',d)
|
||||
up = UserProfile.objects.get( user = user )
|
||||
message = render_to_string('emails/confirm_email_change.txt', d)
|
||||
up = UserProfile.objects.get(user=user)
|
||||
meta = up.get_meta()
|
||||
if 'old_emails' not in meta:
|
||||
meta['old_emails'] = []
|
||||
@@ -528,6 +537,7 @@ def confirm_email_change(request, key):
|
||||
|
||||
return render_to_response("email_change_successful.html", d)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def change_name_request(request):
|
||||
''' Log a request for a new name. '''
|
||||
@@ -535,18 +545,18 @@ def change_name_request(request):
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
pnc = PendingNameChange.objects.get(user = request.user)
|
||||
pnc = PendingNameChange.objects.get(user=request.user)
|
||||
except PendingNameChange.DoesNotExist:
|
||||
pnc = PendingNameChange()
|
||||
pnc.user = request.user
|
||||
pnc.new_name = request.POST['new_name']
|
||||
pnc.rationale = request.POST['rationale']
|
||||
if len(pnc.new_name)<2:
|
||||
return HttpResponse(json.dumps({'success':False,'error':'Name required'}))
|
||||
if len(pnc.rationale)<2:
|
||||
return HttpResponse(json.dumps({'success':False,'error':'Rationale required'}))
|
||||
if len(pnc.new_name) < 2:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Name required'}))
|
||||
if len(pnc.rationale) < 2:
|
||||
return HttpResponse(json.dumps({'success': False, 'error': 'Rationale required'}))
|
||||
pnc.save()
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -4,6 +4,7 @@ from django.conf import settings
|
||||
|
||||
import views
|
||||
|
||||
|
||||
class TrackMiddleware:
|
||||
def process_request(self, request):
|
||||
try:
|
||||
@@ -11,34 +12,34 @@ class TrackMiddleware:
|
||||
# names/passwords.
|
||||
if request.META['PATH_INFO'] in ['/event', '/login']:
|
||||
return
|
||||
|
||||
|
||||
# Removes passwords from the tracking logs
|
||||
# WARNING: This list needs to be changed whenever we change
|
||||
# password handling functionality.
|
||||
# password handling functionality.
|
||||
#
|
||||
# As of the time of this comment, only 'password' is used
|
||||
# The rest are there for future extension.
|
||||
# The rest are there for future extension.
|
||||
#
|
||||
# Passwords should never be sent as GET requests, but
|
||||
# Passwords should never be sent as GET requests, but
|
||||
# this can happen due to older browser bugs. We censor
|
||||
# this too.
|
||||
#
|
||||
# this too.
|
||||
#
|
||||
# We should manually confirm no passwords make it into log
|
||||
# files when we change this.
|
||||
# files when we change this.
|
||||
|
||||
censored_strings = ['password', 'newpassword', 'new_password',
|
||||
censored_strings = ['password', 'newpassword', 'new_password',
|
||||
'oldpassword', 'old_password']
|
||||
post_dict = dict(request.POST)
|
||||
get_dict = dict(request.GET)
|
||||
for string in censored_strings:
|
||||
if string in post_dict:
|
||||
post_dict[string] = '*'*8
|
||||
if string in get_dict:
|
||||
get_dict[string] = '*'*8
|
||||
for string in censored_strings:
|
||||
if string in post_dict:
|
||||
post_dict[string] = '*' * 8
|
||||
if string in get_dict:
|
||||
get_dict[string] = '*' * 8
|
||||
|
||||
event = {'GET': dict(get_dict),
|
||||
'POST': dict(post_dict)}
|
||||
|
||||
event = { 'GET' : dict(get_dict),
|
||||
'POST' : dict(post_dict)}
|
||||
|
||||
# TODO: Confirm no large file uploads
|
||||
event = json.dumps(event)
|
||||
event = event[:512]
|
||||
|
||||
@@ -10,61 +10,64 @@ from django.conf import settings
|
||||
|
||||
log = logging.getLogger("tracking")
|
||||
|
||||
|
||||
def log_event(event):
|
||||
event_str = json.dumps(event)
|
||||
log.info(event_str[:settings.TRACK_MAX_EVENT])
|
||||
|
||||
|
||||
def user_track(request):
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
try: # TODO: Do the same for many of the optional META parameters
|
||||
username = request.user.username
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
|
||||
try:
|
||||
scookie = request.META['HTTP_COOKIE'] # Get cookies
|
||||
scookie = ";".join([c.split('=')[1] for c in scookie.split(";") if "sessionid" in c]).strip() # Extract session ID
|
||||
except:
|
||||
try:
|
||||
scookie = request.META['HTTP_COOKIE'] # Get cookies
|
||||
scookie = ";".join([c.split('=')[1] for c in scookie.split(";") if "sessionid" in c]).strip() # Extract session ID
|
||||
except:
|
||||
scookie = ""
|
||||
|
||||
try:
|
||||
try:
|
||||
agent = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
# TODO: Move a bunch of this into log_event
|
||||
event = {
|
||||
"username" : username,
|
||||
"session" : scookie,
|
||||
"ip" : request.META['REMOTE_ADDR'],
|
||||
"event_source" : "browser",
|
||||
"event_type" : request.GET['event_type'],
|
||||
"event" : request.GET['event'],
|
||||
"agent" : agent,
|
||||
"page" : request.GET['page'],
|
||||
"username": username,
|
||||
"session": scookie,
|
||||
"ip": request.META['REMOTE_ADDR'],
|
||||
"event_source": "browser",
|
||||
"event_type": request.GET['event_type'],
|
||||
"event": request.GET['event'],
|
||||
"agent": agent,
|
||||
"page": request.GET['page'],
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
log_event(event)
|
||||
return HttpResponse('success')
|
||||
|
||||
|
||||
def server_track(request, event_type, event, page=None):
|
||||
try:
|
||||
try:
|
||||
username = request.user.username
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
|
||||
try:
|
||||
try:
|
||||
agent = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
except:
|
||||
agent = ''
|
||||
|
||||
event = {
|
||||
"username" : username,
|
||||
"ip" : request.META['REMOTE_ADDR'],
|
||||
"event_source" : "server",
|
||||
"event_type" : event_type,
|
||||
"event" : event,
|
||||
"agent" : agent,
|
||||
"page" : page,
|
||||
"username": username,
|
||||
"ip": request.META['REMOTE_ADDR'],
|
||||
"event_source": "server",
|
||||
"event_type": event_type,
|
||||
"event": event,
|
||||
"agent": agent,
|
||||
"page": page,
|
||||
"time": datetime.datetime.utcnow().isoformat(),
|
||||
}
|
||||
log_event(event)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
This module aims to give a little more fine-tuned control of caching and cache
|
||||
invalidation. Import these instead of django.core.cache.
|
||||
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
Note that 'default' is being preserved for user session caching, which we're
|
||||
not migrating so as not to inconvenience users by logging them all out.
|
||||
"""
|
||||
from functools import wraps
|
||||
@@ -16,26 +16,27 @@ try:
|
||||
except Exception:
|
||||
cache = cache.cache
|
||||
|
||||
|
||||
def cache_if_anonymous(view_func):
|
||||
"""
|
||||
Many of the pages in edX are identical when the user is not logged
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
|
||||
in, but should not be cached when the user is logged in (because
|
||||
of the navigation bar at the top with the username).
|
||||
|
||||
The django middleware cache does not handle this correctly, because
|
||||
we access the session to put the csrf token in the header. This adds
|
||||
the cookie to the vary header, and so every page is cached seperately
|
||||
for each user (because each user has a different csrf token).
|
||||
|
||||
|
||||
Note that this decorator should only be used on views that do not
|
||||
contain the csrftoken within the html. The csrf token can be included
|
||||
in the header by ordering the decorators as such:
|
||||
|
||||
|
||||
@ensure_csrftoken
|
||||
@cache_if_anonymous
|
||||
def myView(request):
|
||||
"""
|
||||
|
||||
|
||||
@wraps(view_func)
|
||||
def _decorated(request, *args, **kwargs):
|
||||
if not request.user.is_authenticated():
|
||||
@@ -45,12 +46,12 @@ def cache_if_anonymous(view_func):
|
||||
if not response:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
cache.set(cache_key, response, 60 * 3)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
else:
|
||||
#Don't use the cache
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
|
||||
return _decorated
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from functools import wraps
|
||||
import copy
|
||||
import json
|
||||
|
||||
|
||||
def expect_json(view_function):
|
||||
@wraps(view_function)
|
||||
def expect_json_with_cloned_request(request, *args, **kwargs):
|
||||
|
||||
@@ -6,11 +6,13 @@ from django.utils.encoding import smart_str
|
||||
import hashlib
|
||||
import urllib
|
||||
|
||||
|
||||
def fasthash(string):
|
||||
m = hashlib.new("md4")
|
||||
m.update(string)
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
def safe_key(key, key_prefix, version):
|
||||
safe_key = urllib.quote_plus(smart_str(key))
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ from django.http import HttpResponseServerError
|
||||
|
||||
log = logging.getLogger("mitx")
|
||||
|
||||
|
||||
class ExceptionLoggingMiddleware(object):
|
||||
"""Just here to log unchecked exceptions that go all the way up the Django
|
||||
"""Just here to log unchecked exceptions that go all the way up the Django
|
||||
stack"""
|
||||
|
||||
if not settings.TEMPLATE_DEBUG:
|
||||
def process_exception(self, request, exception):
|
||||
log.exception(exception)
|
||||
return HttpResponseServerError("Server Error - Please try again later.")
|
||||
|
||||
|
||||
@@ -14,53 +14,57 @@ from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
import capa.calc
|
||||
import track.views
|
||||
|
||||
|
||||
def calculate(request):
|
||||
''' Calculator in footer of every page. '''
|
||||
equation = request.GET['equation']
|
||||
try:
|
||||
try:
|
||||
result = capa.calc.evaluator({}, {}, equation)
|
||||
except:
|
||||
event = {'error':map(str,sys.exc_info()),
|
||||
'equation':equation}
|
||||
event = {'error': map(str, sys.exc_info()),
|
||||
'equation': equation}
|
||||
track.views.server_track(request, 'error:calc', event, page='calc')
|
||||
return HttpResponse(json.dumps({'result':'Invalid syntax'}))
|
||||
return HttpResponse(json.dumps({'result':str(result)}))
|
||||
return HttpResponse(json.dumps({'result': 'Invalid syntax'}))
|
||||
return HttpResponse(json.dumps({'result': str(result)}))
|
||||
|
||||
|
||||
def send_feedback(request):
|
||||
''' Feeback mechanism in footer of every page. '''
|
||||
try:
|
||||
try:
|
||||
username = request.user.username
|
||||
email = request.user.email
|
||||
except:
|
||||
except:
|
||||
username = "anonymous"
|
||||
email = "anonymous"
|
||||
|
||||
try:
|
||||
browser = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
browser = "Unknown"
|
||||
|
||||
feedback = render_to_string("feedback_email.txt",
|
||||
{"subject":request.POST['subject'],
|
||||
"url": request.POST['url'],
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"feedback": request.POST['message'],
|
||||
"email":email,
|
||||
"browser":browser,
|
||||
"user":username})
|
||||
|
||||
send_mail("MITx Feedback / " +request.POST['subject'],
|
||||
feedback,
|
||||
try:
|
||||
browser = request.META['HTTP_USER_AGENT']
|
||||
except:
|
||||
browser = "Unknown"
|
||||
|
||||
feedback = render_to_string("feedback_email.txt",
|
||||
{"subject": request.POST['subject'],
|
||||
"url": request.POST['url'],
|
||||
"time": datetime.datetime.now().isoformat(),
|
||||
"feedback": request.POST['message'],
|
||||
"email": email,
|
||||
"browser": browser,
|
||||
"user": username})
|
||||
|
||||
send_mail("MITx Feedback / " + request.POST['subject'],
|
||||
feedback,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[ settings.DEFAULT_FEEDBACK_EMAIL ],
|
||||
fail_silently = False
|
||||
[settings.DEFAULT_FEEDBACK_EMAIL],
|
||||
fail_silently=False
|
||||
)
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
|
||||
def info(request):
|
||||
''' Info page (link from main header) '''
|
||||
return render_to_response("info.html", {})
|
||||
|
||||
|
||||
# From http://djangosnippets.org/snippets/1042/
|
||||
def parse_accept_header(accept):
|
||||
"""Parse the Accept header *accept*, returning a list with pairs of
|
||||
@@ -82,6 +86,7 @@ def parse_accept_header(accept):
|
||||
result.sort(lambda x, y: -cmp(x[2], y[2]))
|
||||
return result
|
||||
|
||||
|
||||
def accepts(request, media_type):
|
||||
"""Return whether this request has an Accept header that matches type"""
|
||||
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -14,29 +14,30 @@ from pyparsing import StringEnd, Optional, Forward
|
||||
from pyparsing import CaselessLiteral, Group, StringEnd
|
||||
from pyparsing import NoMatch, stringEnd, alphanums
|
||||
|
||||
default_functions = {'sin' : numpy.sin,
|
||||
'cos' : numpy.cos,
|
||||
'tan' : numpy.tan,
|
||||
default_functions = {'sin': numpy.sin,
|
||||
'cos': numpy.cos,
|
||||
'tan': numpy.tan,
|
||||
'sqrt': numpy.sqrt,
|
||||
'log10':numpy.log10,
|
||||
'log2':numpy.log2,
|
||||
'log10': numpy.log10,
|
||||
'log2': numpy.log2,
|
||||
'ln': numpy.log,
|
||||
'arccos':numpy.arccos,
|
||||
'arcsin':numpy.arcsin,
|
||||
'arctan':numpy.arctan,
|
||||
'abs':numpy.abs
|
||||
'arccos': numpy.arccos,
|
||||
'arcsin': numpy.arcsin,
|
||||
'arctan': numpy.arctan,
|
||||
'abs': numpy.abs
|
||||
}
|
||||
default_variables = {'j':numpy.complex(0,1),
|
||||
'e':numpy.e,
|
||||
'pi':numpy.pi,
|
||||
'k':scipy.constants.k,
|
||||
'c':scipy.constants.c,
|
||||
'T':298.15,
|
||||
'q':scipy.constants.e
|
||||
default_variables = {'j': numpy.complex(0, 1),
|
||||
'e': numpy.e,
|
||||
'pi': numpy.pi,
|
||||
'k': scipy.constants.k,
|
||||
'c': scipy.constants.c,
|
||||
'T': 298.15,
|
||||
'q': scipy.constants.e
|
||||
}
|
||||
|
||||
log = logging.getLogger("mitx.courseware.capa")
|
||||
|
||||
|
||||
class UndefinedVariable(Exception):
|
||||
def raiseself(self):
|
||||
''' Helper so we can use inside of a lambda '''
|
||||
@@ -44,28 +45,31 @@ class UndefinedVariable(Exception):
|
||||
|
||||
|
||||
general_whitespace = re.compile('[^\w]+')
|
||||
|
||||
|
||||
def check_variables(string, variables):
|
||||
''' Confirm the only variables in string are defined.
|
||||
''' Confirm the only variables in string are defined.
|
||||
|
||||
Pyparsing uses a left-to-right parser, which makes the more
|
||||
elegant approach pretty hopeless.
|
||||
elegant approach pretty hopeless.
|
||||
|
||||
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
|
||||
undefined_variable = achar + Word(alphanums)
|
||||
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
|
||||
varnames = varnames | undefined_variable'''
|
||||
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
|
||||
bad_variables = list()
|
||||
for v in possible_variables:
|
||||
if len(v) == 0:
|
||||
continue
|
||||
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
|
||||
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
|
||||
continue
|
||||
if v not in variables:
|
||||
if v not in variables:
|
||||
bad_variables.append(v)
|
||||
if len(bad_variables)>0:
|
||||
if len(bad_variables) > 0:
|
||||
raise UndefinedVariable(' '.join(bad_variables))
|
||||
|
||||
|
||||
def evaluator(variables, functions, string, cs=False):
|
||||
''' Evaluate an expression. Variables are passed as a dictionary
|
||||
from string to value. Unary functions are passed as a dictionary
|
||||
@@ -84,147 +88,152 @@ def evaluator(variables, functions, string, cs=False):
|
||||
all_variables = copy.copy(default_variables)
|
||||
all_functions = copy.copy(default_functions)
|
||||
|
||||
if not cs:
|
||||
if not cs:
|
||||
all_variables = lower_dict(all_variables)
|
||||
all_functions = lower_dict(all_functions)
|
||||
|
||||
all_variables.update(variables)
|
||||
all_variables.update(variables)
|
||||
all_functions.update(functions)
|
||||
|
||||
if not cs:
|
||||
|
||||
if not cs:
|
||||
string_cs = string.lower()
|
||||
all_functions = lower_dict(all_functions)
|
||||
all_variables = lower_dict(all_variables)
|
||||
CasedLiteral = CaselessLiteral
|
||||
CasedLiteral = CaselessLiteral
|
||||
else:
|
||||
string_cs = string
|
||||
CasedLiteral = Literal
|
||||
|
||||
check_variables(string_cs, set(all_variables.keys()+all_functions.keys()))
|
||||
check_variables(string_cs, set(all_variables.keys() + all_functions.keys()))
|
||||
|
||||
if string.strip() == "":
|
||||
return float('nan')
|
||||
ops = { "^" : operator.pow,
|
||||
"*" : operator.mul,
|
||||
"/" : operator.truediv,
|
||||
"+" : operator.add,
|
||||
"-" : operator.sub,
|
||||
ops = {"^": operator.pow,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
}
|
||||
# We eliminated extreme ones, since they're rarely used, and potentially
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# confusing. They may also conflict with variables if we ever allow e.g.
|
||||
# 5R instead of 5*R
|
||||
suffixes={'%':0.01,'k':1e3,'M':1e6,'G':1e9,
|
||||
'T':1e12,#'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c':1e-2,'m':1e-3,'u':1e-6,
|
||||
'n':1e-9,'p':1e-12}#,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
suffixes = {'%': 0.01, 'k': 1e3, 'M': 1e6, 'G': 1e9,
|
||||
'T': 1e12,# 'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
|
||||
'c': 1e-2, 'm': 1e-3, 'u': 1e-6,
|
||||
'n': 1e-9, 'p': 1e-12}# ,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
|
||||
|
||||
def super_float(text):
|
||||
''' Like float, but with si extensions. 1k goes to 1000'''
|
||||
if text[-1] in suffixes:
|
||||
return float(text[:-1])*suffixes[text[-1]]
|
||||
return float(text[:-1]) * suffixes[text[-1]]
|
||||
else:
|
||||
return float(text)
|
||||
|
||||
def number_parse_action(x): # [ '7' ] -> [ 7 ]
|
||||
def number_parse_action(x): # [ '7' ] -> [ 7 ]
|
||||
return [super_float("".join(x))]
|
||||
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
|
||||
x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
|
||||
|
||||
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
|
||||
x = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
|
||||
x.reverse()
|
||||
x=reduce(lambda a,b:b**a, x)
|
||||
x = reduce(lambda a, b: b ** a, x)
|
||||
return x
|
||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
||||
|
||||
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
|
||||
if len(x) == 1:
|
||||
return x[0]
|
||||
if 0 in x:
|
||||
return float('nan')
|
||||
x = [1./e for e in x if isinstance(e, numbers.Number)] # Ignore ||
|
||||
return 1./sum(x)
|
||||
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
||||
x = [1. / e for e in x if isinstance(e, numbers.Number)] # Ignore ||
|
||||
return 1. / sum(x)
|
||||
|
||||
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
|
||||
total = 0.0
|
||||
op = ops['+']
|
||||
for e in x:
|
||||
if e in set('+-'):
|
||||
op = ops[e]
|
||||
else:
|
||||
total=op(total, e)
|
||||
total = op(total, e)
|
||||
return total
|
||||
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
||||
|
||||
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
|
||||
prod = 1.0
|
||||
op = ops['*']
|
||||
for e in x:
|
||||
if e in set('*/'):
|
||||
op = ops[e]
|
||||
else:
|
||||
prod=op(prod, e)
|
||||
prod = op(prod, e)
|
||||
return prod
|
||||
|
||||
def func_parse_action(x):
|
||||
return [all_functions[x[0]](x[1])]
|
||||
|
||||
number_suffix=reduce(lambda a,b:a|b, map(Literal,suffixes.keys()), NoMatch()) # SI suffixes and percent
|
||||
(dot,minus,plus,times,div,lpar,rpar,exp)=map(Literal,".-+*/()^")
|
||||
|
||||
number_part=Word(nums)
|
||||
inner_number = ( number_part+Optional("."+number_part) ) | ("."+number_part) # 0.33 or 7 or .34
|
||||
number=Optional(minus | plus)+ inner_number + \
|
||||
Optional(CaselessLiteral("E")+Optional("-")+number_part)+ \
|
||||
Optional(number_suffix) # 0.33k or -17
|
||||
number=number.setParseAction( number_parse_action ) # Convert to number
|
||||
|
||||
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) # SI suffixes and percent
|
||||
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^")
|
||||
|
||||
number_part = Word(nums)
|
||||
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) # 0.33 or 7 or .34
|
||||
number = Optional(minus | plus) + inner_number + \
|
||||
Optional(CaselessLiteral("E") + Optional("-") + number_part) + \
|
||||
Optional(number_suffix) # 0.33k or -17
|
||||
number = number.setParseAction(number_parse_action) # Convert to number
|
||||
|
||||
# Predefine recursive variables
|
||||
expr = Forward()
|
||||
expr = Forward()
|
||||
factor = Forward()
|
||||
|
||||
|
||||
def sreduce(f, l):
|
||||
''' Same as reduce, but handle len 1 and len 0 lists sensibly '''
|
||||
if len(l)==0:
|
||||
if len(l) == 0:
|
||||
return NoMatch()
|
||||
if len(l)==1:
|
||||
if len(l) == 1:
|
||||
return l[0]
|
||||
return reduce(f, l)
|
||||
|
||||
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
|
||||
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
|
||||
# Special case for no variables because of how we understand PyParsing is put together
|
||||
if len(all_variables)>0:
|
||||
# We sort the list so that var names (like "e2") match before
|
||||
if len(all_variables) > 0:
|
||||
# We sort the list so that var names (like "e2") match before
|
||||
# mathematical constants (like "e"). This is kind of a hack.
|
||||
all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
|
||||
varnames = sreduce(lambda x,y:x|y, map(lambda x: CasedLiteral(x), all_variables_keys))
|
||||
varnames.setParseAction(lambda x:map(lambda y:all_variables[y], x))
|
||||
varnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_variables_keys))
|
||||
varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x))
|
||||
else:
|
||||
varnames=NoMatch()
|
||||
# Same thing for functions.
|
||||
if len(all_functions)>0:
|
||||
funcnames = sreduce(lambda x,y:x|y, map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
function = funcnames+lpar.suppress()+expr+rpar.suppress()
|
||||
varnames = NoMatch()
|
||||
# Same thing for functions.
|
||||
if len(all_functions) > 0:
|
||||
funcnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_functions.keys()))
|
||||
function = funcnames + lpar.suppress() + expr + rpar.suppress()
|
||||
function.setParseAction(func_parse_action)
|
||||
else:
|
||||
function = NoMatch()
|
||||
|
||||
atom = number | function | varnames | lpar+expr+rpar
|
||||
factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6
|
||||
paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k
|
||||
paritem=paritem.setParseAction(parallel)
|
||||
term = paritem + ZeroOrMore((times|div)+paritem) # 7 * 5 / 4 - 3
|
||||
atom = number | function | varnames | lpar + expr + rpar
|
||||
factor << (atom + ZeroOrMore(exp + atom)).setParseAction(exp_parse_action) # 7^6
|
||||
paritem = factor + ZeroOrMore(Literal('||') + factor) # 5k || 4k
|
||||
paritem = paritem.setParseAction(parallel)
|
||||
term = paritem + ZeroOrMore((times | div) + paritem) # 7 * 5 / 4 - 3
|
||||
term = term.setParseAction(prod_parse_action)
|
||||
expr << Optional((plus|minus)) + term + ZeroOrMore((plus|minus)+term) # -5 + 4 - 3
|
||||
expr=expr.setParseAction(sum_parse_action)
|
||||
return (expr+stringEnd).parseString(string)[0]
|
||||
expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3
|
||||
expr = expr.setParseAction(sum_parse_action)
|
||||
return (expr + stringEnd).parseString(string)[0]
|
||||
|
||||
if __name__=='__main__':
|
||||
variables={'R1':2.0, 'R3':4.0}
|
||||
functions={'sin':numpy.sin, 'cos':numpy.cos}
|
||||
print "X",evaluator(variables, functions, "10000||sin(7+5)-6k")
|
||||
print "X",evaluator(variables, functions, "13")
|
||||
print evaluator({'R1': 2.0, 'R3':4.0}, {}, "13")
|
||||
if __name__ == '__main__':
|
||||
variables = {'R1': 2.0, 'R3': 4.0}
|
||||
functions = {'sin': numpy.sin, 'cos': numpy.cos}
|
||||
print "X", evaluator(variables, functions, "10000||sin(7+5)-6k")
|
||||
print "X", evaluator(variables, functions, "13")
|
||||
print evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13")
|
||||
|
||||
print evaluator({'e1':1,'e2':1.0,'R3':7,'V0':5,'R5':15,'I1':1,'R4':6}, {},"e2")
|
||||
print evaluator({'e1': 1, 'e2': 1.0, 'R3': 7, 'V0': 5, 'R5': 15, 'I1': 1, 'R4': 6}, {}, "e2")
|
||||
|
||||
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
|
||||
print evaluator({},{}, "-1")
|
||||
print evaluator({},{}, "-(7+5)")
|
||||
print evaluator({},{}, "-0.33")
|
||||
print evaluator({},{}, "-.33")
|
||||
print evaluator({},{}, "5+1*j")
|
||||
print evaluator({},{}, "j||1")
|
||||
print evaluator({},{}, "e^(j*pi)")
|
||||
print evaluator({},{}, "5+7 QWSEKO")
|
||||
print evaluator({}, {}, "-1")
|
||||
print evaluator({}, {}, "-(7+5)")
|
||||
print evaluator({}, {}, "-0.33")
|
||||
print evaluator({}, {}, "-.33")
|
||||
print evaluator({}, {}, "5+1*j")
|
||||
print evaluator({}, {}, "j||1")
|
||||
print evaluator({}, {}, "e^(j*pi)")
|
||||
print evaluator({}, {}, "5+7 QWSEKO")
|
||||
|
||||
@@ -37,7 +37,7 @@ from util import contextualize_text
|
||||
import responsetypes
|
||||
|
||||
# dict of tagname, Response Class -- this should come from auto-registering
|
||||
response_tag_dict = dict([(x.response_tag,x) for x in responsetypes.__all__])
|
||||
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
|
||||
|
||||
entry_types = ['textline', 'schematic', 'textbox', 'imageinput', 'optioninput', 'choicegroup', 'radiogroup', 'checkboxgroup']
|
||||
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
|
||||
@@ -57,13 +57,14 @@ global_context = {'random': random,
|
||||
'eia': eia}
|
||||
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["responseparam", "answer", "script","hintgroup"]
|
||||
html_problem_semantics = ["responseparam", "answer", "script", "hintgroup"]
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# main class for this module
|
||||
|
||||
|
||||
class LoncapaProblem(object):
|
||||
'''
|
||||
Main class for capa Problems.
|
||||
@@ -79,7 +80,7 @@ class LoncapaProblem(object):
|
||||
- id (string): identifier for this problem; often a filename (no spaces)
|
||||
- state (dict): student state
|
||||
- seed (int): random number generator seed (int)
|
||||
- system (I4xSystem): I4xSystem instance which provides OS, rendering, and user context
|
||||
- system (I4xSystem): I4xSystem instance which provides OS, rendering, and user context
|
||||
|
||||
'''
|
||||
|
||||
@@ -118,7 +119,7 @@ class LoncapaProblem(object):
|
||||
# the dict has keys = xml subtree of Response, values = Response instance
|
||||
self._preprocess_problem(self.tree)
|
||||
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
if not self.student_answers: # True when student_answers is an empty dict
|
||||
self.set_initial_display()
|
||||
|
||||
def do_reset(self):
|
||||
@@ -132,7 +133,7 @@ class LoncapaProblem(object):
|
||||
def set_initial_display(self):
|
||||
initial_answers = dict()
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder,'get_initial_display'):
|
||||
if hasattr(responder, 'get_initial_display'):
|
||||
initial_answers.update(responder.get_initial_display())
|
||||
|
||||
self.student_answers = initial_answers
|
||||
@@ -160,11 +161,11 @@ class LoncapaProblem(object):
|
||||
'''
|
||||
maxscore = 0
|
||||
for response, responder in self.responders.iteritems():
|
||||
if hasattr(responder,'get_max_score'):
|
||||
if hasattr(responder, 'get_max_score'):
|
||||
try:
|
||||
maxscore += responder.get_max_score()
|
||||
except Exception:
|
||||
log.debug('responder %s failed to properly return from get_max_score()' % responder) # FIXME
|
||||
log.debug('responder %s failed to properly return from get_max_score()' % responder) # FIXME
|
||||
raise
|
||||
else:
|
||||
maxscore += len(self.responder_answers[response])
|
||||
@@ -181,7 +182,7 @@ class LoncapaProblem(object):
|
||||
try:
|
||||
correct += self.correct_map.get_npoints(key)
|
||||
except Exception:
|
||||
log.error('key=%s, correct_map = %s' % (key,self.correct_map))
|
||||
log.error('key=%s, correct_map = %s' % (key, self.correct_map))
|
||||
raise
|
||||
|
||||
if (not self.student_answers) or len(self.student_answers) == 0:
|
||||
@@ -193,19 +194,19 @@ class LoncapaProblem(object):
|
||||
|
||||
def update_score(self, score_msg, queuekey):
|
||||
'''
|
||||
Deliver grading response (e.g. from async code checking) to
|
||||
the specific ResponseType that requested grading
|
||||
|
||||
Deliver grading response (e.g. from async code checking) to
|
||||
the specific ResponseType that requested grading
|
||||
|
||||
Returns an updated CorrectMap
|
||||
'''
|
||||
cmap = CorrectMap()
|
||||
cmap.update(self.correct_map)
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder,'update_score'):
|
||||
if hasattr(responder, 'update_score'):
|
||||
# Each LoncapaResponse will update the specific entries of 'cmap' that it's responsible for
|
||||
cmap = responder.update_score(score_msg, cmap, queuekey)
|
||||
self.correct_map.set_dict(cmap.get_dict())
|
||||
return cmap
|
||||
return cmap
|
||||
|
||||
def is_queued(self):
|
||||
'''
|
||||
@@ -232,7 +233,7 @@ class LoncapaProblem(object):
|
||||
newcmap = CorrectMap() # start new with empty CorrectMap
|
||||
# log.debug('Responders: %s' % self.responders)
|
||||
for responder in self.responders.values():
|
||||
results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading
|
||||
results = responder.evaluate_answers(answers, oldcmap) # call the responsetype instance to do the actual grading
|
||||
newcmap.update(results)
|
||||
self.correct_map = newcmap
|
||||
# log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
|
||||
@@ -285,23 +286,23 @@ class LoncapaProblem(object):
|
||||
file = inc.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = self.system.filestore.open(file) # open using I4xSystem OSFS filestore
|
||||
ifp = self.system.filestore.open(file) # open using I4xSystem OSFS filestore
|
||||
except Exception as err:
|
||||
log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True)))
|
||||
log.error('Cannot find file %s in %s' % (file,self.system.filestore))
|
||||
log.error('Error %s in problem xml include: %s' % (err, etree.tostring(inc, pretty_print=True)))
|
||||
log.error('Cannot find file %s in %s' % (file, self.system.filestore))
|
||||
if not self.system.get('DEBUG'): # if debugging, don't fail - just log error
|
||||
raise
|
||||
else: continue
|
||||
try:
|
||||
incxml = etree.XML(ifp.read()) # read in and convert to XML
|
||||
except Exception as err:
|
||||
log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True)))
|
||||
log.error('Error %s in problem xml include: %s' % (err, etree.tostring(inc, pretty_print=True)))
|
||||
log.error('Cannot parse XML in %s' % (file))
|
||||
if not self.system.get('DEBUG'): # if debugging, don't fail - just log error
|
||||
raise
|
||||
else: continue
|
||||
parent = inc.getparent() # insert new XML into tree in place of inlcude
|
||||
parent.insert(parent.index(inc),incxml)
|
||||
parent.insert(parent.index(inc), incxml)
|
||||
parent.remove(inc)
|
||||
log.debug('Included %s into %s' % (file, self.problem_id))
|
||||
|
||||
@@ -330,9 +331,9 @@ class LoncapaProblem(object):
|
||||
abs_dir = os.path.normpath(dir)
|
||||
log.debug("appending to path: %s" % abs_dir)
|
||||
path.append(abs_dir)
|
||||
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
|
||||
'''
|
||||
Extract content of <script>...</script> from the problem.xml file, and exec it in the
|
||||
@@ -353,7 +354,7 @@ class LoncapaProblem(object):
|
||||
return context
|
||||
|
||||
def _execute_scripts(self, scripts, context):
|
||||
'''
|
||||
'''
|
||||
Executes scripts in the given context.
|
||||
'''
|
||||
original_path = sys.path
|
||||
@@ -391,7 +392,7 @@ class LoncapaProblem(object):
|
||||
|
||||
Used by get_html.
|
||||
'''
|
||||
if problemtree.tag=='script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
|
||||
if problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type'):
|
||||
# leave javascript intact.
|
||||
return problemtree
|
||||
|
||||
@@ -424,8 +425,8 @@ class LoncapaProblem(object):
|
||||
'status': status,
|
||||
'id': problemtree.get('id'),
|
||||
'feedback': {'message': msg,
|
||||
'hint' : hint,
|
||||
'hintmode' : hintmode,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,
|
||||
}
|
||||
},
|
||||
use='capa_input')
|
||||
@@ -443,7 +444,7 @@ class LoncapaProblem(object):
|
||||
if tree.tag in html_transforms:
|
||||
tree.tag = html_transforms[problemtree.tag]['tag']
|
||||
else:
|
||||
for (key, value) in problemtree.items(): # copy attributes over if not innocufying
|
||||
for (key, value) in problemtree.items(): # copy attributes over if not innocufying
|
||||
tree.set(key, value)
|
||||
|
||||
tree.text = problemtree.text
|
||||
@@ -466,7 +467,7 @@ class LoncapaProblem(object):
|
||||
self.responders = {}
|
||||
for response in tree.xpath('//' + "|//".join(response_tag_dict)):
|
||||
response_id_str = self.problem_id + "_" + str(response_id)
|
||||
response.set('id',response_id_str) # create and save ID for this response
|
||||
response.set('id', response_id_str) # create and save ID for this response
|
||||
response_id += 1
|
||||
|
||||
answer_id = 1
|
||||
@@ -478,7 +479,7 @@ class LoncapaProblem(object):
|
||||
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
|
||||
answer_id = answer_id + 1
|
||||
|
||||
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
|
||||
responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response
|
||||
self.responders[response] = responder # save in list in self
|
||||
|
||||
# get responder answers (do this only once, since there may be a performance cost, eg with externalresponse)
|
||||
@@ -487,9 +488,9 @@ class LoncapaProblem(object):
|
||||
try:
|
||||
self.responder_answers[response] = self.responders[response].get_answers()
|
||||
except:
|
||||
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
|
||||
log.debug('responder %s failed to properly return get_answers()' % self.responders[response]) # FIXME
|
||||
raise
|
||||
|
||||
|
||||
# <solution>...</solution> may not be associated with any specific response; give IDs for those separately
|
||||
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
|
||||
solution_id = 1
|
||||
|
||||
@@ -34,9 +34,10 @@ class DemoSystem(object):
|
||||
context_dict.update(context)
|
||||
return self.lookup.get_template(template_filename).render(**context_dict)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Check Problem Files')
|
||||
parser.add_argument("command", choices=['test', 'show']) # Watch? Render? Open?
|
||||
parser.add_argument("command", choices=['test', 'show']) # Watch? Render? Open?
|
||||
parser.add_argument("files", nargs="+", type=argparse.FileType('r'))
|
||||
parser.add_argument("--seed", required=False, type=int)
|
||||
parser.add_argument("--log-level", required=False, default="INFO",
|
||||
@@ -67,13 +68,14 @@ def main():
|
||||
|
||||
# In case we want to do anything else here.
|
||||
|
||||
|
||||
def command_show(problem):
|
||||
"""Display the text for this problem"""
|
||||
print problem.get_html()
|
||||
|
||||
|
||||
|
||||
def command_test(problem):
|
||||
# We're going to trap stdout/stderr from the problems (yes, some print)
|
||||
# We're going to trap stdout/stderr from the problems (yes, some print)
|
||||
old_stdout, old_stderr = sys.stdout, sys.stderr
|
||||
try:
|
||||
sys.stdout = StringIO()
|
||||
@@ -82,7 +84,7 @@ def command_test(problem):
|
||||
check_that_suggested_answers_work(problem)
|
||||
check_that_blanks_fail(problem)
|
||||
|
||||
log_captured_output(sys.stdout,
|
||||
log_captured_output(sys.stdout,
|
||||
"captured stdout from {0}".format(problem))
|
||||
log_captured_output(sys.stderr,
|
||||
"captured stderr from {0}".format(problem))
|
||||
@@ -91,9 +93,10 @@ def command_test(problem):
|
||||
finally:
|
||||
sys.stdout, sys.stderr = old_stdout, old_stderr
|
||||
|
||||
|
||||
def check_that_blanks_fail(problem):
|
||||
"""Leaving it blank should never work. Neither should a space."""
|
||||
blank_answers = dict((answer_id, u"")
|
||||
blank_answers = dict((answer_id, u"")
|
||||
for answer_id in problem.get_question_answers())
|
||||
grading_results = problem.grade_answers(blank_answers)
|
||||
try:
|
||||
@@ -113,7 +116,7 @@ def check_that_suggested_answers_work(problem):
|
||||
* Displayed answers use units but acceptable ones do not.
|
||||
- L1e0.xml
|
||||
- Presents itself as UndefinedVariable (when it tries to pass to calc)
|
||||
* "a or d" is what's displayed, but only "a" or "d" is accepted, not the
|
||||
* "a or d" is what's displayed, but only "a" or "d" is accepted, not the
|
||||
string "a or d".
|
||||
- L1-e00.xml
|
||||
"""
|
||||
@@ -129,14 +132,14 @@ def check_that_suggested_answers_work(problem):
|
||||
log.debug("Real answers: {0}".format(real_answers))
|
||||
if real_answers:
|
||||
try:
|
||||
real_results = dict((answer_id, result) for answer_id, result
|
||||
real_results = dict((answer_id, result) for answer_id, result
|
||||
in problem.grade_answers(all_answers).items()
|
||||
if answer_id in real_answers)
|
||||
log.debug(real_results)
|
||||
assert(all(result == 'correct'
|
||||
for answer_id, result in real_results.items()))
|
||||
except UndefinedVariable as uv_exc:
|
||||
log.error("The variable \"{0}\" specified in the ".format(uv_exc) +
|
||||
log.error("The variable \"{0}\" specified in the ".format(uv_exc) +
|
||||
"solution isn't recognized (is it a units measure?).")
|
||||
except AssertionError:
|
||||
log.error("The following generated answers were not accepted for {0}:"
|
||||
@@ -148,6 +151,7 @@ def check_that_suggested_answers_work(problem):
|
||||
log.error("Uncaught error in {0}".format(problem))
|
||||
log.exception(ex)
|
||||
|
||||
|
||||
def log_captured_output(output_stream, stream_name):
|
||||
output_stream.seek(0)
|
||||
output_text = output_stream.read()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
# Used by responsetypes and capa_problem
|
||||
|
||||
|
||||
class CorrectMap(object):
|
||||
'''
|
||||
Stores map between answer_id and response evaluation result for each question
|
||||
@@ -18,11 +19,11 @@ class CorrectMap(object):
|
||||
|
||||
Behaves as a dict.
|
||||
'''
|
||||
def __init__(self,*args,**kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cmap = dict() # start with empty dict
|
||||
self.items = self.cmap.items
|
||||
self.keys = self.cmap.keys
|
||||
self.set(*args,**kwargs)
|
||||
self.set(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, *args, **kwargs):
|
||||
return self.cmap.__getitem__(*args, **kwargs)
|
||||
@@ -35,9 +36,9 @@ class CorrectMap(object):
|
||||
self.cmap[answer_id] = {'correctness': correctness,
|
||||
'npoints': npoints,
|
||||
'msg': msg,
|
||||
'hint' : hint,
|
||||
'hintmode' : hintmode,
|
||||
'queuekey' : queuekey,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,
|
||||
'queuekey': queuekey,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
@@ -49,69 +50,69 @@ class CorrectMap(object):
|
||||
'''
|
||||
return self.cmap
|
||||
|
||||
def set_dict(self,correct_map):
|
||||
def set_dict(self, correct_map):
|
||||
'''
|
||||
set internal dict to provided correct_map dict
|
||||
for graceful migration, if correct_map is a one-level dict, then convert it to the new
|
||||
dict of dicts format.
|
||||
'''
|
||||
if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict):
|
||||
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
|
||||
self.__init__() # empty current dict
|
||||
for k in correct_map: self.set(k,correct_map[k]) # create new dict entries
|
||||
for k in correct_map: self.set(k, correct_map[k]) # create new dict entries
|
||||
else:
|
||||
self.cmap = correct_map
|
||||
|
||||
def is_correct(self,answer_id):
|
||||
def is_correct(self, answer_id):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
|
||||
return None
|
||||
|
||||
def is_queued(self,answer_id):
|
||||
def is_queued(self, answer_id):
|
||||
return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] is not None
|
||||
|
||||
def is_right_queuekey(self, answer_id, test_key):
|
||||
return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] == test_key
|
||||
|
||||
def get_npoints(self,answer_id):
|
||||
def get_npoints(self, answer_id):
|
||||
if self.is_correct(answer_id):
|
||||
npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct
|
||||
npoints = self.cmap[answer_id].get('npoints', 1) # default to 1 point if correct
|
||||
return npoints or 1
|
||||
return 0 # if not correct, return 0
|
||||
|
||||
def set_property(self,answer_id,property,value):
|
||||
def set_property(self, answer_id, property, value):
|
||||
if answer_id in self.cmap: self.cmap[answer_id][property] = value
|
||||
else: self.cmap[answer_id] = {property:value}
|
||||
else: self.cmap[answer_id] = {property: value}
|
||||
|
||||
def get_property(self,answer_id,property,default=None):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id].get(property,default)
|
||||
def get_property(self, answer_id, property, default=None):
|
||||
if answer_id in self.cmap: return self.cmap[answer_id].get(property, default)
|
||||
return default
|
||||
|
||||
def get_correctness(self,answer_id):
|
||||
return self.get_property(answer_id,'correctness')
|
||||
def get_correctness(self, answer_id):
|
||||
return self.get_property(answer_id, 'correctness')
|
||||
|
||||
def get_msg(self,answer_id):
|
||||
return self.get_property(answer_id,'msg','')
|
||||
def get_msg(self, answer_id):
|
||||
return self.get_property(answer_id, 'msg', '')
|
||||
|
||||
def get_hint(self,answer_id):
|
||||
return self.get_property(answer_id,'hint','')
|
||||
def get_hint(self, answer_id):
|
||||
return self.get_property(answer_id, 'hint', '')
|
||||
|
||||
def get_hintmode(self,answer_id):
|
||||
return self.get_property(answer_id,'hintmode',None)
|
||||
def get_hintmode(self, answer_id):
|
||||
return self.get_property(answer_id, 'hintmode', None)
|
||||
|
||||
def set_hint_and_mode(self,answer_id,hint,hintmode):
|
||||
def set_hint_and_mode(self, answer_id, hint, hintmode):
|
||||
'''
|
||||
- hint : (string) HTML text for hint
|
||||
- hintmode : (string) mode for hint display ('always' or 'on_request')
|
||||
'''
|
||||
self.set_property(answer_id,'hint',hint)
|
||||
self.set_property(answer_id,'hintmode',hintmode)
|
||||
self.set_property(answer_id, 'hint', hint)
|
||||
self.set_property(answer_id, 'hintmode', hintmode)
|
||||
|
||||
def update(self,other_cmap):
|
||||
def update(self, other_cmap):
|
||||
'''
|
||||
Update this CorrectMap with the contents of another CorrectMap
|
||||
'''
|
||||
if not isinstance(other_cmap,CorrectMap):
|
||||
if not isinstance(other_cmap, CorrectMap):
|
||||
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
||||
self.cmap.update(other_cmap.get_dict())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
""" Standard resistor codes.
|
||||
http://en.wikipedia.org/wiki/Electronic_color_code
|
||||
"""
|
||||
E6=[10,15,22,33,47,68]
|
||||
E12=[10,12,15,18,22,27,33,39,47,56,68,82]
|
||||
E24=[10,12,15,18,22,27,33,39,47,56,68,82,11,13,16,20,24,30,36,43,51,62,75,91]
|
||||
E48=[100,121,147,178,215,261,316,383,464,562,681,825,105,127,154,187,226,274,332,402,487,590,715,866,110,133,162,196,237,287,348,422,511,619,750,909,115,140,169,205,249,301,365,442,536,649,787,953]
|
||||
E96=[100,121,147,178,215,261,316,383,464,562,681,825,102,124,150,182,221,267,324,392,475,576,698,845,105,127,154,187,226,274,332,402,487,590,715,866,107,130,158,191,232,280,340,412,499,604,732,887,110,133,162,196,237,287,348,422,511,619,750,909,113,137,165,200,243,294,357,432,523,634,768,931,115,140,169,205,249,301,365,442,536,649,787,953,118,143,174,210,255,309,374,453,549,665,806,976]
|
||||
E192=[100,121,147,178,215,261,316,383,464,562,681,825,101,123,149,180,218,264,320,388,470,569,690,835,102,124,150,182,221,267,324,392,475,576,698,845,104,126,152,184,223,271,328,397,481,583,706,856,105,127,154,187,226,274,332,402,487,590,715,866,106,129,156,189,229,277,336,407,493,597,723,876,107,130,158,191,232,280,340,412,499,604,732,887,109,132,160,193,234,284,344,417,505,612,741,898,110,133,162,196,237,287,348,422,511,619,750,909,111,135,164,198,240,291,352,427,517,626,759,920,113,137,165,200,243,294,357,432,523,634,768,931,114,138,167,203,246,298,361,437,530,642,777,942,115,140,169,205,249,301,365,442,536,649,787,953,117,142,172,208,252,305,370,448,542,657,796,965,118,143,174,210,255,309,374,453,549,665,806,976,120,145,176,213,258,312,379,459,556,673,816,988]
|
||||
|
||||
E6 = [10, 15, 22, 33, 47, 68]
|
||||
E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
|
||||
E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91]
|
||||
E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953]
|
||||
E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976]
|
||||
E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988]
|
||||
|
||||
@@ -26,37 +26,39 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
|
||||
|
||||
import logging
|
||||
import re
|
||||
import shlex # for splitting quoted strings
|
||||
import shlex # for splitting quoted strings
|
||||
|
||||
from lxml import etree
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
def get_input_xml_tags():
|
||||
''' Eventually, this will be for all registered input types '''
|
||||
return SimpleInput.get_xml_tags()
|
||||
|
||||
class SimpleInput():# XModule
|
||||
|
||||
class SimpleInput():# XModule
|
||||
'''
|
||||
Type for simple inputs -- plain HTML with a form element
|
||||
'''
|
||||
|
||||
xml_tags = {} ## Maps tags to functions
|
||||
xml_tags = {} # # Maps tags to functions
|
||||
|
||||
def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
|
||||
def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'):
|
||||
'''
|
||||
Instantiate a SimpleInput class. Arguments:
|
||||
|
||||
- system : I4xSystem instance which provides OS, rendering, and user context
|
||||
- system : I4xSystem instance which provides OS, rendering, and user context
|
||||
- xml : Element tree of this Input element
|
||||
- item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string
|
||||
- track_url : URL used for tracking - string
|
||||
- state : a dictionary with optional keys:
|
||||
- state : a dictionary with optional keys:
|
||||
* Value
|
||||
* ID
|
||||
* Status (answered, unanswered, unsubmitted)
|
||||
* Feedback (dictionary containing keys for hints, errors, or other
|
||||
* Feedback (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt)
|
||||
- use :
|
||||
'''
|
||||
@@ -66,11 +68,11 @@ class SimpleInput():# XModule
|
||||
self.system = system
|
||||
if not state: state = {}
|
||||
|
||||
## ID should only come from one place.
|
||||
## ID should only come from one place.
|
||||
## If it comes from multiple, we use state first, XML second, and parameter
|
||||
## third. Since we don't make this guarantee, we can swap this around in
|
||||
## the future if there's a more logical order.
|
||||
if item_id: self.id = item_id
|
||||
## third. Since we don't make this guarantee, we can swap this around in
|
||||
## the future if there's a more logical order.
|
||||
if item_id: self.id = item_id
|
||||
if xml.get('id'): self.id = xml.get('id')
|
||||
if 'id' in state: self.id = state['id']
|
||||
|
||||
@@ -81,14 +83,14 @@ class SimpleInput():# XModule
|
||||
self.msg = ''
|
||||
feedback = state.get('feedback')
|
||||
if feedback is not None:
|
||||
self.msg = feedback.get('message','')
|
||||
self.hint = feedback.get('hint','')
|
||||
self.hintmode = feedback.get('hintmode',None)
|
||||
|
||||
self.msg = feedback.get('message', '')
|
||||
self.hint = feedback.get('hint', '')
|
||||
self.hintmode = feedback.get('hintmode', None)
|
||||
|
||||
# put hint above msg if to be displayed
|
||||
if self.hintmode == 'always':
|
||||
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
|
||||
|
||||
|
||||
self.status = 'unanswered'
|
||||
if 'status' in state:
|
||||
self.status = state['status']
|
||||
@@ -104,17 +106,20 @@ class SimpleInput():# XModule
|
||||
def get_html(self):
|
||||
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
|
||||
|
||||
|
||||
def register_render_function(fn, names=None, cls=SimpleInput):
|
||||
if names is None:
|
||||
SimpleInput.xml_tags[fn.__name__] = fn
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def wrapped():
|
||||
return fn
|
||||
return wrapped
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def optioninput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -124,7 +129,7 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
|
||||
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
||||
'''
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
options = element.get('options')
|
||||
if not options:
|
||||
raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
|
||||
@@ -134,14 +139,14 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
oset = [x[1:-1] for x in list(oset)]
|
||||
|
||||
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
|
||||
osetdict = [(oset[x],oset[x]) for x in range(len(oset)) ] # make ordered list with (key,value) same
|
||||
osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same
|
||||
# TODO: allow ordering to be randomized
|
||||
|
||||
context={'id':eid,
|
||||
'value':value,
|
||||
'state':status,
|
||||
'msg':msg,
|
||||
'options':osetdict,
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'msg': msg,
|
||||
'options': osetdict,
|
||||
}
|
||||
|
||||
html = render_template("optioninput.html", context)
|
||||
@@ -149,6 +154,7 @@ def optioninput(element, value, status, render_template, msg=''):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
@register_render_function
|
||||
@@ -159,23 +165,23 @@ def choicegroup(element, value, status, render_template, msg=''):
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
|
||||
ie random, top, bottom.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
if element.get('type') == "MultipleChoice":
|
||||
type="radio"
|
||||
type = "radio"
|
||||
elif element.get('type') == "TrueFalse":
|
||||
type="checkbox"
|
||||
type = "checkbox"
|
||||
else:
|
||||
type="radio"
|
||||
choices=[]
|
||||
type = "radio"
|
||||
choices = []
|
||||
for choice in element:
|
||||
if not choice.tag=='choice':
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
|
||||
ctext = ""
|
||||
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
|
||||
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
|
||||
if choice.text is not None:
|
||||
ctext += choice.text # TODO: fix order?
|
||||
choices.append((choice.get("name"),ctext))
|
||||
context={'id':eid, 'value':value, 'state':status, 'input_type':type, 'choices':choices, 'inline':True, 'name_array_suffix':''}
|
||||
choices.append((choice.get("name"), ctext))
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'inline': True, 'name_array_suffix': ''}
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
@@ -193,9 +199,9 @@ def extract_choices(element):
|
||||
choices = []
|
||||
|
||||
for choice in element:
|
||||
if not choice.tag=='choice':
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.extract_choices] \
|
||||
Expected a <choice> tag; got %s instead"
|
||||
Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
choice_text = ''.join([etree.tostring(x) for x in choice])
|
||||
|
||||
@@ -203,6 +209,7 @@ def extract_choices(element):
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
@register_render_function
|
||||
@@ -211,15 +218,16 @@ def radiogroup(element, value, status, render_template, msg=''):
|
||||
Radio button inputs: (multiple choice)
|
||||
'''
|
||||
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = { 'id':eid, 'value':value, 'state':status, 'input_type': 'radio', 'choices':choices, 'inline': False, 'name_array_suffix': '[]' }
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
@register_render_function
|
||||
@@ -228,44 +236,46 @@ def checkboxgroup(element, value, status, render_template, msg=''):
|
||||
Checkbox inputs: (select one or more choices)
|
||||
'''
|
||||
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = { 'id':eid, 'value':value, 'state':status, 'input_type': 'checkbox', 'choices':choices, 'inline': False, 'name_array_suffix': '[]' }
|
||||
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
@register_render_function
|
||||
def textline(element, value, status, render_template, msg=""):
|
||||
'''
|
||||
Simple text line input, with optional size specification.
|
||||
'''
|
||||
if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||
return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg)
|
||||
eid=element.get('id')
|
||||
return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg)
|
||||
eid = element.get('id')
|
||||
if eid is None:
|
||||
msg = 'textline has no id: it probably appears outside of a known response type'
|
||||
msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','<unavailable>')
|
||||
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
|
||||
raise Exception(msg)
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value,escapedict) # otherwise, answers with quotes in them crashes the system!
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg, 'hidden': hidden}
|
||||
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system!
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden}
|
||||
html = render_template("textinput.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
if True: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
if True: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
|
||||
raise
|
||||
return xhtml
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -278,16 +288,17 @@ def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
uses a <span id=display_eid>`{::}`</span>
|
||||
and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
|
||||
'msg':msg, 'hidden' : hidden,
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
|
||||
'msg': msg, 'hidden': hidden,
|
||||
}
|
||||
html = render_template("textinput_dynamath.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
## TODO: Make a wrapper for <codeinput>
|
||||
@register_render_function
|
||||
@@ -297,31 +308,32 @@ def textbox(element, value, status, render_template, msg=''):
|
||||
evaluating the code, eg error messages, and output from the code tests.
|
||||
|
||||
'''
|
||||
eid=element.get('id')
|
||||
count = int(eid.split('_')[-2])-1 # HACK
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
rows = element.get('rows') or '30'
|
||||
cols = element.get('cols') or '80'
|
||||
mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
|
||||
hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
linenumbers = element.get('linenumbers') # for CodeMirror
|
||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
|
||||
'mode':mode, 'linenumbers':linenumbers,
|
||||
'rows':rows, 'cols':cols,
|
||||
'hidden' : hidden,
|
||||
hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
linenumbers = element.get('linenumbers') # for CodeMirror
|
||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg,
|
||||
'mode': mode, 'linenumbers': linenumbers,
|
||||
'rows': rows, 'cols': cols,
|
||||
'hidden': hidden,
|
||||
}
|
||||
html = render_template("textbox.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
newmsg = 'error %s in rendering message' % (str(err).replace('<','<'))
|
||||
newmsg += '<br/>Original message: %s' % msg.replace('<','<')
|
||||
newmsg = 'error %s in rendering message' % (str(err).replace('<', '<'))
|
||||
newmsg += '<br/>Original message: %s' % msg.replace('<', '<')
|
||||
context['msg'] = newmsg
|
||||
html = render_template("textbox.html", context)
|
||||
xhtml = etree.XML(html)
|
||||
return xhtml
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@register_render_function
|
||||
def schematic(element, value, status, render_template, msg=''):
|
||||
@@ -333,19 +345,20 @@ def schematic(element, value, status, render_template, msg=''):
|
||||
initial_value = element.get('initial_value')
|
||||
submit_analyses = element.get('submit_analyses')
|
||||
context = {
|
||||
'id':eid,
|
||||
'value':value,
|
||||
'initial_value':initial_value,
|
||||
'state':status,
|
||||
'width':width,
|
||||
'height':height,
|
||||
'parts':parts,
|
||||
'analyses':analyses,
|
||||
'submit_analyses':submit_analyses,
|
||||
'id': eid,
|
||||
'value': value,
|
||||
'initial_value': initial_value,
|
||||
'state': status,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'parts': parts,
|
||||
'analyses': analyses,
|
||||
'submit_analyses': submit_analyses,
|
||||
}
|
||||
html = render_template("schematicinput.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
### TODO: Move out of inputtypes
|
||||
@register_render_function
|
||||
@@ -357,17 +370,17 @@ def math(element, value, status, render_template, msg=''):
|
||||
Examples:
|
||||
|
||||
<m display="jsmath">$\displaystyle U(r)=4 U_0 </m>
|
||||
<m>$r_0$</m>
|
||||
<m>$r_0$</m>
|
||||
|
||||
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
|
||||
|
||||
TODO: use shorter tags (but this will require converting problem XML files!)
|
||||
'''
|
||||
mathstr = re.sub('\$(.*)\$','[mathjaxinline]\\1[/mathjaxinline]',element.text)
|
||||
mathstr = re.sub('\$(.*)\$', '[mathjaxinline]\\1[/mathjaxinline]', element.text)
|
||||
mtag = 'mathjax'
|
||||
if not '\\displaystyle' in mathstr: mtag += 'inline'
|
||||
else: mathstr = mathstr.replace('\\displaystyle','')
|
||||
mathstr = mathstr.replace('mathjaxinline]','%s]'%mtag)
|
||||
else: mathstr = mathstr.replace('\\displaystyle', '')
|
||||
mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
|
||||
|
||||
#if '\\displaystyle' in mathstr:
|
||||
# isinline = False
|
||||
@@ -376,13 +389,13 @@ def math(element, value, status, render_template, msg=''):
|
||||
# isinline = True
|
||||
# html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
|
||||
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,saxutils.escape(element.tail))
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<','<')
|
||||
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<','<')
|
||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<', '<')
|
||||
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<', '<')
|
||||
msg += "</font></html>"
|
||||
log.error(msg)
|
||||
return etree.XML(msg)
|
||||
@@ -393,6 +406,7 @@ def math(element, value, status, render_template, msg=''):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def solution(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -401,19 +415,20 @@ def solution(element, value, status, render_template, msg=''):
|
||||
is pressed. Note that the solution content is NOT sent with the HTML. It is obtained
|
||||
by a JSON call.
|
||||
'''
|
||||
eid=element.get('id')
|
||||
eid = element.get('id')
|
||||
size = element.get('size')
|
||||
context = {'id':eid,
|
||||
'value':value,
|
||||
'state':status,
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'size': size,
|
||||
'msg':msg,
|
||||
'msg': msg,
|
||||
}
|
||||
html = render_template("solutionspan.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@register_render_function
|
||||
def imageinput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
@@ -429,12 +444,12 @@ def imageinput(element, value, status, render_template, msg=''):
|
||||
width = element.get('width')
|
||||
|
||||
# if value is of the form [x,y] then parse it and send along coordinates of previous answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]',value.strip().replace(' ',''))
|
||||
m = re.match('\[([0-9]+),([0-9]+)]', value.strip().replace(' ', ''))
|
||||
if m:
|
||||
(gx,gy) = [int(x)-15 for x in m.groups()]
|
||||
(gx, gy) = [int(x) - 15 for x in m.groups()]
|
||||
else:
|
||||
(gx,gy) = (0,0)
|
||||
|
||||
(gx, gy) = (0, 0)
|
||||
|
||||
context = {
|
||||
'id': eid,
|
||||
'value': value,
|
||||
@@ -443,7 +458,7 @@ def imageinput(element, value, status, render_template, msg=''):
|
||||
'src': src,
|
||||
'gx': gx,
|
||||
'gy': gy,
|
||||
'state': status, # to change
|
||||
'state': status, # to change
|
||||
'msg': msg, # to change
|
||||
}
|
||||
html = render_template("imageinput.html", context)
|
||||
|
||||
@@ -26,25 +26,28 @@ from calc import evaluator, UndefinedVariable
|
||||
from correctmap import CorrectMap
|
||||
from util import *
|
||||
from lxml import etree
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
|
||||
|
||||
class LoncapaProblemError(Exception):
|
||||
'''
|
||||
Error in specification of a problem
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class ResponseError(Exception):
|
||||
'''
|
||||
Error for failure in processing a response
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class StudentInputError(Exception):
|
||||
pass
|
||||
|
||||
@@ -52,6 +55,7 @@ class StudentInputError(Exception):
|
||||
#
|
||||
# Main base class for CAPA responsetypes
|
||||
|
||||
|
||||
class LoncapaResponse(object):
|
||||
'''
|
||||
Base class for CAPA responsetypes. Each response type (ie a capa question,
|
||||
@@ -60,7 +64,7 @@ class LoncapaResponse(object):
|
||||
|
||||
- get_score : evaluate the given student answers, and return a CorrectMap
|
||||
- get_answers : provide a dict of the expected answers for this problem
|
||||
|
||||
|
||||
Each subclass must also define the following attributes:
|
||||
|
||||
- response_tag : xhtml tag identifying this response (used in auto-registering)
|
||||
@@ -81,7 +85,7 @@ class LoncapaResponse(object):
|
||||
- hint_tag : xhtml tag identifying hint associated with this response inside hintgroup
|
||||
|
||||
'''
|
||||
__metaclass__=abc.ABCMeta # abc = Abstract Base Class
|
||||
__metaclass__ = abc.ABCMeta # abc = Abstract Base Class
|
||||
|
||||
response_tag = None
|
||||
hint_tag = None
|
||||
@@ -89,7 +93,7 @@ class LoncapaResponse(object):
|
||||
max_inputfields = None
|
||||
allowed_inputfields = []
|
||||
required_attributes = []
|
||||
|
||||
|
||||
def __init__(self, xml, inputfields, context, system=None):
|
||||
'''
|
||||
Init is passed the following arguments:
|
||||
@@ -97,7 +101,7 @@ class LoncapaResponse(object):
|
||||
- xml : ElementTree of this Response
|
||||
- inputfields : ordered list of ElementTrees for each input entry field in this Response
|
||||
- context : script processor context
|
||||
- system : I4xSystem instance which provides OS, rendering, and user context
|
||||
- system : I4xSystem instance which provides OS, rendering, and user context
|
||||
|
||||
'''
|
||||
self.xml = xml
|
||||
@@ -107,35 +111,35 @@ class LoncapaResponse(object):
|
||||
|
||||
for abox in inputfields:
|
||||
if abox.tag not in self.allowed_inputfields:
|
||||
msg = "%s: cannot have input field %s" % (unicode(self),abox.tag)
|
||||
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
msg = "%s: cannot have input field %s" % (unicode(self), abox.tag)
|
||||
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
if self.max_inputfields and len(inputfields)>self.max_inputfields:
|
||||
msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields)
|
||||
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
if self.max_inputfields and len(inputfields) > self.max_inputfields:
|
||||
msg = "%s: cannot have more than %s input fields" % (unicode(self), self.max_inputfields)
|
||||
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
for prop in self.required_attributes:
|
||||
if not xml.get(prop):
|
||||
msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop)
|
||||
msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>')
|
||||
msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self), prop)
|
||||
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response
|
||||
if self.max_inputfields==1:
|
||||
self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response
|
||||
if self.max_inputfields == 1:
|
||||
self.answer_id = self.answer_ids[0] # for convenience
|
||||
|
||||
self.default_answer_map = {} # dict for default answer map (provided in input elements)
|
||||
for entry in self.inputfields:
|
||||
answer = entry.get('correct_answer')
|
||||
answer = entry.get('correct_answer')
|
||||
if answer:
|
||||
self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
|
||||
if hasattr(self,'setup_response'):
|
||||
if hasattr(self, 'setup_response'):
|
||||
self.setup_response()
|
||||
|
||||
def render_html(self,renderer):
|
||||
def render_html(self, renderer):
|
||||
'''
|
||||
Return XHTML Element tree representation of this Response.
|
||||
|
||||
@@ -150,7 +154,7 @@ class LoncapaResponse(object):
|
||||
tree.tail = self.xml.tail
|
||||
return tree
|
||||
|
||||
def evaluate_answers(self,student_answers,old_cmap):
|
||||
def evaluate_answers(self, student_answers, old_cmap):
|
||||
'''
|
||||
Called by capa_problem.LoncapaProblem to evaluate student answers, and to
|
||||
generate hints (if any).
|
||||
@@ -190,14 +194,14 @@ class LoncapaResponse(object):
|
||||
'''
|
||||
if not hintfn in self.context:
|
||||
msg = 'missing specified hint function %s in script context' % hintfn
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
try:
|
||||
self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap)
|
||||
except Exception as err:
|
||||
msg = 'Error %s in evaluating hint function %s' % (err,hintfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
raise ResponseError(msg)
|
||||
return
|
||||
|
||||
@@ -214,18 +218,18 @@ class LoncapaResponse(object):
|
||||
# <text>You have inverted the slope in the question. The slope is
|
||||
# (y2-y1)/(x2 - x1) you have the slope as (x2-x1)/(y2-y1).</text>
|
||||
# </hintpart>
|
||||
# </hintgroup>
|
||||
# </hintgroup>
|
||||
# </formularesponse>
|
||||
|
||||
if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self,'check_hint_condition'):
|
||||
if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self, 'check_hint_condition'):
|
||||
rephints = hintgroup.findall(self.hint_tag)
|
||||
hints_to_show = self.check_hint_condition(rephints,student_answers)
|
||||
hintmode = hintgroup.get('mode','always') # can be 'on_request' or 'always' (default)
|
||||
hints_to_show = self.check_hint_condition(rephints, student_answers)
|
||||
hintmode = hintgroup.get('mode', 'always') # can be 'on_request' or 'always' (default)
|
||||
for hintpart in hintgroup.findall('hintpart'):
|
||||
if hintpart.get('on') in hints_to_show:
|
||||
hint_text = hintpart.find('text').text
|
||||
aid = self.answer_ids[-1] # make the hint appear after the last answer box in this response
|
||||
new_cmap.set_hint_and_mode(aid,hint_text,hintmode)
|
||||
new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
|
||||
log.debug('after hint: new_cmap = %s' % new_cmap)
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -249,7 +253,7 @@ class LoncapaResponse(object):
|
||||
'''
|
||||
pass
|
||||
|
||||
def check_hint_condition(self,hxml_set,student_answers):
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
'''
|
||||
Return a list of hints to show.
|
||||
|
||||
@@ -266,6 +270,7 @@ class LoncapaResponse(object):
|
||||
def __unicode__(self):
|
||||
return u'LoncapaProblem Response %s' % self.xml.tag
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
class ChoiceResponse(LoncapaResponse):
|
||||
'''
|
||||
@@ -310,8 +315,8 @@ class ChoiceResponse(LoncapaResponse):
|
||||
|
||||
'''
|
||||
|
||||
response_tag = 'choiceresponse'
|
||||
max_inputfields = 1
|
||||
response_tag = 'choiceresponse'
|
||||
max_inputfields = 1
|
||||
allowed_inputfields = ['checkboxgroup', 'radiogroup']
|
||||
|
||||
def setup_response(self):
|
||||
@@ -328,17 +333,17 @@ class ChoiceResponse(LoncapaResponse):
|
||||
Initialize name attributes in <choice> tags for this response.
|
||||
'''
|
||||
|
||||
for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice',
|
||||
for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice',
|
||||
id=self.xml.get('id'))):
|
||||
choice.set("name", "choice_"+str(index))
|
||||
choice.set("name", "choice_" + str(index))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
|
||||
student_answer = student_answers.get(self.answer_id, [])
|
||||
|
||||
|
||||
if not isinstance(student_answer, list):
|
||||
student_answer = [student_answer]
|
||||
|
||||
|
||||
student_answer = set(student_answer)
|
||||
|
||||
required_selected = len(self.correct_choices - student_answer) == 0
|
||||
@@ -347,15 +352,16 @@ class ChoiceResponse(LoncapaResponse):
|
||||
correct = required_selected & no_extra_selected
|
||||
|
||||
if correct:
|
||||
return CorrectMap(self.answer_id,'correct')
|
||||
return CorrectMap(self.answer_id, 'correct')
|
||||
else:
|
||||
return CorrectMap(self.answer_id,'incorrect')
|
||||
return CorrectMap(self.answer_id, 'incorrect')
|
||||
|
||||
def get_answers(self):
|
||||
return { self.answer_id : self.correct_choices }
|
||||
return {self.answer_id: self.correct_choices}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MultipleChoiceResponse(LoncapaResponse):
|
||||
# TODO: handle direction and randomize
|
||||
snippets = [{'snippet': '''<multiplechoiceresponse direction="vertical" randomize="yes">
|
||||
@@ -373,28 +379,28 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
allowed_inputfields = ['choicegroup']
|
||||
|
||||
def setup_response(self):
|
||||
self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
|
||||
self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
|
||||
|
||||
# define correct choices (after calling secondary setup)
|
||||
xml = self.xml
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id'))
|
||||
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
|
||||
self.correct_choices = [choice.get('name') for choice in cxml]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
|
||||
'''
|
||||
i=0
|
||||
i = 0
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
rtype = response.get('type')
|
||||
if rtype not in ["MultipleChoice"]:
|
||||
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
|
||||
for choice in list(response):
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
i+=1
|
||||
choice.set("name", "choice_" + str(i))
|
||||
i += 1
|
||||
else:
|
||||
choice.set("name", "choice_"+choice.get("name"))
|
||||
choice.set("name", "choice_" + choice.get("name"))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
@@ -402,39 +408,41 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
'''
|
||||
# log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices))
|
||||
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
|
||||
return CorrectMap(self.answer_id,'correct')
|
||||
return CorrectMap(self.answer_id, 'correct')
|
||||
else:
|
||||
return CorrectMap(self.answer_id,'incorrect')
|
||||
return CorrectMap(self.answer_id, 'incorrect')
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_choices}
|
||||
return {self.answer_id: self.correct_choices}
|
||||
|
||||
|
||||
class TrueFalseResponse(MultipleChoiceResponse):
|
||||
|
||||
response_tag = 'truefalseresponse'
|
||||
|
||||
def mc_setup_response(self):
|
||||
i=0
|
||||
i = 0
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
response.set("type", "TrueFalse")
|
||||
for choice in list(response):
|
||||
if choice.get("name") is None:
|
||||
choice.set("name", "choice_"+str(i))
|
||||
i+=1
|
||||
choice.set("name", "choice_" + str(i))
|
||||
i += 1
|
||||
else:
|
||||
choice.set("name", "choice_"+choice.get("name"))
|
||||
|
||||
choice.set("name", "choice_" + choice.get("name"))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
correct = set(self.correct_choices)
|
||||
answers = set(student_answers.get(self.answer_id, []))
|
||||
|
||||
|
||||
if correct == answers:
|
||||
return CorrectMap( self.answer_id , 'correct')
|
||||
|
||||
return CorrectMap(self.answer_id ,'incorrect')
|
||||
return CorrectMap(self.answer_id, 'correct')
|
||||
|
||||
return CorrectMap(self.answer_id, 'incorrect')
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class OptionResponse(LoncapaResponse):
|
||||
'''
|
||||
TODO: handle direction and randomize
|
||||
@@ -456,19 +464,20 @@ class OptionResponse(LoncapaResponse):
|
||||
cmap = CorrectMap()
|
||||
amap = self.get_answers()
|
||||
for aid in amap:
|
||||
if aid in student_answers and student_answers[aid]==amap[aid]:
|
||||
cmap.set(aid,'correct')
|
||||
if aid in student_answers and student_answers[aid] == amap[aid]:
|
||||
cmap.set(aid, 'correct')
|
||||
else:
|
||||
cmap.set(aid,'incorrect')
|
||||
cmap.set(aid, 'incorrect')
|
||||
return cmap
|
||||
|
||||
def get_answers(self):
|
||||
amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields])
|
||||
amap = dict([(af.get('id'), af.get('correct')) for af in self.answer_fields])
|
||||
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
|
||||
return amap
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class NumericalResponse(LoncapaResponse):
|
||||
|
||||
response_tag = 'numericalresponse'
|
||||
@@ -497,25 +506,26 @@ class NumericalResponse(LoncapaResponse):
|
||||
'''Grade a numeric response '''
|
||||
student_answer = student_answers[self.answer_id]
|
||||
try:
|
||||
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance)
|
||||
# We should catch this explicitly.
|
||||
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer), complex(self.correct_answer), self.tolerance)
|
||||
# We should catch this explicitly.
|
||||
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
|
||||
# But we'd need to confirm
|
||||
except:
|
||||
except:
|
||||
raise StudentInputError('Invalid input -- please use a number only')
|
||||
|
||||
if correct:
|
||||
return CorrectMap(self.answer_id,'correct')
|
||||
return CorrectMap(self.answer_id, 'correct')
|
||||
else:
|
||||
return CorrectMap(self.answer_id,'incorrect')
|
||||
return CorrectMap(self.answer_id, 'incorrect')
|
||||
|
||||
# TODO: add check_hint_condition(self,hxml_set,student_answers)
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
return {self.answer_id: self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class StringResponse(LoncapaResponse):
|
||||
|
||||
response_tag = 'stringresponse'
|
||||
@@ -530,28 +540,29 @@ class StringResponse(LoncapaResponse):
|
||||
def get_score(self, student_answers):
|
||||
'''Grade a string response '''
|
||||
student_answer = student_answers[self.answer_id].strip()
|
||||
correct = self.check_string(self.correct_answer,student_answer)
|
||||
return CorrectMap(self.answer_id,'correct' if correct else 'incorrect')
|
||||
correct = self.check_string(self.correct_answer, student_answer)
|
||||
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
|
||||
|
||||
def check_string(self,expected,given):
|
||||
if self.xml.get('type')=='ci': return given.lower() == expected.lower()
|
||||
def check_string(self, expected, given):
|
||||
if self.xml.get('type') == 'ci': return given.lower() == expected.lower()
|
||||
return given == expected
|
||||
|
||||
def check_hint_condition(self,hxml_set,student_answers):
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
given = student_answers[self.answer_id].strip()
|
||||
hints_to_show = []
|
||||
for hxml in hxml_set:
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(hxml.get('answer'),self.context).strip()
|
||||
if self.check_string(correct_answer,given): hints_to_show.append(name)
|
||||
correct_answer = contextualize_text(hxml.get('answer'), self.context).strip()
|
||||
if self.check_string(correct_answer, given): hints_to_show.append(name)
|
||||
log.debug('hints_to_show = %s' % hints_to_show)
|
||||
return hints_to_show
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
return {self.answer_id: self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CustomResponse(LoncapaResponse):
|
||||
'''
|
||||
Custom response. The python code to be run should be in <answer>...</answer>
|
||||
@@ -592,7 +603,7 @@ def sympy_check2():
|
||||
</customresponse>'''}]
|
||||
|
||||
response_tag = 'customresponse'
|
||||
allowed_inputfields = ['textline','textbox']
|
||||
allowed_inputfields = ['textline', 'textbox']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
@@ -607,7 +618,7 @@ def sympy_check2():
|
||||
self.code = None
|
||||
answer = None
|
||||
try:
|
||||
answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0]
|
||||
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
|
||||
except IndexError:
|
||||
# print "xml = ",etree.tostring(xml,pretty_print=True)
|
||||
|
||||
@@ -619,8 +630,8 @@ def sympy_check2():
|
||||
if cfn in self.context:
|
||||
self.code = self.context[cfn]
|
||||
else:
|
||||
msg = "%s: can't find cfn %s in context" % (unicode(self),cfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
msg = "%s: can't find cfn %s in context" % (unicode(self), cfn)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
if not self.code:
|
||||
@@ -631,7 +642,7 @@ def sympy_check2():
|
||||
else:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filesystem.open('src/'+answer_src).read()
|
||||
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
@@ -641,52 +652,52 @@ def sympy_check2():
|
||||
of each key removed (the string before the first "_").
|
||||
'''
|
||||
|
||||
log.debug('%s: student_answers=%s' % (unicode(self),student_answers))
|
||||
log.debug('%s: student_answers=%s' % (unicode(self), student_answers))
|
||||
|
||||
idset = sorted(self.answer_ids) # ordered list of answer id's
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset] # ordered list of answers
|
||||
submission = [student_answers[k] for k in idset] # ordered list of answers
|
||||
except Exception as err:
|
||||
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
|
||||
msg += '\n idset = %s, error = %s' % (idset,err)
|
||||
msg += '\n idset = %s, error = %s' % (idset, err)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
# global variable in context which holds the Presentation MathML from dynamic math input
|
||||
dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses
|
||||
dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses
|
||||
|
||||
# if there is only one box, and it's empty, then don't evaluate
|
||||
if len(idset)==1 and not submission[0]:
|
||||
return CorrectMap(idset[0],'incorrect',msg='<font color="red">No answer entered!</font>')
|
||||
if len(idset) == 1 and not submission[0]:
|
||||
return CorrectMap(idset[0], 'incorrect', msg='<font color="red">No answer entered!</font>')
|
||||
|
||||
correct = ['unknown'] * len(idset)
|
||||
messages = [''] * len(idset)
|
||||
|
||||
# put these in the context of the check function evaluator
|
||||
# note that this doesn't help the "cfn" version - only the exec version
|
||||
self.context.update({'xml' : self.xml, # our subtree
|
||||
'response_id' : self.myid, # my ID
|
||||
self.context.update({'xml': self.xml, # our subtree
|
||||
'response_id': self.myid, # my ID
|
||||
'expect': self.expect, # expected answer (if given as attribute)
|
||||
'submission':submission, # ordered list of student answers from entry boxes in our subtree
|
||||
'idset':idset, # ordered list of ID's of all entry boxes in our subtree
|
||||
'dynamath':dynamath, # ordered list of all javascript inputs in our subtree
|
||||
'answers':student_answers, # dict of student's responses, with keys being entry box IDs
|
||||
'correct':correct, # the list to be filled in by the check function
|
||||
'messages':messages, # the list of messages to be filled in by the check function
|
||||
'options':self.xml.get('options'), # any options to be passed to the cfn
|
||||
'testdat':'hello world',
|
||||
'submission': submission, # ordered list of student answers from entry boxes in our subtree
|
||||
'idset': idset, # ordered list of ID's of all entry boxes in our subtree
|
||||
'dynamath': dynamath, # ordered list of all javascript inputs in our subtree
|
||||
'answers': student_answers, # dict of student's responses, with keys being entry box IDs
|
||||
'correct': correct, # the list to be filled in by the check function
|
||||
'messages': messages, # the list of messages to be filled in by the check function
|
||||
'options': self.xml.get('options'), # any options to be passed to the cfn
|
||||
'testdat': 'hello world',
|
||||
})
|
||||
|
||||
# pass self.system.debug to cfn
|
||||
# pass self.system.debug to cfn
|
||||
self.context['debug'] = self.system.DEBUG
|
||||
|
||||
# exec the check function
|
||||
if type(self.code)==str:
|
||||
if type(self.code) == str:
|
||||
try:
|
||||
exec self.code in self.context['global_context'], self.context
|
||||
except Exception as err:
|
||||
print "oops in customresponse (code) error %s" % err
|
||||
print "context = ",self.context
|
||||
print "context = ", self.context
|
||||
print traceback.format_exc()
|
||||
else: # self.code is not a string; assume its a function
|
||||
|
||||
@@ -695,44 +706,44 @@ def sympy_check2():
|
||||
ret = None
|
||||
log.debug(" submission = %s" % submission)
|
||||
try:
|
||||
answer_given = submission[0] if (len(idset)==1) else submission
|
||||
answer_given = submission[0] if (len(idset) == 1) else submission
|
||||
# handle variable number of arguments in check function, for backwards compatibility
|
||||
# with various Tutor2 check functions
|
||||
args = [self.expect,answer_given,student_answers,self.answer_ids[0]]
|
||||
args = [self.expect, answer_given, student_answers, self.answer_ids[0]]
|
||||
argspec = inspect.getargspec(fn)
|
||||
nargs = len(argspec.args)-len(argspec.defaults or [])
|
||||
nargs = len(argspec.args) - len(argspec.defaults or [])
|
||||
kwargs = {}
|
||||
for argname in argspec.args[nargs:]:
|
||||
kwargs[argname] = self.context[argname] if argname in self.context else None
|
||||
|
||||
log.debug('[customresponse] answer_given=%s' % answer_given)
|
||||
log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
|
||||
log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs, args, kwargs))
|
||||
|
||||
ret = fn(*args[:nargs],**kwargs)
|
||||
ret = fn(*args[:nargs], **kwargs)
|
||||
except Exception as err:
|
||||
log.error("oops in customresponse (cfn) error %s" % err)
|
||||
# print "context = ",self.context
|
||||
log.error(traceback.format_exc())
|
||||
raise Exception("oops in customresponse (cfn) error %s" % err)
|
||||
log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
|
||||
if type(ret)==dict:
|
||||
correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset)
|
||||
if type(ret) == dict:
|
||||
correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset)
|
||||
msg = ret['msg']
|
||||
|
||||
if 1:
|
||||
# try to clean up message html
|
||||
msg = '<html>'+msg+'</html>'
|
||||
msg = msg.replace('<','<')
|
||||
msg = '<html>' + msg + '</html>'
|
||||
msg = msg.replace('<', '<')
|
||||
#msg = msg.replace('<','<')
|
||||
msg = etree.tostring(fromstring_bs(msg,convertEntities=None),pretty_print=True)
|
||||
msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True)
|
||||
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
|
||||
msg = msg.replace(' ','')
|
||||
msg = msg.replace(' ', '')
|
||||
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
|
||||
msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg)
|
||||
msg = re.sub('(?ms)<html>(.*)</html>', '\\1', msg)
|
||||
|
||||
messages[0] = msg
|
||||
else:
|
||||
correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset)
|
||||
correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset)
|
||||
|
||||
# build map giving "correct"ness of the answer(s)
|
||||
correct_map = CorrectMap()
|
||||
@@ -750,14 +761,15 @@ def sympy_check2():
|
||||
but for simplicity, if an "expect" attribute was given by the content author
|
||||
ie <customresponse expect="foo" ...> then that.
|
||||
'''
|
||||
if len(self.answer_ids)>1:
|
||||
if len(self.answer_ids) > 1:
|
||||
return self.default_answer_map
|
||||
if self.expect:
|
||||
return {self.answer_ids[0] : self.expect}
|
||||
return {self.answer_ids[0]: self.expect}
|
||||
return self.default_answer_map
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SymbolicResponse(CustomResponse):
|
||||
"""
|
||||
Symbolic math response checking, using symmath library.
|
||||
@@ -776,15 +788,16 @@ class SymbolicResponse(CustomResponse):
|
||||
response_tag = 'symbolicresponse'
|
||||
|
||||
def setup_response(self):
|
||||
self.xml.set('cfn','symmath_check')
|
||||
self.xml.set('cfn', 'symmath_check')
|
||||
code = "from symmath import *"
|
||||
exec code in self.context,self.context
|
||||
exec code in self.context, self.context
|
||||
CustomResponse.setup_response(self)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class CodeResponse(LoncapaResponse):
|
||||
'''
|
||||
'''
|
||||
Grade student code using an external server, called 'xqueue'
|
||||
In contrast to ExternalResponse, CodeResponse has following behavior:
|
||||
1) Goes through a queueing system
|
||||
@@ -797,20 +810,20 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
self.url = xml.get('url', "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url
|
||||
self.url = xml.get('url', "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url
|
||||
|
||||
answer = xml.find('answer')
|
||||
if answer is not None:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filesystem.open('src/'+answer_src).read()
|
||||
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
msg = '%s: Missing answer script code for coderesponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.tests = xml.get('tests')
|
||||
@@ -822,7 +835,7 @@ class CodeResponse(LoncapaResponse):
|
||||
penv = {}
|
||||
penv['__builtins__'] = globals()['__builtins__']
|
||||
try:
|
||||
exec(self.code,penv,penv)
|
||||
exec(self.code, penv, penv)
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: Error in problem reference code' % err)
|
||||
raise Exception(err)
|
||||
@@ -843,7 +856,7 @@ class CodeResponse(LoncapaResponse):
|
||||
self.context.update({'submission': submission})
|
||||
extra_payload = {'edX_student_response': json.dumps(submission)}
|
||||
|
||||
r, queuekey = self._send_to_queue(extra_payload) # TODO: Perform checks on the xqueue response
|
||||
r, queuekey = self._send_to_queue(extra_payload) # TODO: Perform checks on the xqueue response
|
||||
|
||||
# Non-null CorrectMap['queuekey'] indicates that the problem has been submitted
|
||||
cmap = CorrectMap()
|
||||
@@ -870,37 +883,37 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
# Replace 'oldcmap' with new grading results if queuekey matches.
|
||||
# If queuekey does not match, we keep waiting for the score_msg that will match
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
msg = rxml.find('message').text.replace(' ',' ')
|
||||
oldcmap.set(self.answer_id, correctness=self.context['correct'][0], msg=msg, queuekey=None) # Queuekey is consumed
|
||||
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
|
||||
msg = rxml.find('message').text.replace(' ', ' ')
|
||||
oldcmap.set(self.answer_id, correctness=self.context['correct'][0], msg=msg, queuekey=None) # Queuekey is consumed
|
||||
else:
|
||||
log.debug('CodeResponse: queuekey %d does not match for answer_id=%s.' % (queuekey, self.answer_id))
|
||||
log.debug('CodeResponse: queuekey %d does not match for answer_id=%s.' % (queuekey, self.answer_id))
|
||||
|
||||
return oldcmap
|
||||
return oldcmap
|
||||
|
||||
# CodeResponse differentiates from ExternalResponse in the behavior of 'get_answers'. CodeResponse.get_answers
|
||||
# does NOT require a queue submission, and the answer is computed (extracted from problem XML) locally.
|
||||
def get_answers(self):
|
||||
anshtml = '<font color="blue"><span class="code-answer"><br/><pre>%s</pre><br/></span></font>' % self.answer
|
||||
return { self.answer_id: anshtml }
|
||||
|
||||
return {self.answer_id: anshtml}
|
||||
|
||||
def get_initial_display(self):
|
||||
return { self.answer_id: self.initial_display }
|
||||
return {self.answer_id: self.initial_display}
|
||||
|
||||
# CodeResponse._send_to_queue implements the same interface as defined for ExternalResponse's 'get_score'
|
||||
def _send_to_queue(self, extra_payload):
|
||||
# Prepare payload
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
header = { 'return_url': self.system.xqueue_callback_url }
|
||||
header = {'return_url': self.system.xqueue_callback_url}
|
||||
|
||||
# Queuekey generation
|
||||
h = hashlib.md5()
|
||||
h.update(str(self.system.seed))
|
||||
h.update(str(time.time()))
|
||||
queuekey = int(h.hexdigest(),16)
|
||||
header.update({'queuekey': queuekey})
|
||||
queuekey = int(h.hexdigest(), 16)
|
||||
header.update({'queuekey': queuekey})
|
||||
|
||||
payload = {'xqueue_header': json.dumps(header), # TODO: 'xqueue_header' should eventually be derived from a config file
|
||||
payload = {'xqueue_header': json.dumps(header), # TODO: 'xqueue_header' should eventually be derived from a config file
|
||||
'xml': xmlstr,
|
||||
'edX_cmd': 'get_score',
|
||||
'edX_tests': self.tests,
|
||||
@@ -920,10 +933,11 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ExternalResponse(LoncapaResponse):
|
||||
'''
|
||||
Grade the students input using an external server.
|
||||
|
||||
|
||||
Typically used by coding problems.
|
||||
|
||||
'''
|
||||
@@ -938,7 +952,7 @@ answer = """
|
||||
def inc(n):
|
||||
return n+1
|
||||
"""
|
||||
preamble = """
|
||||
preamble = """
|
||||
import sympy
|
||||
"""
|
||||
test_program = """
|
||||
@@ -967,30 +981,30 @@ main()
|
||||
</externalresponse>'''}]
|
||||
|
||||
response_tag = 'externalresponse'
|
||||
allowed_inputfields = ['textline','textbox']
|
||||
allowed_inputfields = ['textline', 'textbox']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||
|
||||
# answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
|
||||
answer = xml.find('answer')
|
||||
if answer is not None:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filesystem.open('src/'+answer_src).read()
|
||||
self.code = self.system.filesystem.open('src/' + answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.tests = xml.get('tests')
|
||||
|
||||
def do_external_request(self,cmd,extra_payload):
|
||||
def do_external_request(self, cmd, extra_payload):
|
||||
'''
|
||||
Perform HTTP request / post to external server.
|
||||
|
||||
@@ -1000,29 +1014,29 @@ main()
|
||||
Return XML tree of response (from response body)
|
||||
'''
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
payload = {'xml': xmlstr,
|
||||
'edX_cmd' : cmd,
|
||||
payload = {'xml': xmlstr,
|
||||
'edX_cmd': cmd,
|
||||
'edX_tests': self.tests,
|
||||
'processor' : self.code,
|
||||
'processor': self.code,
|
||||
}
|
||||
payload.update(extra_payload)
|
||||
|
||||
try:
|
||||
r = requests.post(self.url,data=payload) # call external server
|
||||
r = requests.post(self.url, data=payload) # call external server
|
||||
except Exception as err:
|
||||
msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url)
|
||||
msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
if self.system.DEBUG: log.info('response = %s' % r.text)
|
||||
|
||||
if (not r.text ) or (not r.text.strip()):
|
||||
if (not r.text) or (not r.text.strip()):
|
||||
raise Exception('Error: no response from external server url=%s' % self.url)
|
||||
|
||||
try:
|
||||
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||
except Exception as err:
|
||||
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text)
|
||||
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
@@ -1034,24 +1048,24 @@ main()
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset]
|
||||
except Exception as err:
|
||||
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers))
|
||||
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err, self.answer_ids, student_answers))
|
||||
raise Exception(err)
|
||||
|
||||
self.context.update({'submission':submission})
|
||||
self.context.update({'submission': submission})
|
||||
|
||||
extra_payload = {'edX_student_response': json.dumps(submission)}
|
||||
|
||||
try:
|
||||
rxml = self.do_external_request('get_score',extra_payload)
|
||||
rxml = self.do_external_request('get_score', extra_payload)
|
||||
except Exception as err:
|
||||
log.error('Error %s' % err)
|
||||
if self.system.DEBUG:
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) )))
|
||||
cmap.set_property(self.answer_ids[0],'msg','<font color="red" size="+2">%s</font>' % str(err).replace('<','<'))
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset))))
|
||||
cmap.set_property(self.answer_ids[0], 'msg', '<font color="red" size="+2">%s</font>' % str(err).replace('<', '<'))
|
||||
return cmap
|
||||
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||
admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
}
|
||||
self.context['correct'] = ['correct']
|
||||
@@ -1061,7 +1075,7 @@ main()
|
||||
# create CorrectMap
|
||||
for key in idset:
|
||||
idx = idset.index(key)
|
||||
msg = rxml.find('message').text.replace(' ',' ') if idx==0 else None
|
||||
msg = rxml.find('message').text.replace(' ', ' ') if idx == 0 else None
|
||||
cmap.set(key, self.context['correct'][idx], msg=msg)
|
||||
|
||||
return cmap
|
||||
@@ -1071,19 +1085,19 @@ main()
|
||||
Use external server to get expected answers
|
||||
'''
|
||||
try:
|
||||
rxml = self.do_external_request('get_answers',{})
|
||||
rxml = self.do_external_request('get_answers', {})
|
||||
exans = json.loads(rxml.find('expected').text)
|
||||
except Exception as err:
|
||||
log.error('Error %s' % err)
|
||||
if self.system.DEBUG:
|
||||
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<','<')
|
||||
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<', '<')
|
||||
exans = [''] * len(self.answer_ids)
|
||||
exans[0] = msg
|
||||
|
||||
if not (len(exans)==len(self.answer_ids)):
|
||||
log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans)))
|
||||
|
||||
if not (len(exans) == len(self.answer_ids)):
|
||||
log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids), len(exans)))
|
||||
raise Exception('Short response from external server')
|
||||
return dict(zip(self.answer_ids,exans))
|
||||
return dict(zip(self.answer_ids, exans))
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -1104,8 +1118,8 @@ class FormulaResponse(LoncapaResponse):
|
||||
</text>
|
||||
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="$I">
|
||||
<responseparam description="Numerical Tolerance" type="tolerance"
|
||||
default="0.00001" name="tol" />
|
||||
<textline size="40" math="1" />
|
||||
default="0.00001" name="tol" />
|
||||
<textline size="40" math="1" />
|
||||
</formularesponse>
|
||||
|
||||
</problem>'''}]
|
||||
@@ -1133,11 +1147,11 @@ class FormulaResponse(LoncapaResponse):
|
||||
typeslist = []
|
||||
else:
|
||||
typeslist = ts.split(',')
|
||||
if 'ci' in typeslist: # Case insensitive
|
||||
if 'ci' in typeslist: # Case insensitive
|
||||
self.case_sensitive = False
|
||||
elif 'cs' in typeslist: # Case sensitive
|
||||
elif 'cs' in typeslist: # Case sensitive
|
||||
self.case_sensitive = True
|
||||
else: # Default
|
||||
else: # Default
|
||||
self.case_sensitive = False
|
||||
|
||||
def get_score(self, student_answers):
|
||||
@@ -1145,13 +1159,13 @@ class FormulaResponse(LoncapaResponse):
|
||||
correctness = self.check_formula(self.correct_answer, given, self.samples)
|
||||
return CorrectMap(self.answer_id, correctness)
|
||||
|
||||
def check_formula(self,expected, given, samples):
|
||||
variables=samples.split('@')[0].split(',')
|
||||
numsamples=int(samples.split('@')[1].split('#')[1])
|
||||
sranges=zip(*map(lambda x:map(float, x.split(",")),
|
||||
def check_formula(self, expected, given, samples):
|
||||
variables = samples.split('@')[0].split(',')
|
||||
numsamples = int(samples.split('@')[1].split('#')[1])
|
||||
sranges = zip(*map(lambda x: map(float, x.split(",")),
|
||||
samples.split('@')[1].split('#')[0].split(':')))
|
||||
|
||||
ranges=dict(zip(variables, sranges))
|
||||
ranges = dict(zip(variables, sranges))
|
||||
for i in range(numsamples):
|
||||
instructor_variables = self.strip_dict(dict(self.context))
|
||||
student_variables = dict()
|
||||
@@ -1160,16 +1174,16 @@ class FormulaResponse(LoncapaResponse):
|
||||
instructor_variables[str(var)] = value
|
||||
student_variables[str(var)] = value
|
||||
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected))
|
||||
instructor_result = evaluator(instructor_variables,dict(),expected, cs = self.case_sensitive)
|
||||
try:
|
||||
instructor_result = evaluator(instructor_variables, dict(), expected, cs=self.case_sensitive)
|
||||
try:
|
||||
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
|
||||
student_result = evaluator(student_variables,
|
||||
dict(),
|
||||
given,
|
||||
cs = self.case_sensitive)
|
||||
given,
|
||||
cs=self.case_sensitive)
|
||||
except UndefinedVariable as uv:
|
||||
log.debug('formularesponse: undefined variable in given=%s' % given)
|
||||
raise StudentInputError(uv.message+" not permitted in answer")
|
||||
raise StudentInputError(uv.message + " not permitted in answer")
|
||||
except Exception as err:
|
||||
#traceback.print_exc()
|
||||
log.debug('formularesponse: error %s in formula' % err)
|
||||
@@ -1184,33 +1198,34 @@ class FormulaResponse(LoncapaResponse):
|
||||
''' Takes a dict. Returns an identical dict, with all non-word
|
||||
keys and all non-numeric values stripped out. All values also
|
||||
converted to float. Used so we can safely use Python contexts.
|
||||
'''
|
||||
d=dict([(k, numpy.complex(d[k])) for k in d if type(k)==str and \
|
||||
'''
|
||||
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and \
|
||||
k.isalnum() and \
|
||||
isinstance(d[k], numbers.Number)])
|
||||
return d
|
||||
|
||||
def check_hint_condition(self,hxml_set,student_answers):
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
given = student_answers[self.answer_id]
|
||||
hints_to_show = []
|
||||
for hxml in hxml_set:
|
||||
samples = hxml.get('samples')
|
||||
name = hxml.get('name')
|
||||
correct_answer = contextualize_text(hxml.get('answer'),self.context)
|
||||
correct_answer = contextualize_text(hxml.get('answer'), self.context)
|
||||
try:
|
||||
correctness = self.check_formula(correct_answer, given, samples)
|
||||
except Exception:
|
||||
correctness = 'incorrect'
|
||||
if correctness=='correct':
|
||||
if correctness == 'correct':
|
||||
hints_to_show.append(name)
|
||||
log.debug('hints_to_show = %s' % hints_to_show)
|
||||
return hints_to_show
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id:self.correct_answer}
|
||||
return {self.answer_id: self.correct_answer}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SchematicResponse(LoncapaResponse):
|
||||
|
||||
response_tag = 'schematicresponse'
|
||||
@@ -1221,14 +1236,14 @@ class SchematicResponse(LoncapaResponse):
|
||||
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used
|
||||
self.code = self.system.filestore.open('src/' + answer_src).read() # Untested; never used
|
||||
else:
|
||||
self.code = answer.text
|
||||
|
||||
def get_score(self, student_answers):
|
||||
from capa_problem import global_context
|
||||
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission':submission})
|
||||
self.context.update({'submission': submission})
|
||||
exec self.code in global_context, self.context
|
||||
cmap = CorrectMap()
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct'])))
|
||||
@@ -1240,6 +1255,7 @@ class SchematicResponse(LoncapaResponse):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ImageResponse(LoncapaResponse):
|
||||
"""
|
||||
Handle student response for image input: the input is a click on an image,
|
||||
@@ -1248,7 +1264,7 @@ class ImageResponse(LoncapaResponse):
|
||||
|
||||
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That
|
||||
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
should contain one or more <imageinput> stanzas. Each <imageinput> should specify
|
||||
a rectangle, given as an attribute, defining the correct answer.
|
||||
"""
|
||||
snippets = [{'snippet': '''<imageresponse>
|
||||
@@ -1267,24 +1283,24 @@ class ImageResponse(LoncapaResponse):
|
||||
correct_map = CorrectMap()
|
||||
expectedset = self.get_answers()
|
||||
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
|
||||
given = student_answers[aid] # this should be a string of the form '[x,y]'
|
||||
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
|
||||
given = student_answers[aid] # this should be a string of the form '[x,y]'
|
||||
|
||||
# parse expected answer
|
||||
# TODO: Compile regexp on file load
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',expectedset[aid].strip().replace(' ',''))
|
||||
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', ''))
|
||||
if not m:
|
||||
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
|
||||
pretty_print=True))
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] '+msg)
|
||||
(llx,lly,urx,ury) = [int(x) for x in m.groups()]
|
||||
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
|
||||
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
|
||||
|
||||
# parse given answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ',''))
|
||||
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
|
||||
if not m:
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given))
|
||||
(gx,gy) = [int(x) for x in m.groups()]
|
||||
|
||||
raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid, given))
|
||||
(gx, gy) = [int(x) for x in m.groups()]
|
||||
|
||||
# answer is correct if (x,y) is within the specified rectangle
|
||||
if (llx <= gx <= urx) and (lly <= gy <= ury):
|
||||
correct_map.set(aid, 'correct')
|
||||
@@ -1293,11 +1309,10 @@ class ImageResponse(LoncapaResponse):
|
||||
return correct_map
|
||||
|
||||
def get_answers(self):
|
||||
return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements])
|
||||
return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements])
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
__all__ = [ CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse ]
|
||||
|
||||
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse]
|
||||
|
||||
@@ -4,6 +4,7 @@ from calc import evaluator, UndefinedVariable
|
||||
#
|
||||
# Utility functions used in CAPA responsetypes
|
||||
|
||||
|
||||
def compare_with_tolerance(v1, v2, tol):
|
||||
''' Compare v1 to v2 with maximum tolerance tol
|
||||
tol is relative if it ends in %; otherwise, it is absolute
|
||||
@@ -14,17 +15,18 @@ def compare_with_tolerance(v1, v2, tol):
|
||||
|
||||
'''
|
||||
relative = tol.endswith('%')
|
||||
if relative:
|
||||
tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
|
||||
if relative:
|
||||
tolerance_rel = evaluator(dict(), dict(), tol[:-1]) * 0.01
|
||||
tolerance = tolerance_rel * max(abs(v1), abs(v2))
|
||||
else:
|
||||
tolerance = evaluator(dict(),dict(),tol)
|
||||
return abs(v1-v2) <= tolerance
|
||||
else:
|
||||
tolerance = evaluator(dict(), dict(), tol)
|
||||
return abs(v1 - v2) <= tolerance
|
||||
|
||||
def contextualize_text(text, context): # private
|
||||
''' Takes a string with variables. E.g. $a+$b.
|
||||
|
||||
def contextualize_text(text, context): # private
|
||||
''' Takes a string with variables. E.g. $a+$b.
|
||||
Does a substitution of those variables from the context '''
|
||||
if not text: return text
|
||||
for key in sorted(context, lambda x,y:cmp(len(y),len(x))):
|
||||
text=text.replace('$'+key, str(context[key]))
|
||||
for key in sorted(context, lambda x, y: cmp(len(y), len(x))):
|
||||
text = text.replace('$' + key, str(context[key]))
|
||||
return text
|
||||
|
||||
@@ -13,4 +13,3 @@
|
||||
# limitations under the License.
|
||||
|
||||
lookup = None
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from django.http import HttpResponse
|
||||
from . import middleware
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
context_instance = Context(dictionary)
|
||||
# add dictionary to context_instance
|
||||
@@ -43,6 +44,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
|
||||
template = middleware.lookup[namespace].get_template(template_name)
|
||||
return template.render(**context_dictionary)
|
||||
|
||||
|
||||
def render_to_response(template_name, dictionary, context_instance=None, namespace='main', **kwargs):
|
||||
"""
|
||||
Returns a HttpResponse whose content is filled with the result of calling
|
||||
|
||||
@@ -3,7 +3,7 @@ Progress class for modules. Represents where a student is in a module.
|
||||
|
||||
Useful things to know:
|
||||
- Use Progress.to_js_status_str() to convert a progress into a simple
|
||||
status string to pass to js.
|
||||
status string to pass to js.
|
||||
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
|
||||
string to pass to js.
|
||||
|
||||
@@ -11,11 +11,12 @@ In particular, these functions have a canonical handing of None.
|
||||
|
||||
For most subclassing needs, you should only need to reimplement
|
||||
frac() and __str__().
|
||||
'''
|
||||
'''
|
||||
|
||||
from collections import namedtuple
|
||||
import numbers
|
||||
|
||||
|
||||
class Progress(object):
|
||||
'''Represents a progress of a/b (a out of b done)
|
||||
|
||||
@@ -37,7 +38,7 @@ class Progress(object):
|
||||
if not (isinstance(a, numbers.Number) and
|
||||
isinstance(b, numbers.Number)):
|
||||
raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b))
|
||||
|
||||
|
||||
if not (0 <= a <= b and b > 0):
|
||||
raise ValueError(
|
||||
'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b))
|
||||
@@ -66,13 +67,12 @@ class Progress(object):
|
||||
'''
|
||||
return self.frac()[0] > 0
|
||||
|
||||
|
||||
def inprogress(self):
|
||||
''' Returns True if fractional progress is strictly between 0 and 1.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
'''
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
return a > 0 and a < b
|
||||
|
||||
@@ -83,15 +83,14 @@ class Progress(object):
|
||||
checking is done at construction time.
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
return a==b
|
||||
|
||||
return a == b
|
||||
|
||||
def ternary_str(self):
|
||||
''' Return a string version of this progress: either
|
||||
"none", "in_progress", or "done".
|
||||
|
||||
subclassing note: implemented in terms of frac()
|
||||
'''
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
if a == 0:
|
||||
return "none"
|
||||
@@ -111,8 +110,7 @@ class Progress(object):
|
||||
def __ne__(self, other):
|
||||
''' The opposite of equal'''
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
''' Return a string representation of this string.
|
||||
|
||||
@@ -147,7 +145,6 @@ class Progress(object):
|
||||
return "NA"
|
||||
return progress.ternary_str()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def to_js_detail_str(progress):
|
||||
'''
|
||||
|
||||
@@ -20,27 +20,31 @@ from xmodule.graders import Score, aggregate_scores
|
||||
from xmodule.progress import Progress
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class I4xSystem(object):
|
||||
'''
|
||||
This is an abstraction such that x_modules can function independent
|
||||
of the courseware (e.g. import into other types of courseware, LMS,
|
||||
This is an abstraction such that x_modules can function independent
|
||||
of the courseware (e.g. import into other types of courseware, LMS,
|
||||
or if we want to have a sandbox server for user-contributed content)
|
||||
'''
|
||||
def __init__(self):
|
||||
self.ajax_url = '/'
|
||||
self.track_function = lambda x: None
|
||||
self.filestore = fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__)))
|
||||
self.render_function = lambda x: {} # Probably incorrect
|
||||
self.render_function = lambda x: {} # Probably incorrect
|
||||
self.module_from_xml = lambda x: None # May need a real impl...
|
||||
self.exception404 = Exception
|
||||
self.DEBUG = True
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
i4xs = I4xSystem()
|
||||
|
||||
|
||||
class ModelsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
@@ -51,42 +55,42 @@ class ModelsTest(unittest.TestCase):
|
||||
self.assertEqual(str(vc), vc_str)
|
||||
|
||||
def test_calc(self):
|
||||
variables={'R1':2.0, 'R3':4.0}
|
||||
functions={'sin':numpy.sin, 'cos':numpy.cos}
|
||||
variables = {'R1': 2.0, 'R3': 4.0}
|
||||
functions = {'sin': numpy.sin, 'cos': numpy.cos}
|
||||
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356"))<0.01)
|
||||
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3':4.0}, {}, "13"), 13)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356")) < 0.01)
|
||||
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13)
|
||||
self.assertEqual(calc.evaluator(variables, functions, "13"), 13)
|
||||
self.assertEqual(calc.evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5)
|
||||
self.assertEqual(calc.evaluator({},{}, "-1"), -1)
|
||||
self.assertEqual(calc.evaluator({},{}, "-0.33"), -.33)
|
||||
self.assertEqual(calc.evaluator({},{}, "-.33"), -.33)
|
||||
self.assertEqual(calc.evaluator({}, {}, "-1"), -1)
|
||||
self.assertEqual(calc.evaluator({}, {}, "-0.33"), -.33)
|
||||
self.assertEqual(calc.evaluator({}, {}, "-.33"), -.33)
|
||||
self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41"))<0.01)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)")+1)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1")-0.5-0.5j)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41")) < 0.01)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025")) < 0.001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001)
|
||||
variables['t'] = 1.0
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t")-1.0)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T")-1.0)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True)-1.0)<0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True)-298)<0.2)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001)
|
||||
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2)
|
||||
exception_happened = False
|
||||
try:
|
||||
calc.evaluator({},{}, "5+7 QWSEKO")
|
||||
try:
|
||||
calc.evaluator({}, {}, "5+7 QWSEKO")
|
||||
except:
|
||||
exception_happened = True
|
||||
self.assertTrue(exception_happened)
|
||||
|
||||
try:
|
||||
calc.evaluator({'r1':5},{}, "r1+r2")
|
||||
try:
|
||||
calc.evaluator({'r1': 5}, {}, "r1+r2")
|
||||
except calc.UndefinedVariable:
|
||||
pass
|
||||
|
||||
|
||||
self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0)
|
||||
|
||||
exception_happened = False
|
||||
try:
|
||||
try:
|
||||
calc.evaluator(variables, functions, "r1*r3", cs=True)
|
||||
except:
|
||||
exception_happened = True
|
||||
@@ -95,55 +99,58 @@ class ModelsTest(unittest.TestCase):
|
||||
#-----------------------------------------------------------------------------
|
||||
# tests of capa_problem inputtypes
|
||||
|
||||
|
||||
class MultiChoiceTest(unittest.TestCase):
|
||||
def test_MC_grade(self):
|
||||
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
|
||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_foil3'}
|
||||
correct_answers = {'1_2_1': 'choice_foil3'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
false_answers = {'1_2_1':'choice_foil2'}
|
||||
false_answers = {'1_2_1': 'choice_foil2'}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
def test_MC_bare_grades(self):
|
||||
multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml"
|
||||
multichoice_file = os.path.dirname(__file__) + "/test_files/multi_bare.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_2'}
|
||||
correct_answers = {'1_2_1': 'choice_2'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
false_answers = {'1_2_1':'choice_1'}
|
||||
false_answers = {'1_2_1': 'choice_1'}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
|
||||
def test_TF_grade(self):
|
||||
truefalse_file = os.path.dirname(__file__)+"/test_files/truefalse.xml"
|
||||
truefalse_file = os.path.dirname(__file__) + "/test_files/truefalse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(truefalse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']}
|
||||
correct_answers = {'1_2_1': ['choice_foil2', 'choice_foil1']}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
false_answers = {'1_2_1':['choice_foil1']}
|
||||
false_answers = {'1_2_1': ['choice_foil1']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil1', 'choice_foil3']}
|
||||
false_answers = {'1_2_1': ['choice_foil1', 'choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil3']}
|
||||
false_answers = {'1_2_1': ['choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']}
|
||||
false_answers = {'1_2_1': ['choice_foil1', 'choice_foil2', 'choice_foil3']}
|
||||
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
|
||||
|
||||
class ImageResponseTest(unittest.TestCase):
|
||||
def test_ir_grade(self):
|
||||
imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml"
|
||||
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'(490,11)-(556,98)',
|
||||
'1_2_2':'(242,202)-(296,276)'}
|
||||
test_answers = {'1_2_1':'[500,20]',
|
||||
'1_2_2':'[250,300]',
|
||||
correct_answers = {'1_2_1': '(490,11)-(556,98)',
|
||||
'1_2_2': '(242,202)-(296,276)'}
|
||||
test_answers = {'1_2_1': '[500,20]',
|
||||
'1_2_2': '[250,300]',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
|
||||
|
||||
|
||||
|
||||
class SymbolicResponseTest(unittest.TestCase):
|
||||
def test_sr_grade(self):
|
||||
raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test
|
||||
symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml"
|
||||
raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test
|
||||
symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
@@ -216,8 +223,8 @@ class SymbolicResponseTest(unittest.TestCase):
|
||||
</math>
|
||||
''',
|
||||
}
|
||||
wrong_answers = {'1_2_1':'2',
|
||||
'1_2_1_dynamath':'''
|
||||
wrong_answers = {'1_2_1': '2',
|
||||
'1_2_1_dynamath': '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mn>2</mn>
|
||||
@@ -226,7 +233,8 @@ class SymbolicResponseTest(unittest.TestCase):
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
|
||||
|
||||
class OptionResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Run this with
|
||||
@@ -234,120 +242,124 @@ class OptionResponseTest(unittest.TestCase):
|
||||
python manage.py test courseware.OptionResponseTest
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml"
|
||||
optionresponse_file = os.path.dirname(__file__) + "/test_files/optionresponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(optionresponse_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'True',
|
||||
'1_2_2':'False'}
|
||||
test_answers = {'1_2_1':'True',
|
||||
'1_2_2':'True',
|
||||
correct_answers = {'1_2_1': 'True',
|
||||
'1_2_2': 'False'}
|
||||
test_answers = {'1_2_1': 'True',
|
||||
'1_2_2': 'True',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
|
||||
|
||||
|
||||
class FormulaResponseWithHintTest(unittest.TestCase):
|
||||
'''
|
||||
Test Formula response problem with a hint
|
||||
This problem also uses calc.
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/formularesponse_with_hint.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/formularesponse_with_hint.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'2.5*x-5.0'}
|
||||
test_answers = {'1_2_1':'0.4*x-5.0'}
|
||||
correct_answers = {'1_2_1': '2.5*x-5.0'}
|
||||
test_answers = {'1_2_1': '0.4*x-5.0'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
cmap = test_lcp.grade_answers(test_answers)
|
||||
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertTrue('You have inverted' in cmap.get_hint('1_2_1'))
|
||||
|
||||
|
||||
class StringResponseWithHintTest(unittest.TestCase):
|
||||
'''
|
||||
Test String response problem with a hint
|
||||
'''
|
||||
def test_or_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/stringresponse_with_hint.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/stringresponse_with_hint.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'Michigan'}
|
||||
test_answers = {'1_2_1':'Minnesota'}
|
||||
correct_answers = {'1_2_1': 'Michigan'}
|
||||
test_answers = {'1_2_1': 'Minnesota'}
|
||||
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
|
||||
cmap = test_lcp.grade_answers(test_answers)
|
||||
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
|
||||
self.assertTrue('St. Paul' in cmap.get_hint('1_2_1'))
|
||||
|
||||
|
||||
class CodeResponseTest(unittest.TestCase):
|
||||
'''
|
||||
Test CodeResponse
|
||||
|
||||
'''
|
||||
def test_update_score(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/coderesponse.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
|
||||
|
||||
# CodeResponse requires internal CorrectMap state. Build it now in the 'queued' state
|
||||
old_cmap = CorrectMap()
|
||||
answer_ids = sorted(test_lcp.get_question_answers().keys())
|
||||
numAnswers = len(answer_ids)
|
||||
for i in range(numAnswers):
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000+i))
|
||||
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000 + i))
|
||||
|
||||
# Message format inherited from ExternalResponse
|
||||
correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
xserver_msgs = {'correct': correct_score_msg,
|
||||
'incorrect': incorrect_score_msg,
|
||||
correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>"
|
||||
xserver_msgs = {'correct': correct_score_msg,
|
||||
'incorrect': incorrect_score_msg,
|
||||
}
|
||||
|
||||
# Incorrect queuekey, state should not be updated
|
||||
for correctness in ['correct', 'incorrect']:
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
test_lcp.correct_map.update(old_cmap) # Deep copy
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
test_lcp.correct_map.update(old_cmap) # Deep copy
|
||||
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=0)
|
||||
self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison
|
||||
self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison
|
||||
|
||||
for i in range(numAnswers):
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered
|
||||
|
||||
# Correct queuekey, state should be updated
|
||||
for correctness in ['correct', 'incorrect']:
|
||||
for i in range(numAnswers): # Target specific answer_id's
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
for i in range(numAnswers): # Target specific answer_id's
|
||||
test_lcp.correct_map = CorrectMap()
|
||||
test_lcp.correct_map.update(old_cmap)
|
||||
|
||||
new_cmap = CorrectMap()
|
||||
new_cmap.update(old_cmap)
|
||||
new_cmap.set(answer_id=answer_ids[i], correctness=correctness, msg='MESSAGE', queuekey=None)
|
||||
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000+i)
|
||||
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
|
||||
self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict())
|
||||
|
||||
for j in range(numAnswers):
|
||||
if j == i:
|
||||
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered
|
||||
else:
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered
|
||||
|
||||
|
||||
class ChoiceResponseTest(unittest.TestCase):
|
||||
|
||||
def test_cr_rb_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/choiceresponse_radio.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/choiceresponse_radio.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':'choice_2',
|
||||
correct_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': ['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': 'choice_2',
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||
|
||||
def test_cr_cb_grade(self):
|
||||
problem_file = os.path.dirname(__file__)+"/test_files/choiceresponse_checkbox.xml"
|
||||
problem_file = os.path.dirname(__file__) + "/test_files/choiceresponse_checkbox.xml"
|
||||
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs)
|
||||
correct_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':['choice_2', 'choice_3'],
|
||||
'1_4_1':['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1':'choice_2',
|
||||
'1_3_1':'choice_2',
|
||||
'1_4_1':['choice_2', 'choice_3'],
|
||||
correct_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': ['choice_2', 'choice_3'],
|
||||
'1_4_1': ['choice_2', 'choice_3']}
|
||||
test_answers = {'1_2_1': 'choice_2',
|
||||
'1_3_1': 'choice_2',
|
||||
'1_4_1': ['choice_2', 'choice_3'],
|
||||
}
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
|
||||
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect')
|
||||
@@ -356,11 +368,12 @@ class ChoiceResponseTest(unittest.TestCase):
|
||||
#-----------------------------------------------------------------------------
|
||||
# Grading tests
|
||||
|
||||
|
||||
class GradesheetTest(unittest.TestCase):
|
||||
|
||||
def test_weighted_grading(self):
|
||||
scores = []
|
||||
Score.__sub__=lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
|
||||
Score.__sub__ = lambda me, other: (me.earned - other.earned) + (me.possible - other.possible)
|
||||
|
||||
all, graded = aggregate_scores(scores)
|
||||
self.assertEqual(all, Score(earned=0, possible=0, graded=False, section="summary"))
|
||||
@@ -381,197 +394,194 @@ class GradesheetTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(all, Score(earned=5, possible=15, graded=False, section="summary"))
|
||||
self.assertAlmostEqual(graded, Score(earned=5, possible=10, graded=True, section="summary"))
|
||||
|
||||
|
||||
class GraderTest(unittest.TestCase):
|
||||
|
||||
empty_gradesheet = {
|
||||
}
|
||||
|
||||
|
||||
incomplete_gradesheet = {
|
||||
'Homework': [],
|
||||
'Lab': [],
|
||||
'Midterm' : [],
|
||||
'Midterm': [],
|
||||
}
|
||||
|
||||
|
||||
test_gradesheet = {
|
||||
'Homework': [Score(earned=2, possible=20.0, graded=True, section='hw1'),
|
||||
Score(earned=16, possible=16.0, graded=True, section='hw2')],
|
||||
#The dropped scores should be from the assignments that don't exist yet
|
||||
|
||||
'Lab': [Score(earned=1, possible=2.0, graded=True, section='lab1'), #Dropped
|
||||
|
||||
'Lab': [Score(earned=1, possible=2.0, graded=True, section='lab1'), # Dropped
|
||||
Score(earned=1, possible=1.0, graded=True, section='lab2'),
|
||||
Score(earned=1, possible=1.0, graded=True, section='lab3'),
|
||||
Score(earned=5, possible=25.0, graded=True, section='lab4'), #Dropped
|
||||
Score(earned=3, possible=4.0, graded=True, section='lab5'), #Dropped
|
||||
Score(earned=5, possible=25.0, graded=True, section='lab4'), # Dropped
|
||||
Score(earned=3, possible=4.0, graded=True, section='lab5'), # Dropped
|
||||
Score(earned=6, possible=7.0, graded=True, section='lab6'),
|
||||
Score(earned=5, possible=6.0, graded=True, section='lab7')],
|
||||
|
||||
'Midterm' : [Score(earned=50.5, possible=100, graded=True, section="Midterm Exam"),],
|
||||
|
||||
'Midterm': [Score(earned=50.5, possible=100, graded=True, section="Midterm Exam"), ],
|
||||
}
|
||||
|
||||
|
||||
def test_SingleSectionGrader(self):
|
||||
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
|
||||
lab4Grader = graders.SingleSectionGrader("Lab", "lab4")
|
||||
badLabGrader = graders.SingleSectionGrader("Lab", "lab42")
|
||||
|
||||
for graded in [midtermGrader.grade(self.empty_gradesheet),
|
||||
midtermGrader.grade(self.incomplete_gradesheet),
|
||||
|
||||
for graded in [midtermGrader.grade(self.empty_gradesheet),
|
||||
midtermGrader.grade(self.incomplete_gradesheet),
|
||||
badLabGrader.grade(self.test_gradesheet)]:
|
||||
self.assertEqual( len(graded['section_breakdown']), 1 )
|
||||
self.assertEqual( graded['percent'], 0.0 )
|
||||
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
self.assertEqual(graded['percent'], 0.0)
|
||||
|
||||
graded = midtermGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.505 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.505)
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
|
||||
graded = lab4Grader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.2 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.2)
|
||||
self.assertEqual(len(graded['section_breakdown']), 1)
|
||||
|
||||
def test_AssignmentFormatGrader(self):
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
noDropGrader = graders.AssignmentFormatGrader("Homework", 12, 0)
|
||||
#Even though the minimum number is 3, this should grade correctly when 7 assignments are found
|
||||
overflowGrader = graders.AssignmentFormatGrader("Lab", 3, 2)
|
||||
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
|
||||
|
||||
|
||||
|
||||
#Test the grading of an empty gradesheet
|
||||
for graded in [ homeworkGrader.grade(self.empty_gradesheet),
|
||||
for graded in [homeworkGrader.grade(self.empty_gradesheet),
|
||||
noDropGrader.grade(self.empty_gradesheet),
|
||||
homeworkGrader.grade(self.incomplete_gradesheet),
|
||||
noDropGrader.grade(self.incomplete_gradesheet) ]:
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
noDropGrader.grade(self.incomplete_gradesheet)]:
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
#Make sure the breakdown includes 12 sections, plus one summary
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
graded = homeworkGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.11 ) # 100% + 10% / 10 assignments
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.11) # 100% + 10% / 10 assignments
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
graded = noDropGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0916666666666666 ) # 100% + 10% / 12 assignments
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.0916666666666666) # 100% + 10% / 12 assignments
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
graded = overflowGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.8880952380952382 ) # 100% + 10% / 5 assignments
|
||||
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.8880952380952382) # 100% + 10% / 5 assignments
|
||||
self.assertEqual(len(graded['section_breakdown']), 7 + 1)
|
||||
|
||||
graded = labGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.9226190476190477 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 7 + 1 )
|
||||
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.9226190476190477)
|
||||
self.assertEqual(len(graded['section_breakdown']), 7 + 1)
|
||||
|
||||
def test_WeightedSubsectionsGrader(self):
|
||||
#First, a few sub graders
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
labGrader = graders.AssignmentFormatGrader("Lab", 7, 3)
|
||||
midtermGrader = graders.SingleSectionGrader("Midterm", "Midterm Exam")
|
||||
|
||||
weightedGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25),
|
||||
(midtermGrader, midtermGrader.category, 0.5)] )
|
||||
|
||||
overOneWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5),
|
||||
(midtermGrader, midtermGrader.category, 0.5)] )
|
||||
|
||||
|
||||
weightedGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.25), (labGrader, labGrader.category, 0.25),
|
||||
(midtermGrader, midtermGrader.category, 0.5)])
|
||||
|
||||
overOneWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.5), (labGrader, labGrader.category, 0.5),
|
||||
(midtermGrader, midtermGrader.category, 0.5)])
|
||||
|
||||
#The midterm should have all weight on this one
|
||||
zeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.5)] )
|
||||
|
||||
zeroWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.5)])
|
||||
|
||||
#This should always have a final percent of zero
|
||||
allZeroWeightsGrader = graders.WeightedSubsectionsGrader( [(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.0)] )
|
||||
|
||||
emptyGrader = graders.WeightedSubsectionsGrader( [] )
|
||||
|
||||
allZeroWeightsGrader = graders.WeightedSubsectionsGrader([(homeworkGrader, homeworkGrader.category, 0.0), (labGrader, labGrader.category, 0.0),
|
||||
(midtermGrader, midtermGrader.category, 0.0)])
|
||||
|
||||
emptyGrader = graders.WeightedSubsectionsGrader([])
|
||||
|
||||
graded = weightedGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.5106547619047619)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = overOneWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.7688095238095238 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.7688095238095238)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = zeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.2525 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.2525)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
for graded in [ weightedGrader.grade(self.empty_gradesheet),
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
for graded in [weightedGrader.grade(self.empty_gradesheet),
|
||||
weightedGrader.grade(self.incomplete_gradesheet),
|
||||
zeroWeightsGrader.grade(self.empty_gradesheet),
|
||||
allZeroWeightsGrader.grade(self.empty_gradesheet)]:
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), 0)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 0)
|
||||
|
||||
def test_graderFromConf(self):
|
||||
|
||||
|
||||
#Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
|
||||
#in test_graders.WeightedSubsectionsGrader, but generate the graders with confs.
|
||||
|
||||
|
||||
weightedGrader = graders.grader_from_conf([
|
||||
{
|
||||
'type' : "Homework",
|
||||
'min_count' : 12,
|
||||
'drop_count' : 2,
|
||||
'short_label' : "HW",
|
||||
'weight' : 0.25,
|
||||
'type': "Homework",
|
||||
'min_count': 12,
|
||||
'drop_count': 2,
|
||||
'short_label': "HW",
|
||||
'weight': 0.25,
|
||||
},
|
||||
{
|
||||
'type' : "Lab",
|
||||
'min_count' : 7,
|
||||
'drop_count' : 3,
|
||||
'category' : "Labs",
|
||||
'weight' : 0.25
|
||||
'type': "Lab",
|
||||
'min_count': 7,
|
||||
'drop_count': 3,
|
||||
'category': "Labs",
|
||||
'weight': 0.25
|
||||
},
|
||||
{
|
||||
'type' : "Midterm",
|
||||
'name' : "Midterm Exam",
|
||||
'short_label' : "Midterm",
|
||||
'weight' : 0.5,
|
||||
'type': "Midterm",
|
||||
'name': "Midterm Exam",
|
||||
'short_label': "Midterm",
|
||||
'weight': 0.5,
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
emptyGrader = graders.grader_from_conf([])
|
||||
|
||||
|
||||
graded = weightedGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.5106547619047619)
|
||||
self.assertEqual(len(graded['section_breakdown']), (12 + 1) + (7 + 1) + 1)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 3)
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.0)
|
||||
self.assertEqual(len(graded['section_breakdown']), 0)
|
||||
self.assertEqual(len(graded['grade_breakdown']), 0)
|
||||
|
||||
#Test that graders can also be used instead of lists of dictionaries
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
|
||||
|
||||
|
||||
graded = homeworkGrader2.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.11 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
self.assertAlmostEqual(graded['percent'], 0.11)
|
||||
self.assertEqual(len(graded['section_breakdown']), 12 + 1)
|
||||
|
||||
#TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Module progress tests
|
||||
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
fraction between 0 and 1.
|
||||
@@ -590,7 +600,7 @@ class ProgressTest(unittest.TestCase):
|
||||
|
||||
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)
|
||||
@@ -635,7 +645,7 @@ class ProgressTest(unittest.TestCase):
|
||||
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")
|
||||
@@ -648,7 +658,7 @@ class ProgressTest(unittest.TestCase):
|
||||
|
||||
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")
|
||||
@@ -673,7 +683,7 @@ class ProgressTest(unittest.TestCase):
|
||||
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())
|
||||
|
||||
|
||||
@@ -198,12 +198,12 @@ class CapaModule(XModule):
|
||||
if self.system.DEBUG:
|
||||
log.exception(err)
|
||||
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<','<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<')
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
html = msg
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
content = {'name': self.metadata['display_name'],
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
@@ -336,7 +336,7 @@ class CapaModule(XModule):
|
||||
score_msg = get['response']
|
||||
self.lcp.update_score(score_msg, queuekey)
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
@@ -379,7 +379,7 @@ class CapaModule(XModule):
|
||||
if not name.endswith('[]'):
|
||||
answers[name] = get[key]
|
||||
else:
|
||||
name = name[:-2]
|
||||
name = name[:-2]
|
||||
answers[name] = get.getlist(key)
|
||||
|
||||
return answers
|
||||
@@ -430,7 +430,7 @@ class CapaModule(XModule):
|
||||
if self.system.DEBUG:
|
||||
msg = "Error checking problem: " + str(err)
|
||||
msg += '\nTraceback:\n' + traceback.format_exc()
|
||||
return {'success':msg}
|
||||
return {'success': msg}
|
||||
traceback.print_exc()
|
||||
raise Exception("error in capa_module")
|
||||
|
||||
|
||||
@@ -9,22 +9,21 @@ from fs.errors import ResourceNotFoundError
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded without a start date. " + str(self.id))
|
||||
except ValueError, e:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'")
|
||||
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
|
||||
@@ -44,15 +43,15 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@property
|
||||
def title(self):
|
||||
return self.metadata['display_name']
|
||||
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
return self.location.course
|
||||
|
||||
|
||||
@property
|
||||
def wiki_namespace(self):
|
||||
return self.location.course
|
||||
|
||||
@property
|
||||
def org(self):
|
||||
return self.location.org
|
||||
return self.location.org
|
||||
|
||||
@@ -48,7 +48,7 @@ def grader_from_conf(conf):
|
||||
"""
|
||||
if isinstance(conf, CourseGrader):
|
||||
return conf
|
||||
|
||||
|
||||
subgraders = []
|
||||
for subgraderconf in conf:
|
||||
subgraderconf = subgraderconf.copy()
|
||||
@@ -57,45 +57,45 @@ def grader_from_conf(conf):
|
||||
if 'min_count' in subgraderconf:
|
||||
#This is an AssignmentFormatGrader
|
||||
subgrader = AssignmentFormatGrader(**subgraderconf)
|
||||
subgraders.append( (subgrader, subgrader.category, weight) )
|
||||
subgraders.append((subgrader, subgrader.category, weight))
|
||||
elif 'name' in subgraderconf:
|
||||
#This is an SingleSectionGrader
|
||||
subgrader = SingleSectionGrader(**subgraderconf)
|
||||
subgraders.append( (subgrader, subgrader.category, weight) )
|
||||
subgraders.append((subgrader, subgrader.category, weight))
|
||||
else:
|
||||
raise ValueError("Configuration has no appropriate grader class.")
|
||||
|
||||
|
||||
except (TypeError, ValueError) as error:
|
||||
errorString = "Unable to parse grader configuration:\n " + str(subgraderconf) + "\n Error was:\n " + str(error)
|
||||
log.critical(errorString)
|
||||
raise ValueError(errorString)
|
||||
|
||||
return WeightedSubsectionsGrader( subgraders )
|
||||
|
||||
return WeightedSubsectionsGrader(subgraders)
|
||||
|
||||
|
||||
class CourseGrader(object):
|
||||
"""
|
||||
A course grader takes the totaled scores for each graded section (that a student has
|
||||
started) in the course. From these scores, the grader calculates an overall percentage
|
||||
A course grader takes the totaled scores for each graded section (that a student has
|
||||
started) in the course. From these scores, the grader calculates an overall percentage
|
||||
grade. The grader should also generate information about how that score was calculated,
|
||||
to be displayed in graphs or charts.
|
||||
|
||||
|
||||
A grader has one required method, grade(), which is passed a grade_sheet. The grade_sheet
|
||||
contains scores for all graded section that the student has started. If a student has
|
||||
a score of 0 for that section, it may be missing from the grade_sheet. The grade_sheet
|
||||
is keyed by section format. Each value is a list of Score namedtuples for each section
|
||||
that has the matching section format.
|
||||
|
||||
|
||||
The grader outputs a dictionary with the following keys:
|
||||
- percent: Contaisn a float value, which is the final percentage score for the student.
|
||||
- section_breakdown: This is a list of dictionaries which provide details on sections
|
||||
that were graded. These are used for display in a graph or chart. The format for a
|
||||
that were graded. These are used for display in a graph or chart. The format for a
|
||||
section_breakdown dictionary is explained below.
|
||||
- grade_breakdown: This is a list of dictionaries which provide details on the contributions
|
||||
of the final percentage grade. This is a higher level breakdown, for when the grade is constructed
|
||||
of a few very large sections (such as Homeworks, Labs, a Midterm, and a Final). The format for
|
||||
a grade_breakdown is explained below. This section is optional.
|
||||
|
||||
|
||||
A dictionary in the section_breakdown list has the following keys:
|
||||
percent: A float percentage for the section.
|
||||
label: A short string identifying the section. Preferably fixed-length. E.g. "HW 3".
|
||||
@@ -104,71 +104,72 @@ class CourseGrader(object):
|
||||
in the display (for example, by color).
|
||||
prominent: A boolean value indicating that this section should be displayed as more prominent
|
||||
than other items.
|
||||
|
||||
|
||||
A dictionary in the grade_breakdown list has the following keys:
|
||||
percent: A float percentage in the breakdown. All percents should add up to the final percentage.
|
||||
detail: A string explanation of this breakdown. E.g. "Homework - 10% of a possible 15%"
|
||||
category: A string identifying the category. Items with the same category are grouped together
|
||||
in the display (for example, by color).
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
|
||||
@abc.abstractmethod
|
||||
def grade(self, grade_sheet):
|
||||
raise NotImplementedError
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class WeightedSubsectionsGrader(CourseGrader):
|
||||
"""
|
||||
This grader takes a list of tuples containing (grader, category_name, weight) and computes
|
||||
a final grade by totalling the contribution of each sub grader and multiplying it by the
|
||||
given weight. For example, the sections may be
|
||||
given weight. For example, the sections may be
|
||||
[ (homeworkGrader, "Homework", 0.15), (labGrader, "Labs", 0.15), (midtermGrader, "Midterm", 0.30), (finalGrader, "Final", 0.40) ]
|
||||
All items in section_breakdown for each subgrader will be combined. A grade_breakdown will be
|
||||
composed using the score from each grader.
|
||||
|
||||
|
||||
Note that the sum of the weights is not take into consideration. If the weights add up to
|
||||
a value > 1, the student may end up with a percent > 100%. This allows for sections that
|
||||
are extra credit.
|
||||
"""
|
||||
def __init__(self, sections):
|
||||
self.sections = sections
|
||||
|
||||
|
||||
def grade(self, grade_sheet):
|
||||
total_percent = 0.0
|
||||
section_breakdown = []
|
||||
grade_breakdown = []
|
||||
|
||||
|
||||
for subgrader, category, weight in self.sections:
|
||||
subgrade_result = subgrader.grade(grade_sheet)
|
||||
|
||||
|
||||
weightedPercent = subgrade_result['percent'] * weight
|
||||
section_detail = "{0} = {1:.1%} of a possible {2:.0%}".format(category, weightedPercent, weight)
|
||||
|
||||
|
||||
total_percent += weightedPercent
|
||||
section_breakdown += subgrade_result['section_breakdown']
|
||||
grade_breakdown.append( {'percent' : weightedPercent, 'detail' : section_detail, 'category' : category} )
|
||||
|
||||
return {'percent' : total_percent,
|
||||
'section_breakdown' : section_breakdown,
|
||||
'grade_breakdown' : grade_breakdown}
|
||||
grade_breakdown.append({'percent': weightedPercent, 'detail': section_detail, 'category': category})
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': section_breakdown,
|
||||
'grade_breakdown': grade_breakdown}
|
||||
|
||||
|
||||
class SingleSectionGrader(CourseGrader):
|
||||
"""
|
||||
This grades a single section with the format 'type' and the name 'name'.
|
||||
|
||||
|
||||
If the name is not appropriate for the short short_label or category, they each may
|
||||
be specified individually.
|
||||
"""
|
||||
def __init__(self, type, name, short_label = None, category = None):
|
||||
def __init__(self, type, name, short_label=None, category=None):
|
||||
self.type = type
|
||||
self.name = name
|
||||
self.short_label = short_label or name
|
||||
self.category = category or name
|
||||
|
||||
|
||||
def grade(self, grade_sheet):
|
||||
foundScore = None
|
||||
if self.type in grade_sheet:
|
||||
@@ -176,58 +177,59 @@ class SingleSectionGrader(CourseGrader):
|
||||
if score.section == self.name:
|
||||
foundScore = score
|
||||
break
|
||||
|
||||
|
||||
if foundScore:
|
||||
percent = foundScore.earned / float(foundScore.possible)
|
||||
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
|
||||
percent = percent,
|
||||
earned = float(foundScore.earned),
|
||||
possible = float(foundScore.possible))
|
||||
|
||||
percent = foundScore.earned / float(foundScore.possible)
|
||||
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(name=self.name,
|
||||
percent=percent,
|
||||
earned=float(foundScore.earned),
|
||||
possible=float(foundScore.possible))
|
||||
|
||||
else:
|
||||
percent = 0.0
|
||||
detail = "{name} - 0% (?/?)".format(name = self.name)
|
||||
|
||||
detail = "{name} - 0% (?/?)".format(name=self.name)
|
||||
|
||||
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
|
||||
|
||||
return {'percent' : percent,
|
||||
'section_breakdown' : breakdown,
|
||||
|
||||
return {'percent': percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AssignmentFormatGrader(CourseGrader):
|
||||
"""
|
||||
Grades all sections matching the format 'type' with an equal weight. A specified
|
||||
number of lowest scores can be dropped from the calculation. The minimum number of
|
||||
sections in this format must be specified (even if those sections haven't been
|
||||
written yet).
|
||||
|
||||
|
||||
min_count defines how many assignments are expected throughout the course. Placeholder
|
||||
scores (of 0) will be inserted if the number of matching sections in the course is < min_count.
|
||||
If there number of matching sections in the course is > min_count, min_count will be ignored.
|
||||
|
||||
category should be presentable to the user, but may not appear. When the grade breakdown is
|
||||
|
||||
category should be presentable to the user, but may not appear. When the grade breakdown is
|
||||
displayed, scores from the same category will be similar (for example, by color).
|
||||
|
||||
|
||||
section_type is a string that is the type of a singular section. For example, for Labs it
|
||||
would be "Lab". This defaults to be the same as category.
|
||||
|
||||
|
||||
short_label is similar to section_type, but shorter. For example, for Homework it would be
|
||||
"HW".
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, type, min_count, drop_count, category = None, section_type = None, short_label = None):
|
||||
def __init__(self, type, min_count, drop_count, category=None, section_type=None, short_label=None):
|
||||
self.type = type
|
||||
self.min_count = min_count
|
||||
self.drop_count = drop_count
|
||||
self.category = category or self.type
|
||||
self.section_type = section_type or self.type
|
||||
self.short_label = short_label or self.type
|
||||
|
||||
|
||||
def grade(self, grade_sheet):
|
||||
def totalWithDrops(breakdown, drop_count):
|
||||
#create an array of tuples with (index, mark), sorted by mark['percent'] descending
|
||||
sorted_breakdown = sorted( enumerate(breakdown), key=lambda x: -x[1]['percent'] )
|
||||
sorted_breakdown = sorted(enumerate(breakdown), key=lambda x: -x[1]['percent'])
|
||||
# A list of the indices of the dropped scores
|
||||
dropped_indices = []
|
||||
if drop_count > 0:
|
||||
@@ -236,44 +238,42 @@ class AssignmentFormatGrader(CourseGrader):
|
||||
for index, mark in enumerate(breakdown):
|
||||
if index not in dropped_indices:
|
||||
aggregate_score += mark['percent']
|
||||
|
||||
|
||||
if (len(breakdown) - drop_count > 0):
|
||||
aggregate_score /= len(breakdown) - drop_count
|
||||
|
||||
|
||||
return aggregate_score, dropped_indices
|
||||
|
||||
|
||||
#Figure the homework scores
|
||||
scores = grade_sheet.get(self.type, [])
|
||||
breakdown = []
|
||||
for i in range( max(self.min_count, len(scores)) ):
|
||||
for i in range(max(self.min_count, len(scores))):
|
||||
if i < len(scores):
|
||||
percentage = scores[i].earned / float(scores[i].possible)
|
||||
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
|
||||
section_type = self.section_type,
|
||||
name = scores[i].section,
|
||||
percent = percentage,
|
||||
earned = float(scores[i].earned),
|
||||
possible = float(scores[i].possible) )
|
||||
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index=i + 1,
|
||||
section_type=self.section_type,
|
||||
name=scores[i].section,
|
||||
percent=percentage,
|
||||
earned=float(scores[i].earned),
|
||||
possible=float(scores[i].possible))
|
||||
else:
|
||||
percentage = 0
|
||||
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index = i+1, section_type = self.section_type)
|
||||
|
||||
short_label = "{short_label} {index:02d}".format(index = i+1, short_label = self.short_label)
|
||||
|
||||
breakdown.append( {'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category} )
|
||||
|
||||
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index=i + 1, section_type=self.section_type)
|
||||
|
||||
short_label = "{short_label} {index:02d}".format(index=i + 1, short_label=self.short_label)
|
||||
|
||||
breakdown.append({'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category})
|
||||
|
||||
total_percent, dropped_indices = totalWithDrops(breakdown, self.drop_count)
|
||||
|
||||
|
||||
for dropped_index in dropped_indices:
|
||||
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count = self.drop_count, section_type=self.section_type) }
|
||||
|
||||
|
||||
total_detail = "{section_type} Average = {percent:.0%}".format(percent = total_percent, section_type = self.section_type)
|
||||
total_label = "{short_label} Avg".format(short_label = self.short_label)
|
||||
breakdown.append( {'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True} )
|
||||
|
||||
|
||||
return {'percent' : total_percent,
|
||||
'section_breakdown' : breakdown,
|
||||
breakdown[dropped_index]['mark'] = {'detail': "The lowest {drop_count} {section_type} scores are dropped.".format(drop_count=self.drop_count, section_type=self.section_type)}
|
||||
|
||||
total_detail = "{section_type} Average = {percent:.0%}".format(percent=total_percent, section_type=self.section_type)
|
||||
total_label = "{short_label} Avg".format(short_label=self.short_label)
|
||||
breakdown.append({'percent': total_percent, 'label': total_label, 'detail': total_detail, 'category': self.category, 'prominent': True})
|
||||
|
||||
return {'percent': total_percent,
|
||||
'section_breakdown': breakdown,
|
||||
#No grade_breakdown here
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class Location(_LocationBase):
|
||||
def check_list(list_):
|
||||
for val in list_:
|
||||
if val is not None and INVALID_CHARS.search(val) is not None:
|
||||
log.debug('invalid characters val="%s", list_="%s"' % (val,list_))
|
||||
log.debug('invalid characters val="%s", list_="%s"' % (val, list_))
|
||||
raise InvalidLocationError(location)
|
||||
|
||||
if isinstance(location, basestring):
|
||||
@@ -169,7 +169,7 @@ class ModuleStore(object):
|
||||
calls to get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_items(self, location, depth=0):
|
||||
"""
|
||||
Returns a list of XModuleDescriptor instances for the items
|
||||
|
||||
@@ -10,5 +10,6 @@ class ItemNotFoundError(Exception):
|
||||
class InsufficientSpecificationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidLocationError(Exception):
|
||||
pass
|
||||
|
||||
@@ -56,6 +56,7 @@ def location_to_query(location):
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class MongoModuleStore(ModuleStore):
|
||||
"""
|
||||
A Mongodb backed ModuleStore
|
||||
|
||||
@@ -51,6 +51,7 @@ def test_invalid_locations():
|
||||
assert_raises(InvalidLocationError, Location, None)
|
||||
assert_raises(InvalidLocationError, Location, "tag://org/course/category/name with spaces/revision")
|
||||
|
||||
|
||||
def test_equality():
|
||||
assert_equals(
|
||||
Location('tag', 'org', 'course', 'category', 'name'),
|
||||
|
||||
@@ -53,7 +53,7 @@ class XMLModuleStore(ModuleStore):
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager,self.data_dir))
|
||||
log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
|
||||
log.debug('default_class = %s' % self.default_class)
|
||||
|
||||
for course_dir in os.listdir(self.data_dir):
|
||||
|
||||
@@ -3,7 +3,7 @@ Progress class for modules. Represents where a student is in a module.
|
||||
|
||||
Useful things to know:
|
||||
- Use Progress.to_js_status_str() to convert a progress into a simple
|
||||
status string to pass to js.
|
||||
status string to pass to js.
|
||||
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
|
||||
string to pass to js.
|
||||
|
||||
@@ -11,11 +11,12 @@ In particular, these functions have a canonical handing of None.
|
||||
|
||||
For most subclassing needs, you should only need to reimplement
|
||||
frac() and __str__().
|
||||
'''
|
||||
'''
|
||||
|
||||
from collections import namedtuple
|
||||
import numbers
|
||||
|
||||
|
||||
class Progress(object):
|
||||
'''Represents a progress of a/b (a out of b done)
|
||||
|
||||
@@ -37,7 +38,7 @@ class Progress(object):
|
||||
if not (isinstance(a, numbers.Number) and
|
||||
isinstance(b, numbers.Number)):
|
||||
raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b))
|
||||
|
||||
|
||||
if not (0 <= a <= b and b > 0):
|
||||
raise ValueError(
|
||||
'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b))
|
||||
@@ -66,13 +67,12 @@ class Progress(object):
|
||||
'''
|
||||
return self.frac()[0] > 0
|
||||
|
||||
|
||||
def inprogress(self):
|
||||
''' Returns True if fractional progress is strictly between 0 and 1.
|
||||
|
||||
subclassing note: implemented in terms of frac(), assumes sanity
|
||||
checking is done at construction time.
|
||||
'''
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
return a > 0 and a < b
|
||||
|
||||
@@ -83,15 +83,14 @@ class Progress(object):
|
||||
checking is done at construction time.
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
return a==b
|
||||
|
||||
return a == b
|
||||
|
||||
def ternary_str(self):
|
||||
''' Return a string version of this progress: either
|
||||
"none", "in_progress", or "done".
|
||||
|
||||
subclassing note: implemented in terms of frac()
|
||||
'''
|
||||
'''
|
||||
(a, b) = self.frac()
|
||||
if a == 0:
|
||||
return "none"
|
||||
@@ -111,8 +110,7 @@ class Progress(object):
|
||||
def __ne__(self, other):
|
||||
''' The opposite of equal'''
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
''' Return a string representation of this string.
|
||||
|
||||
@@ -147,7 +145,6 @@ class Progress(object):
|
||||
return "NA"
|
||||
return progress.ternary_str()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def to_js_detail_str(progress):
|
||||
'''
|
||||
|
||||
@@ -2,9 +2,11 @@ import json
|
||||
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
|
||||
class Module(XModule):
|
||||
def get_html(self):
|
||||
return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id)
|
||||
|
||||
@@ -53,9 +53,9 @@ class SequenceModule(XModule):
|
||||
|
||||
def handle_ajax(self, dispatch, get): # TODO: bounds checking
|
||||
''' get = request.POST instance '''
|
||||
if dispatch=='goto_position':
|
||||
if dispatch == 'goto_position':
|
||||
self.position = int(get['position'])
|
||||
return json.dumps({'success':True})
|
||||
return json.dumps({'success': True})
|
||||
raise self.system.exception404
|
||||
|
||||
def render(self):
|
||||
@@ -81,7 +81,7 @@ class SequenceModule(XModule):
|
||||
# of script, even if it occurs mid-string. Do this after json.dumps()ing
|
||||
# so that we can be sure of the quotations being used
|
||||
import re
|
||||
params = {'items': re.sub(r'(?i)</(script)', r'\u003c/\1', json.dumps(contents)), # ?i = re.IGNORECASE for py2.6 compatability
|
||||
params = {'items': re.sub(r'(?i)</(script)', r'\u003c/\1', json.dumps(contents)), # ?i = re.IGNORECASE for py2.6 compatability
|
||||
'element_id': self.location.html_id(),
|
||||
'item_id': self.id,
|
||||
'position': self.position,
|
||||
|
||||
@@ -36,7 +36,7 @@ class VideoModule(XModule):
|
||||
if dispatch == 'goto_position':
|
||||
self.position = int(float(get['position']))
|
||||
log.info(u"NEW POSITION {0}".format(self.position))
|
||||
return json.dumps({'success':True})
|
||||
return json.dumps({'success': True})
|
||||
raise Http404()
|
||||
|
||||
def get_progress(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ from functools import partial
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
def dummy_track(event_type, event):
|
||||
pass
|
||||
|
||||
@@ -171,11 +172,11 @@ class XModule(object):
|
||||
return None
|
||||
|
||||
def max_score(self):
|
||||
''' Maximum score. Two notes:
|
||||
''' Maximum score. Two notes:
|
||||
* This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another
|
||||
* In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code
|
||||
should get fixed), and (b) break some analytics we plan to put in place.
|
||||
'''
|
||||
should get fixed), and (b) break some analytics we plan to put in place.
|
||||
'''
|
||||
return None
|
||||
|
||||
def get_html(self):
|
||||
@@ -193,8 +194,8 @@ class XModule(object):
|
||||
return None
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
''' dispatch is last part of the URL.
|
||||
get is a dictionary-like object '''
|
||||
''' dispatch is last part of the URL.
|
||||
get is a dictionary-like object '''
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
from django.utils.simplejson import dumps
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from certificates.models import GeneratedCertificate
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = """
|
||||
This command finds all GeneratedCertificate objects that do not have a
|
||||
certificate generated. These come into being when a user requests a
|
||||
certificate, or when grade_all_students is called (for pre-generating
|
||||
help = """
|
||||
This command finds all GeneratedCertificate objects that do not have a
|
||||
certificate generated. These come into being when a user requests a
|
||||
certificate, or when grade_all_students is called (for pre-generating
|
||||
certificates).
|
||||
|
||||
|
||||
It returns a json formatted list of users and their user ids
|
||||
"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
users = GeneratedCertificate.objects.filter(
|
||||
download_url = None )
|
||||
download_url=None)
|
||||
user_output = [{'user_id':user.user_id, 'name':user.name}
|
||||
for user in users]
|
||||
self.stdout.write(dumps(user_output) + "\n")
|
||||
|
||||
@@ -90,4 +90,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
complete_apps = ['certificates']
|
||||
|
||||
@@ -88,4 +88,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
complete_apps = ['certificates']
|
||||
|
||||
@@ -89,4 +89,4 @@ class Migration(SchemaMigration):
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['certificates']
|
||||
complete_apps = ['certificates']
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding field 'GeneratedCertificate.graded_certificate_id'
|
||||
db.add_column('certificates_generatedcertificate', 'graded_certificate_id', self.gf('django.db.models.fields.CharField')(max_length=32, null=True), keep_default=False)
|
||||
|
||||
@@ -17,9 +18,8 @@ class Migration(SchemaMigration):
|
||||
# Adding field 'GeneratedCertificate.grade'
|
||||
db.add_column('certificates_generatedcertificate', 'grade', self.gf('django.db.models.fields.CharField')(max_length=5, null=True), keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting field 'GeneratedCertificate.graded_certificate_id'
|
||||
db.delete_column('certificates_generatedcertificate', 'graded_certificate_id')
|
||||
|
||||
@@ -29,7 +29,6 @@ class Migration(SchemaMigration):
|
||||
# Deleting field 'GeneratedCertificate.grade'
|
||||
db.delete_column('certificates_generatedcertificate', 'grade')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,20 +4,19 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding field 'GeneratedCertificate.name'
|
||||
db.add_column('certificates_generatedcertificate', 'name', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting field 'GeneratedCertificate.name'
|
||||
db.delete_column('certificates_generatedcertificate', 'name')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,20 +4,19 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'GeneratedCertificate.certificate_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'certificate_id', self.gf('django.db.models.fields.CharField')(max_length=32, null=True))
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Changing field 'GeneratedCertificate.certificate_id'
|
||||
db.alter_column('certificates_generatedcertificate', 'certificate_id', self.gf('django.db.models.fields.CharField')(default=None, max_length=32))
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'RevokedCertificate'
|
||||
db.create_table('certificates_revokedcertificate', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -23,13 +24,11 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
db.send_create_signal('certificates', ['RevokedCertificate'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Deleting model 'RevokedCertificate'
|
||||
db.delete_table('certificates_revokedcertificate')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.db import models
|
||||
'''
|
||||
Certificates are created for a student and an offering of a course.
|
||||
|
||||
When a certificate is generated, a unique ID is generated so that
|
||||
When a certificate is generated, a unique ID is generated so that
|
||||
the certificate can be verified later. The ID is a UUID4, so that
|
||||
it can't be easily guessed and so that it is unique. Even though
|
||||
we save these generated certificates (for later verification), we
|
||||
@@ -15,7 +15,7 @@ also record the UUID so that if we regenerate the certificate it
|
||||
will have the same UUID.
|
||||
|
||||
If certificates are being generated on the fly, a GeneratedCertificate
|
||||
should be created with the user, certificate_id, and enabled set
|
||||
should be created with the user, certificate_id, and enabled set
|
||||
when a student requests a certificate. When the certificate has been
|
||||
generated, the download_url should be set.
|
||||
|
||||
@@ -26,119 +26,119 @@ needs to be set to true.
|
||||
|
||||
'''
|
||||
|
||||
|
||||
class GeneratedCertificate(models.Model):
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
# This is the name at the time of request
|
||||
name = models.CharField(blank=True, max_length=255)
|
||||
|
||||
|
||||
certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
graded_certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
|
||||
|
||||
download_url = models.CharField(max_length=128, null=True)
|
||||
graded_download_url = models.CharField(max_length=128, null=True)
|
||||
|
||||
|
||||
grade = models.CharField(max_length=5, null=True)
|
||||
|
||||
|
||||
# enabled should only be true if the student has earned a grade in the course
|
||||
# The student must have a grade and request a certificate for enabled to be True
|
||||
enabled = models.BooleanField(default=False)
|
||||
|
||||
|
||||
|
||||
class RevokedCertificate(models.Model):
|
||||
"""
|
||||
This model is for when a GeneratedCertificate must be regenerated. This model
|
||||
contains all the same fields, to store a record of what the GeneratedCertificate
|
||||
was before it was revoked (at which time all of it's information can change when
|
||||
it is regenerated).
|
||||
|
||||
|
||||
GeneratedCertificate may be deleted once they are revoked, and then created again.
|
||||
For this reason, the only link between a GeneratedCertificate and RevokedCertificate
|
||||
is that they share the same user.
|
||||
"""
|
||||
####-------------------New Fields--------------------####
|
||||
explanation = models.TextField(blank=True)
|
||||
|
||||
|
||||
####---------Fields from GeneratedCertificate---------####
|
||||
user = models.ForeignKey(User, db_index=True)
|
||||
# This is the name at the time of request
|
||||
name = models.CharField(blank=True, max_length=255)
|
||||
|
||||
|
||||
certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
graded_certificate_id = models.CharField(max_length=32, null=True, default=None)
|
||||
|
||||
|
||||
download_url = models.CharField(max_length=128, null=True)
|
||||
graded_download_url = models.CharField(max_length=128, null=True)
|
||||
|
||||
|
||||
grade = models.CharField(max_length=5, null=True)
|
||||
|
||||
|
||||
enabled = models.BooleanField(default=False)
|
||||
|
||||
|
||||
|
||||
def revoke_certificate(certificate, explanation):
|
||||
"""
|
||||
This method takes a GeneratedCertificate. It records its information from the certificate
|
||||
into a RevokedCertificate, and then marks the certificate as needing regenerating.
|
||||
into a RevokedCertificate, and then marks the certificate as needing regenerating.
|
||||
When the new certificiate is regenerated it will have new IDs and download URLS.
|
||||
|
||||
Once this method has been called, it is safe to delete the certificate, or modify the
|
||||
|
||||
Once this method has been called, it is safe to delete the certificate, or modify the
|
||||
certificate's name or grade until it has been generated again.
|
||||
"""
|
||||
revoked = RevokedCertificate( user = certificate.user,
|
||||
name = certificate.name,
|
||||
certificate_id = certificate.certificate_id,
|
||||
graded_certificate_id = certificate.graded_certificate_id,
|
||||
download_url = certificate.download_url,
|
||||
graded_download_url = certificate.graded_download_url,
|
||||
grade = certificate.grade,
|
||||
enabled = certificate.enabled)
|
||||
|
||||
revoked = RevokedCertificate(user=certificate.user,
|
||||
name=certificate.name,
|
||||
certificate_id=certificate.certificate_id,
|
||||
graded_certificate_id=certificate.graded_certificate_id,
|
||||
download_url=certificate.download_url,
|
||||
graded_download_url=certificate.graded_download_url,
|
||||
grade=certificate.grade,
|
||||
enabled=certificate.enabled)
|
||||
|
||||
revoked.explanation = explanation
|
||||
|
||||
|
||||
certificate.certificate_id = None
|
||||
certificate.graded_certificate_id = None
|
||||
certificate.download_url = None
|
||||
certificate.graded_download_url = None
|
||||
|
||||
|
||||
certificate.save()
|
||||
revoked.save()
|
||||
|
||||
|
||||
|
||||
|
||||
def certificate_state_for_student(student, grade):
|
||||
'''
|
||||
This returns a dictionary with a key for state, and other information. The state is one of the
|
||||
following:
|
||||
|
||||
|
||||
unavailable - A student is not eligible for a certificate.
|
||||
requestable - A student is eligible to request a certificate
|
||||
generating - A student has requested a certificate, but it is not generated yet.
|
||||
downloadable - The certificate has been requested and is available for download.
|
||||
|
||||
|
||||
If the state is "downloadable", the dictionary also contains "download_url" and "graded_download_url".
|
||||
|
||||
|
||||
'''
|
||||
|
||||
|
||||
if grade:
|
||||
#TODO: Remove the following after debugging
|
||||
if settings.DEBUG_SURVEY:
|
||||
return {'state' : 'requestable' }
|
||||
|
||||
return {'state': 'requestable'}
|
||||
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(user = student)
|
||||
generated_certificate = GeneratedCertificate.objects.get(user=student)
|
||||
if generated_certificate.enabled:
|
||||
if generated_certificate.download_url:
|
||||
return {'state' : 'downloadable',
|
||||
'download_url' : generated_certificate.download_url,
|
||||
'graded_download_url' : generated_certificate.graded_download_url}
|
||||
return {'state': 'downloadable',
|
||||
'download_url': generated_certificate.download_url,
|
||||
'graded_download_url': generated_certificate.graded_download_url}
|
||||
else:
|
||||
return {'state' : 'generating'}
|
||||
return {'state': 'generating'}
|
||||
else:
|
||||
# If enabled=False, it may have been pre-generated but not yet requested
|
||||
# Our output will be the same as if the GeneratedCertificate did not exist
|
||||
pass
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
pass
|
||||
return {'state' : 'requestable'}
|
||||
return {'state': 'requestable'}
|
||||
else:
|
||||
# No grade, no certificate. No exceptions
|
||||
return {'state' : 'unavailable'}
|
||||
return {'state': 'unavailable'}
|
||||
|
||||
@@ -18,76 +18,74 @@ from student.models import UserProfile
|
||||
|
||||
log = logging.getLogger("mitx.certificates")
|
||||
|
||||
|
||||
@login_required
|
||||
def certificate_request(request):
|
||||
''' Attempt to send a certificate. '''
|
||||
if not settings.END_COURSE_ENABLED:
|
||||
raise Http404
|
||||
|
||||
|
||||
if request.method == "POST":
|
||||
honor_code_verify = request.POST.get('cert_request_honor_code_verify', 'false')
|
||||
name_verify = request.POST.get('cert_request_name_verify', 'false')
|
||||
id_verify = request.POST.get('cert_request_id_verify', 'false')
|
||||
error = ''
|
||||
|
||||
|
||||
def return_error(error):
|
||||
return HttpResponse(json.dumps({'success':False,
|
||||
'error': error }))
|
||||
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': error}))
|
||||
|
||||
if honor_code_verify != 'true':
|
||||
error += 'Please verify that you have followed the honor code to receive a certificate. '
|
||||
|
||||
|
||||
if name_verify != 'true':
|
||||
error += 'Please verify that your name is correct to receive a certificate. '
|
||||
|
||||
|
||||
if id_verify != 'true':
|
||||
error += 'Please certify that you understand the unique ID on the certificate. '
|
||||
|
||||
|
||||
if len(error) > 0:
|
||||
return return_error(error)
|
||||
|
||||
|
||||
survey_response = record_exit_survey(request, internal_request=True)
|
||||
if not survey_response['success']:
|
||||
return return_error( survey_response['error'] )
|
||||
|
||||
return return_error(survey_response['error'])
|
||||
|
||||
grade = None
|
||||
student_gradesheet = grades.grade_sheet(request.user)
|
||||
grade = student_gradesheet['grade']
|
||||
|
||||
|
||||
if not grade:
|
||||
return return_error('You have not earned a grade in this course. ')
|
||||
|
||||
|
||||
generate_certificate(request.user, grade)
|
||||
|
||||
return HttpResponse(json.dumps({'success':True}))
|
||||
|
||||
|
||||
return HttpResponse(json.dumps({'success': True}))
|
||||
|
||||
else:
|
||||
#This is not a POST, we should render the page with the form
|
||||
|
||||
|
||||
grade_sheet = grades.grade_sheet(request.user)
|
||||
certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
|
||||
|
||||
|
||||
if certificate_state['state'] != "requestable":
|
||||
return redirect("/profile")
|
||||
|
||||
|
||||
user_info = UserProfile.objects.get(user=request.user)
|
||||
|
||||
|
||||
took_survey = student_took_survey(user_info)
|
||||
if settings.DEBUG_SURVEY:
|
||||
took_survey = False
|
||||
survey_list = []
|
||||
if not took_survey:
|
||||
survey_list = exit_survey_list_for_student(request.user)
|
||||
|
||||
|
||||
context = {'certificate_state' : certificate_state,
|
||||
'took_survey' : took_survey,
|
||||
'survey_list' : survey_list,
|
||||
'name' : user_info.name }
|
||||
|
||||
|
||||
return render_to_response('cert_request.html', context)
|
||||
|
||||
context = {'certificate_state': certificate_state,
|
||||
'took_survey': took_survey,
|
||||
'survey_list': survey_list,
|
||||
'name': user_info.name}
|
||||
|
||||
return render_to_response('cert_request.html', context)
|
||||
|
||||
|
||||
# This method should only be called if the user has a grade and has requested a certificate
|
||||
@@ -96,11 +94,11 @@ def generate_certificate(user, grade):
|
||||
# states for a GeneratedCertificate object
|
||||
if grade and user.is_active:
|
||||
generated_certificate = None
|
||||
|
||||
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(user = user)
|
||||
generated_certificate = GeneratedCertificate.objects.get(user=user)
|
||||
except GeneratedCertificate.DoesNotExist:
|
||||
generated_certificate = GeneratedCertificate(user = user)
|
||||
generated_certificate = GeneratedCertificate(user=user)
|
||||
|
||||
generated_certificate.enabled = True
|
||||
if generated_certificate.graded_download_url and (generated_certificate.grade != grade):
|
||||
@@ -114,8 +112,8 @@ def generate_certificate(user, grade):
|
||||
ungraded_dl_url=generated_certificate.download_url,
|
||||
userid=user.id))
|
||||
revoke_certificate(generated_certificate, "The grade on this certificate may be inaccurate.")
|
||||
|
||||
user_name = UserProfile.objects.get(user = user).name
|
||||
|
||||
user_name = UserProfile.objects.get(user=user).name
|
||||
if generated_certificate.download_url and (generated_certificate.name != user_name):
|
||||
log.critical(u"A Certificate has been pre-generated with the name of "
|
||||
"{gen_name} but current name is {user_name} (user id is "
|
||||
@@ -128,22 +126,21 @@ def generate_certificate(user, grade):
|
||||
userid=user.id))
|
||||
revoke_certificate(generated_certificate, "The name on this certificate may be inaccurate.")
|
||||
|
||||
|
||||
generated_certificate.grade = grade
|
||||
generated_certificate.name = user_name
|
||||
generated_certificate.save()
|
||||
|
||||
|
||||
certificate_id = generated_certificate.certificate_id
|
||||
|
||||
|
||||
log.debug("Generating certificate for " + str(user.username) + " with ID: " + str(certificate_id))
|
||||
|
||||
|
||||
# TODO: If the certificate was pre-generated, send the email that it is ready to download
|
||||
if certificate_state_for_student(user, grade)['state'] == "downloadable":
|
||||
subject = render_to_string('emails/certificate_ready_subject.txt',{})
|
||||
subject = render_to_string('emails/certificate_ready_subject.txt', {})
|
||||
subject = ''.join(subject.splitlines())
|
||||
message = render_to_string('emails/certificate_ready.txt',{})
|
||||
|
||||
res=send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email,])
|
||||
|
||||
message = render_to_string('emails/certificate_ready.txt', {})
|
||||
|
||||
res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email, ])
|
||||
|
||||
else:
|
||||
log.warning("Asked to generate a certificate for student " + str(user.username) + " but with a grade of " + str(grade) + " and active status " + str(user.is_active))
|
||||
|
||||
@@ -3,10 +3,11 @@ import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class ServerCircuit(models.Model):
|
||||
# Later, add owner, who can edit, part of what app, etc.
|
||||
# Later, add owner, who can edit, part of what app, etc.
|
||||
name = models.CharField(max_length=32, unique=True, db_index=True)
|
||||
schematic = models.TextField(blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name+":"+self.schematic[:8]
|
||||
return self.name + ":" + self.schematic[:8]
|
||||
|
||||
@@ -11,8 +11,9 @@ from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
|
||||
from models import ServerCircuit
|
||||
|
||||
|
||||
def circuit_line(circuit):
|
||||
''' Returns string for an appropriate input element for a circuit.
|
||||
''' Returns string for an appropriate input element for a circuit.
|
||||
TODO: Rename. '''
|
||||
if not circuit.isalnum():
|
||||
raise Http404()
|
||||
@@ -28,10 +29,11 @@ def circuit_line(circuit):
|
||||
circuit_line.set('width', '640')
|
||||
circuit_line.set('height', '480')
|
||||
circuit_line.set('name', 'schematic')
|
||||
circuit_line.set('id', 'schematic_'+circuit)
|
||||
circuit_line.set('value', schematic) # We do it this way for security -- guarantees users cannot put funny stuff in schematic.
|
||||
circuit_line.set('id', 'schematic_' + circuit)
|
||||
circuit_line.set('value', schematic) # We do it this way for security -- guarantees users cannot put funny stuff in schematic.
|
||||
return xml.etree.ElementTree.tostring(circuit_line)
|
||||
|
||||
|
||||
def edit_circuit(request, circuit):
|
||||
try:
|
||||
sc = ServerCircuit.objects.get(name=circuit)
|
||||
@@ -40,11 +42,12 @@ def edit_circuit(request, circuit):
|
||||
|
||||
if not circuit.isalnum():
|
||||
raise Http404()
|
||||
response = render_to_response('edit_circuit.html', {'name':circuit,
|
||||
'circuit_line':circuit_line(circuit)})
|
||||
response = render_to_response('edit_circuit.html', {'name': circuit,
|
||||
'circuit_line': circuit_line(circuit)})
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
def save_circuit(request, circuit):
|
||||
if not circuit.isalnum():
|
||||
raise Http404()
|
||||
@@ -63,4 +66,3 @@ def save_circuit(request, circuit):
|
||||
response = HttpResponse(json_str, mimetype='application/json')
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Course settings module. All settings in the global_settings are
|
||||
first applied, and then any settings in the settings.DATA_DIR/course_settings.json
|
||||
are applied. A setting must be in ALL_CAPS.
|
||||
|
||||
are applied. A setting must be in ALL_CAPS.
|
||||
|
||||
Settings are used by calling
|
||||
|
||||
from courseware.course_settings import course_settings
|
||||
|
||||
Note that courseware.course_settings.course_settings is not a module -- it's an object. So
|
||||
Note that courseware.course_settings.course_settings is not a module -- it's an object. So
|
||||
importing individual settings is not possible:
|
||||
|
||||
from courseware.course_settings.course_settings import GRADER # This won't work.
|
||||
@@ -24,69 +24,67 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
global_settings_json = """
|
||||
{
|
||||
"GRADER" : [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Lab",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"category" : "Labs",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Midterm",
|
||||
"name" : "Midterm Exam",
|
||||
"short_label" : "Midterm",
|
||||
"weight" : 0.3
|
||||
},
|
||||
{
|
||||
"type" : "Final",
|
||||
"name" : "Final Exam",
|
||||
"short_label" : "Final",
|
||||
"weight" : 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"A" : 0.87,
|
||||
"B" : 0.7,
|
||||
"C" : 0.6
|
||||
}
|
||||
"GRADER" : [
|
||||
{
|
||||
"type" : "Homework",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"short_label" : "HW",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Lab",
|
||||
"min_count" : 12,
|
||||
"drop_count" : 2,
|
||||
"category" : "Labs",
|
||||
"weight" : 0.15
|
||||
},
|
||||
{
|
||||
"type" : "Midterm",
|
||||
"name" : "Midterm Exam",
|
||||
"short_label" : "Midterm",
|
||||
"weight" : 0.3
|
||||
},
|
||||
{
|
||||
"type" : "Final",
|
||||
"name" : "Final Exam",
|
||||
"short_label" : "Final",
|
||||
"weight" : 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS" : {
|
||||
"A" : 0.87,
|
||||
"B" : 0.7,
|
||||
"C" : 0.6
|
||||
}
|
||||
}
|
||||
"""
|
||||
"""
|
||||
|
||||
|
||||
class Settings(object):
|
||||
def __init__(self):
|
||||
|
||||
|
||||
# Load the global settings as a dictionary
|
||||
global_settings = json.loads(global_settings_json)
|
||||
|
||||
|
||||
|
||||
# Load the course settings as a dictionary
|
||||
course_settings = {}
|
||||
try:
|
||||
# TODO: this doesn't work with multicourse
|
||||
with open( settings.DATA_DIR + "/course_settings.json") as course_settings_file:
|
||||
with open(settings.DATA_DIR + "/course_settings.json") as course_settings_file:
|
||||
course_settings_string = course_settings_file.read()
|
||||
course_settings = json.loads(course_settings_string)
|
||||
except IOError:
|
||||
log.warning("Unable to load course settings file from " + str(settings.DATA_DIR) + "/course_settings.json")
|
||||
|
||||
|
||||
|
||||
# Override any global settings with the course settings
|
||||
global_settings.update(course_settings)
|
||||
|
||||
|
||||
# Now, set the properties from the course settings on ourselves
|
||||
for setting in global_settings:
|
||||
setting_value = global_settings[setting]
|
||||
setattr(self, setting, setting_value)
|
||||
|
||||
|
||||
# Here is where we should parse any configurations, so that we can fail early
|
||||
self.GRADER = graders.grader_from_conf(self.GRADER)
|
||||
|
||||
|
||||
@@ -12,15 +12,16 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_course(course_id, course_must_be_open=True, course_required=True):
|
||||
"""
|
||||
Given a course_id, this returns the course object. By default,
|
||||
if the course is not found or the course is not open yet, this
|
||||
method will raise a 404.
|
||||
|
||||
|
||||
If course_must_be_open is False, the course will be returned
|
||||
without a 404 even if it is not open.
|
||||
|
||||
|
||||
If course_required is False, a course_id of None is acceptable. The
|
||||
course returned will be None. Even if the course is not required,
|
||||
if a course_id is given that does not exist a 404 will be raised.
|
||||
@@ -32,10 +33,10 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
|
||||
course = modulestore().get_item(course_loc)
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
|
||||
if course_must_be_open and not course.has_started():
|
||||
raise Http404("This course has not yet started.")
|
||||
|
||||
|
||||
return course
|
||||
|
||||
|
||||
@@ -44,10 +45,12 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
|
||||
|
||||
def course_static_url(course):
|
||||
return settings.STATIC_URL + "/" + course.metadata['data_dir'] + "/"
|
||||
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
return course_static_url(course) + "images/course_image.jpg"
|
||||
|
||||
|
||||
|
||||
def get_course_about_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course about page, given the key for the section.
|
||||
@@ -78,7 +81,7 @@ def get_course_about_section(course, section_key):
|
||||
'effort', 'end_date', 'prerequisites']:
|
||||
try:
|
||||
with course.system.resources_fs.open(path("about") / section_key + ".html") as htmlFile:
|
||||
return htmlFile.read().decode('utf-8').format(COURSE_STATIC_URL = course_static_url(course) )
|
||||
return htmlFile.read().decode('utf-8').format(COURSE_STATIC_URL=course_static_url(course))
|
||||
except ResourceNotFoundError:
|
||||
log.warning("Missing about section {key} in course {url}".format(key=section_key, url=course.location.url()))
|
||||
return None
|
||||
@@ -91,6 +94,7 @@ def get_course_about_section(course, section_key):
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
|
||||
def get_course_info_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course info page, given the key for the section.
|
||||
@@ -111,7 +115,7 @@ def get_course_info_section(course, section_key):
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing info section {key} in course {url}".format(key=section_key, url=course.location.url()))
|
||||
return "! Info section missing !"
|
||||
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding model 'StudentModule'
|
||||
db.create_table('courseware_studentmodule', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
@@ -24,16 +25,14 @@ class Migration(SchemaMigration):
|
||||
# Adding unique constraint on 'StudentModule', fields ['student', 'module_id', 'module_type']
|
||||
db.create_unique('courseware_studentmodule', ['student_id', 'module_id', 'module_type'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Removing unique constraint on 'StudentModule', fields ['student', 'module_id', 'module_type']
|
||||
db.delete_unique('courseware_studentmodule', ['student_id', 'module_id', 'module_type'])
|
||||
|
||||
# Deleting model 'StudentModule'
|
||||
db.delete_table('courseware_studentmodule')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Adding index on 'StudentModule', fields ['created']
|
||||
db.create_index('courseware_studentmodule', ['created'])
|
||||
|
||||
@@ -23,9 +24,8 @@ class Migration(SchemaMigration):
|
||||
# Adding index on 'StudentModule', fields ['module_id']
|
||||
db.create_index('courseware_studentmodule', ['module_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Removing index on 'StudentModule', fields ['module_id']
|
||||
db.delete_index('courseware_studentmodule', ['module_id'])
|
||||
|
||||
@@ -41,7 +41,6 @@ class Migration(SchemaMigration):
|
||||
# Removing index on 'StudentModule', fields ['created']
|
||||
db.delete_index('courseware_studentmodule', ['created'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -4,10 +4,11 @@ from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
|
||||
# Removing unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student']
|
||||
db.delete_unique('courseware_studentmodule', ['module_id', 'module_type', 'student_id'])
|
||||
|
||||
@@ -20,9 +21,8 @@ class Migration(SchemaMigration):
|
||||
# Adding unique constraint on 'StudentModule', fields ['module_id', 'student']
|
||||
db.create_unique('courseware_studentmodule', ['module_id', 'student_id'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
|
||||
# Removing unique constraint on 'StudentModule', fields ['module_id', 'student']
|
||||
db.delete_unique('courseware_studentmodule', ['module_id', 'student_id'])
|
||||
|
||||
@@ -35,7 +35,6 @@ class Migration(SchemaMigration):
|
||||
# Adding unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student']
|
||||
db.create_unique('courseware_studentmodule', ['module_id', 'module_type', 'student_id'])
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
|
||||
@@ -63,7 +63,6 @@ class StudentModule(models.Model):
|
||||
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
|
||||
|
||||
|
||||
|
||||
class StudentModuleCache(object):
|
||||
"""
|
||||
A cache of StudentModules for a specific student
|
||||
@@ -84,7 +83,7 @@ class StudentModuleCache(object):
|
||||
# that can be put into a single query
|
||||
self.cache = []
|
||||
chunk_size = 500
|
||||
for id_chunk in [module_ids[i:i+chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
||||
for id_chunk in [module_ids[i:i + chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
|
||||
self.cache.extend(StudentModule.objects.filter(
|
||||
student=user,
|
||||
module_state_key__in=id_chunk)
|
||||
|
||||
@@ -36,7 +36,7 @@ class I4xSystem(object):
|
||||
|
||||
ajax_url - the url where ajax calls to the encapsulating module go.
|
||||
xqueue_callback_url - the url where external queueing system (e.g. for grading)
|
||||
returns its response
|
||||
returns its response
|
||||
track_function - function of (event_type, event), intended for logging
|
||||
or otherwise tracking the event.
|
||||
TODO: Not used, and has inconsistent args in different
|
||||
@@ -278,7 +278,7 @@ def replace_static_urls(module, prefix):
|
||||
with urls that are /static/<prefix>/...
|
||||
"""
|
||||
original_get_html = module.get_html
|
||||
|
||||
|
||||
@wraps(original_get_html)
|
||||
def get_html():
|
||||
return replace_urls(original_get_html(), staticfiles_prefix=prefix)
|
||||
@@ -308,9 +308,9 @@ def add_histogram(module):
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
github_url = multicourse_settings.get_course_github_url(coursename)
|
||||
fn = module_xml.get('filename')
|
||||
if module_xml.tag=='problem': fn = 'problems/' + fn # grrr
|
||||
if module_xml.tag == 'problem': fn = 'problems/' + fn # grrr
|
||||
edit_link = (github_url + '/tree/master/' + fn) if github_url is not None else None
|
||||
if module_xml.tag=='problem': edit_link += '.xml' # grrr
|
||||
if module_xml.tag == 'problem': edit_link += '.xml' # grrr
|
||||
else:
|
||||
edit_link = False
|
||||
|
||||
@@ -328,13 +328,14 @@ def add_histogram(module):
|
||||
module.get_html = get_html
|
||||
return module
|
||||
|
||||
|
||||
# TODO: TEMPORARY BYPASS OF AUTH!
|
||||
@csrf_exempt
|
||||
def xqueue_callback(request, userid, id, dispatch):
|
||||
# Parse xqueue response
|
||||
get = request.POST.copy()
|
||||
try:
|
||||
header = json.loads(get.pop('xqueue_header')[0]) # 'dict'
|
||||
header = json.loads(get.pop('xqueue_header')[0]) # 'dict'
|
||||
except Exception as err:
|
||||
msg = "Error in xqueue_callback %s: Invalid return format" % err
|
||||
raise Exception(msg)
|
||||
@@ -344,12 +345,12 @@ def xqueue_callback(request, userid, id, dispatch):
|
||||
|
||||
student_module_cache = StudentModuleCache(user, modulestore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'",
|
||||
id, request.user)
|
||||
raise Http404
|
||||
|
||||
|
||||
oldgrade = instance_module.grade
|
||||
old_instance_state = instance_module.state
|
||||
|
||||
@@ -360,7 +361,7 @@ def xqueue_callback(request, userid, id, dispatch):
|
||||
# We go through the "AJAX" path
|
||||
# So far, the only dispatch from xqueue will be 'score_update'
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, get) # Can ignore the "ajax" return in 'xqueue_callback'
|
||||
ajax_return = instance.handle_ajax(dispatch, get) # Can ignore the "ajax" return in 'xqueue_callback'
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
@@ -374,6 +375,7 @@ def xqueue_callback(request, userid, id, dispatch):
|
||||
|
||||
return HttpResponse("")
|
||||
|
||||
|
||||
def modx_dispatch(request, dispatch=None, id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.
|
||||
|
||||
@@ -392,7 +394,7 @@ def modx_dispatch(request, dispatch=None, id=None):
|
||||
|
||||
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'",
|
||||
id, request.user)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
class completion(object):
|
||||
def __init__(self, **d):
|
||||
self.dict = dict({'duration_total':0,
|
||||
'duration_watched':0,
|
||||
'done':True,
|
||||
'questions_correct':0,
|
||||
'questions_incorrect':0,
|
||||
'questions_total':0})
|
||||
if d:
|
||||
self.dict = dict({'duration_total': 0,
|
||||
'duration_watched': 0,
|
||||
'done': True,
|
||||
'questions_correct': 0,
|
||||
'questions_incorrect': 0,
|
||||
'questions_total': 0})
|
||||
if d:
|
||||
self.dict.update(d)
|
||||
|
||||
def __getitem__(self, key):
|
||||
@@ -23,7 +23,7 @@ class completion(object):
|
||||
'questions_correct',
|
||||
'questions_incorrect',
|
||||
'questions_total']:
|
||||
result[item] = result[item]+other.dict[item]
|
||||
result[item] = result[item] + other.dict[item]
|
||||
return completion(**result)
|
||||
|
||||
def __contains__(self, key):
|
||||
@@ -33,6 +33,6 @@ class completion(object):
|
||||
return repr(self.dict)
|
||||
|
||||
if __name__ == '__main__':
|
||||
dict1=completion(duration_total=5)
|
||||
dict2=completion(duration_total=7)
|
||||
print dict1+dict2
|
||||
dict1 = completion(duration_total=5)
|
||||
dict2 = completion(duration_total=7)
|
||||
print dict1 + dict2
|
||||
|
||||
@@ -31,6 +31,7 @@ log = logging.getLogger("mitx.courseware")
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
|
||||
def user_groups(user):
|
||||
if not user.is_authenticated():
|
||||
return []
|
||||
@@ -62,15 +63,15 @@ def courses(request):
|
||||
for course in courses:
|
||||
universities[course.org].append(course)
|
||||
|
||||
return render_to_response("courses.html", { 'universities': universities })
|
||||
return render_to_response("courses.html", {'universities': universities})
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def gradebook(request, course_id):
|
||||
if 'course_admin' not in user_groups(request.user):
|
||||
raise Http404
|
||||
course = check_course(course_id)
|
||||
|
||||
|
||||
|
||||
student_objects = User.objects.all()[:100]
|
||||
student_info = []
|
||||
|
||||
@@ -168,7 +169,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
- HTTPresponse
|
||||
'''
|
||||
course = check_course(course_id)
|
||||
|
||||
|
||||
def clean(s):
|
||||
''' Fixes URLs -- we convert spaces to _ in URLs to prevent
|
||||
funny encoding characters and keep the URLs readable. This undoes
|
||||
@@ -258,18 +259,18 @@ def course_info(request, course_id):
|
||||
|
||||
return render_to_response('info.html', {'course': course})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def course_about(request, course_id):
|
||||
def registered_for_course(course, user):
|
||||
if user.is_authenticated():
|
||||
return CourseEnrollment.objects.filter(user = user, course_id=course.id).exists()
|
||||
return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists()
|
||||
else:
|
||||
return False
|
||||
course = check_course(course_id, course_must_be_open=False)
|
||||
registered = registered_for_course(course, request.user)
|
||||
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
|
||||
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -281,7 +282,7 @@ def university_profile(request, org_id):
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
|
||||
# Only grab courses for this org...
|
||||
courses=[c for c in all_courses if c.org == org_id]
|
||||
courses = [c for c in all_courses if c.org == org_id]
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
from datetime import datetime
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def heartbeat(request):
|
||||
"""
|
||||
Simple view that a loadbalancer can check to verify that the app is up
|
||||
|
||||
@@ -25,18 +25,18 @@ from django.conf import settings
|
||||
#-----------------------------------------------------------------------------
|
||||
# load course settings
|
||||
|
||||
if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be replaced by reading an XML file
|
||||
if hasattr(settings, 'COURSE_SETTINGS'): # in the future, this could be replaced by reading an XML file
|
||||
COURSE_SETTINGS = settings.COURSE_SETTINGS
|
||||
|
||||
elif hasattr(settings,'COURSE_NAME'): # backward compatibility
|
||||
elif hasattr(settings, 'COURSE_NAME'): # backward compatibility
|
||||
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
|
||||
'title': settings.COURSE_TITLE,
|
||||
'title': settings.COURSE_TITLE,
|
||||
'location': settings.COURSE_LOCATION,
|
||||
},
|
||||
}
|
||||
else: # default to 6.002_Spring_2012
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
|
||||
'title': 'Circuits and Electronics',
|
||||
'title': 'Circuits and Electronics',
|
||||
'location': 'i4x://edx/6002xs12/course/6.002 Spring 2012',
|
||||
},
|
||||
}
|
||||
@@ -44,6 +44,7 @@ else: # default to 6.002_Spring_2012
|
||||
#-----------------------------------------------------------------------------
|
||||
# wrapper functions around course settings
|
||||
|
||||
|
||||
def get_coursename_from_request(request):
|
||||
if 'coursename' in request.session:
|
||||
coursename = request.session['coursename']
|
||||
@@ -51,6 +52,7 @@ def get_coursename_from_request(request):
|
||||
else: coursename = None
|
||||
return coursename
|
||||
|
||||
|
||||
def get_course_settings(coursename):
|
||||
if not coursename:
|
||||
if hasattr(settings, 'COURSE_DEFAULT'):
|
||||
@@ -94,14 +96,18 @@ def get_course_title(coursename):
|
||||
def get_course_number(coursename):
|
||||
return get_course_property(coursename, 'number')
|
||||
|
||||
|
||||
def get_course_github_url(coursename):
|
||||
return get_course_property(coursename,'github_url')
|
||||
return get_course_property(coursename, 'github_url')
|
||||
|
||||
|
||||
def get_course_default_chapter(coursename):
|
||||
return get_course_property(coursename,'default_chapter')
|
||||
return get_course_property(coursename, 'default_chapter')
|
||||
|
||||
|
||||
def get_course_default_section(coursename):
|
||||
return get_course_property(coursename,'default_section')
|
||||
|
||||
return get_course_property(coursename, 'default_section')
|
||||
|
||||
|
||||
def get_course_location(coursename):
|
||||
return get_course_property(coursename, 'location')
|
||||
|
||||
@@ -3,12 +3,12 @@ from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from multicourse import multicourse_settings
|
||||
|
||||
|
||||
def mitxhome(request):
|
||||
''' Home page (link from main header). List of courses. '''
|
||||
if settings.DEBUG:
|
||||
print "[djangoapps.multicourse.mitxhome] MITX_ROOT_URL = " + settings.MITX_ROOT_URL
|
||||
if settings.ENABLE_MULTICOURSE:
|
||||
context = {'courseinfo' : multicourse_settings.COURSE_SETTINGS}
|
||||
context = {'courseinfo': multicourse_settings.COURSE_SETTINGS}
|
||||
return render_to_response("mitxhome.html", context)
|
||||
return info(request)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Source: django-simplewiki. GPL license.
|
||||
# Source: django-simplewiki. GPL license.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Source: django-simplewiki. GPL license.
|
||||
# Source: django-simplewiki. GPL license.
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
@@ -6,17 +6,21 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from models import Article, Revision, Permission, ArticleAttachment
|
||||
|
||||
|
||||
class RevisionInline(admin.TabularInline):
|
||||
model = Revision
|
||||
extra = 1
|
||||
|
||||
|
||||
class RevisionAdmin(admin.ModelAdmin):
|
||||
list_display = ('article', '__unicode__', 'revision_date', 'revision_user', 'revision_text')
|
||||
search_fields = ('article', 'counter')
|
||||
|
||||
|
||||
class AttachmentAdmin(admin.ModelAdmin):
|
||||
list_display = ('article', '__unicode__', 'uploaded_on', 'uploaded_by')
|
||||
|
||||
|
||||
class ArticleAdminForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = self.cleaned_data
|
||||
@@ -30,16 +34,19 @@ class ArticleAdminForm(forms.ModelForm):
|
||||
raise forms.ValidationError(_('Article slug and parent must be '
|
||||
'unique together.'))
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
|
||||
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
list_display = ('created_by', 'slug', 'modified_on', 'namespace')
|
||||
search_fields = ('slug',)
|
||||
prepopulated_fields = {'slug': ('title',) }
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
inlines = [RevisionInline]
|
||||
form = ArticleAdminForm
|
||||
save_on_top = True
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == 'current_revision':
|
||||
# Try to determine the id of the article being edited
|
||||
@@ -53,6 +60,7 @@ class ArticleAdmin(admin.ModelAdmin):
|
||||
return db_field.formfield(**kwargs)
|
||||
return super(ArticleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
|
||||
class PermissionAdmin(admin.ModelAdmin):
|
||||
search_fields = ('article', 'counter')
|
||||
|
||||
|
||||
@@ -26,19 +26,19 @@ try:
|
||||
except:
|
||||
from markdown import etree
|
||||
|
||||
|
||||
class CircuitExtension(markdown.Extension):
|
||||
def __init__(self, configs):
|
||||
for key, value in configs :
|
||||
for key, value in configs:
|
||||
self.setConfig(key, value)
|
||||
|
||||
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
## Because Markdown treats contigous lines as one block of text, it is hard to match
|
||||
## a regex that must occupy the whole line (like the circuit regex). This is why we have
|
||||
## a preprocessor that inspects the lines and replaces the matched lines with text that is
|
||||
## easier to match
|
||||
md.preprocessors.add('circuit', CircuitPreprocessor(md), "_begin")
|
||||
|
||||
md.preprocessors.add('circuit', CircuitPreprocessor(md), "_begin")
|
||||
|
||||
pattern = CircuitLink(r'processed-schematic:(?P<data>.*?)processed-schematic-end')
|
||||
pattern.md = md
|
||||
pattern.ext = self
|
||||
@@ -47,16 +47,16 @@ class CircuitExtension(markdown.Extension):
|
||||
|
||||
class CircuitPreprocessor(markdown.preprocessors.Preprocessor):
|
||||
preRegex = re.compile(r'^circuit-schematic:(?P<data>.*)$')
|
||||
|
||||
|
||||
def run(self, lines):
|
||||
def convertLine(line):
|
||||
m = self.preRegex.match(line)
|
||||
if m:
|
||||
return 'processed-schematic:{0}processed-schematic-end'.format( m.group('data') )
|
||||
return 'processed-schematic:{0}processed-schematic-end'.format(m.group('data'))
|
||||
else:
|
||||
return line
|
||||
|
||||
return [ convertLine(line) for line in lines ]
|
||||
|
||||
return [convertLine(line) for line in lines]
|
||||
|
||||
|
||||
class CircuitLink(markdown.inlinepatterns.Pattern):
|
||||
@@ -64,9 +64,9 @@ class CircuitLink(markdown.inlinepatterns.Pattern):
|
||||
data = m.group('data')
|
||||
data = escape(data)
|
||||
return etree.fromstring("<div align='center'><input type='hidden' parts='' value='" + data + "' analyses='' class='schematic ctrls' width='640' height='480'/></div>")
|
||||
|
||||
|
||||
|
||||
|
||||
def makeExtension(configs=None):
|
||||
to_return = CircuitExtension(configs=configs)
|
||||
print "circuit returning " , to_return
|
||||
print "circuit returning ", to_return
|
||||
return to_return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user