Cleaning up pep8 violations

This commit is contained in:
Calen Pennington
2012-07-23 14:44:40 -04:00
parent 7ef8b6ac1e
commit 1d1a9173a4
128 changed files with 2200 additions and 2006 deletions

View File

@@ -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):

View File

@@ -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()

View File

@@ -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)

View File

@@ -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.

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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):
"""

View File

@@ -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.

View File

@@ -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:

View File

@@ -15,4 +15,3 @@ admin.site.register(CourseEnrollment)
admin.site.register(Registration)
admin.site.register(PendingNameChange)

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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']

View File

@@ -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']

View File

@@ -124,4 +124,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['student']
complete_apps = ['student']

View File

@@ -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']

View File

@@ -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']

View File

@@ -130,4 +130,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['student']
complete_apps = ['student']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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']

View File

@@ -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()

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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))

View File

@@ -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.")

View File

@@ -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", ""))

View File

@@ -1 +0,0 @@

View File

@@ -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")

View File

@@ -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

View File

@@ -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()

View File

@@ -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())

View File

@@ -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]

View File

@@ -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 = {'"': '&quot;'}
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('<','&lt;'))
newmsg += '<br/>Original message: %s' % msg.replace('<','&lt;')
newmsg = 'error %s in rendering message' % (str(err).replace('<', '&lt;'))
newmsg += '<br/>Original message: %s' % msg.replace('<', '&lt;')
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('<','&lt;')
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<','&lt;')
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('<', '&lt;')
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<', '&lt;')
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)

View File

@@ -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('&#60;','&lt;')
msg = '<html>' + msg + '</html>'
msg = msg.replace('&#60;', '&lt;')
#msg = msg.replace('&lt;','<')
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('&#13;','')
msg = msg.replace('&#13;', '')
#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('&nbsp;','&#160;')
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('&nbsp;', '&#160;')
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('<','&lt;'))
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('<', '&lt;'))
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('&nbsp;','&#160;') if idx==0 else None
msg = rxml.find('message').text.replace('&nbsp;', '&#160;') 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('<','&lt;')
msg = '<font color=red size=+2>%s</font>' % str(err).replace('<', '&lt;')
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]

View File

@@ -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

View File

@@ -13,4 +13,3 @@
# limitations under the License.
lookup = None

View File

@@ -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

View File

@@ -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):
'''

View File

@@ -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())

View File

@@ -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('<','&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','&lt;')
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
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")

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -10,5 +10,6 @@ class ItemNotFoundError(Exception):
class InsufficientSpecificationError(Exception):
pass
class InvalidLocationError(Exception):
pass

View File

@@ -56,6 +56,7 @@ def location_to_query(location):
return query
class MongoModuleStore(ModuleStore):
"""
A Mongodb backed ModuleStore

View File

@@ -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'),

View File

@@ -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):

View File

@@ -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):
'''

View File

@@ -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)

View File

@@ -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,

View File

@@ -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):

View File

@@ -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 ""

View File

@@ -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")

View File

@@ -90,4 +90,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['certificates']
complete_apps = ['certificates']

View File

@@ -88,4 +88,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['certificates']
complete_apps = ['certificates']

View File

@@ -89,4 +89,4 @@ class Migration(SchemaMigration):
}
}
complete_apps = ['certificates']
complete_apps = ['certificates']

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'}

View File

@@ -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))

View File

@@ -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]

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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'},

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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')

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
# Source: django-simplewiki. GPL license.
# Source: django-simplewiki. GPL license.
import os
import sys

View File

@@ -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')

View File

@@ -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