diff --git a/cms/djangoapps/contentstore/tests/__init__.py b/cms/djangoapps/contentstore/tests/__init__.py index 8b137891791fe96927ad78e64b0aad7bded08bdc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/cms/djangoapps/contentstore/tests/__init__.py +++ b/cms/djangoapps/contentstore/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 4ab152adb1aa052863393c82dc1e67a78b558f0e..ade7c4e75d756b0d269cda81746a0a567c3b19ef 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -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): diff --git a/cms/djangoapps/github_sync/__init__.py b/cms/djangoapps/github_sync/__init__.py index 9f490119af79d8050aa0fc48a99aeeda0a1e609f..c142ca289788daea225aec835175244bfbde2378 100644 --- a/cms/djangoapps/github_sync/__init__.py +++ b/cms/djangoapps/github_sync/__init__.py @@ -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() diff --git a/cms/djangoapps/github_sync/tests/test_views.py b/cms/djangoapps/github_sync/tests/test_views.py index 9e8095a67b1f30ca6ffde45b7685f7a554524a13..f46e7f7db3150fac63675f4905119593ef184b15 100644 --- a/cms/djangoapps/github_sync/tests/test_views.py +++ b/cms/djangoapps/github_sync/tests/test_views.py @@ -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) - diff --git a/common/djangoapps/cache_toolbox/core.py b/common/djangoapps/cache_toolbox/core.py index b2802e7fec831129c4ec48d4e6fba8f1c24b684f..208be34a73a4ad76a83c080587dd77cc4b98f893 100644 --- a/common/djangoapps/cache_toolbox/core.py +++ b/common/djangoapps/cache_toolbox/core.py @@ -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. diff --git a/common/djangoapps/cache_toolbox/middleware.py b/common/djangoapps/cache_toolbox/middleware.py index 97f0bdb2af4d00fdd531a57137da6e132f2f91e7..fd24bbf18c3612ff435d2cadc94502049d225559 100644 --- a/common/djangoapps/cache_toolbox/middleware.py +++ b/common/djangoapps/cache_toolbox/middleware.py @@ -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) diff --git a/common/djangoapps/cache_toolbox/model.py b/common/djangoapps/cache_toolbox/model.py index 8ac8f0d2499d98af14be249e3f9b1389965505da..688b054bd55444ba6f94eee633a3d910daa05d53 100644 --- a/common/djangoapps/cache_toolbox/model.py +++ b/common/djangoapps/cache_toolbox/model.py @@ -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 diff --git a/common/djangoapps/cache_toolbox/relation.py b/common/djangoapps/cache_toolbox/relation.py index 38d985aa9476f7feed3f84bd31eb22f98faade2c..da41c574db92036e6eef12de7faa1c56679c52ba 100644 --- a/common/djangoapps/cache_toolbox/relation.py +++ b/common/djangoapps/cache_toolbox/relation.py @@ -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() diff --git a/common/djangoapps/cache_toolbox/templatetags/cache_toolbox.py b/common/djangoapps/cache_toolbox/templatetags/cache_toolbox.py index feea2af1c8a216bc2e661902f1855b5bb0d3b811..0f746aecfb06a797c28224a1150456b18b2734cd 100644 --- a/common/djangoapps/cache_toolbox/templatetags/cache_toolbox.py +++ b/common/djangoapps/cache_toolbox/templatetags/cache_toolbox.py @@ -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): """ diff --git a/common/djangoapps/django_future/csrf.py b/common/djangoapps/django_future/csrf.py index ba5c3a7791df152771ab06f68ba3f22b9c347eb1..151d7f60136e221e9412a1ee0ba89c2beea623c8 100644 --- a/common/djangoapps/django_future/csrf.py +++ b/common/djangoapps/django_future/csrf.py @@ -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. diff --git a/common/djangoapps/pipeline_mako/__init__.py b/common/djangoapps/pipeline_mako/__init__.py index f100d95916689d6799effe1737b3cf78dc0c2253..4703b53e52dea02241533556caf82a88a266543c 100644 --- a/common/djangoapps/pipeline_mako/__init__.py +++ b/common/djangoapps/pipeline_mako/__init__.py @@ -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: diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index 1391f264827a0813288a897e35cf470a5ef2d869..ec3b708ca703661f183dd4ef7e518994e414f5cd 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -15,4 +15,3 @@ admin.site.register(CourseEnrollment) admin.site.register(Registration) admin.site.register(PendingNameChange) - diff --git a/common/djangoapps/student/management/commands/6002exportusers.py b/common/djangoapps/student/management/commands/6002exportusers.py index 0ea458408cab0cec4a09fa80af9021c95e2ecd61..fcf565fb83b70555ff351ff8e6140532cf9567ee 100644 --- a/common/djangoapps/student/management/commands/6002exportusers.py +++ b/common/djangoapps/student/management/commands/6002exportusers.py @@ -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: diff --git a/common/djangoapps/student/management/commands/6002importusers.py b/common/djangoapps/student/management/commands/6002importusers.py index e6d801edb547db925e8049546207104a9f71aec6..64be84d91019b756f586655cdd5a97ad9855d197 100644 --- a/common/djangoapps/student/management/commands/6002importusers.py +++ b/common/djangoapps/student/management/commands/6002importusers.py @@ -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 diff --git a/common/djangoapps/student/management/commands/assigngroups.py b/common/djangoapps/student/management/commands/assigngroups.py index 87c15bb1ab94770774d6af416974df9bb799d65d..fb7bfc85cd92a4b1f7e12e9745d9abf3a5e2f484 100644 --- a/common/djangoapps/student/management/commands/assigngroups.py +++ b/common/djangoapps/student/management/commands/assigngroups.py @@ -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 diff --git a/common/djangoapps/student/management/commands/emaillist.py b/common/djangoapps/student/management/commands/emaillist.py index 019e98aac50d60ad9de574e8429859b035273c2b..4011c41bd2f01f9d438c15ce66d0a32baeee773b 100644 --- a/common/djangoapps/student/management/commands/emaillist.py +++ b/common/djangoapps/student/management/commands/emaillist.py @@ -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() diff --git a/common/djangoapps/student/management/commands/massemail.py b/common/djangoapps/student/management/commands/massemail.py index 306ae51c0e3c42eadd31f26ad6d2d04c652daa46..c6f6e5f6d44a7e9021ffcc2c3a42424bb36f54fd 100644 --- a/common/djangoapps/student/management/commands/massemail.py +++ b/common/djangoapps/student/management/commands/massemail.py @@ -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) diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py index 661c4e4fb705086d9373539b719045e23fc9634c..4ea75f972b09904f39dac6f9eee40efc5e622c8a 100644 --- a/common/djangoapps/student/management/commands/massemailtxt.py +++ b/common/djangoapps/student/management/commands/massemailtxt.py @@ -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() diff --git a/common/djangoapps/student/management/commands/userinfo.py b/common/djangoapps/student/management/commands/userinfo.py index 72b4d2ebd686b76dbf571e2079480a7d08c9da83..e45899528464c06f2fe0583867d5c2d4fc7a1caa 100644 --- a/common/djangoapps/student/management/commands/userinfo.py +++ b/common/djangoapps/student/management/commands/userinfo.py @@ -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() diff --git a/common/djangoapps/student/migrations/0001_initial.py b/common/djangoapps/student/migrations/0001_initial.py index cab5690ea7e1afb1e40e133819e2a5565559cb9e..d5766ca823a73e7e452c988b57198f63baec67f9 100644 --- a/common/djangoapps/student/migrations/0001_initial.py +++ b/common/djangoapps/student/migrations/0001_initial.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py b/common/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py index a75f164f3af351f510185eee6b557a10a2cb76f7..27aea40a5cb7fd339049cb548621d5d9b78c5f2e 100644 --- a/common/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py +++ b/common/djangoapps/student/migrations/0002_text_to_varchar_and_indexes.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0003_auto__add_usertestgroup.py b/common/djangoapps/student/migrations/0003_auto__add_usertestgroup.py index 4765d635781520f8e30a49ba3a216369ee31769f..a63abf9469808974d9e7db22e075e0bb182a42a2 100644 --- a/common/djangoapps/student/migrations/0003_auto__add_usertestgroup.py +++ b/common/djangoapps/student/migrations/0003_auto__add_usertestgroup.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0004_add_email_index.py b/common/djangoapps/student/migrations/0004_add_email_index.py index c95e36ae9643498673a7e15f085889dee0847411..60af424f65f9b47e3ad68455d5cc42cbcf1b6ff5 100644 --- a/common/djangoapps/student/migrations/0004_add_email_index.py +++ b/common/djangoapps/student/migrations/0004_add_email_index.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0005_name_change.py b/common/djangoapps/student/migrations/0005_name_change.py index 77265bcddd085e702ac2da377ad9ac671805fc00..24c393cccee8047e60adb1142ccb287964d87746 100644 --- a/common/djangoapps/student/migrations/0005_name_change.py +++ b/common/djangoapps/student/migrations/0005_name_change.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0006_expand_meta_field.py b/common/djangoapps/student/migrations/0006_expand_meta_field.py index 19fd402cef5f3925846785b276c353d24c3615ab..7fc3094cccc518d4454699a348e2c8c93758eec1 100644 --- a/common/djangoapps/student/migrations/0006_expand_meta_field.py +++ b/common/djangoapps/student/migrations/0006_expand_meta_field.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0007_convert_to_utf8.py b/common/djangoapps/student/migrations/0007_convert_to_utf8.py index 84a3299c8713af3f7a795c22382e923eaa395e1e..7a96496ad04ee646273abf2af8e38da00fa1f88e 100644 --- a/common/djangoapps/student/migrations/0007_convert_to_utf8.py +++ b/common/djangoapps/student/migrations/0007_convert_to_utf8.py @@ -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'}, diff --git a/common/djangoapps/student/migrations/0008__auto__add_courseregistration.py b/common/djangoapps/student/migrations/0008__auto__add_courseregistration.py index 7b10821f4e115457986b5457c298c9adb0933096..22ef55913af9470c3439a8aa9732dd834b5899b5 100644 --- a/common/djangoapps/student/migrations/0008__auto__add_courseregistration.py +++ b/common/djangoapps/student/migrations/0008__auto__add_courseregistration.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py b/common/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py index 5dcdd509de56be46c4b3c3dca045c85c7f930ba4..c5af13d34c5b35e4952f0a434d451c7319e77986 100644 --- a/common/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py +++ b/common/djangoapps/student/migrations/0009_auto__del_courseregistration__add_courseenrollment.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py b/common/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py index 47a79e43cd68a67216a1da2ac8b94c1f4aebadbe..153a166b5ed688a86a8f6b866110a82f80405c52 100644 --- a/common/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py +++ b/common/djangoapps/student/migrations/0010_auto__chg_field_courseenrollment_course_id.py @@ -124,4 +124,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['student'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py b/common/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py index bb74eed0b488e804790fd21b948bb23d6d1e7a5d..e8417f53ef4a1f908220e4498cc0ed4d01f38225 100644 --- a/common/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py +++ b/common/djangoapps/student/migrations/0011_auto__chg_field_courseenrollment_user__del_unique_courseenrollment_use.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py b/common/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py index e77d47f2b1c5cdfd92ad33151531cfa085f96bdc..ce46bf50fa644dcb6307dda80646b85535f54060 100644 --- a/common/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py +++ b/common/djangoapps/student/migrations/0012_auto__add_field_userprofile_gender__add_field_userprofile_date_of_birt.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0013_auto__chg_field_userprofile_meta.py b/common/djangoapps/student/migrations/0013_auto__chg_field_userprofile_meta.py index 9c2e6620b7e086bc69befbd50adaccafa74eb18d..de81c6bf53d2c0cc0b15b244f44b763a9dcbe044 100644 --- a/common/djangoapps/student/migrations/0013_auto__chg_field_userprofile_meta.py +++ b/common/djangoapps/student/migrations/0013_auto__chg_field_userprofile_meta.py @@ -130,4 +130,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['student'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0014_auto__del_courseenrollment.py b/common/djangoapps/student/migrations/0014_auto__del_courseenrollment.py index 964ef1cf728bcda23f9aa772bb55c726bf27c69a..926c38c55f4e8dcf7ba039ab42a1ca4a7d92bc15 100644 --- a/common/djangoapps/student/migrations/0014_auto__del_courseenrollment.py +++ b/common/djangoapps/student/migrations/0014_auto__del_courseenrollment.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id.py b/common/djangoapps/student/migrations/0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id.py index 5b4882a614b76f6a22e54595de8007c746080049..3cde0c755ce67d98e46684005a5fbc23e50dfcae 100644 --- a/common/djangoapps/student/migrations/0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id.py +++ b/common/djangoapps/student/migrations/0015_auto__add_courseenrollment__add_unique_courseenrollment_user_course_id.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country.py b/common/djangoapps/student/migrations/0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country.py index 38e25db095cf45327a88302481eaa7ba50d1dac4..58f013742ece3213f047f7983957b643a47d2ab1 100644 --- a/common/djangoapps/student/migrations/0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country.py +++ b/common/djangoapps/student/migrations/0016_auto__add_field_courseenrollment_date__chg_field_userprofile_country.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0017_rename_date_to_created.py b/common/djangoapps/student/migrations/0017_rename_date_to_created.py index 9b387ed2e95a5fe36c3d82f70665f96d7fdb03c6..6bde313d8bfb5486badde8935830247ba0ebc915 100644 --- a/common/djangoapps/student/migrations/0017_rename_date_to_created.py +++ b/common/djangoapps/student/migrations/0017_rename_date_to_created.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0018_auto.py b/common/djangoapps/student/migrations/0018_auto.py index 1f91bf9d4a3789433e33d38c1a258b304a105ec0..df0cc3fbdb00f2bbb07bf32646c6f9195012b745 100644 --- a/common/djangoapps/student/migrations/0018_auto.py +++ b/common/djangoapps/student/migrations/0018_auto.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/migrations/0019_create_approved_demographic_fields_fall_2012.py b/common/djangoapps/student/migrations/0019_create_approved_demographic_fields_fall_2012.py index d260e263f71cb5be9a352f8122c03c08482d3f35..7c0207f4f4c2c576f611864fd1dbeb2f852515b4 100644 --- a/common/djangoapps/student/migrations/0019_create_approved_demographic_fields_fall_2012.py +++ b/common/djangoapps/student/migrations/0019_create_approved_demographic_fields_fall_2012.py @@ -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'] \ No newline at end of file + complete_apps = ['student'] diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index ca5081254e0c8521cba4d1c4afba03e397421c58..49d3381303b58a510212b4aeca47ef2d01152c75 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -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() - diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 143a22683ecaa9a4c4dac7176d74dc6fbba5876a..449ea1d02d933339b6a65ddaf4d9901bfac456df 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -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 diff --git a/common/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py index 3beabeb6900e3e0b5b95deef2624accee87bfa04..52d914aeef705d5dde542a9d4d325a7b9fd9f734 100644 --- a/common/djangoapps/track/middleware.py +++ b/common/djangoapps/track/middleware.py @@ -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 - - event = { 'GET' : dict(get_dict), - 'POST' : dict(post_dict)} - + 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)} + # TODO: Confirm no large file uploads event = json.dumps(event) event = event[:512] diff --git a/common/djangoapps/track/views.py b/common/djangoapps/track/views.py index 6f2e0e5155841aa4c28d5049d91f59e40d444b7f..a60d8bef28a2919dc62a2ec5c09e807b9d0b1fca 100644 --- a/common/djangoapps/track/views.py +++ b/common/djangoapps/track/views.py @@ -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) diff --git a/common/djangoapps/util/cache.py b/common/djangoapps/util/cache.py index e253b5b63327c8164c6d1182b5d90b4e169e7f34..85b8ed3369760208cc0cd0b3662b582bbe235fd1 100644 --- a/common/djangoapps/util/cache.py +++ b/common/djangoapps/util/cache.py @@ -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 - \ No newline at end of file + diff --git a/common/djangoapps/util/json_request.py b/common/djangoapps/util/json_request.py index 4066cfc88995f0b6b7b4eeede974587c089eda04..c2fad16d7058a600ae3348086e446011a3582091 100644 --- a/common/djangoapps/util/json_request.py +++ b/common/djangoapps/util/json_request.py @@ -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): diff --git a/common/djangoapps/util/memcache.py b/common/djangoapps/util/memcache.py index 3da65a1b514bd90ad08328260501a8e9a80ef7a6..540cf9653978632336c863e86aeb20248ab36138 100644 --- a/common/djangoapps/util/memcache.py +++ b/common/djangoapps/util/memcache.py @@ -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)) diff --git a/common/djangoapps/util/middleware.py b/common/djangoapps/util/middleware.py index eeffa2668c421d801fb4de677b28311388c41a40..ce7b961766ea0c85b485771ac23eb15a6d9e89cf 100644 --- a/common/djangoapps/util/middleware.py +++ b/common/djangoapps/util/middleware.py @@ -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.") - diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index c1f2bb39ea5b20e62d01f2f632a47bdf9ca6560f..e38056934f720a12b67189cd6a1b48614d6b3922 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -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: + + try: browser = request.META['HTTP_USER_AGENT'] - except: + except: browser = "Unknown" - - feedback = render_to_string("feedback_email.txt", - {"subject":request.POST['subject'], - "url": request.POST['url'], + + 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}) + "feedback": request.POST['message'], + "email": email, + "browser": browser, + "user": username}) - send_mail("MITx Feedback / " +request.POST['subject'], - feedback, + 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", "")) diff --git a/common/lib/capa/capa/__init__.py b/common/lib/capa/capa/__init__.py index 8b137891791fe96927ad78e64b0aad7bded08bdc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/common/lib/capa/capa/__init__.py +++ b/common/lib/capa/capa/__init__.py @@ -1 +0,0 @@ - diff --git a/common/lib/capa/capa/calc.py b/common/lib/capa/capa/calc.py index 42bb5c3112182003c7f419397693ec987c6d4aa8..7979a33d84fd5d7e484aaea8185c7c4cd29e7559 100644 --- a/common/lib/capa/capa/calc.py +++ b/common/lib/capa/capa/calc.py @@ -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") diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 5ccb98f090eb53ab0358a296a96caaee0ec0af85..13ab9d9eb1da026dc944c916a6917de8b77e147c 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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 diff --git a/common/lib/capa/capa/checker.py b/common/lib/capa/capa/checker.py index 2139035d2afb015a1920150969129238f242cf85..f583a5ea7d3e1d84470c70f8b8294ebe2721aa87 100755 --- a/common/lib/capa/capa/checker.py +++ b/common/lib/capa/capa/checker.py @@ -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() diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 11c5bb75f127cb65b6380fc898b0465f4c4ebdb4..c727626a339bc7f0648d34efb9cf8564c758864c 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -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()) - + diff --git a/common/lib/capa/capa/eia.py b/common/lib/capa/capa/eia.py index 362dc33a2d5bbb3a9ded6a51bf2b1b4e5ebcb460..b41f205576c1928cfbd49d9bd20eda3f0ecb16fd 100644 --- a/common/lib/capa/capa/eia.py +++ b/common/lib/capa/capa/eia.py @@ -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] diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index d809f98ed2f3ee3274bf50ccb8417aad51a09ef7..3acd4e78586484a153e5b1bb13876aba4c101b73 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -26,37 +26,39 @@ Each input type takes the xml tree as 'element', the previous answer as 'value', import logging import re -import shlex # for splitting quoted strings +import shlex # for splitting quoted strings from lxml import etree import xml.sax.saxutils as saxutils log = logging.getLogger('mitx.' + __name__) + def get_input_xml_tags(): ''' Eventually, this will be for all registered input types ''' return SimpleInput.get_xml_tags() -class SimpleInput():# XModule + +class SimpleInput():# XModule ''' Type for simple inputs -- plain HTML with a form element ''' - xml_tags = {} ## Maps tags to functions + xml_tags = {} # # Maps tags to functions - def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): + def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'): ''' Instantiate a SimpleInput class. Arguments: - - system : I4xSystem instance which provides OS, rendering, and user context + - system : I4xSystem instance which provides OS, rendering, and user context - xml : Element tree of this Input element - item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string - track_url : URL used for tracking - string - - state : a dictionary with optional keys: + - state : a dictionary with optional keys: * Value * ID * Status (answered, unanswered, unsubmitted) - * Feedback (dictionary containing keys for hints, errors, or other + * Feedback (dictionary containing keys for hints, errors, or other feedback from previous attempt) - use : ''' @@ -66,11 +68,11 @@ class SimpleInput():# XModule self.system = system if not state: state = {} - ## ID should only come from one place. + ## ID should only come from one place. ## If it comes from multiple, we use state first, XML second, and parameter - ## third. Since we don't make this guarantee, we can swap this around in - ## the future if there's a more logical order. - if item_id: self.id = item_id + ## third. Since we don't make this guarantee, we can swap this around in + ## the future if there's a more logical order. + if item_id: self.id = item_id if xml.get('id'): self.id = xml.get('id') if 'id' in state: self.id = state['id'] @@ -81,14 +83,14 @@ class SimpleInput():# XModule self.msg = '' feedback = state.get('feedback') if feedback is not None: - self.msg = feedback.get('message','') - self.hint = feedback.get('hint','') - self.hintmode = feedback.get('hintmode',None) - + self.msg = feedback.get('message', '') + self.hint = feedback.get('hint', '') + self.hintmode = feedback.get('hintmode', None) + # put hint above msg if to be displayed if self.hintmode == 'always': self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg - + self.status = 'unanswered' if 'status' in state: self.status = state['status'] @@ -104,17 +106,20 @@ class SimpleInput():# XModule def get_html(self): return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg) + def register_render_function(fn, names=None, cls=SimpleInput): if names is None: SimpleInput.xml_tags[fn.__name__] = fn else: raise NotImplementedError + def wrapped(): return fn return wrapped #----------------------------------------------------------------------------- + @register_render_function def optioninput(element, value, status, render_template, msg=''): ''' @@ -124,7 +129,7 @@ def optioninput(element, value, status, render_template, msg=''): <optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text> ''' - eid=element.get('id') + eid = element.get('id') options = element.get('options') if not options: raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element)) @@ -134,14 +139,14 @@ def optioninput(element, value, status, render_template, msg=''): oset = [x[1:-1] for x in list(oset)] # osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs - osetdict = [(oset[x],oset[x]) for x in range(len(oset)) ] # make ordered list with (key,value) same + osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same # TODO: allow ordering to be randomized - - context={'id':eid, - 'value':value, - 'state':status, - 'msg':msg, - 'options':osetdict, + + context = {'id': eid, + 'value': value, + 'state': status, + 'msg': msg, + 'options': osetdict, } html = render_template("optioninput.html", context) @@ -149,6 +154,7 @@ def optioninput(element, value, status, render_template, msg=''): #----------------------------------------------------------------------------- + # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. @register_render_function @@ -159,23 +165,23 @@ def choicegroup(element, value, status, render_template, msg=''): TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute, ie random, top, bottom. ''' - eid=element.get('id') + eid = element.get('id') if element.get('type') == "MultipleChoice": - type="radio" + type = "radio" elif element.get('type') == "TrueFalse": - type="checkbox" + type = "checkbox" else: - type="radio" - choices=[] + type = "radio" + choices = [] for choice in element: - if not choice.tag=='choice': + if not choice.tag == 'choice': raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag) ctext = "" - ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it? + ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it? if choice.text is not None: ctext += choice.text # TODO: fix order? - choices.append((choice.get("name"),ctext)) - context={'id':eid, 'value':value, 'state':status, 'input_type':type, 'choices':choices, 'inline':True, 'name_array_suffix':''} + choices.append((choice.get("name"), ctext)) + context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'inline': True, 'name_array_suffix': ''} html = render_template("choicegroup.html", context) return etree.XML(html) @@ -193,9 +199,9 @@ def extract_choices(element): choices = [] for choice in element: - if not choice.tag=='choice': + if not choice.tag == 'choice': raise Exception("[courseware.capa.inputtypes.extract_choices] \ - Expected a <choice> tag; got %s instead" + Expected a <choice> tag; got %s instead" % choice.tag) choice_text = ''.join([etree.tostring(x) for x in choice]) @@ -203,6 +209,7 @@ def extract_choices(element): return choices + # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. @register_render_function @@ -211,15 +218,16 @@ def radiogroup(element, value, status, render_template, msg=''): Radio button inputs: (multiple choice) ''' - eid=element.get('id') + eid = element.get('id') choices = extract_choices(element) - context = { 'id':eid, 'value':value, 'state':status, 'input_type': 'radio', 'choices':choices, 'inline': False, 'name_array_suffix': '[]' } + context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'} html = render_template("choicegroup.html", context) return etree.XML(html) + # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. @register_render_function @@ -228,44 +236,46 @@ def checkboxgroup(element, value, status, render_template, msg=''): Checkbox inputs: (select one or more choices) ''' - eid=element.get('id') + eid = element.get('id') choices = extract_choices(element) - context = { 'id':eid, 'value':value, 'state':status, 'input_type': 'checkbox', 'choices':choices, 'inline': False, 'name_array_suffix': '[]' } + context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'inline': False, 'name_array_suffix': '[]'} html = render_template("choicegroup.html", context) return etree.XML(html) + @register_render_function def textline(element, value, status, render_template, msg=""): ''' Simple text line input, with optional size specification. ''' if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x - return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg) - eid=element.get('id') + return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg) + eid = element.get('id') if eid is None: msg = 'textline has no id: it probably appears outside of a known response type' - msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','<unavailable>') + msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>') raise Exception(msg) - count = int(eid.split('_')[-2])-1 # HACK + count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') - hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden + hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden escapedict = {'"': '"'} - value = saxutils.escape(value,escapedict) # otherwise, answers with quotes in them crashes the system! - context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg, 'hidden': hidden} + value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system! + context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden} html = render_template("textinput.html", context) try: xhtml = etree.XML(html) except Exception as err: - if True: # TODO needs to be self.system.DEBUG - but can't access system + if True: # TODO needs to be self.system.DEBUG - but can't access system log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html) raise return xhtml #----------------------------------------------------------------------------- + @register_render_function def textline_dynamath(element, value, status, render_template, msg=''): ''' @@ -278,16 +288,17 @@ def textline_dynamath(element, value, status, render_template, msg=''): uses a <span id=display_eid>`{::}`</span> and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return. ''' - eid=element.get('id') - count = int(eid.split('_')[-2])-1 # HACK + eid = element.get('id') + count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') - hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden - context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, - 'msg':msg, 'hidden' : hidden, + hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, + 'msg': msg, 'hidden': hidden, } html = render_template("textinput_dynamath.html", context) return etree.XML(html) + #----------------------------------------------------------------------------- ## TODO: Make a wrapper for <codeinput> @register_render_function @@ -297,31 +308,32 @@ def textbox(element, value, status, render_template, msg=''): evaluating the code, eg error messages, and output from the code tests. ''' - eid=element.get('id') - count = int(eid.split('_')[-2])-1 # HACK + eid = element.get('id') + count = int(eid.split('_')[-2]) - 1 # HACK size = element.get('size') rows = element.get('rows') or '30' cols = element.get('cols') or '80' mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml" - hidden = element.get('hidden','') # if specified, then textline is hidden and id is stored in div of name given by hidden - linenumbers = element.get('linenumbers') # for CodeMirror - if not value: value = element.text # if no student input yet, then use the default input given by the problem - context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg, - 'mode':mode, 'linenumbers':linenumbers, - 'rows':rows, 'cols':cols, - 'hidden' : hidden, + hidden = element.get('hidden', '') # if specified, then textline is hidden and id is stored in div of name given by hidden + linenumbers = element.get('linenumbers') # for CodeMirror + if not value: value = element.text # if no student input yet, then use the default input given by the problem + context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, + 'mode': mode, 'linenumbers': linenumbers, + 'rows': rows, 'cols': cols, + 'hidden': hidden, } html = render_template("textbox.html", context) try: xhtml = etree.XML(html) except Exception as err: - newmsg = 'error %s in rendering message' % (str(err).replace('<','<')) - newmsg += '<br/>Original message: %s' % msg.replace('<','<') + newmsg = 'error %s in rendering message' % (str(err).replace('<', '<')) + newmsg += '<br/>Original message: %s' % msg.replace('<', '<') context['msg'] = newmsg html = render_template("textbox.html", context) xhtml = etree.XML(html) return xhtml + #----------------------------------------------------------------------------- @register_render_function def schematic(element, value, status, render_template, msg=''): @@ -333,19 +345,20 @@ def schematic(element, value, status, render_template, msg=''): initial_value = element.get('initial_value') submit_analyses = element.get('submit_analyses') context = { - 'id':eid, - 'value':value, - 'initial_value':initial_value, - 'state':status, - 'width':width, - 'height':height, - 'parts':parts, - 'analyses':analyses, - 'submit_analyses':submit_analyses, + 'id': eid, + 'value': value, + 'initial_value': initial_value, + 'state': status, + 'width': width, + 'height': height, + 'parts': parts, + 'analyses': analyses, + 'submit_analyses': submit_analyses, } html = render_template("schematicinput.html", context) return etree.XML(html) + #----------------------------------------------------------------------------- ### TODO: Move out of inputtypes @register_render_function @@ -357,17 +370,17 @@ def math(element, value, status, render_template, msg=''): Examples: <m display="jsmath">$\displaystyle U(r)=4 U_0 </m> - <m>$r_0$</m> + <m>$r_0$</m> We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline] TODO: use shorter tags (but this will require converting problem XML files!) ''' - mathstr = re.sub('\$(.*)\$','[mathjaxinline]\\1[/mathjaxinline]',element.text) + mathstr = re.sub('\$(.*)\$', '[mathjaxinline]\\1[/mathjaxinline]', element.text) mtag = 'mathjax' if not '\\displaystyle' in mathstr: mtag += 'inline' - else: mathstr = mathstr.replace('\\displaystyle','') - mathstr = mathstr.replace('mathjaxinline]','%s]'%mtag) + else: mathstr = mathstr.replace('\\displaystyle', '') + mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag) #if '\\displaystyle' in mathstr: # isinline = False @@ -376,13 +389,13 @@ def math(element, value, status, render_template, msg=''): # isinline = True # html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail}) - html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,saxutils.escape(element.tail)) + html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail)) try: xhtml = etree.XML(html) except Exception as err: - if False: # TODO needs to be self.system.DEBUG - but can't access system - msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<','<') - msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<','<') + if False: # TODO needs to be self.system.DEBUG - but can't access system + msg = "<html><font color='red'><p>Error %s</p>" % str(err).replace('<', '<') + msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<', '<') msg += "</font></html>" log.error(msg) return etree.XML(msg) @@ -393,6 +406,7 @@ def math(element, value, status, render_template, msg=''): #----------------------------------------------------------------------------- + @register_render_function def solution(element, value, status, render_template, msg=''): ''' @@ -401,19 +415,20 @@ def solution(element, value, status, render_template, msg=''): is pressed. Note that the solution content is NOT sent with the HTML. It is obtained by a JSON call. ''' - eid=element.get('id') + eid = element.get('id') size = element.get('size') - context = {'id':eid, - 'value':value, - 'state':status, + context = {'id': eid, + 'value': value, + 'state': status, 'size': size, - 'msg':msg, + 'msg': msg, } html = render_template("solutionspan.html", context) return etree.XML(html) #----------------------------------------------------------------------------- + @register_render_function def imageinput(element, value, status, render_template, msg=''): ''' @@ -429,12 +444,12 @@ def imageinput(element, value, status, render_template, msg=''): width = element.get('width') # if value is of the form [x,y] then parse it and send along coordinates of previous answer - m = re.match('\[([0-9]+),([0-9]+)]',value.strip().replace(' ','')) + m = re.match('\[([0-9]+),([0-9]+)]', value.strip().replace(' ', '')) if m: - (gx,gy) = [int(x)-15 for x in m.groups()] + (gx, gy) = [int(x) - 15 for x in m.groups()] else: - (gx,gy) = (0,0) - + (gx, gy) = (0, 0) + context = { 'id': eid, 'value': value, @@ -443,7 +458,7 @@ def imageinput(element, value, status, render_template, msg=''): 'src': src, 'gx': gx, 'gy': gy, - 'state': status, # to change + 'state': status, # to change 'msg': msg, # to change } html = render_template("imageinput.html", context) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 21a3083c13625ad24fe1e390ee8f6c92194974cd..b232664cd56c5cda6f90ce94e1c45e50e9d8b2ec 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -26,25 +26,28 @@ from calc import evaluator, UndefinedVariable from correctmap import CorrectMap from util import * from lxml import etree -from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? +from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? log = logging.getLogger('mitx.' + __name__) #----------------------------------------------------------------------------- # Exceptions + class LoncapaProblemError(Exception): ''' Error in specification of a problem ''' pass + class ResponseError(Exception): ''' Error for failure in processing a response ''' pass + class StudentInputError(Exception): pass @@ -52,6 +55,7 @@ class StudentInputError(Exception): # # Main base class for CAPA responsetypes + class LoncapaResponse(object): ''' Base class for CAPA responsetypes. Each response type (ie a capa question, @@ -60,7 +64,7 @@ class LoncapaResponse(object): - get_score : evaluate the given student answers, and return a CorrectMap - get_answers : provide a dict of the expected answers for this problem - + Each subclass must also define the following attributes: - response_tag : xhtml tag identifying this response (used in auto-registering) @@ -81,7 +85,7 @@ class LoncapaResponse(object): - hint_tag : xhtml tag identifying hint associated with this response inside hintgroup ''' - __metaclass__=abc.ABCMeta # abc = Abstract Base Class + __metaclass__ = abc.ABCMeta # abc = Abstract Base Class response_tag = None hint_tag = None @@ -89,7 +93,7 @@ class LoncapaResponse(object): max_inputfields = None allowed_inputfields = [] required_attributes = [] - + def __init__(self, xml, inputfields, context, system=None): ''' Init is passed the following arguments: @@ -97,7 +101,7 @@ class LoncapaResponse(object): - xml : ElementTree of this Response - inputfields : ordered list of ElementTrees for each input entry field in this Response - context : script processor context - - system : I4xSystem instance which provides OS, rendering, and user context + - system : I4xSystem instance which provides OS, rendering, and user context ''' self.xml = xml @@ -107,35 +111,35 @@ class LoncapaResponse(object): for abox in inputfields: if abox.tag not in self.allowed_inputfields: - msg = "%s: cannot have input field %s" % (unicode(self),abox.tag) - msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>') + msg = "%s: cannot have input field %s" % (unicode(self), abox.tag) + msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) - if self.max_inputfields and len(inputfields)>self.max_inputfields: - msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields) - msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>') + if self.max_inputfields and len(inputfields) > self.max_inputfields: + msg = "%s: cannot have more than %s input fields" % (unicode(self), self.max_inputfields) + msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) for prop in self.required_attributes: if not xml.get(prop): - msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop) - msg += "\nSee XML source line %s" % getattr(xml,'sourceline','<unavailable>') + msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self), prop) + msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) - self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response - if self.max_inputfields==1: + self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response + if self.max_inputfields == 1: self.answer_id = self.answer_ids[0] # for convenience self.default_answer_map = {} # dict for default answer map (provided in input elements) for entry in self.inputfields: - answer = entry.get('correct_answer') + answer = entry.get('correct_answer') if answer: self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context) - if hasattr(self,'setup_response'): + if hasattr(self, 'setup_response'): self.setup_response() - def render_html(self,renderer): + def render_html(self, renderer): ''' Return XHTML Element tree representation of this Response. @@ -150,7 +154,7 @@ class LoncapaResponse(object): tree.tail = self.xml.tail return tree - def evaluate_answers(self,student_answers,old_cmap): + def evaluate_answers(self, student_answers, old_cmap): ''' Called by capa_problem.LoncapaProblem to evaluate student answers, and to generate hints (if any). @@ -190,14 +194,14 @@ class LoncapaResponse(object): ''' if not hintfn in self.context: msg = 'missing specified hint function %s in script context' % hintfn - msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>') + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) try: self.context[hintfn](self.answer_ids, student_answers, new_cmap, old_cmap) except Exception as err: - msg = 'Error %s in evaluating hint function %s' % (err,hintfn) - msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>') + msg = 'Error %s in evaluating hint function %s' % (err, hintfn) + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') raise ResponseError(msg) return @@ -214,18 +218,18 @@ class LoncapaResponse(object): # <text>You have inverted the slope in the question. The slope is # (y2-y1)/(x2 - x1) you have the slope as (x2-x1)/(y2-y1).</text> # </hintpart> - # </hintgroup> + # </hintgroup> # </formularesponse> - if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self,'check_hint_condition'): + if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self, 'check_hint_condition'): rephints = hintgroup.findall(self.hint_tag) - hints_to_show = self.check_hint_condition(rephints,student_answers) - hintmode = hintgroup.get('mode','always') # can be 'on_request' or 'always' (default) + hints_to_show = self.check_hint_condition(rephints, student_answers) + hintmode = hintgroup.get('mode', 'always') # can be 'on_request' or 'always' (default) for hintpart in hintgroup.findall('hintpart'): if hintpart.get('on') in hints_to_show: hint_text = hintpart.find('text').text aid = self.answer_ids[-1] # make the hint appear after the last answer box in this response - new_cmap.set_hint_and_mode(aid,hint_text,hintmode) + new_cmap.set_hint_and_mode(aid, hint_text, hintmode) log.debug('after hint: new_cmap = %s' % new_cmap) @abc.abstractmethod @@ -249,7 +253,7 @@ class LoncapaResponse(object): ''' pass - def check_hint_condition(self,hxml_set,student_answers): + def check_hint_condition(self, hxml_set, student_answers): ''' Return a list of hints to show. @@ -266,6 +270,7 @@ class LoncapaResponse(object): def __unicode__(self): return u'LoncapaProblem Response %s' % self.xml.tag + #----------------------------------------------------------------------------- class ChoiceResponse(LoncapaResponse): ''' @@ -310,8 +315,8 @@ class ChoiceResponse(LoncapaResponse): ''' - response_tag = 'choiceresponse' - max_inputfields = 1 + response_tag = 'choiceresponse' + max_inputfields = 1 allowed_inputfields = ['checkboxgroup', 'radiogroup'] def setup_response(self): @@ -328,17 +333,17 @@ class ChoiceResponse(LoncapaResponse): Initialize name attributes in <choice> tags for this response. ''' - for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice', + for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice', id=self.xml.get('id'))): - choice.set("name", "choice_"+str(index)) + choice.set("name", "choice_" + str(index)) def get_score(self, student_answers): student_answer = student_answers.get(self.answer_id, []) - + if not isinstance(student_answer, list): student_answer = [student_answer] - + student_answer = set(student_answer) required_selected = len(self.correct_choices - student_answer) == 0 @@ -347,15 +352,16 @@ class ChoiceResponse(LoncapaResponse): correct = required_selected & no_extra_selected if correct: - return CorrectMap(self.answer_id,'correct') + return CorrectMap(self.answer_id, 'correct') else: - return CorrectMap(self.answer_id,'incorrect') + return CorrectMap(self.answer_id, 'incorrect') def get_answers(self): - return { self.answer_id : self.correct_choices } + return {self.answer_id: self.correct_choices} #----------------------------------------------------------------------------- + class MultipleChoiceResponse(LoncapaResponse): # TODO: handle direction and randomize snippets = [{'snippet': '''<multiplechoiceresponse direction="vertical" randomize="yes"> @@ -373,28 +379,28 @@ class MultipleChoiceResponse(LoncapaResponse): allowed_inputfields = ['choicegroup'] def setup_response(self): - self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes + self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes # define correct choices (after calling secondary setup) xml = self.xml - cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id')) + cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) self.correct_choices = [choice.get('name') for choice in cxml] def mc_setup_response(self): ''' Initialize name attributes in <choice> stanzas in the <choicegroup> in this response. ''' - i=0 + i = 0 for response in self.xml.xpath("choicegroup"): rtype = response.get('type') if rtype not in ["MultipleChoice"]: response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid for choice in list(response): if choice.get("name") is None: - choice.set("name", "choice_"+str(i)) - i+=1 + choice.set("name", "choice_" + str(i)) + i += 1 else: - choice.set("name", "choice_"+choice.get("name")) + choice.set("name", "choice_" + choice.get("name")) def get_score(self, student_answers): ''' @@ -402,39 +408,41 @@ class MultipleChoiceResponse(LoncapaResponse): ''' # log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices)) if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: - return CorrectMap(self.answer_id,'correct') + return CorrectMap(self.answer_id, 'correct') else: - return CorrectMap(self.answer_id,'incorrect') + return CorrectMap(self.answer_id, 'incorrect') def get_answers(self): - return {self.answer_id:self.correct_choices} + return {self.answer_id: self.correct_choices} + class TrueFalseResponse(MultipleChoiceResponse): response_tag = 'truefalseresponse' def mc_setup_response(self): - i=0 + i = 0 for response in self.xml.xpath("choicegroup"): response.set("type", "TrueFalse") for choice in list(response): if choice.get("name") is None: - choice.set("name", "choice_"+str(i)) - i+=1 + choice.set("name", "choice_" + str(i)) + i += 1 else: - choice.set("name", "choice_"+choice.get("name")) - + choice.set("name", "choice_" + choice.get("name")) + def get_score(self, student_answers): correct = set(self.correct_choices) answers = set(student_answers.get(self.answer_id, [])) - + if correct == answers: - return CorrectMap( self.answer_id , 'correct') - - return CorrectMap(self.answer_id ,'incorrect') + return CorrectMap(self.answer_id, 'correct') + + return CorrectMap(self.answer_id, 'incorrect') #----------------------------------------------------------------------------- + class OptionResponse(LoncapaResponse): ''' TODO: handle direction and randomize @@ -456,19 +464,20 @@ class OptionResponse(LoncapaResponse): cmap = CorrectMap() amap = self.get_answers() for aid in amap: - if aid in student_answers and student_answers[aid]==amap[aid]: - cmap.set(aid,'correct') + if aid in student_answers and student_answers[aid] == amap[aid]: + cmap.set(aid, 'correct') else: - cmap.set(aid,'incorrect') + cmap.set(aid, 'incorrect') return cmap def get_answers(self): - amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields]) + amap = dict([(af.get('id'), af.get('correct')) for af in self.answer_fields]) # log.debug('%s: expected answers=%s' % (unicode(self),amap)) return amap #----------------------------------------------------------------------------- + class NumericalResponse(LoncapaResponse): response_tag = 'numericalresponse' @@ -497,25 +506,26 @@ class NumericalResponse(LoncapaResponse): '''Grade a numeric response ''' student_answer = student_answers[self.answer_id] try: - correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance) - # We should catch this explicitly. + correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer), complex(self.correct_answer), self.tolerance) + # We should catch this explicitly. # I think this is just pyparsing.ParseException, calc.UndefinedVariable: # But we'd need to confirm - except: + except: raise StudentInputError('Invalid input -- please use a number only') if correct: - return CorrectMap(self.answer_id,'correct') + return CorrectMap(self.answer_id, 'correct') else: - return CorrectMap(self.answer_id,'incorrect') + return CorrectMap(self.answer_id, 'incorrect') # TODO: add check_hint_condition(self,hxml_set,student_answers) def get_answers(self): - return {self.answer_id:self.correct_answer} + return {self.answer_id: self.correct_answer} #----------------------------------------------------------------------------- + class StringResponse(LoncapaResponse): response_tag = 'stringresponse' @@ -530,28 +540,29 @@ class StringResponse(LoncapaResponse): def get_score(self, student_answers): '''Grade a string response ''' student_answer = student_answers[self.answer_id].strip() - correct = self.check_string(self.correct_answer,student_answer) - return CorrectMap(self.answer_id,'correct' if correct else 'incorrect') + correct = self.check_string(self.correct_answer, student_answer) + return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect') - def check_string(self,expected,given): - if self.xml.get('type')=='ci': return given.lower() == expected.lower() + def check_string(self, expected, given): + if self.xml.get('type') == 'ci': return given.lower() == expected.lower() return given == expected - def check_hint_condition(self,hxml_set,student_answers): + def check_hint_condition(self, hxml_set, student_answers): given = student_answers[self.answer_id].strip() hints_to_show = [] for hxml in hxml_set: name = hxml.get('name') - correct_answer = contextualize_text(hxml.get('answer'),self.context).strip() - if self.check_string(correct_answer,given): hints_to_show.append(name) + correct_answer = contextualize_text(hxml.get('answer'), self.context).strip() + if self.check_string(correct_answer, given): hints_to_show.append(name) log.debug('hints_to_show = %s' % hints_to_show) return hints_to_show def get_answers(self): - return {self.answer_id:self.correct_answer} + return {self.answer_id: self.correct_answer} #----------------------------------------------------------------------------- + class CustomResponse(LoncapaResponse): ''' Custom response. The python code to be run should be in <answer>...</answer> @@ -592,7 +603,7 @@ def sympy_check2(): </customresponse>'''}] response_tag = 'customresponse' - allowed_inputfields = ['textline','textbox'] + allowed_inputfields = ['textline', 'textbox'] def setup_response(self): xml = self.xml @@ -607,7 +618,7 @@ def sympy_check2(): self.code = None answer = None try: - answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] + answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] except IndexError: # print "xml = ",etree.tostring(xml,pretty_print=True) @@ -619,8 +630,8 @@ def sympy_check2(): if cfn in self.context: self.code = self.context[cfn] else: - msg = "%s: can't find cfn %s in context" % (unicode(self),cfn) - msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>') + msg = "%s: can't find cfn %s in context" % (unicode(self), cfn) + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) if not self.code: @@ -631,7 +642,7 @@ def sympy_check2(): else: answer_src = answer.get('src') if answer_src is not None: - self.code = self.system.filesystem.open('src/'+answer_src).read() + self.code = self.system.filesystem.open('src/' + answer_src).read() else: self.code = answer.text @@ -641,52 +652,52 @@ def sympy_check2(): of each key removed (the string before the first "_"). ''' - log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) + log.debug('%s: student_answers=%s' % (unicode(self), student_answers)) idset = sorted(self.answer_ids) # ordered list of answer id's try: - submission = [student_answers[k] for k in idset] # ordered list of answers + submission = [student_answers[k] for k in idset] # ordered list of answers except Exception as err: msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers - msg += '\n idset = %s, error = %s' % (idset,err) + msg += '\n idset = %s, error = %s' % (idset, err) log.error(msg) raise Exception(msg) # global variable in context which holds the Presentation MathML from dynamic math input - dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses + dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses # if there is only one box, and it's empty, then don't evaluate - if len(idset)==1 and not submission[0]: - return CorrectMap(idset[0],'incorrect',msg='<font color="red">No answer entered!</font>') + if len(idset) == 1 and not submission[0]: + return CorrectMap(idset[0], 'incorrect', msg='<font color="red">No answer entered!</font>') correct = ['unknown'] * len(idset) messages = [''] * len(idset) # put these in the context of the check function evaluator # note that this doesn't help the "cfn" version - only the exec version - self.context.update({'xml' : self.xml, # our subtree - 'response_id' : self.myid, # my ID + self.context.update({'xml': self.xml, # our subtree + 'response_id': self.myid, # my ID 'expect': self.expect, # expected answer (if given as attribute) - 'submission':submission, # ordered list of student answers from entry boxes in our subtree - 'idset':idset, # ordered list of ID's of all entry boxes in our subtree - 'dynamath':dynamath, # ordered list of all javascript inputs in our subtree - 'answers':student_answers, # dict of student's responses, with keys being entry box IDs - 'correct':correct, # the list to be filled in by the check function - 'messages':messages, # the list of messages to be filled in by the check function - 'options':self.xml.get('options'), # any options to be passed to the cfn - 'testdat':'hello world', + 'submission': submission, # ordered list of student answers from entry boxes in our subtree + 'idset': idset, # ordered list of ID's of all entry boxes in our subtree + 'dynamath': dynamath, # ordered list of all javascript inputs in our subtree + 'answers': student_answers, # dict of student's responses, with keys being entry box IDs + 'correct': correct, # the list to be filled in by the check function + 'messages': messages, # the list of messages to be filled in by the check function + 'options': self.xml.get('options'), # any options to be passed to the cfn + 'testdat': 'hello world', }) - # pass self.system.debug to cfn + # pass self.system.debug to cfn self.context['debug'] = self.system.DEBUG # exec the check function - if type(self.code)==str: + if type(self.code) == str: try: exec self.code in self.context['global_context'], self.context except Exception as err: print "oops in customresponse (code) error %s" % err - print "context = ",self.context + print "context = ", self.context print traceback.format_exc() else: # self.code is not a string; assume its a function @@ -695,44 +706,44 @@ def sympy_check2(): ret = None log.debug(" submission = %s" % submission) try: - answer_given = submission[0] if (len(idset)==1) else submission + answer_given = submission[0] if (len(idset) == 1) else submission # handle variable number of arguments in check function, for backwards compatibility # with various Tutor2 check functions - args = [self.expect,answer_given,student_answers,self.answer_ids[0]] + args = [self.expect, answer_given, student_answers, self.answer_ids[0]] argspec = inspect.getargspec(fn) - nargs = len(argspec.args)-len(argspec.defaults or []) + nargs = len(argspec.args) - len(argspec.defaults or []) kwargs = {} for argname in argspec.args[nargs:]: kwargs[argname] = self.context[argname] if argname in self.context else None log.debug('[customresponse] answer_given=%s' % answer_given) - log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs)) + log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs, args, kwargs)) - ret = fn(*args[:nargs],**kwargs) + ret = fn(*args[:nargs], **kwargs) except Exception as err: log.error("oops in customresponse (cfn) error %s" % err) # print "context = ",self.context log.error(traceback.format_exc()) raise Exception("oops in customresponse (cfn) error %s" % err) log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret) - if type(ret)==dict: - correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset) + if type(ret) == dict: + correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset) msg = ret['msg'] if 1: # try to clean up message html - msg = '<html>'+msg+'</html>' - msg = msg.replace('<','<') + msg = '<html>' + msg + '</html>' + msg = msg.replace('<', '<') #msg = msg.replace('<','<') - msg = etree.tostring(fromstring_bs(msg,convertEntities=None),pretty_print=True) + msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True) #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) - msg = msg.replace(' ','') + msg = msg.replace(' ', '') #msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 - msg = re.sub('(?ms)<html>(.*)</html>','\\1',msg) + msg = re.sub('(?ms)<html>(.*)</html>', '\\1', msg) messages[0] = msg else: - correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset) + correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset) # build map giving "correct"ness of the answer(s) correct_map = CorrectMap() @@ -750,14 +761,15 @@ def sympy_check2(): but for simplicity, if an "expect" attribute was given by the content author ie <customresponse expect="foo" ...> then that. ''' - if len(self.answer_ids)>1: + if len(self.answer_ids) > 1: return self.default_answer_map if self.expect: - return {self.answer_ids[0] : self.expect} + return {self.answer_ids[0]: self.expect} return self.default_answer_map #----------------------------------------------------------------------------- + class SymbolicResponse(CustomResponse): """ Symbolic math response checking, using symmath library. @@ -776,15 +788,16 @@ class SymbolicResponse(CustomResponse): response_tag = 'symbolicresponse' def setup_response(self): - self.xml.set('cfn','symmath_check') + self.xml.set('cfn', 'symmath_check') code = "from symmath import *" - exec code in self.context,self.context + exec code in self.context, self.context CustomResponse.setup_response(self) #----------------------------------------------------------------------------- + class CodeResponse(LoncapaResponse): - ''' + ''' Grade student code using an external server, called 'xqueue' In contrast to ExternalResponse, CodeResponse has following behavior: 1) Goes through a queueing system @@ -797,20 +810,20 @@ class CodeResponse(LoncapaResponse): def setup_response(self): xml = self.xml - self.url = xml.get('url', "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url + self.url = xml.get('url', "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/") # FIXME -- hardcoded url answer = xml.find('answer') if answer is not None: answer_src = answer.get('src') if answer_src is not None: - self.code = self.system.filesystem.open('src/'+answer_src).read() + self.code = self.system.filesystem.open('src/' + answer_src).read() else: self.code = answer.text - else: # no <answer> stanza; get code from <script> + else: # no <answer> stanza; get code from <script> self.code = self.context['script_code'] if not self.code: msg = '%s: Missing answer script code for coderesponse' % unicode(self) - msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>') + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) self.tests = xml.get('tests') @@ -822,7 +835,7 @@ class CodeResponse(LoncapaResponse): penv = {} penv['__builtins__'] = globals()['__builtins__'] try: - exec(self.code,penv,penv) + exec(self.code, penv, penv) except Exception as err: log.error('Error in CodeResponse %s: Error in problem reference code' % err) raise Exception(err) @@ -843,7 +856,7 @@ class CodeResponse(LoncapaResponse): self.context.update({'submission': submission}) extra_payload = {'edX_student_response': json.dumps(submission)} - r, queuekey = self._send_to_queue(extra_payload) # TODO: Perform checks on the xqueue response + r, queuekey = self._send_to_queue(extra_payload) # TODO: Perform checks on the xqueue response # Non-null CorrectMap['queuekey'] indicates that the problem has been submitted cmap = CorrectMap() @@ -870,37 +883,37 @@ class CodeResponse(LoncapaResponse): # Replace 'oldcmap' with new grading results if queuekey matches. # If queuekey does not match, we keep waiting for the score_msg that will match - if oldcmap.is_right_queuekey(self.answer_id, queuekey): - msg = rxml.find('message').text.replace(' ',' ') - oldcmap.set(self.answer_id, correctness=self.context['correct'][0], msg=msg, queuekey=None) # Queuekey is consumed + if oldcmap.is_right_queuekey(self.answer_id, queuekey): + msg = rxml.find('message').text.replace(' ', ' ') + oldcmap.set(self.answer_id, correctness=self.context['correct'][0], msg=msg, queuekey=None) # Queuekey is consumed else: - log.debug('CodeResponse: queuekey %d does not match for answer_id=%s.' % (queuekey, self.answer_id)) + log.debug('CodeResponse: queuekey %d does not match for answer_id=%s.' % (queuekey, self.answer_id)) - return oldcmap + return oldcmap # CodeResponse differentiates from ExternalResponse in the behavior of 'get_answers'. CodeResponse.get_answers # does NOT require a queue submission, and the answer is computed (extracted from problem XML) locally. def get_answers(self): anshtml = '<font color="blue"><span class="code-answer"><br/><pre>%s</pre><br/></span></font>' % self.answer - return { self.answer_id: anshtml } - + return {self.answer_id: anshtml} + def get_initial_display(self): - return { self.answer_id: self.initial_display } + return {self.answer_id: self.initial_display} # CodeResponse._send_to_queue implements the same interface as defined for ExternalResponse's 'get_score' def _send_to_queue(self, extra_payload): # Prepare payload xmlstr = etree.tostring(self.xml, pretty_print=True) - header = { 'return_url': self.system.xqueue_callback_url } + header = {'return_url': self.system.xqueue_callback_url} # Queuekey generation h = hashlib.md5() h.update(str(self.system.seed)) h.update(str(time.time())) - queuekey = int(h.hexdigest(),16) - header.update({'queuekey': queuekey}) + queuekey = int(h.hexdigest(), 16) + header.update({'queuekey': queuekey}) - payload = {'xqueue_header': json.dumps(header), # TODO: 'xqueue_header' should eventually be derived from a config file + payload = {'xqueue_header': json.dumps(header), # TODO: 'xqueue_header' should eventually be derived from a config file 'xml': xmlstr, 'edX_cmd': 'get_score', 'edX_tests': self.tests, @@ -920,10 +933,11 @@ class CodeResponse(LoncapaResponse): #----------------------------------------------------------------------------- + class ExternalResponse(LoncapaResponse): ''' Grade the students input using an external server. - + Typically used by coding problems. ''' @@ -938,7 +952,7 @@ answer = """ def inc(n): return n+1 """ -preamble = """ +preamble = """ import sympy """ test_program = """ @@ -967,30 +981,30 @@ main() </externalresponse>'''}] response_tag = 'externalresponse' - allowed_inputfields = ['textline','textbox'] + allowed_inputfields = ['textline', 'textbox'] def setup_response(self): xml = self.xml - self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL # answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer = xml.find('answer') if answer is not None: answer_src = answer.get('src') if answer_src is not None: - self.code = self.system.filesystem.open('src/'+answer_src).read() + self.code = self.system.filesystem.open('src/' + answer_src).read() else: self.code = answer.text else: # no <answer> stanza; get code from <script> self.code = self.context['script_code'] if not self.code: msg = '%s: Missing answer script code for externalresponse' % unicode(self) - msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>') + msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>') raise LoncapaProblemError(msg) self.tests = xml.get('tests') - def do_external_request(self,cmd,extra_payload): + def do_external_request(self, cmd, extra_payload): ''' Perform HTTP request / post to external server. @@ -1000,29 +1014,29 @@ main() Return XML tree of response (from response body) ''' xmlstr = etree.tostring(self.xml, pretty_print=True) - payload = {'xml': xmlstr, - 'edX_cmd' : cmd, + payload = {'xml': xmlstr, + 'edX_cmd': cmd, 'edX_tests': self.tests, - 'processor' : self.code, + 'processor': self.code, } payload.update(extra_payload) try: - r = requests.post(self.url,data=payload) # call external server + r = requests.post(self.url, data=payload) # call external server except Exception as err: - msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url) + msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url) log.error(msg) raise Exception(msg) if self.system.DEBUG: log.info('response = %s' % r.text) - if (not r.text ) or (not r.text.strip()): + if (not r.text) or (not r.text.strip()): raise Exception('Error: no response from external server url=%s' % self.url) try: rxml = etree.fromstring(r.text) # response is XML; prase it except Exception as err: - msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text) + msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text) log.error(msg) raise Exception(msg) @@ -1034,24 +1048,24 @@ main() try: submission = [student_answers[k] for k in idset] except Exception as err: - log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers)) + log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err, self.answer_ids, student_answers)) raise Exception(err) - self.context.update({'submission':submission}) + self.context.update({'submission': submission}) extra_payload = {'edX_student_response': json.dumps(submission)} try: - rxml = self.do_external_request('get_score',extra_payload) + rxml = self.do_external_request('get_score', extra_payload) except Exception as err: log.error('Error %s' % err) if self.system.DEBUG: - cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) ))) - cmap.set_property(self.answer_ids[0],'msg','<font color="red" size="+2">%s</font>' % str(err).replace('<','<')) + cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset)))) + cmap.set_property(self.answer_ids[0], 'msg', '<font color="red" size="+2">%s</font>' % str(err).replace('<', '<')) return cmap ad = rxml.find('awarddetail').text - admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses + admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses 'WRONG_FORMAT': 'incorrect', } self.context['correct'] = ['correct'] @@ -1061,7 +1075,7 @@ main() # create CorrectMap for key in idset: idx = idset.index(key) - msg = rxml.find('message').text.replace(' ',' ') if idx==0 else None + msg = rxml.find('message').text.replace(' ', ' ') if idx == 0 else None cmap.set(key, self.context['correct'][idx], msg=msg) return cmap @@ -1071,19 +1085,19 @@ main() Use external server to get expected answers ''' try: - rxml = self.do_external_request('get_answers',{}) + rxml = self.do_external_request('get_answers', {}) exans = json.loads(rxml.find('expected').text) except Exception as err: log.error('Error %s' % err) if self.system.DEBUG: - msg = '<font color=red size=+2>%s</font>' % str(err).replace('<','<') + msg = '<font color=red size=+2>%s</font>' % str(err).replace('<', '<') exans = [''] * len(self.answer_ids) exans[0] = msg - - if not (len(exans)==len(self.answer_ids)): - log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans))) + + if not (len(exans) == len(self.answer_ids)): + log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids), len(exans))) raise Exception('Short response from external server') - return dict(zip(self.answer_ids,exans)) + return dict(zip(self.answer_ids, exans)) #----------------------------------------------------------------------------- @@ -1104,8 +1118,8 @@ class FormulaResponse(LoncapaResponse): </text> <formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="$I"> <responseparam description="Numerical Tolerance" type="tolerance" - default="0.00001" name="tol" /> - <textline size="40" math="1" /> + default="0.00001" name="tol" /> + <textline size="40" math="1" /> </formularesponse> </problem>'''}] @@ -1133,11 +1147,11 @@ class FormulaResponse(LoncapaResponse): typeslist = [] else: typeslist = ts.split(',') - if 'ci' in typeslist: # Case insensitive + if 'ci' in typeslist: # Case insensitive self.case_sensitive = False - elif 'cs' in typeslist: # Case sensitive + elif 'cs' in typeslist: # Case sensitive self.case_sensitive = True - else: # Default + else: # Default self.case_sensitive = False def get_score(self, student_answers): @@ -1145,13 +1159,13 @@ class FormulaResponse(LoncapaResponse): correctness = self.check_formula(self.correct_answer, given, self.samples) return CorrectMap(self.answer_id, correctness) - def check_formula(self,expected, given, samples): - variables=samples.split('@')[0].split(',') - numsamples=int(samples.split('@')[1].split('#')[1]) - sranges=zip(*map(lambda x:map(float, x.split(",")), + def check_formula(self, expected, given, samples): + variables = samples.split('@')[0].split(',') + numsamples = int(samples.split('@')[1].split('#')[1]) + sranges = zip(*map(lambda x: map(float, x.split(",")), samples.split('@')[1].split('#')[0].split(':'))) - ranges=dict(zip(variables, sranges)) + ranges = dict(zip(variables, sranges)) for i in range(numsamples): instructor_variables = self.strip_dict(dict(self.context)) student_variables = dict() @@ -1160,16 +1174,16 @@ class FormulaResponse(LoncapaResponse): instructor_variables[str(var)] = value student_variables[str(var)] = value #log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected)) - instructor_result = evaluator(instructor_variables,dict(),expected, cs = self.case_sensitive) - try: + instructor_result = evaluator(instructor_variables, dict(), expected, cs=self.case_sensitive) + try: #log.debug('formula: student_vars=%s, given=%s' % (student_variables,given)) student_result = evaluator(student_variables, dict(), - given, - cs = self.case_sensitive) + given, + cs=self.case_sensitive) except UndefinedVariable as uv: log.debug('formularesponse: undefined variable in given=%s' % given) - raise StudentInputError(uv.message+" not permitted in answer") + raise StudentInputError(uv.message + " not permitted in answer") except Exception as err: #traceback.print_exc() log.debug('formularesponse: error %s in formula' % err) @@ -1184,33 +1198,34 @@ class FormulaResponse(LoncapaResponse): ''' Takes a dict. Returns an identical dict, with all non-word keys and all non-numeric values stripped out. All values also converted to float. Used so we can safely use Python contexts. - ''' - d=dict([(k, numpy.complex(d[k])) for k in d if type(k)==str and \ + ''' + d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and \ k.isalnum() and \ isinstance(d[k], numbers.Number)]) return d - def check_hint_condition(self,hxml_set,student_answers): + def check_hint_condition(self, hxml_set, student_answers): given = student_answers[self.answer_id] hints_to_show = [] for hxml in hxml_set: samples = hxml.get('samples') name = hxml.get('name') - correct_answer = contextualize_text(hxml.get('answer'),self.context) + correct_answer = contextualize_text(hxml.get('answer'), self.context) try: correctness = self.check_formula(correct_answer, given, samples) except Exception: correctness = 'incorrect' - if correctness=='correct': + if correctness == 'correct': hints_to_show.append(name) log.debug('hints_to_show = %s' % hints_to_show) return hints_to_show def get_answers(self): - return {self.answer_id:self.correct_answer} + return {self.answer_id: self.correct_answer} #----------------------------------------------------------------------------- + class SchematicResponse(LoncapaResponse): response_tag = 'schematicresponse' @@ -1221,14 +1236,14 @@ class SchematicResponse(LoncapaResponse): answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] answer_src = answer.get('src') if answer_src is not None: - self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used + self.code = self.system.filestore.open('src/' + answer_src).read() # Untested; never used else: self.code = answer.text def get_score(self, student_answers): from capa_problem import global_context submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] - self.context.update({'submission':submission}) + self.context.update({'submission': submission}) exec self.code in global_context, self.context cmap = CorrectMap() cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct']))) @@ -1240,6 +1255,7 @@ class SchematicResponse(LoncapaResponse): #----------------------------------------------------------------------------- + class ImageResponse(LoncapaResponse): """ Handle student response for image input: the input is a click on an image, @@ -1248,7 +1264,7 @@ class ImageResponse(LoncapaResponse): Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse> - should contain one or more <imageinput> stanzas. Each <imageinput> should specify + should contain one or more <imageinput> stanzas. Each <imageinput> should specify a rectangle, given as an attribute, defining the correct answer. """ snippets = [{'snippet': '''<imageresponse> @@ -1267,24 +1283,24 @@ class ImageResponse(LoncapaResponse): correct_map = CorrectMap() expectedset = self.get_answers() - for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza - given = student_answers[aid] # this should be a string of the form '[x,y]' + for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza + given = student_answers[aid] # this should be a string of the form '[x,y]' # parse expected answer # TODO: Compile regexp on file load - m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',expectedset[aid].strip().replace(' ','')) + m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', '')) if not m: msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid], pretty_print=True)) - raise Exception('[capamodule.capa.responsetypes.imageinput] '+msg) - (llx,lly,urx,ury) = [int(x) for x in m.groups()] - + raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg) + (llx, lly, urx, ury) = [int(x) for x in m.groups()] + # parse given answer - m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ','')) + m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) if not m: - raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given)) - (gx,gy) = [int(x) for x in m.groups()] - + raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid, given)) + (gx, gy) = [int(x) for x in m.groups()] + # answer is correct if (x,y) is within the specified rectangle if (llx <= gx <= urx) and (lly <= gy <= ury): correct_map.set(aid, 'correct') @@ -1293,11 +1309,10 @@ class ImageResponse(LoncapaResponse): return correct_map def get_answers(self): - return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements]) + return dict([(ie.get('id'), ie.get('rectangle')) for ie in self.ielements]) #----------------------------------------------------------------------------- # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration -__all__ = [ CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse ] - +__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse] diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 58e701cbc317130f5aad42650e9d76a4f264173b..63a5f43c030c39e499b8085a432a6cfe056367dd 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -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 diff --git a/common/lib/mitxmako/mitxmako/__init__.py b/common/lib/mitxmako/mitxmako/__init__.py index aa8a48bfe7801248f45f83b91e7ffbe39abef830..007b2680ae39afa6edbf9149ebe032412a54d325 100644 --- a/common/lib/mitxmako/mitxmako/__init__.py +++ b/common/lib/mitxmako/mitxmako/__init__.py @@ -13,4 +13,3 @@ # limitations under the License. lookup = None - diff --git a/common/lib/mitxmako/mitxmako/shortcuts.py b/common/lib/mitxmako/mitxmako/shortcuts.py index c601d9326088a3dbca9c2909ead2f5299ab08bba..ba22f2db2075a8b5663c73e418427a519e21717d 100644 --- a/common/lib/mitxmako/mitxmako/shortcuts.py +++ b/common/lib/mitxmako/mitxmako/shortcuts.py @@ -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 diff --git a/common/lib/xmodule/progress.py b/common/lib/xmodule/progress.py index fe4793cca41b6fccb474a3ff82de8f8ddfaee139..70c8ec9da1d0a898d91b8919c538923698b10680 100644 --- a/common/lib/xmodule/progress.py +++ b/common/lib/xmodule/progress.py @@ -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): ''' diff --git a/common/lib/xmodule/tests/__init__.py b/common/lib/xmodule/tests/__init__.py index 23227213a2d4f1a07955761d3b2769764ec26a9a..7bc5b988faa8ef48e12f9d4c81e773b6a8ba8957 100644 --- a/common/lib/xmodule/tests/__init__.py +++ b/common/lib/xmodule/tests/__init__.py @@ -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()) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 5c166be19b2d9148c4d274b15cb3ac2746f667ec..bc1131c88a5dea9ac78e5f0a6e5159cd60f667b3 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -198,12 +198,12 @@ class CapaModule(XModule): if self.system.DEBUG: log.exception(err) msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url()) - msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<','<') - msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<') + msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<') + msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<') html = msg else: raise - + content = {'name': self.metadata['display_name'], 'html': html, 'weight': self.weight, @@ -336,7 +336,7 @@ class CapaModule(XModule): score_msg = get['response'] self.lcp.update_score(score_msg, queuekey) - return dict() # No AJAX return is needed + return dict() # No AJAX return is needed def get_answer(self, get): ''' @@ -379,7 +379,7 @@ class CapaModule(XModule): if not name.endswith('[]'): answers[name] = get[key] else: - name = name[:-2] + name = name[:-2] answers[name] = get.getlist(key) return answers @@ -430,7 +430,7 @@ class CapaModule(XModule): if self.system.DEBUG: msg = "Error checking problem: " + str(err) msg += '\nTraceback:\n' + traceback.format_exc() - return {'success':msg} + return {'success': msg} traceback.print_exc() raise Exception("error in capa_module") diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 42ba2b19340182176d04bb0ffe781903dd66e018..90f52ece1082394df0d2712f7436c7dcab6911c8 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -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 \ No newline at end of file + return self.location.org diff --git a/common/lib/xmodule/xmodule/graders.py b/common/lib/xmodule/xmodule/graders.py index 55dc4e4f7addd4ca9eb9cf6c4070f0e14bd4f35f..4473c8f1b9bfc22e81aaaa9b047fe8346dc6d6f7 100644 --- a/common/lib/xmodule/xmodule/graders.py +++ b/common/lib/xmodule/xmodule/graders.py @@ -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 } diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index d7bcf8d94e1eb17e6b4b590c72052542082e3def..30010a09a783444c12d499ec7b399bdd8de62b63 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py index 4c8c55ffe9f32d5aa4a42b3b0a934312e15b657d..a6dc99883fdbd02b1c6ee270dbc9452d1f06a1a8 100644 --- a/common/lib/xmodule/xmodule/modulestore/exceptions.py +++ b/common/lib/xmodule/xmodule/modulestore/exceptions.py @@ -10,5 +10,6 @@ class ItemNotFoundError(Exception): class InsufficientSpecificationError(Exception): pass + class InvalidLocationError(Exception): pass diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index d649522b6ca35d491318af3a0b807dbb326c8795..7ebee98c16244c103829c777534bc6bc34263332 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -56,6 +56,7 @@ def location_to_query(location): return query + class MongoModuleStore(ModuleStore): """ A Mongodb backed ModuleStore diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_location.py b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py index d598d8ae6d71d6e1ac65c09c7143a7f0c9e6a90b..0aa7b4342236272e4d1c3d30fabde6c1c7fd7da5 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_location.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py @@ -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'), diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 4df2743e70f91f784be8d94cb262bc0a901da00f..9b486c6e8d166f4d29637b16b3e868b0d79684cb 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -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): diff --git a/common/lib/xmodule/xmodule/progress.py b/common/lib/xmodule/xmodule/progress.py index fe4793cca41b6fccb474a3ff82de8f8ddfaee139..70c8ec9da1d0a898d91b8919c538923698b10680 100644 --- a/common/lib/xmodule/xmodule/progress.py +++ b/common/lib/xmodule/xmodule/progress.py @@ -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): ''' diff --git a/common/lib/xmodule/xmodule/schematic_module.py b/common/lib/xmodule/xmodule/schematic_module.py index f95729d4abedfe6538f4b8e9ea00a235e23ef434..21dd33a8974f224135a04b5d401fe7999d54af86 100644 --- a/common/lib/xmodule/xmodule/schematic_module.py +++ b/common/lib/xmodule/xmodule/schematic_module.py @@ -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) diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 012740687607e28a1ee0e9e8210fbb2b26c22e3d..1c1f22a3a1bd4d99df2afe7759b5dbae95942e5c 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -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, diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index ed44a2d422ad9f3d81c077be0f82c16a98d8a955..5403230a7b9e03356017b328f3bf23c8b84471d1 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -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): diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index e1ad27beea33f855ee0e0888159f989d3b23370f..f104163a88c774360d8b71cb6746fcc253ff879c 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -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 "" diff --git a/lms/djangoapps/certificates/management/commands/ungenerated_certs.py b/lms/djangoapps/certificates/management/commands/ungenerated_certs.py index 7755bb8f32c0616e2a13be49bb814e5e42c00c8b..7d384103ab264b742be9e931a2e1990e090cd06f 100644 --- a/lms/djangoapps/certificates/management/commands/ungenerated_certs.py +++ b/lms/djangoapps/certificates/management/commands/ungenerated_certs.py @@ -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") diff --git a/lms/djangoapps/certificates/migrations/0001_added_generatedcertificates.py b/lms/djangoapps/certificates/migrations/0001_added_generatedcertificates.py index 0dc76b31f8395f9681da18ce80fb61128721e3aa..094a439085db9497cfee57e81cfd1b9e0a6a3f46 100644 --- a/lms/djangoapps/certificates/migrations/0001_added_generatedcertificates.py +++ b/lms/djangoapps/certificates/migrations/0001_added_generatedcertificates.py @@ -90,4 +90,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['certificates'] \ No newline at end of file + complete_apps = ['certificates'] diff --git a/lms/djangoapps/certificates/migrations/0002_auto__add_field_generatedcertificate_download_url.py b/lms/djangoapps/certificates/migrations/0002_auto__add_field_generatedcertificate_download_url.py index ec1abd0154d881353115a1cb4ee2489b90d4f479..0019a0c491dff3d560251b7efa12ec6cb146747b 100644 --- a/lms/djangoapps/certificates/migrations/0002_auto__add_field_generatedcertificate_download_url.py +++ b/lms/djangoapps/certificates/migrations/0002_auto__add_field_generatedcertificate_download_url.py @@ -88,4 +88,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['certificates'] \ No newline at end of file + complete_apps = ['certificates'] diff --git a/lms/djangoapps/certificates/migrations/0003_auto__add_field_generatedcertificate_enabled.py b/lms/djangoapps/certificates/migrations/0003_auto__add_field_generatedcertificate_enabled.py index 880494d2265384647183f1e1ace62589f190db61..8c1150f49735ef4a7b74ea4f9d3e8f83bcf82078 100644 --- a/lms/djangoapps/certificates/migrations/0003_auto__add_field_generatedcertificate_enabled.py +++ b/lms/djangoapps/certificates/migrations/0003_auto__add_field_generatedcertificate_enabled.py @@ -89,4 +89,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['certificates'] \ No newline at end of file + complete_apps = ['certificates'] diff --git a/lms/djangoapps/certificates/migrations/0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_.py b/lms/djangoapps/certificates/migrations/0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_.py index ffa6ec71757968fb0c1343098612506983177c94..6d7b41a1d6ddbcb71dd7be490b82b20659c725b9 100644 --- a/lms/djangoapps/certificates/migrations/0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_.py +++ b/lms/djangoapps/certificates/migrations/0004_auto__add_field_generatedcertificate_graded_certificate_id__add_field_.py @@ -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'}, diff --git a/lms/djangoapps/certificates/migrations/0005_auto__add_field_generatedcertificate_name.py b/lms/djangoapps/certificates/migrations/0005_auto__add_field_generatedcertificate_name.py index c463145504db85b0f8da4f7a4e212a2ac3343d04..9b3660a5b3366e441efb9da23a15c6969e90e8de 100644 --- a/lms/djangoapps/certificates/migrations/0005_auto__add_field_generatedcertificate_name.py +++ b/lms/djangoapps/certificates/migrations/0005_auto__add_field_generatedcertificate_name.py @@ -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'}, diff --git a/lms/djangoapps/certificates/migrations/0006_auto__chg_field_generatedcertificate_certificate_id.py b/lms/djangoapps/certificates/migrations/0006_auto__chg_field_generatedcertificate_certificate_id.py index c046c4f9014bf085464618fd9085d064e695f741..947f0f967dc6642d6b7bd53200d51101d8f48107 100644 --- a/lms/djangoapps/certificates/migrations/0006_auto__chg_field_generatedcertificate_certificate_id.py +++ b/lms/djangoapps/certificates/migrations/0006_auto__chg_field_generatedcertificate_certificate_id.py @@ -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'}, diff --git a/lms/djangoapps/certificates/migrations/0007_auto__add_revokedcertificate.py b/lms/djangoapps/certificates/migrations/0007_auto__add_revokedcertificate.py index ee98eee9906058bf0c218d8ecbcac07fffa9f839..03f254867991d1132e32ecce4f5c134eb0cb377a 100644 --- a/lms/djangoapps/certificates/migrations/0007_auto__add_revokedcertificate.py +++ b/lms/djangoapps/certificates/migrations/0007_auto__add_revokedcertificate.py @@ -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'}, diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index d3323eed77c6cd8611b88f1705bf2077188059c5..5815db64caf0baf870336aa54e764b567c08bf4f 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -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'} diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index 354e8cc4d4503f4e017a406d6d992e757d903a8a..6341133c522281c778690865a5c882dcdc948c23 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -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)) diff --git a/lms/djangoapps/circuit/models.py b/lms/djangoapps/circuit/models.py index cc103e1af83a25621a3b88ea3d0ebe7034823f50..21a70bcb259cc576ad69181ddcdc4e0b711df54f 100644 --- a/lms/djangoapps/circuit/models.py +++ b/lms/djangoapps/circuit/models.py @@ -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] diff --git a/lms/djangoapps/circuit/views.py b/lms/djangoapps/circuit/views.py index 96fcc83115a7bfc7f99bd6e950b2368e92b20db3..9711e0648cf713fd9b86d9182a390aede9520004 100644 --- a/lms/djangoapps/circuit/views.py +++ b/lms/djangoapps/circuit/views.py @@ -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 - diff --git a/lms/djangoapps/courseware/course_settings.py b/lms/djangoapps/courseware/course_settings.py index d9876677d67f8e0d23c6bd4cec300f1a783b5332..5b2348bee64a9dcad7fe7e3022042f72bdfaa69c 100644 --- a/lms/djangoapps/courseware/course_settings.py +++ b/lms/djangoapps/courseware/course_settings.py @@ -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) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index f92efbe4eb7a2941eef901843a09e6e9449f5973..272cc210ac733f12e1d88c22f9233643e1b93bbc 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -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)) - \ No newline at end of file + diff --git a/lms/djangoapps/courseware/migrations/0001_initial.py b/lms/djangoapps/courseware/migrations/0001_initial.py index 205ed97136d88fcf186fb015478f1618f9d20065..4c29ca17a565e8bcec4eec0354bfea869091850c 100644 --- a/lms/djangoapps/courseware/migrations/0001_initial.py +++ b/lms/djangoapps/courseware/migrations/0001_initial.py @@ -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'}, diff --git a/lms/djangoapps/courseware/migrations/0002_add_indexes.py b/lms/djangoapps/courseware/migrations/0002_add_indexes.py index 02f28c9f78f8d785d0df39542380ee4c35bf99b6..7de33b4c52cb13224b4f0c6563d812c81ad41ba2 100644 --- a/lms/djangoapps/courseware/migrations/0002_add_indexes.py +++ b/lms/djangoapps/courseware/migrations/0002_add_indexes.py @@ -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'}, diff --git a/lms/djangoapps/courseware/migrations/0003_done_grade_cache.py b/lms/djangoapps/courseware/migrations/0003_done_grade_cache.py index f2fedcf1a83d3a7630dc3dddd5ad5733a46aa61f..96b320bc8f85a987e28968a2c7254f3dfc202890 100644 --- a/lms/djangoapps/courseware/migrations/0003_done_grade_cache.py +++ b/lms/djangoapps/courseware/migrations/0003_done_grade_cache.py @@ -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'}, diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index afb7f1c49492d44fb2da8023a6c69b96f3ea33c7..3b0ca7fdcf08cf3c7ce9e57e274d1f11f3b6eb7b 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -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) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 00ffb8608baa1c4400efad72262573553affd9d6..0ba58396285448290925f4b83f020866b7e370b2 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -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) diff --git a/lms/djangoapps/courseware/progress.py b/lms/djangoapps/courseware/progress.py index ad2cb725b4c19c9ffeabbc8c6e545ac00c2e1f89..b9dd3fa7818040cf90268b94a4d0df12626ad4dc 100644 --- a/lms/djangoapps/courseware/progress.py +++ b/lms/djangoapps/courseware/progress.py @@ -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 diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 3fc691daa7fbf83dca34ede85a58dbef506f8cff..f273778a3cd9743e4682cb9955b5c333bf29645a 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -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() diff --git a/lms/djangoapps/heartbeat/views.py b/lms/djangoapps/heartbeat/views.py index 879c9e60165ffc898f79ef63ccc75ac02152ec95..1d053d3da02a78b499546f36de4b30f383df2533 100644 --- a/lms/djangoapps/heartbeat/views.py +++ b/lms/djangoapps/heartbeat/views.py @@ -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 diff --git a/lms/djangoapps/multicourse/multicourse_settings.py b/lms/djangoapps/multicourse/multicourse_settings.py index 3c1710c53588d9716ea229e092b516e5f39c4ae0..894bfa588c7377f3e490b1122e45e4df3e8e468b 100644 --- a/lms/djangoapps/multicourse/multicourse_settings.py +++ b/lms/djangoapps/multicourse/multicourse_settings.py @@ -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') diff --git a/lms/djangoapps/multicourse/views.py b/lms/djangoapps/multicourse/views.py index 9d081cf5cf2dc04b9a18e5c9c9a9f2dac15e14ae..da9ccb77a6659d28da2bbb721f7b334e69f023db 100644 --- a/lms/djangoapps/multicourse/views.py +++ b/lms/djangoapps/multicourse/views.py @@ -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) - diff --git a/lms/djangoapps/simplewiki/__init__.py b/lms/djangoapps/simplewiki/__init__.py index 8c4ebf2d517cb3e5894588735be129e94035b641..9f9c3324198deec54963abc6433c5c39060949c9 100644 --- a/lms/djangoapps/simplewiki/__init__.py +++ b/lms/djangoapps/simplewiki/__init__.py @@ -1,4 +1,4 @@ -# Source: django-simplewiki. GPL license. +# Source: django-simplewiki. GPL license. import os import sys diff --git a/lms/djangoapps/simplewiki/admin.py b/lms/djangoapps/simplewiki/admin.py index 8d1094bbc02c4498d34bf440b5de8d1f32d2dd5a..e4cf8c2f56b2a42457d776667c05fcc66cd8909d 100644 --- a/lms/djangoapps/simplewiki/admin.py +++ b/lms/djangoapps/simplewiki/admin.py @@ -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') diff --git a/lms/djangoapps/simplewiki/mdx_circuit.py b/lms/djangoapps/simplewiki/mdx_circuit.py index bc58ad91edb18d68e9b6df2af135d1da6ded7a49..4ec7501341bfc4c8ff5bd3239e634c17a638004c 100755 --- a/lms/djangoapps/simplewiki/mdx_circuit.py +++ b/lms/djangoapps/simplewiki/mdx_circuit.py @@ -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 diff --git a/lms/djangoapps/simplewiki/mdx_image.py b/lms/djangoapps/simplewiki/mdx_image.py index 956641baa70ecf02eae0341325b876e8b8721927..af0413f84132014705efeab395098b55b103c25e 100755 --- a/lms/djangoapps/simplewiki/mdx_image.py +++ b/lms/djangoapps/simplewiki/mdx_image.py @@ -28,31 +28,32 @@ except: class ImageExtension(markdown.Extension): def __init__(self, configs): - for key, value in configs : + for key, value in configs: self.setConfig(key, value) - + def add_inline(self, md, name, klass, re): pattern = klass(re) pattern.md = md pattern.ext = self md.inlinePatterns.add(name, pattern, "<reference") - + def extendMarkdown(self, md, md_globals): - self.add_inline(md, 'image', ImageLink, + self.add_inline(md, 'image', ImageLink, r'^(?P<proto>([^:/?#])+://)?(?P<domain>([^/?#]*)/)?(?P<path>[^?#]*\.(?P<ext>[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$') + class ImageLink(markdown.inlinepatterns.Pattern): def handleMatch(self, m): img = etree.Element('img') - proto = m.group('proto') or "http://" + proto = m.group('proto') or "http://" domain = m.group('domain') - path = m.group('path') - ext = m.group('ext') - + path = m.group('path') + ext = m.group('ext') + # A fixer upper if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS: if domain: - src = proto+domain+path + src = proto + domain + path elif path: # We need a nice way to source local attachments... src = "/wiki/media/" + path + ".upload" @@ -60,10 +61,11 @@ class ImageLink(markdown.inlinepatterns.Pattern): src = '' img.set('src', src) return img - -def makeExtension(configs=None) : + + +def makeExtension(configs=None): return ImageExtension(configs=configs) if __name__ == "__main__": import doctest - doctest.testmod() \ No newline at end of file + doctest.testmod() diff --git a/lms/djangoapps/simplewiki/mdx_mathjax.py b/lms/djangoapps/simplewiki/mdx_mathjax.py index e694ca861e754ad76199be2eb7b41aa286b3fe12..a9148511e32580228ea538c625e95863beb6ce2f 100644 --- a/lms/djangoapps/simplewiki/mdx_mathjax.py +++ b/lms/djangoapps/simplewiki/mdx_mathjax.py @@ -8,6 +8,7 @@ try: except: from markdown import etree, AtomicString + class MathJaxPattern(markdown.inlinepatterns.Pattern): def __init__(self): @@ -18,12 +19,13 @@ class MathJaxPattern(markdown.inlinepatterns.Pattern): el.text = AtomicString(m.group(2) + m.group(3) + m.group(2)) return el + class MathJaxExtension(markdown.Extension): def extendMarkdown(self, md, md_globals): # Needs to come before escape matching because \ is pretty important in LaTeX md.inlinePatterns.add('mathjax', MathJaxPattern(), '<escape') + def makeExtension(configs=None): return MathJaxExtension(configs) - diff --git a/lms/djangoapps/simplewiki/mdx_video.py b/lms/djangoapps/simplewiki/mdx_video.py index cc15e39a977278965aca14ee4a8b7576273af964..f27b1b63ba3dc33fb55c0aa7f8f293d384c8e21c 100755 --- a/lms/djangoapps/simplewiki/mdx_video.py +++ b/lms/djangoapps/simplewiki/mdx_video.py @@ -138,6 +138,7 @@ except: version = "0.1.6" + class VideoExtension(markdown.Extension): def __init__(self, configs): self.config = { @@ -187,6 +188,7 @@ class VideoExtension(markdown.Extension): self.add_inline(md, 'youtube', Youtube, r'([^(]|^)http://www\.youtube\.com/watch\?\S*v=(?P<youtubeargs>[A-Za-z0-9_&=-]+)\S*') + class Bliptv(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/%s' % m.group('bliptvfile') @@ -194,6 +196,7 @@ class Bliptv(markdown.inlinepatterns.Pattern): height = self.ext.config['bliptv_height'][0] return flash_object(url, width, height) + class Dailymotion(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://www.dailymotion.com/swf/%s' % m.group('dailymotionid').split('/')[-1] @@ -201,6 +204,7 @@ class Dailymotion(markdown.inlinepatterns.Pattern): height = self.ext.config['dailymotion_height'][0] return flash_object(url, width, height) + class Gametrailers(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://www.gametrailers.com/remote_wrap.php?mid=%s' % \ @@ -209,6 +213,7 @@ class Gametrailers(markdown.inlinepatterns.Pattern): height = self.ext.config['gametrailers_height'][0] return flash_object(url, width, height) + class Metacafe(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://www.metacafe.com/fplayer/%s.swf' % m.group('metacafeid') @@ -216,6 +221,7 @@ class Metacafe(markdown.inlinepatterns.Pattern): height = self.ext.config['metacafe_height'][0] return flash_object(url, width, height) + class Veoh(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://www.veoh.com/videodetails2.swf?permalinkId=%s' % m.group('veohid') @@ -223,6 +229,7 @@ class Veoh(markdown.inlinepatterns.Pattern): height = self.ext.config['veoh_height'][0] return flash_object(url, width, height) + class Vimeo(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://vimeo.com/moogaloop.swf?clip_id=%s&server=vimeo.com' % m.group('vimeoid') @@ -230,6 +237,7 @@ class Vimeo(markdown.inlinepatterns.Pattern): height = self.ext.config['vimeo_height'][0] return flash_object(url, width, height) + class Yahoo(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = "http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" @@ -243,6 +251,7 @@ class Yahoo(markdown.inlinepatterns.Pattern): obj.append(param) return obj + class Youtube(markdown.inlinepatterns.Pattern): def handleMatch(self, m): url = 'http://www.youtube.com/v/%s' % m.group('youtubeargs') @@ -250,6 +259,7 @@ class Youtube(markdown.inlinepatterns.Pattern): height = self.ext.config['youtube_height'][0] return flash_object(url, width, height) + def flash_object(url, width, height): obj = etree.Element('object') obj.set('type', 'application/x-shockwave-flash') @@ -270,7 +280,8 @@ def flash_object(url, width, height): #obj.append(param) return obj -def makeExtension(configs=None) : + +def makeExtension(configs=None): return VideoExtension(configs=configs) if __name__ == "__main__": diff --git a/lms/djangoapps/simplewiki/mdx_wikipath.py b/lms/djangoapps/simplewiki/mdx_wikipath.py index d9381e4bee4793c925bffad0398aac87250c060c..17c2b655916782f3894dc1a6baf9a75329bcf595 100755 --- a/lms/djangoapps/simplewiki/mdx_wikipath.py +++ b/lms/djangoapps/simplewiki/mdx_wikipath.py @@ -33,49 +33,49 @@ class WikiPathExtension(markdown.Extension): def __init__(self, configs): # set extension defaults self.config = { - 'default_namespace' : ['edX', 'Default namespace for when one isn\'t specified.'], - 'html_class' : ['wikipath', 'CSS hook. Leave blank for none.'] + 'default_namespace': ['edX', 'Default namespace for when one isn\'t specified.'], + 'html_class': ['wikipath', 'CSS hook. Leave blank for none.'] } - + # Override defaults with user settings - for key, value in configs : + for key, value in configs: # self.config[key][0] = value self.setConfig(key, value) - - + def extendMarkdown(self, md, md_globals): self.md = md - + # append to end of inline patterns - WIKI_RE = r'\[(?P<linkTitle>.+?)\]\(wiki:(?P<wikiTitle>[a-zA-Z\d/_-]*)\)' + WIKI_RE = r'\[(?P<linkTitle>.+?)\]\(wiki:(?P<wikiTitle>[a-zA-Z\d/_-]*)\)' wikiPathPattern = WikiPath(WIKI_RE, self.config) wikiPathPattern.md = md md.inlinePatterns.add('wikipath', wikiPathPattern, "<reference") + class WikiPath(markdown.inlinepatterns.Pattern): def __init__(self, pattern, config): markdown.inlinepatterns.Pattern.__init__(self, pattern) self.config = config - - def handleMatch(self, m) : + + def handleMatch(self, m): article_title = m.group('wikiTitle') if article_title.startswith("/"): article_title = article_title[1:] - + if not "/" in article_title: article_title = self.config['default_namespace'][0] + "/" + article_title - + url = "../" + article_title label = m.group('linkTitle') a = etree.Element('a') a.set('href', url) a.text = label - + if self.config['html_class'][0]: a.set('class', self.config['html_class'][0]) - + return a - + def _getMeta(self): """ Return meta data or config data. """ base_url = self.config['base_url'][0] @@ -87,7 +87,8 @@ class WikiPath(markdown.inlinepatterns.Pattern): html_class = self.md.Meta['wiki_html_class'][0] return base_url, html_class -def makeExtension(configs=None) : + +def makeExtension(configs=None): return WikiPathExtension(configs=configs) if __name__ == "__main__": diff --git a/lms/djangoapps/simplewiki/migrations/0001_initial.py b/lms/djangoapps/simplewiki/migrations/0001_initial.py index 542c39248cf44dd063b05935c671f3dc89c88a53..b56a28295af40f74e17e1364924aa3920b45f702 100644 --- a/lms/djangoapps/simplewiki/migrations/0001_initial.py +++ b/lms/djangoapps/simplewiki/migrations/0001_initial.py @@ -82,7 +82,6 @@ class Migration(SchemaMigration): )) db.create_unique('simplewiki_permission_can_read', ['permission_id', 'user_id']) - def backwards(self, orm): # Removing unique constraint on 'Article', fields ['slug', 'parent'] db.delete_unique('simplewiki_article', ['slug', 'parent_id']) @@ -108,7 +107,6 @@ class Migration(SchemaMigration): # Removing M2M table for field can_read on 'Permission' db.delete_table('simplewiki_permission_can_read') - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, @@ -215,4 +213,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['simplewiki'] \ No newline at end of file + complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py b/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py index f81d8d3431d6af04828d535c2c5486aeb955f46f..79f1c195e1f05aa7b5888900448792b0b3380d20 100644 --- a/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py +++ b/lms/djangoapps/simplewiki/migrations/0002_unique_slugs.py @@ -4,6 +4,7 @@ from south.db import db from south.v2 import DataMigration from django.db import models + class Migration(DataMigration): def forwards(self, orm): @@ -16,11 +17,11 @@ class Migration(DataMigration): while new_name in unique_slugs: i += 1 new_name = article.slug + str(i) - print "Changing" , article.slug , "to" , new_name + print "Changing", article.slug, "to", new_name article.slug = new_name article.save() - - unique_slugs.add( article.slug ) + + unique_slugs.add(article.slug) def backwards(self, orm): "Write your backwards methods here." diff --git a/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py b/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py index 0ad2bb6caef2d193f28420ab344c691791094b6f..85bfdadb009b5aebadfce5b8ed7cba888f79bc49 100644 --- a/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py +++ b/lms/djangoapps/simplewiki/migrations/0003_auto__add_namespace__del_field_article_parent__add_field_article_names.py @@ -29,7 +29,6 @@ class Migration(SchemaMigration): # Adding unique constraint on 'Article', fields ['namespace', 'slug'] db.create_unique('simplewiki_article', ['namespace_id', 'slug']) - def backwards(self, orm): # Removing unique constraint on 'Article', fields ['namespace', 'slug'] db.delete_unique('simplewiki_article', ['namespace_id', 'slug']) @@ -48,7 +47,6 @@ class Migration(SchemaMigration): # Adding unique constraint on 'Article', fields ['slug', 'parent'] db.create_unique('simplewiki_article', ['slug', 'parent_id']) - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, @@ -160,4 +158,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['simplewiki'] \ No newline at end of file + complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py b/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py index 5f61eb9d1dce1b3f85494d99cc6ea064f4882d86..69adeb4641cb452df2512db813fc1ecdc2f10b36 100644 --- a/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py +++ b/lms/djangoapps/simplewiki/migrations/0004_multicourse_data_migration.py @@ -4,20 +4,21 @@ from south.db import db from south.v2 import DataMigration from django.db import models + class Migration(DataMigration): def forwards(self, orm): namespace6002x, created = orm.Namespace.objects.get_or_create(name="6.002xS12") if created: namespace6002x.save() - + for article in orm.Article.objects.all(): article.namespace = namespace6002x article.save() def backwards(self, orm): raise RuntimeError("Cannot reverse this migration.") - + models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, diff --git a/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py b/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py index 1ed6b11b67a5afd1b7bb00f0c76eb00107d12386..c37fe135449632eb7e86005bdf84c8e84efcb297 100644 --- a/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py +++ b/lms/djangoapps/simplewiki/migrations/0005_auto__add_unique_namespace_name.py @@ -11,12 +11,10 @@ class Migration(SchemaMigration): # Adding unique constraint on 'Namespace', fields ['name'] db.create_unique('simplewiki_namespace', ['name']) - def backwards(self, orm): # Removing unique constraint on 'Namespace', fields ['name'] db.delete_unique('simplewiki_namespace', ['name']) - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, @@ -128,4 +126,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['simplewiki'] \ No newline at end of file + complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0006_auto.py b/lms/djangoapps/simplewiki/migrations/0006_auto.py index 2f7c698f04e432aa9ec532a3e44218051333e9e2..b5b18c39c0eb879791258ffe205bac1e8a998d01 100644 --- a/lms/djangoapps/simplewiki/migrations/0006_auto.py +++ b/lms/djangoapps/simplewiki/migrations/0006_auto.py @@ -11,12 +11,10 @@ class Migration(SchemaMigration): # Adding index on 'Namespace', fields ['name'] db.create_index('simplewiki_namespace', ['name']) - def backwards(self, orm): # Removing index on 'Namespace', fields ['name'] db.delete_index('simplewiki_namespace', ['name']) - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, @@ -128,4 +126,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['simplewiki'] \ No newline at end of file + complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/migrations/0007_auto.py b/lms/djangoapps/simplewiki/migrations/0007_auto.py index ee8806e630bf1dcf5d6511ec96a03f63f64e8860..6e3071e4d3d9c07f8d1156f4c7572f4d861bb310 100644 --- a/lms/djangoapps/simplewiki/migrations/0007_auto.py +++ b/lms/djangoapps/simplewiki/migrations/0007_auto.py @@ -11,12 +11,10 @@ class Migration(SchemaMigration): # Removing index on 'Namespace', fields ['name'] db.delete_index('simplewiki_namespace', ['name']) - def backwards(self, orm): # Adding index on 'Namespace', fields ['name'] db.create_index('simplewiki_namespace', ['name']) - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, @@ -128,4 +126,4 @@ class Migration(SchemaMigration): } } - complete_apps = ['simplewiki'] \ No newline at end of file + complete_apps = ['simplewiki'] diff --git a/lms/djangoapps/simplewiki/models.py b/lms/djangoapps/simplewiki/models.py index d01f8e0f71c7da0ae0362553be9714df1436d360..68da6a0d711d0462026630e8e902a9d01d12951a 100644 --- a/lms/djangoapps/simplewiki/models.py +++ b/lms/djangoapps/simplewiki/models.py @@ -16,26 +16,26 @@ from util.cache import cache class ShouldHaveExactlyOneRootSlug(Exception): pass + class Namespace(models.Model): name = models.CharField(max_length=30, unique=True, verbose_name=_('namespace')) # TODO: We may want to add permissions, etc later - + @classmethod def ensure_namespace(cls, name): try: - namespace = Namespace.objects.get(name__exact = name) + namespace = Namespace.objects.get(name__exact=name) except Namespace.DoesNotExist: new_namespace = Namespace(name=name) new_namespace.save() - - + class Article(models.Model): """Wiki article referring to Revision model for actual content. 'slug' and 'title' field should be maintained centrally, since users aren't allowed to change them, anyways. """ - + title = models.CharField(max_length=512, verbose_name=_('Article title'), blank=False) slug = models.SlugField(max_length=100, verbose_name=_('slug'), @@ -43,8 +43,8 @@ class Article(models.Model): blank=True) namespace = models.ForeignKey(Namespace, verbose_name=_('Namespace')) created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True) - created_on = models.DateTimeField(auto_now_add = 1) - modified_on = models.DateTimeField(auto_now_add = 1) + created_on = models.DateTimeField(auto_now_add=1) + modified_on = models.DateTimeField(auto_now_add=1) locked = models.BooleanField(default=False, verbose_name=_('Locked for editing')) permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'), blank=True, null=True, @@ -56,11 +56,11 @@ class Article(models.Model): blank=True, null=True) def attachments(self): - return ArticleAttachment.objects.filter(article__exact = self) - + return ArticleAttachment.objects.filter(article__exact=self) + def get_path(self): return self.namespace.name + "/" + self.slug - + @classmethod def get_article(cls, article_path): """ @@ -70,8 +70,7 @@ class Article(models.Model): """ #TODO: Verify the path, throw a meaningful error? namespace, slug = article_path.split("/") - return Article.objects.get( slug__exact = slug, namespace__name__exact = namespace) - + return Article.objects.get(slug__exact=slug, namespace__name__exact=namespace) @classmethod def get_root(cls, namespace): @@ -79,7 +78,7 @@ class Article(models.Model): except the very first time the wiki is loaded, in which case the user is prompted to create this article.""" try: - return Article.objects.filter(slug__exact = "", namespace__name__exact = namespace)[0] + return Article.objects.filter(slug__exact="", namespace__name__exact=namespace)[0] except: raise ShouldHaveExactlyOneRootSlug() @@ -95,7 +94,7 @@ class Article(models.Model): # return cls.get_url_reverse(path[1:], a, return_list+[article]) # except Exception, e: # return None - + def can_read(self, user): """ Check read permissions and return True/False.""" if user.is_superuser: @@ -132,32 +131,34 @@ class Article(models.Model): return unicode(_('Root article')) else: return self.slug - + class Meta: unique_together = (('slug', 'namespace'),) verbose_name = _('Article') verbose_name_plural = _('Articles') + def get_attachment_filepath(instance, filename): """Store file, appending new extension for added security""" dir_ = WIKI_ATTACHMENTS + instance.article.get_url() - dir_ = '/'.join(filter(lambda x: x!='', dir_.split('/'))) + dir_ = '/'.join(filter(lambda x: x != '', dir_.split('/'))) if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_): os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_) return dir_ + '/' + filename + '.upload' + class ArticleAttachment(models.Model): article = models.ForeignKey(Article, verbose_name=_('Article')) file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment')) uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True) - uploaded_on = models.DateTimeField(auto_now_add = True, verbose_name=_('Upload date')) - + uploaded_on = models.DateTimeField(auto_now_add=True, verbose_name=_('Upload date')) + def download_url(self): return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename())) - + def filename(self): return '.'.join(self.file.name.split('/')[-1].split('.')[:-1]) - + def get_size(self): try: size = self.file.size @@ -167,13 +168,13 @@ class ArticleAttachment(models.Model): def filename(self): return '.'.join(self.file.name.split('/')[-1].split('.')[:-1]) - + def is_image(self): fname = self.filename().split('.') if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS: return True return False - + def get_thumb(self): return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE) @@ -181,27 +182,27 @@ class ArticleAttachment(models.Model): return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL) def mk_thumbs(self): - self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force':True}) - self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force':True}) + self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force': True}) + self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force': True}) def mk_thumb(self, width, height, force=False): """Requires Python Imaging Library (PIL)""" if not self.get_size(): return False - + if not self.is_image(): return False - + base_path = os.path.dirname(self.file.path) orig_name = self.filename().split('.') thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename) - + if force or not os.path.exists(thumb_filepath): try: import Image - img = Image.open(self.file.path) - img.thumbnail((width,height), Image.ANTIALIAS) + img = Image.open(self.file.path) + img.thumbnail((width, height), Image.ANTIALIAS) img.save(thumb_filepath) except IOError: return False @@ -210,52 +211,53 @@ class ArticleAttachment(models.Model): def get_thumb_impl(self, width, height): """Requires Python Imaging Library (PIL)""" - + if not self.get_size(): return False - + if not self.is_image(): return False - + self.mk_thumb(width, height) - + orig_name = self.filename().split('.') thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1]) - thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() +'/' + thumb_filename - + thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() + '/' + thumb_filename + return thumb_url def __unicode__(self): return self.filename() - + + class Revision(models.Model): - + article = models.ForeignKey(Article, verbose_name=_('Article')) - revision_text = models.CharField(max_length=255, blank=True, null=True, + revision_text = models.CharField(max_length=255, blank=True, null=True, verbose_name=_('Description of change')) - revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), + revision_user = models.ForeignKey(User, verbose_name=_('Modified by'), blank=True, null=True, related_name='wiki_revision_user') - revision_date = models.DateTimeField(auto_now_add = True, verbose_name=_('Revision date')) + revision_date = models.DateTimeField(auto_now_add=True, verbose_name=_('Revision date')) contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)')) contents_parsed = models.TextField(editable=False, blank=True, null=True) counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False) previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False) - + # Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should # be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed # from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't # show up in the history. deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0) - + def get_user(self): return self.revision_user if self.revision_user else _('Anonymous') - + # Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in - # save that update the revision or reject the save if contents haven't changed + # save that update the revision or reject the save if contents haven't changed def adminSetDeleted(self, deleted): self.deleted = deleted super(Revision, self).save() - + def save(self, **kwargs): # Check if contents have changed... if not, silently ignore save if self.article and self.article.current_revision: @@ -265,7 +267,7 @@ class Revision(models.Model): import datetime self.article.modified_on = datetime.datetime.now() self.article.save() - + # Increment counter according to previous revision previous_revision = Revision.objects.filter(article=self.article).order_by('-counter') if previous_revision.count() > 0: @@ -280,50 +282,50 @@ class Revision(models.Model): # Create pre-parsed contents - no need to parse on-the-fly ext = WIKI_MARKDOWN_EXTENSIONS - ext += ["wikipath(default_namespace=%s)" % self.article.namespace.name ] + ext += ["wikipath(default_namespace=%s)" % self.article.namespace.name] self.contents_parsed = markdown(self.contents, extensions=ext, safe_mode='escape',) super(Revision, self).save(**kwargs) - + def delete(self, **kwargs): """If a current revision is deleted, then regress to the previous revision or insert a stub, if no other revisions are available""" article = self.article if article.current_revision == self: - prev_revision = Revision.objects.filter(article__exact = article, - pk__not = self.pk).order_by('-counter') + prev_revision = Revision.objects.filter(article__exact=article, + pk__not=self.pk).order_by('-counter') if prev_revision: article.current_revision = prev_revision[0] article.save() else: - r = Revision(article=article, - revision_user = article.created_by) + r = Revision(article=article, + revision_user=article.created_by) r.contents = unicode(_('Auto-generated stub')) - r.revision_text= unicode(_('Auto-generated stub')) + r.revision_text = unicode(_('Auto-generated stub')) r.save() article.current_revision = r article.save() super(Revision, self).delete(**kwargs) - + def get_diff(self): if (self.deleted == 1): yield "Article Deletion" return - + if self.previous_revision: previous = self.previous_revision.contents.splitlines(1) else: previous = [] - + # Todo: difflib.HtmlDiff would look pretty for our history pages! diff = difflib.unified_diff(previous, self.contents.splitlines(1)) # let's skip the preamble diff.next(); diff.next(); diff.next() - + for d in diff: yield d - + def __unicode__(self): return "r%d" % self.counter @@ -331,33 +333,45 @@ class Revision(models.Model): verbose_name = _('article revision') verbose_name_plural = _('article revisions') + class Permission(models.Model): - permission_name = models.CharField(max_length = 255, verbose_name=_('Permission name')) + permission_name = models.CharField(max_length=255, verbose_name=_('Permission name')) can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write', help_text=_('Select none to grant anonymous access.')) can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read', help_text=_('Select none to grant anonymous access.')) + def __unicode__(self): return self.permission_name + class Meta: verbose_name = _('Article permission') verbose_name_plural = _('Article permissions') + class RevisionForm(forms.ModelForm): - contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows':8, 'cols':50})) + contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows': 8, 'cols': 50})) + class Meta: model = Revision fields = ['contents', 'revision_text'] + + class RevisionFormWithTitle(forms.ModelForm): title = forms.CharField(label=_('Title')) + class Meta: model = Revision fields = ['title', 'contents', 'revision_text'] + + class CreateArticleForm(RevisionForm): title = forms.CharField(label=_('Title')) + class Meta: model = Revision - fields = ['title', 'contents',] + fields = ['title', 'contents', ] + def set_revision(sender, *args, **kwargs): """Signal handler to ensure that a new revision is always chosen as the diff --git a/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py b/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py index 18c6332b1af0f0831b1118f260fbf1c177bdb146..6325aeb2bd68fd939b006a3555f0c0c0d48ead89 100644 --- a/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py +++ b/lms/djangoapps/simplewiki/templatetags/simplewiki_utils.py @@ -7,11 +7,13 @@ from simplewiki.wiki_settings import * register = template.Library() + @register.filter() def prepend_media_url(value): """Prepend user defined media root to url""" return settings.MEDIA_URL + value + @register.filter() def urlquote(value): """Prepend user defined media root to url""" diff --git a/lms/djangoapps/simplewiki/tests.py b/lms/djangoapps/simplewiki/tests.py index 2247054b354559ab535df60bb5dc65c2aa5be686..6b6048580528e0051272d460c9bc1e7d75563a43 100644 --- a/lms/djangoapps/simplewiki/tests.py +++ b/lms/djangoapps/simplewiki/tests.py @@ -7,6 +7,7 @@ Replace these with more appropriate tests for your application. from django.test import TestCase + class SimpleTest(TestCase): def test_basic_addition(self): """ @@ -20,4 +21,3 @@ Another way to test that 1 + 1 is equal to 2. >>> 1 + 1 == 2 True """} - diff --git a/lms/djangoapps/simplewiki/urls.py b/lms/djangoapps/simplewiki/urls.py index 2d01c06bf9514851972d89eb8404325ae4e44ff4..6179345f9a5af963f32dc7a76b4c07bad957eeeb 100644 --- a/lms/djangoapps/simplewiki/urls.py +++ b/lms/djangoapps/simplewiki/urls.py @@ -14,6 +14,6 @@ urlpatterns = patterns('', url(r'^search_related' + article_slug, 'simplewiki.views.search_add_related', name='search_related'), url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'), url(r'^revision_feed' + namespace + r'/(?P<page>[0-9]+)?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'), - url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), - url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), #Just an alias for the search, but you usually don't submit a search term + url(r'^search' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_search_articles'), + url(r'^list' + namespace + r'?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), # Just an alias for the search, but you usually don't submit a search term ) diff --git a/lms/djangoapps/simplewiki/views.py b/lms/djangoapps/simplewiki/views.py index a0f1463fe5b079b6d3e09701251a307580a868a4..77fadc49baf4ccd8504b999e1757f3c572dc7a93 100644 --- a/lms/djangoapps/simplewiki/views.py +++ b/lms/djangoapps/simplewiki/views.py @@ -15,9 +15,10 @@ from xmodule.modulestore.django import modulestore from models import Revision, Article, Namespace, CreateArticleForm, RevisionFormWithTitle, RevisionForm import wiki_settings - -def wiki_reverse(wiki_page, article = None, course = None, namespace=None, args=[], kwargs={}): - kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'} + + +def wiki_reverse(wiki_page, article=None, course=None, namespace=None, args=[], kwargs={}): + kwargs = dict(kwargs) # TODO: Figure out why if I don't do this kwargs sometimes contains {'article_path'} if not 'course_id' in kwargs and course: kwargs['course_id'] = course.id if not 'article_path' in kwargs and article: @@ -25,86 +26,91 @@ def wiki_reverse(wiki_page, article = None, course = None, namespace=None, args= if not 'namespace' in kwargs and namespace: kwargs['namespace'] = namespace return reverse(wiki_page, kwargs=kwargs, args=args) - -def update_template_dictionary(dictionary, request = None, course = None, article = None, revision = None): + + +def update_template_dictionary(dictionary, request=None, course=None, article=None, revision=None): if article: dictionary['wiki_article'] = article - dictionary['wiki_title'] = article.title #TODO: What is the title when viewing the article in a course? + dictionary['wiki_title'] = article.title # TODO: What is the title when viewing the article in a course? if not course and 'namespace' not in dictionary: dictionary['namespace'] = article.namespace.name - + if course: dictionary['course'] = course if 'namespace' not in dictionary: dictionary['namespace'] = course.wiki_namespace else: dictionary['course'] = None - + if revision: dictionary['wiki_article_revision'] = revision dictionary['wiki_current_revision_deleted'] = not (revision.deleted == 0) - + if request: dictionary.update(csrf(request)) - + + def view(request, article_path, course_id=None): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, article_path, course ) + + (article, err) = get_article(request, article_path, course) if err: return err - + perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True) if perm_err: return perm_err - + d = {} update_template_dictionary(d, request, course, article, article.current_revision) return render_to_response('simplewiki/simplewiki_view.html', d) - + + def view_revision(request, revision_number, article_path, course_id=None): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, article_path, course ) + + (article, err) = get_article(request, article_path, course) if err: return err - + try: revision = Revision.objects.get(counter=int(revision_number), article=article) except: d = {'wiki_err_norevision': revision_number} update_template_dictionary(d, request, course, article) return render_to_response('simplewiki/simplewiki_error.html', d) - + perm_err = check_permissions(request, article, course, check_read=True, check_deleted=True, revision=revision) if perm_err: return perm_err - + d = {} update_template_dictionary(d, request, course, article, revision) - + return render_to_response('simplewiki/simplewiki_view.html', d) + def root_redirect(request, course_id=None): course = check_course(course_id, course_required=False) - + #TODO: Add a default namespace to settings. namespace = course.wiki_namespace if course else "edX" - + try: root = Article.get_root(namespace) - return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id' : course_id, 'article_path' : root.get_path()} )) + return HttpResponseRedirect(reverse('wiki_view', kwargs={'course_id': course_id, 'article_path': root.get_path()})) except: # If the root is not found, we probably are loading this class for the first time # We should make sure the namespace exists so the root article can be created. Namespace.ensure_namespace(namespace) - + err = not_found(request, namespace + '/', course) return err -def create(request, article_path, course_id=None): + +def create(request, article_path, course_id=None): course = check_course(course_id, course_required=False) - + article_path_components = article_path.split('/') # Ensure the namespace exists @@ -112,27 +118,27 @@ def create(request, article_path, course_id=None): d = {'wiki_err_no_namespace': True} update_template_dictionary(d, request, course) return render_to_response('simplewiki/simplewiki_error.html', d) - + namespace = None try: - namespace = Namespace.objects.get(name__exact = article_path_components[0]) + namespace = Namespace.objects.get(name__exact=article_path_components[0]) except Namespace.DoesNotExist, ValueError: d = {'wiki_err_bad_namespace': True} update_template_dictionary(d, request, course) return render_to_response('simplewiki/simplewiki_error.html', d) - + # See if the article already exists article_slug = article_path_components[1] if len(article_path_components) >= 2 else '' #TODO: Make sure the slug only contains legal characters (which is already done a bit by the url regex) - + try: - existing_article = Article.objects.get(namespace = namespace, slug__exact = article_slug) + existing_article = Article.objects.get(namespace=namespace, slug__exact=article_slug) #It already exists, so we just redirect to view the article return HttpResponseRedirect(wiki_reverse("wiki_view", existing_article, course)) except Article.DoesNotExist: #This is good. The article doesn't exist pass - + #TODO: Once we have permissions for namespaces, we should check for create permissions #check_permissions(request, #namespace#, check_locked=False, check_write=True, check_deleted=True) @@ -151,21 +157,22 @@ def create(request, article_path, course_id=None): new_revision.revision_user = request.user new_revision.article = article new_revision.save() - + return HttpResponseRedirect(wiki_reverse("wiki_view", article, course)) else: - f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', article_slug), - 'contents':_('Headline\n===\n\n')}) - - d = {'wiki_form': f, 'create_article' : True, 'namespace' : namespace.name} + f = CreateArticleForm(initial={'title': request.GET.get('wiki_article_name', article_slug), + 'contents': _('Headline\n===\n\n')}) + + d = {'wiki_form': f, 'create_article': True, 'namespace': namespace.name} update_template_dictionary(d, request, course) return render_to_response('simplewiki/simplewiki_edit.html', d) + def edit(request, article_path, course_id=None): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, article_path, course ) + + (article, err) = get_article(request, article_path, course) if err: return err @@ -178,21 +185,21 @@ def edit(request, article_path, course_id=None): EditForm = RevisionFormWithTitle else: EditForm = RevisionForm - + if request.method == 'POST': f = EditForm(request.POST) if f.is_valid(): new_revision = f.save(commit=False) new_revision.article = article - + if request.POST.__contains__('delete'): - if (article.current_revision.deleted == 1): #This article has already been deleted. Redirect + if (article.current_revision.deleted == 1): # This article has already been deleted. Redirect return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) new_revision.contents = "" new_revision.deleted = 1 elif not new_revision.get_diff(): return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) - + if not request.user.is_anonymous(): new_revision.revision_user = request.user new_revision.save() @@ -202,17 +209,18 @@ def edit(request, article_path, course_id=None): return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) else: startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n' - + f = EditForm({'contents': startContents, 'title': article.title}) - + d = {'wiki_form': f} update_template_dictionary(d, request, course, article) return render_to_response('simplewiki/simplewiki_edit.html', d) + def history(request, article_path, page=1, course_id=None): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, article_path, course ) + + (article, err) = get_article(request, article_path, course) if err: return err @@ -221,22 +229,22 @@ def history(request, article_path, page=1, course_id=None): return perm_err page_size = 10 - + if page == None: page = 1 try: p = int(page) except ValueError: p = 1 - - history = Revision.objects.filter(article__exact = article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article') - + + history = Revision.objects.filter(article__exact=article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article') + if request.method == 'POST': - if request.POST.__contains__('revision'): #They selected a version, but they can be either deleting or changing the version + if request.POST.__contains__('revision'): # They selected a version, but they can be either deleting or changing the version perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) if perm_err: return perm_err - + redirectURL = wiki_reverse('wiki_view', article, course) try: r = int(request.POST['revision']) @@ -246,7 +254,7 @@ def history(request, article_path, page=1, course_id=None): article.save() elif request.POST.__contains__('view'): redirectURL = wiki_reverse('wiki_view_revision', course=course, - kwargs={'revision_number' : revision.counter, 'article_path' : article.get_path()}) + kwargs={'revision_number': revision.counter, 'article_path': article.get_path()}) #The rese of these are admin functions elif request.POST.__contains__('delete') and request.user.is_superuser: if (revision.deleted == 0): @@ -255,7 +263,7 @@ def history(request, article_path, page=1, course_id=None): if (revision.deleted == 2): revision.adminSetDeleted(0) elif request.POST.__contains__('delete_all') and request.user.is_superuser: - Revision.objects.filter(article__exact = article, deleted = 0).update(deleted = 2) + Revision.objects.filter(article__exact=article, deleted=0).update(deleted=2) elif request.POST.__contains__('lock_article'): article.locked = not article.locked article.save() @@ -264,88 +272,88 @@ def history(request, article_path, page=1, course_id=None): pass finally: return HttpResponseRedirect(redirectURL) - # - # + # + # # <input type="submit" name="delete" value="Delete revision"/> # <input type="submit" name="restore" value="Restore revision"/> # <input type="submit" name="delete_all" value="Delete all revisions"> # %else: # <input type="submit" name="delete_article" value="Delete all revisions"> - # - - page_count = (history.count()+(page_size-1)) / page_size + # + + page_count = (history.count() + (page_size - 1)) / page_size if p > page_count: p = 1 - beginItem = (p-1) * page_size - + beginItem = (p - 1) * page_size + next_page = p + 1 if page_count > p else None prev_page = p - 1 if p > 1 else None - - + d = {'wiki_page': p, - 'wiki_next_page': next_page, - 'wiki_prev_page': prev_page, - 'wiki_history': history[beginItem:beginItem+page_size], - 'show_delete_revision' : request.user.is_superuser} + 'wiki_next_page': next_page, + 'wiki_prev_page': prev_page, + 'wiki_history': history[beginItem:beginItem + page_size], + 'show_delete_revision': request.user.is_superuser} update_template_dictionary(d, request, course, article) - + return render_to_response('simplewiki/simplewiki_history.html', d) - + + def revision_feed(request, page=1, namespace=None, course_id=None): course = check_course(course_id, course_required=False) - + page_size = 10 - + if page == None: page = 1 try: p = int(page) except ValueError: p = 1 - + history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision') - - page_count = (history.count()+(page_size-1)) / page_size + + page_count = (history.count() + (page_size - 1)) / page_size if p > page_count: p = 1 - beginItem = (p-1) * page_size - + beginItem = (p - 1) * page_size + next_page = p + 1 if page_count > p else None prev_page = p - 1 if p > 1 else None - + d = {'wiki_page': p, - 'wiki_next_page': next_page, - 'wiki_prev_page': prev_page, - 'wiki_history': history[beginItem:beginItem+page_size], - 'show_delete_revision' : request.user.is_superuser, - 'namespace' : namespace} + 'wiki_next_page': next_page, + 'wiki_prev_page': prev_page, + 'wiki_history': history[beginItem:beginItem + page_size], + 'show_delete_revision': request.user.is_superuser, + 'namespace': namespace} update_template_dictionary(d, request, course) - + return render_to_response('simplewiki/simplewiki_revision_feed.html', d) -def search_articles(request, namespace=None, course_id = None): + +def search_articles(request, namespace=None, course_id=None): course = check_course(course_id, course_required=False) - + # blampe: We should check for the presence of other popular django search # apps and use those if possible. Only fall back on this as a last resort. # Adding some context to results (eg where matches were) would also be nice. - + # todo: maybe do some perm checking here - + if request.method == 'GET': querystring = request.GET.get('value', '').strip() else: querystring = "" - + results = Article.objects.all() if namespace: - results = results.filter(namespace__name__exact = namespace) - + results = results.filter(namespace__name__exact=namespace) + if request.user.is_superuser: results = results.order_by('current_revision__deleted') else: - results = results.filter(current_revision__deleted = 0) - + results = results.filter(current_revision__deleted=0) if querystring: for queryword in querystring.split(): @@ -355,27 +363,28 @@ def search_articles(request, namespace=None, course_id = None): queryword = queryword[1:] else: results._search = lambda x: results.filter(x) - - results = results._search(Q(current_revision__contents__icontains = queryword) | \ - Q(title__icontains = queryword)) - - results = results.select_related('current_revision__deleted','namespace') - - results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower()) ) + + results = results._search(Q(current_revision__contents__icontains=queryword) | \ + Q(title__icontains=queryword)) + + results = results.select_related('current_revision__deleted', 'namespace') + + results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_path().lower())) if len(results) == 1 and querystring: - return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course )) + return HttpResponseRedirect(wiki_reverse('wiki_view', article=results[0], course=course)) else: d = {'wiki_search_results': results, - 'wiki_search_query': querystring, - 'namespace' : namespace} + 'wiki_search_query': querystring, + 'namespace': namespace} update_template_dictionary(d, request, course) return render_to_response('simplewiki/simplewiki_searchresults.html', d) - + + def search_add_related(request, course_id, slug, namespace): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, slug, namespace if namespace else course_id ) + + (article, err) = get_article(request, slug, namespace if namespace else course_id) if err: return err @@ -387,12 +396,12 @@ def search_add_related(request, course_id, slug, namespace): self_pk = request.GET.get('self', None) if search_string: results = [] - related = Article.objects.filter(title__istartswith = search_string) + related = Article.objects.filter(title__istartswith=search_string) others = article.related.all() if self_pk: related = related.exclude(pk=self_pk) if others: - related = related.exclude(related__in = others) + related = related.exclude(related__in=others) related = related.order_by('title')[:10] for item in related: results.append({'id': str(item.id), @@ -400,21 +409,22 @@ def search_add_related(request, course_id, slug, namespace): 'info': item.get_url()}) else: results = [] - + json = simplejson.dumps({'results': results}) return HttpResponse(json, mimetype='application/json') + def add_related(request, course_id, slug, namespace): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, slug, namespace if namespace else course_id ) + + (article, err) = get_article(request, slug, namespace if namespace else course_id) if err: return err - + perm_err = check_permissions(request, article, course, check_write=True, check_locked=True) if perm_err: return perm_err - + try: related_id = request.POST['id'] rel = Article.objects.get(id=related_id) @@ -427,11 +437,12 @@ def add_related(request, course_id, slug, namespace): finally: return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + def remove_related(request, course_id, namespace, slug, related_id): course = check_course(course_id, course_required=False) - - (article, err) = get_article(request, slug, namespace if namespace else course_id ) - + + (article, err) = get_article(request, slug, namespace if namespace else course_id) + if err: return err @@ -449,53 +460,57 @@ def remove_related(request, course_id, namespace, slug, related_id): finally: return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),))) + def random_article(request, course_id=None): course = check_course(course_id, course_required=False) - + from random import randint num_arts = Article.objects.count() - article = Article.objects.all()[randint(0, num_arts-1)] - return HttpResponseRedirect( wiki_reverse('wiki_view', article, course)) - + article = Article.objects.all()[randint(0, num_arts - 1)] + return HttpResponseRedirect(wiki_reverse('wiki_view', article, course)) + + def not_found(request, article_path, course): """Generate a NOT FOUND message for some URL""" d = {'wiki_err_notfound': True, 'article_path': article_path, - 'namespace' : course.wiki_namespace} + 'namespace': course.wiki_namespace} update_template_dictionary(d, request, course) return render_to_response('simplewiki/simplewiki_error.html', d) - + + def get_article(request, article_path, course): err = None article = None - + try: article = Article.get_article(article_path) except Article.DoesNotExist, ValueError: err = not_found(request, article_path, course) - + return (article, err) -def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision = None): + +def check_permissions(request, article, course, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision=None): read_err = check_read and not article.can_read(request.user) - + write_err = check_write and not article.can_write(request.user) - + locked_err = check_locked and article.locked - + if revision is None: revision = article.current_revision deleted_err = check_deleted and not (revision.deleted == 0) if (request.user.is_superuser): deleted_err = False locked_err = False - + if read_err or write_err or locked_err or deleted_err: d = {'wiki_article': article, - 'wiki_err_noread': read_err, - 'wiki_err_nowrite': write_err, - 'wiki_err_locked': locked_err, - 'wiki_err_deleted': deleted_err,} + 'wiki_err_noread': read_err, + 'wiki_err_nowrite': write_err, + 'wiki_err_locked': locked_err, + 'wiki_err_deleted': deleted_err, } update_template_dictionary(d, request, course) # TODO: Make this a little less jarring by just displaying an error # on the current page? (no such redirect happens for an anon upload yet) @@ -511,21 +526,21 @@ def check_permissions(request, article, course, check_read=False, check_write=Fa if wiki_settings.WIKI_REQUIRE_LOGIN_VIEW: - view = login_required(view) - history = login_required(history) - search_articles = login_required(search_articles) - root_redirect = login_required(root_redirect) - revision_feed = login_required(revision_feed) - random_article = login_required(random_article) + view = login_required(view) + history = login_required(history) + search_articles = login_required(search_articles) + root_redirect = login_required(root_redirect) + revision_feed = login_required(revision_feed) + random_article = login_required(random_article) search_add_related = login_required(search_add_related) - not_found = login_required(not_found) - view_revision = login_required(view_revision) - + not_found = login_required(not_found) + view_revision = login_required(view_revision) + if wiki_settings.WIKI_REQUIRE_LOGIN_EDIT: - create = login_required(create) - edit = login_required(edit) - add_related = login_required(add_related) - remove_related = login_required(remove_related) + create = login_required(create) + edit = login_required(edit) + add_related = login_required(add_related) + remove_related = login_required(remove_related) if wiki_settings.WIKI_CONTEXT_PREPROCESSORS: settings.TEMPLATE_CONTEXT_PROCESSORS += wiki_settings.WIKI_CONTEXT_PREPROCESSORS diff --git a/lms/djangoapps/simplewiki/wiki_settings.py b/lms/djangoapps/simplewiki/wiki_settings.py index 88363901a8b4e8a8d9043620d93c7d65182ccca3..6054ab19092f4b8086a2e18a58ea750a19eeda8b 100644 --- a/lms/djangoapps/simplewiki/wiki_settings.py +++ b/lms/djangoapps/simplewiki/wiki_settings.py @@ -94,18 +94,18 @@ WIKI_MARKDOWN_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_MARKDOWN_EXTENSIONS', ]) -WIKI_IMAGE_EXTENSIONS = getattr(settings, +WIKI_IMAGE_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_IMAGE_EXTENSIONS', - ('jpg','jpeg','gif','png','tiff','bmp')) + ('jpg', 'jpeg', 'gif', 'png', 'tiff', 'bmp')) # Planned features -WIKI_PAGE_WIDTH = getattr(settings, +WIKI_PAGE_WIDTH = getattr(settings, 'SIMPLE_WIKI_PAGE_WIDTH', "100%") - -WIKI_PAGE_ALIGN = getattr(settings, + +WIKI_PAGE_ALIGN = getattr(settings, 'SIMPLE_WIKI_PAGE_ALIGN', "center") - -WIKI_IMAGE_THUMB_SIZE = getattr(settings, - 'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200,150)) - -WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings, - 'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100,100)) + +WIKI_IMAGE_THUMB_SIZE = getattr(settings, + 'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200, 150)) + +WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings, + 'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100, 100)) diff --git a/lms/djangoapps/ssl_auth/ssl_auth.py b/lms/djangoapps/ssl_auth/ssl_auth.py index df3029da93d18d64ac5c3c9cede538ab9b834de5..adbb2bf94d7500806aef2f3714711f916a57b477 100755 --- a/lms/djangoapps/ssl_auth/ssl_auth.py +++ b/lms/djangoapps/ssl_auth/ssl_auth.py @@ -8,31 +8,35 @@ from django.contrib.auth.models import User, check_password from django.contrib.auth.backends import ModelBackend from django.contrib.auth.middleware import RemoteUserMiddleware from django.core.exceptions import ImproperlyConfigured -import os, string, re +import os +import string +import re from random import choice from student.models import UserProfile #----------------------------------------------------------------------------- + def ssl_dn_extract_info(dn): ''' Extract username, email address (may be anyuser@anydomain.com) and full name from the SSL DN string. Return (user,email,fullname) if successful, and None otherwise. ''' - ss = re.search('/emailAddress=(.*)@([^/]+)',dn) + ss = re.search('/emailAddress=(.*)@([^/]+)', dn) if ss: user = ss.group(1) - email = "%s@%s" % (user,ss.group(2)) + email = "%s@%s" % (user, ss.group(2)) else: return None - ss = re.search('/CN=([^/]+)/',dn) + ss = re.search('/CN=([^/]+)/', dn) if ss: fullname = ss.group(1) else: return None - return (user,email,fullname) + return (user, email, fullname) + def check_nginx_proxy(request): ''' @@ -40,7 +44,7 @@ def check_nginx_proxy(request): If so, get user info from the SSL DN string and return that, as (user,email,fullname) ''' m = request.META - if m.has_key('HTTP_X_REAL_IP'): # we're behind a nginx reverse proxy, which has already done ssl auth + if m.has_key('HTTP_X_REAL_IP'): # we're behind a nginx reverse proxy, which has already done ssl auth if not m.has_key('HTTP_SSL_CLIENT_S_DN'): return None dn = m['HTTP_SSL_CLIENT_S_DN'] @@ -49,6 +53,7 @@ def check_nginx_proxy(request): #----------------------------------------------------------------------------- + def get_ssl_username(request): x = check_nginx_proxy(request) if x: @@ -62,14 +67,15 @@ def get_ssl_username(request): #----------------------------------------------------------------------------- + class NginxProxyHeaderMiddleware(RemoteUserMiddleware): ''' Django "middleware" function for extracting user information from HTTP request. - + ''' # this field is generated by nginx's reverse proxy - header = 'HTTP_SSL_CLIENT_S_DN' # specify the request.META field to use - + header = 'HTTP_SSL_CLIENT_S_DN' # specify the request.META field to use + def process_request(self, request): # AuthenticationMiddleware is required so that request.user exists. if not hasattr(request, 'user'): @@ -83,10 +89,10 @@ class NginxProxyHeaderMiddleware(RemoteUserMiddleware): #raise ImproperlyConfigured('[ProxyHeaderMiddleware] request.META=%s' % repr(request.META)) try: - username = request.META[self.header] # try the nginx META key first + username = request.META[self.header] # try the nginx META key first except KeyError: try: - env = request._req.subprocess_env # else try the direct apache2 SSL key + env = request._req.subprocess_env # else try the direct apache2 SSL key if env.has_key('SSL_CLIENT_S_DN'): username = env['SSL_CLIENT_S_DN'] else: @@ -117,19 +123,20 @@ class NginxProxyHeaderMiddleware(RemoteUserMiddleware): request.user = user if settings.DEBUG: print "[ssl_auth.ssl_auth.NginxProxyHeaderMiddleware] logging in user=%s" % user auth.login(request, user) - - def clean_username(self,username,request): + + def clean_username(self, username, request): ''' username is the SSL DN string - extract the actual username from it and return ''' info = ssl_dn_extract_info(username) if not info: return None - (username,email,fullname) = info + (username, email, fullname) = info return username #----------------------------------------------------------------------------- + class SSLLoginBackend(ModelBackend): ''' Django authentication back-end which auto-logs-in a user based on having @@ -140,14 +147,14 @@ class SSLLoginBackend(ModelBackend): # remote_user is from the SSL_DN string. It will be non-empty only when # the user has already passed the server authentication, which means # matching with the certificate authority. - if not remote_user: + if not remote_user: # no remote_user, so check username (but don't auto-create user) if not username: return None - return None # pass on to another authenticator backend + return None # pass on to another authenticator backend #raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user)) try: - user = User.objects.get(username=username) # if user already exists don't create it + user = User.objects.get(username=username) # if user already exists don't create it return user except User.DoesNotExist: return None @@ -168,15 +175,15 @@ class SSLLoginBackend(ModelBackend): if not info: #raise ImproperlyConfigured("[SSLLoginBackend] remote_user=%s, info=%s" % (remote_user,info)) return None - (username,email,fullname) = info + (username, email, fullname) = info try: - user = User.objects.get(username=username) # if user already exists don't create it + user = User.objects.get(username=username) # if user already exists don't create it except User.DoesNotExist: if not settings.DEBUG: raise "User does not exist. Not creating user; potential schema consistency issues" #raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info)) - user = User(username=username, password=GenPasswd()) # create new User + user = User(username=username, password=GenPasswd()) # create new User user.is_staff = False user.is_superuser = False # get first, last name from fullname @@ -200,7 +207,7 @@ class SSLLoginBackend(ModelBackend): user.last_name = user.last_name.strip() # save user.save() - + # auto-create user profile up = UserProfile(user=user) up.name = fullname @@ -223,16 +230,17 @@ class SSLLoginBackend(ModelBackend): return User.objects.get(pk=user_id) except User.DoesNotExist: return None - + #----------------------------------------------------------------------------- # OLD! + class AutoLoginBackend: def authenticate(self, username=None, password=None): raise ImproperlyConfigured("in AutoLoginBackend, username=%s" % username) if not os.environ.has_key('HTTPS'): return None - if not os.environ.get('HTTPS')=='on':# only use this back-end if HTTPS on + if not os.environ.get('HTTPS') == 'on':# only use this back-end if HTTPS on return None def GenPasswd(length=8, chars=string.letters + string.digits): @@ -244,7 +252,7 @@ class AutoLoginBackend: user = User(username=username, password=GenPasswd()) user.is_staff = False user.is_superuser = False - # get first, last name + # get first, last name name = os.environ.get('SSL_CLIENT_S_DN_CN').strip() if not name.count(' '): user.first_name = " " @@ -266,7 +274,7 @@ class AutoLoginBackend: tui = user.get_profile() tui.middle_name = mn tui.role = 'Misc' - tui.section = None# no section assigned at first + tui.section = None# no section assigned at first tui.save() # return None return user @@ -274,7 +282,7 @@ class AutoLoginBackend: def get_user(self, user_id): if not os.environ.has_key('HTTPS'): return None - if not os.environ.get('HTTPS')=='on':# only use this back-end if HTTPS on + if not os.environ.get('HTTPS') == 'on':# only use this back-end if HTTPS on return None try: return User.objects.get(pk=user_id) diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index d2dd0dbdb702ecf51246bb0fa32697b98bab525a..90087e06d60616c7b09f5f641efc42e6d3a17e62 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -1,4 +1,4 @@ -# View for semi-static templatized content. +# View for semi-static templatized content. # # List of valid templates is explicitly managed for (short-term) # security reasons. @@ -8,19 +8,20 @@ from django.shortcuts import redirect from django.conf import settings from django_future.csrf import ensure_csrf_cookie -from util.cache import cache_if_anonymous +from util.cache import cache_if_anonymous valid_templates = [] if settings.STATIC_GRAB: - valid_templates = valid_templates+['server-down.html', + valid_templates = valid_templates + ['server-down.html', 'server-error.html' - 'server-overloaded.html', + 'server-overloaded.html', ] -def index(request, template): + +def index(request, template): if template in valid_templates: - return render_to_response('static_templates/' + template, {}) + return render_to_response('static_templates/' + template, {}) else: return redirect('/') @@ -32,15 +33,16 @@ def render(request, template): This view function renders the template sent without checking that it exists. Do not expose template as a regex part of the url. The user should not be able to ender any arbitray template name. The correct usage would be: - + url(r'^jobs$', 'static_template_view.views.render', {'template': 'jobs.html'}, name="jobs") - """ + """ return render_to_response('static_templates/' + template, {}) + def render_404(request): return render_to_response('static_templates/404.html', {}) - + + def render_500(request): return render_to_response('static_templates/server-error.html', {}) - diff --git a/lms/djangoapps/staticbook/views.py b/lms/djangoapps/staticbook/views.py index b7633dd1d30e37fbd1e4730c12be9b1cf7c9b055..948e66b50cacdfdeda78e01716798cd9b4037f29 100644 --- a/lms/djangoapps/staticbook/views.py +++ b/lms/djangoapps/staticbook/views.py @@ -3,10 +3,12 @@ from mitxmako.shortcuts import render_to_response from courseware.courses import check_course + @login_required -def index(request, course_id, page=0): +def index(request, course_id, page=0): course = check_course(course_id) - return render_to_response('staticbook.html',{'page':int(page), 'course': course}) + return render_to_response('staticbook.html', {'page': int(page), 'course': course}) + def index_shifted(request, course_id, page): - return index(request, course_id=course_id, page=int(page)+24) + return index(request, course_id=course_id, page=int(page) + 24) diff --git a/lms/lib/dogfood/check.py b/lms/lib/dogfood/check.py index 6eb34d997607459c8c97ded1dcb4b5f769c7e3da..0a1da38529ad3bb97c9602e82776d81d60c9371c 100644 --- a/lms/lib/dogfood/check.py +++ b/lms/lib/dogfood/check.py @@ -8,34 +8,36 @@ from django.conf import settings import capa.capa_problem as lcp from dogfood.views import update_problem + def GenID(length=8, chars=string.letters + string.digits): return ''.join([choice(chars) for i in range(length)]) randomid = GenID() -def check_problem_code(ans,the_lcp,correct_answers,false_answers): + +def check_problem_code(ans, the_lcp, correct_answers, false_answers): """ ans = student's answer the_lcp = LoncapaProblem instance - + returns dict {'ok':is_ok,'msg': message with iframe} """ pfn = "dog%s" % randomid - pfn += the_lcp.problem_id.replace('filename','') # add problem ID to dogfood problem name - update_problem(pfn,ans,filestore=the_lcp.system.filestore) + pfn += the_lcp.problem_id.replace('filename', '') # add problem ID to dogfood problem name + update_problem(pfn, ans, filestore=the_lcp.system.filestore) msg = '<hr width="100%"/>' - msg += '<iframe src="%s/dogfood/filename%s" width="95%%" height="400" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL,pfn) + msg += '<iframe src="%s/dogfood/filename%s" width="95%%" height="400" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL, pfn) msg += '<hr width="100%"/>' endmsg = """<p><font size="-1" color="purple">Note: if the code text box disappears after clicking on "Check", - please type something in the box to make it refresh properly. This is a + please type something in the box to make it refresh properly. This is a bug with Chrome; it does not happen with Firefox. It is being fixed. </font></p>""" is_ok = True if (not correct_answers) or (not false_answers): - ret = {'ok':is_ok, - 'msg': msg+endmsg, + ret = {'ok': is_ok, + 'msg': msg + endmsg, } return ret @@ -43,19 +45,19 @@ def check_problem_code(ans,the_lcp,correct_answers,false_answers): # check correctness fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn) test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system) - - if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1')=='correct'): + + if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1') == 'correct'): is_ok = False - if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1')=='correct'): + if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1') == 'correct'): is_ok = False - except Exception,err: + except Exception, err: is_ok = False - msg += "<p>Error: %s</p>" % str(err).replace('<','<') - msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<','<') - - ret = {'ok':is_ok, - 'msg': msg+endmsg, + msg += "<p>Error: %s</p>" % str(err).replace('<', '<') + msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<', '<') + + ret = {'ok': is_ok, + 'msg': msg + endmsg, } return ret - - + + diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py index d621603f31facf65f0ef51d05a457916d0d51564..7e4d0a08c5904e19a04d4203b30ac2c98f2f3ef8 100644 --- a/lms/lib/dogfood/views.py +++ b/lms/lib/dogfood/views.py @@ -3,12 +3,12 @@ dogfood.py For using mitx / edX / i4x in checking itself. -df_capa_problem: accepts an XML file for a problem, and renders it. +df_capa_problem: accepts an XML file for a problem, and renders it. ''' import logging import datetime import re -import os # FIXME - use OSFS instead +import os # FIXME - use OSFS instead from fs.osfs import OSFS @@ -39,29 +39,31 @@ import xmodule log = logging.getLogger("mitx.courseware") etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments = True)) + remove_comments=True)) -DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings +DOGFOOD_COURSENAME = 'edx_dogfood' # FIXME - should not be here; maybe in settings -def update_problem(pfn,pxml,coursename=None,overwrite=True,filestore=None): + +def update_problem(pfn, pxml, coursename=None, overwrite=True, filestore=None): ''' update problem with filename pfn, and content (xml) pxml. ''' if not filestore: if not coursename: coursename = DOGFOOD_COURSENAME - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course + xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course pfn2 = settings.DATA_DIR + xp + 'problems/%s.xml' % pfn - fp = open(pfn2,'w') + fp = open(pfn2, 'w') else: pfn2 = 'problems/%s.xml' % pfn - fp = filestore.open(pfn2,'w') + fp = filestore.open(pfn2, 'w') log.debug('[dogfood.update_problem] pfn2=%s' % pfn2) if os.path.exists(pfn2) and not overwrite: return # don't overwrite if already exists and overwrite=False - pxmls = pxml if type(pxml) in [str,unicode] else etree.tostring(pxml,pretty_print=True) + pxmls = pxml if type(pxml) in [str, unicode] else etree.tostring(pxml, pretty_print=True) fp.write(pxmls) fp.close() + def df_capa_problem(request, id=None): ''' dogfood capa problem. @@ -70,7 +72,7 @@ def df_capa_problem(request, id=None): Returns rendered problem. ''' # "WARNING: UNDEPLOYABLE CODE. FOR DEV USE ONLY." - + if settings.DEBUG: log.debug('[lib.dogfood.df_capa_problem] id=%s' % id) @@ -79,58 +81,58 @@ def df_capa_problem(request, id=None): else: coursename = request.session['coursename'] - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course + xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course # Grab the XML corresponding to the request from course.xml module = 'problem' try: xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - except Exception,err: + except Exception, err: log.error("[lib.dogfood.df_capa_problem] error in calling content_parser: %s" % err) xml = None # if problem of given ID does not exist, then create it # do this only if course.xml has a section named "DogfoodProblems" if not xml: - m = re.match('filename([A-Za-z0-9_]+)$',id) # extract problem filename from ID given + m = re.match('filename([A-Za-z0-9_]+)$', id) # extract problem filename from ID given if not m: - raise Exception,'[lib.dogfood.df_capa_problem] Illegal problem id %s' % id + raise Exception, '[lib.dogfood.df_capa_problem] Illegal problem id %s' % id pfn = m.group(1) log.debug('[lib.dogfood.df_capa_problem] creating new problem pfn=%s' % pfn) # add problem to course.xml fn = settings.DATA_DIR + xp + 'course.xml' xml = etree.parse(fn) - seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure! - if seq==None: - raise Exception,"[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!" + seq = xml.find('chapter/section[@name="DogfoodProblems"]/sequential') # assumes simplistic course.xml structure! + if seq == None: + raise Exception, "[lib.dogfood.views.df_capa_problem] missing DogfoodProblems section in course.xml!" newprob = etree.Element('problem') - newprob.set('type','lecture') - newprob.set('showanswer','attempted') - newprob.set('rerandomize','never') - newprob.set('title',pfn) - newprob.set('filename',pfn) - newprob.set('name',pfn) + newprob.set('type', 'lecture') + newprob.set('showanswer', 'attempted') + newprob.set('rerandomize', 'never') + newprob.set('title', pfn) + newprob.set('filename', pfn) + newprob.set('name', pfn) seq.append(newprob) - fp = open(fn,'w') - fp.write(etree.tostring(xml,pretty_print=True)) # write new XML + fp = open(fn, 'w') + fp.write(etree.tostring(xml, pretty_print=True)) # write new XML fp.close() # now create new problem file # update_problem(pfn,'<problem>\n<text>\nThis is a new problem\n</text>\n</problem>\n',coursename,overwrite=False) - + # reset cache entry user = request.user groups = content_parser.user_groups(user) - options = {'dev_content':settings.DEV_CONTENT, - 'groups' : groups} + options = {'dev_content': settings.DEV_CONTENT, + 'groups': groups} filename = xp + 'course.xml' cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups)) log.debug('[lib.dogfood.df_capa_problem] cache_key = %s' % cache_key) #cache.delete(cache_key) - tree = content_parser.course_xml_process(xml) # add ID tags - cache.set(cache_key,etree.tostring(tree),60) + tree = content_parser.course_xml_process(xml) # add ID tags + cache.set(cache_key, etree.tostring(tree), 60) # settings.DEFAULT_GROUPS.append('dev') # force content_parser.course_file to not use cache xml = content_parser.module_xml(request.user, module, 'id', id, coursename) @@ -141,9 +143,10 @@ def df_capa_problem(request, id=None): request.session['dogfood_id'] = id # hand over to quickedit to do the rest - return quickedit(request,id=id,qetemplate='dogfood.html',coursename=coursename) + return quickedit(request, id=id, qetemplate='dogfood.html', coursename=coursename) + -def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): +def quickedit(request, id=None, qetemplate='quickedit.html', coursename=None): ''' quick-edit capa problem. @@ -159,27 +162,27 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): print "a load balanacer" if not request.user.is_staff: - if not ('dogfood_id' in request.session and request.session['dogfood_id']==id): + if not ('dogfood_id' in request.session and request.session['dogfood_id'] == id): return redirect('/') - if id=='course.xml': + if id == 'course.xml': return quickedit_git_reload(request) # get coursename if stored if not coursename: coursename = multicourse_settings.get_coursename_from_request(request) - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course + xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course - def get_lcp(coursename,id): + def get_lcp(coursename, id): # Grab the XML corresponding to the request from course.xml # create empty student state for this problem, if not previously existing - s = StudentModule.objects.filter(student=request.user, + s = StudentModule.objects.filter(student=request.user, module_id=id) student_module_cache = list(s) if s is not None else [] #if len(s) == 0 or s is None: - # smod=StudentModule(student=request.user, + # smod=StudentModule(student=request.user, # module_type = 'problem', - # module_id=id, + # module_id=id, # state=instance.get_state()) # smod.save() # student_module_cache = [smod] @@ -187,27 +190,27 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): module_xml = etree.XML(content_parser.module_xml(request.user, module, 'id', id, coursename)) module_id = module_xml.get('id') log.debug("module_id = %s" % module_id) - (instance,smod,module_type) = get_module(request.user, request, module_xml, student_module_cache, position=None) + (instance, smod, module_type) = get_module(request.user, request, module_xml, student_module_cache, position=None) log.debug('[dogfood.views] instance=%s' % instance) lcp = instance.lcp log.debug('[dogfood.views] lcp=%s' % lcp) pxml = lcp.tree - pxmls = etree.tostring(pxml,pretty_print=True) + pxmls = etree.tostring(pxml, pretty_print=True) return instance, pxmls - def old_get_lcp(coursename,id): + def old_get_lcp(coursename, id): # Grab the XML corresponding to the request from course.xml module = 'problem' xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - - ajax_url = settings.MITX_ROOT_URL + '/modx/'+id+'/' - + + ajax_url = settings.MITX_ROOT_URL + '/modx/' + id + '/' + # Create the module (instance of capa_module.Module) - system = I4xSystem(track_function = make_track_function(request), - render_function = None, - render_template = render_to_string, - ajax_url = ajax_url, - filestore = OSFS(settings.DATA_DIR + xp), + system = I4xSystem(track_function=make_track_function(request), + render_function=None, + render_template=render_to_string, + ajax_url=ajax_url, + filestore=OSFS(settings.DATA_DIR + xp), #role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this ) instance = xmodule.get_module_class(module)(system, @@ -240,7 +243,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): action = request.POST['qesubmit'] if "Revert" in action: msg = "Reverted to original" - elif action=='Change Problem': + elif action == 'Change Problem': key = 'quickedit_%s' % id if not key in request.POST: msg = "oops, missing code key=%s" % key @@ -248,7 +251,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): newcode = request.POST[key] # see if code changed - if str(newcode)==str(pxmls) or '<?xml version="1.0"?>\n'+str(newcode)==str(pxmls): + if str(newcode) == str(pxmls) or '<?xml version="1.0"?>\n' + str(newcode) == str(pxmls): msg = "No changes" else: # check new code @@ -256,16 +259,16 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): try: newxml = etree.fromstring(newcode) isok = True - except Exception,err: + except Exception, err: msg = "Failed to change problem: XML error \"<font color=red>%s</font>\"" % err - + if isok: filename = instance.lcp.fileobject.name - fp = open(filename,'w') # TODO - replace with filestore call? + fp = open(filename, 'w') # TODO - replace with filestore call? fp.write(newcode) fp.close() - msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename - instance, pxmls = get_lcp(coursename,id) + msg = "<font color=green>Problem changed!</font> (<tt>%s</tt>)" % filename + instance, pxmls = get_lcp(coursename, id) lcp = instance.lcp @@ -273,20 +276,21 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): phtml = instance.get_html() # phtml = instance.get_problem_html() - context = {'id':id, - 'msg' : msg, - 'lcp' : lcp, - 'filename' : lcp.fileobject.name, - 'pxmls' : pxmls, - 'phtml' : phtml, - "destroy_js":'', - 'init_js':'', - 'csrf':csrf(request)['csrf_token'], + context = {'id': id, + 'msg': msg, + 'lcp': lcp, + 'filename': lcp.fileobject.name, + 'pxmls': pxmls, + 'phtml': phtml, + "destroy_js": '', + 'init_js': '', + 'csrf': csrf(request)['csrf_token'], } result = render_to_response(qetemplate, context) return result + def quickedit_git_reload(request): ''' reload course.xml and all courseware files for this course, from the git repo. @@ -298,25 +302,25 @@ def quickedit_git_reload(request): # get coursename if stored coursename = multicourse_settings.get_coursename_from_request(request) - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course + xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course msg = "" if 'cancel' in request.POST: return redirect("/courseware") - + if 'gitupdate' in request.POST: import os # FIXME - put at top? #cmd = "cd ../data%s; git reset --hard HEAD; git pull origin %s" % (xp,xp.replace('/','')) - cmd = "cd ../data%s; ./GITRELOAD '%s'" % (xp,xp.replace('/','')) + cmd = "cd ../data%s; ./GITRELOAD '%s'" % (xp, xp.replace('/', '')) msg += '<p>cmd: %s</p>' % cmd ret = os.popen(cmd).read() - msg += '<p><pre>%s</pre></p>' % ret.replace('<','<') + msg += '<p><pre>%s</pre></p>' % ret.replace('<', '<') msg += "<p>git update done!</p>" - context = {'id':id, - 'msg' : msg, - 'coursename' : coursename, - 'csrf':csrf(request)['csrf_token'], + context = {'id': id, + 'msg': msg, + 'coursename': coursename, + 'csrf': csrf(request)['csrf_token'], } result = render_to_response("gitupdate.html", context) diff --git a/lms/lib/loncapa/loncapa_check.py b/lms/lib/loncapa/loncapa_check.py index 961e24ea706aad43b8fb30489dba202a0c008b29..0fd998e00e848273f353c5710b1d4632f20517dc 100644 --- a/lms/lib/loncapa/loncapa_check.py +++ b/lms/lib/loncapa/loncapa_check.py @@ -9,28 +9,29 @@ from __future__ import division import random import math -def lc_random(lower,upper,stepsize): + +def lc_random(lower, upper, stepsize): ''' like random.randrange but lower and upper can be non-integer ''' - nstep = int((upper-lower)/(1.0*stepsize)) - choices = [lower+x*stepsize for x in range(nstep)] + nstep = int((upper - lower) / (1.0 * stepsize)) + choices = [lower + x * stepsize for x in range(nstep)] return random.choice(choices) -def lc_choose(index,*args): + +def lc_choose(index, *args): ''' return args[index] ''' try: - return args[int(index)-1] - except Exception,err: + return args[int(index) - 1] + except Exception, err: pass if len(args): return args[0] - raise Exception,"loncapa_check.lc_choose error, index=%s, args=%s" % (index,args) - -deg2rad = math.pi/180.0 -rad2deg = 180.0/math.pi + raise Exception, "loncapa_check.lc_choose error, index=%s, args=%s" % (index, args) +deg2rad = math.pi / 180.0 +rad2deg = 180.0 / math.pi diff --git a/lms/lib/newrelic_logging/__init__.py b/lms/lib/newrelic_logging/__init__.py index 2c5749e3fab8284a1099ee12c764a6c7ada1bd5b..24101a509191bdecfe8c0f11091fb48112a5ca32 100644 --- a/lms/lib/newrelic_logging/__init__.py +++ b/lms/lib/newrelic_logging/__init__.py @@ -2,6 +2,7 @@ import newrelic.agent import logging + class NewRelicHandler(logging.Handler): def emit(self, record): if record.exc_info is not None: diff --git a/lms/lib/perfstats/middleware.py b/lms/lib/perfstats/middleware.py index 1308dd650afe5b28a6e7f6cad61afefe722d2053..7d3880976ca0086551e62c53e88233f810c5ead3 100644 --- a/lms/lib/perfstats/middleware.py +++ b/lms/lib/perfstats/middleware.py @@ -7,23 +7,24 @@ from django.db import connection import views -class ProfileMiddleware: - def process_request (self, request): + +class ProfileMiddleware: + def process_request(self, request): self.t = time.time() print "Process request" - def process_response (self, request, response): + def process_response(self, request, response): # totalTime = time.time() - self.t # tmpfile = tempfile.NamedTemporaryFile(prefix='sqlprof-t=' + str(totalTime) + "-", delete=False) - + # output = "" # for query in connection.queries: # output += "Time: " + str(query['time']) + "\nQuery: " + query['sql'] + "\n\n" - + # tmpfile.write(output) - + # print "SQL Log file: " , tmpfile.name # tmpfile.close() - + # print "Process response" return response diff --git a/lms/lib/perfstats/views.py b/lms/lib/perfstats/views.py index 7d0695b9fe4e4d47f05d8faba77828e2ef119c49..e4cf3dd5b9e8da3c9104ef752dfe477824c2e1b3 100644 --- a/lms/lib/perfstats/views.py +++ b/lms/lib/perfstats/views.py @@ -3,6 +3,7 @@ import middleware from django.http import HttpResponse + def end_profile(request): names = middleware.restart_profile() return HttpResponse(str(names)) diff --git a/lms/lib/symmath/formula.py b/lms/lib/symmath/formula.py index b7cf2cdb0568c1beea37d8fda73a61d0b729fd25..4aa9f60d304aac30104da4f9754ffeb07b6747af 100644 --- a/lms/lib/symmath/formula.py +++ b/lms/lib/symmath/formula.py @@ -9,7 +9,10 @@ # Acceptes Presentation MathML, Content MathML (and could also do OpenMath) # Provides sympy representation. -import os, sys, string, re +import os +import sys +import string +import re import logging import operator import sympy @@ -39,22 +42,25 @@ os.environ['PYTHONIOENCODING'] = 'utf-8' #----------------------------------------------------------------------------- -class dot(sympy.operations.LatticeOp): # my dot product + +class dot(sympy.operations.LatticeOp): # my dot product zero = sympy.Symbol('dotzero') identity = sympy.Symbol('dotidentity') #class dot(sympy.Mul): # my dot product # is_Mul = False -def _print_dot(self,expr): - return '{((%s) \cdot (%s))}' % (expr.args[0],expr.args[1]) + +def _print_dot(self, expr): + return '{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1]) LatexPrinter._print_dot = _print_dot #----------------------------------------------------------------------------- # unit vectors (for 8.02) -def _print_hat(self,expr): return '\\hat{%s}' % str(expr.args[0]).lower() + +def _print_hat(self, expr): return '\\hat{%s}' % str(expr.args[0]).lower() LatexPrinter._print_hat = _print_hat StrPrinter._print_hat = _print_hat @@ -62,18 +68,20 @@ StrPrinter._print_hat = _print_hat #----------------------------------------------------------------------------- # helper routines + def to_latex(x): - if x==None: return '' + if x == None: return '' # LatexPrinter._print_dot = _print_dot xs = latex(x) - xs = xs.replace(r'\XI','XI') # workaround for strange greek + xs = xs.replace(r'\XI', 'XI') # workaround for strange greek #return '<math>%s{}{}</math>' % (xs[1:-1]) - if xs[0]=='$': - return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6 + if xs[0] == '$': + return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6 return '[mathjax]%s[/mathjax]<br>' % (xs) # for sympy v7 -def my_evalf(expr,chop=False): - if type(expr)==list: + +def my_evalf(expr, chop=False): + if type(expr) == list: try: return [x.evalf(chop=chop) for x in expr] except: @@ -86,51 +94,52 @@ def my_evalf(expr,chop=False): #----------------------------------------------------------------------------- # my version of sympify to import expression into sympy -def my_sympify(expr,normphase=False,matrix=False,abcsym=False,do_qubit=False,symtab=None): + +def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False, symtab=None): # make all lowercase real? if symtab: varset = symtab else: - varset = {'p':sympy.Symbol('p'), - 'g':sympy.Symbol('g'), - 'e':sympy.E, # for exp - 'i':sympy.I, # lowercase i is also sqrt(-1) - 'Q':sympy.Symbol('Q'), # otherwise it is a sympy "ask key" + varset = {'p': sympy.Symbol('p'), + 'g': sympy.Symbol('g'), + 'e': sympy.E, # for exp + 'i': sympy.I, # lowercase i is also sqrt(-1) + 'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key" #'X':sympy.sympify('Matrix([[0,1],[1,0]])'), #'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'), #'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'), - 'ZZ':sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing - 'XI':sympy.Symbol('XI'), # otherwise it is the capital \XI - 'hat':sympy.Function('hat'), # for unit vectors (8.02) + 'ZZ': sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing + 'XI': sympy.Symbol('XI'), # otherwise it is the capital \XI + 'hat': sympy.Function('hat'), # for unit vectors (8.02) } if do_qubit: # turn qubit(...) into Qubit instance - varset.update({'qubit':sympy.physics.quantum.qubit.Qubit, - 'Ket':sympy.physics.quantum.state.Ket, - 'dot':dot, - 'bit':sympy.Function('bit'), + varset.update({'qubit': sympy.physics.quantum.qubit.Qubit, + 'Ket': sympy.physics.quantum.state.Ket, + 'dot': dot, + 'bit': sympy.Function('bit'), }) if abcsym: # consider all lowercase letters as real symbols, in the parsing for letter in string.lowercase: - if letter in varset: # exclude those already done + if letter in varset: # exclude those already done continue - varset.update({letter:sympy.Symbol(letter,real=True)}) + varset.update({letter: sympy.Symbol(letter, real=True)}) - sexpr = sympify(expr,locals=varset) - if normphase: # remove overall phase if sexpr is a list - if type(sexpr)==list: + sexpr = sympify(expr, locals=varset) + if normphase: # remove overall phase if sexpr is a list + if type(sexpr) == list: if sexpr[0].is_number: ophase = sympy.sympify('exp(-I*arg(%s))' % sexpr[0]) - sexpr = [ sympy.Mul(x,ophase) for x in sexpr ] + sexpr = [sympy.Mul(x, ophase) for x in sexpr] def to_matrix(x): # if x is a list of lists, and is rectangular, then return Matrix(x) - if not type(x)==list: + if not type(x) == list: return x for row in x: - if (not type(row)==list): + if (not type(row) == list): return x rdim = len(x[0]) for row in x: - if not len(row)==rdim: + if not len(row) == rdim: return x return sympy.Matrix(x) @@ -141,12 +150,13 @@ def my_sympify(expr,normphase=False,matrix=False,abcsym=False,do_qubit=False,sym #----------------------------------------------------------------------------- # class for symbolic mathematical formulas + class formula(object): ''' Representation of a mathematical formula object. Accepts mathml math expression for constructing, and can produce sympy translation. The formula may or may not include an assignment (=). ''' - def __init__(self,expr,asciimath='',options=None): + def __init__(self, expr, asciimath='', options=None): self.expr = expr.strip() self.asciimath = asciimath self.the_cmathml = None @@ -159,41 +169,41 @@ class formula(object): def is_mathml(self): return '<math ' in self.expr - def fix_greek_in_mathml(self,xml): + def fix_greek_in_mathml(self, xml): def gettag(x): - return re.sub('{http://[^}]+}','',x.tag) + return re.sub('{http://[^}]+}', '', x.tag) for k in xml: tag = gettag(k) - if tag=='mi' or tag=='ci': + if tag == 'mi' or tag == 'ci': usym = unicode(k.text) try: udata = unicodedata.name(usym) - except Exception,err: - udata = None + except Exception, err: + udata = None #print "usym = %s, udata=%s" % (usym,udata) if udata: # eg "GREEK SMALL LETTER BETA" if 'GREEK' in udata: - usym = udata.split(' ')[-1] + usym = udata.split(' ')[-1] if 'SMALL' in udata: usym = usym.lower() #print "greek: ",usym k.text = usym self.fix_greek_in_mathml(k) return xml - def preprocess_pmathml(self,xml): + def preprocess_pmathml(self, xml): ''' Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also to accomodate some sympy conventions (eg hat(i) for \hat{i}). ''' - if type(xml)==str or type(xml)==unicode: + if type(xml) == str or type(xml) == unicode: xml = etree.fromstring(xml) # TODO: wrap in try - xml = self.fix_greek_in_mathml(xml) # convert greek utf letters to greek spelled out in ascii + xml = self.fix_greek_in_mathml(xml) # convert greek utf letters to greek spelled out in ascii def gettag(x): - return re.sub('{http://[^}]+}','',x.tag) + return re.sub('{http://[^}]+}', '', x.tag) # f and g are processed as functions by asciimathml, eg "f-2" turns into "<mrow><mi>f</mi><mo>-</mo></mrow><mn>2</mn>" # this is really terrible for turning into cmathml. @@ -201,12 +211,12 @@ class formula(object): def fix_pmathml(xml): for k in xml: tag = gettag(k) - if tag=='mrow': - if len(k)==2: - if gettag(k[0])=='mi' and k[0].text in ['f','g'] and gettag(k[1])=='mo': + if tag == 'mrow': + if len(k) == 2: + if gettag(k[0]) == 'mi' and k[0].text in ['f', 'g'] and gettag(k[1]) == 'mo': idx = xml.index(k) - xml.insert(idx,deepcopy(k[0])) # drop the <mrow> container - xml.insert(idx+1,deepcopy(k[1])) + xml.insert(idx, deepcopy(k[0])) # drop the <mrow> container + xml.insert(idx + 1, deepcopy(k[1])) xml.remove(k) fix_pmathml(k) @@ -218,16 +228,16 @@ class formula(object): def fix_hat(xml): for k in xml: tag = gettag(k) - if tag=='mover': - if len(k)==2: - if gettag(k[0])=='mi' and gettag(k[1])=='mo' and str(k[1].text)=='^': + if tag == 'mover': + if len(k) == 2: + if gettag(k[0]) == 'mi' and gettag(k[1]) == 'mo' and str(k[1].text) == '^': newk = etree.Element('mi') newk.text = 'hat(%s)' % k[0].text - xml.replace(k,newk) - if gettag(k[0])=='mrow' and gettag(k[0][0])=='mi' and gettag(k[1])=='mo' and str(k[1].text)=='^': + xml.replace(k, newk) + if gettag(k[0]) == 'mrow' and gettag(k[0][0]) == 'mi' and gettag(k[1]) == 'mo' and str(k[1].text) == '^': newk = etree.Element('mi') newk.text = 'hat(%s)' % k[0][0].text - xml.replace(k,newk) + xml.replace(k, newk) fix_hat(k) fix_hat(xml) @@ -240,18 +250,18 @@ class formula(object): # pre-process the presentation mathml before sending it to snuggletex to convert to content mathml try: xml = self.preprocess_pmathml(self.expr) - except Exception,err: + except Exception, err: return "<html>Error! Cannot process pmathml</html>" - pmathml = etree.tostring(xml,pretty_print=True) + pmathml = etree.tostring(xml, pretty_print=True) self.the_pmathml = pmathml # convert to cmathml - self.the_cmathml = self.GetContentMathML(self.asciimath,pmathml) + self.the_cmathml = self.GetContentMathML(self.asciimath, pmathml) return self.the_cmathml - cmathml = property(get_content_mathml,None,None,'content MathML representation') + cmathml = property(get_content_mathml, None, None, 'content MathML representation') - def make_sympy(self,xml=None): + def make_sympy(self, xml=None): ''' Return sympy expression for the math formula. The math formula is converted to Content MathML then that is parsed. @@ -259,15 +269,15 @@ class formula(object): if self.the_sympy: return self.the_sympy - if xml==None: # root + if xml == None: # root if not self.is_mathml(): return my_sympify(self.expr) if self.is_presentation_mathml(): try: cmml = self.cmathml xml = etree.fromstring(str(cmml)) - except Exception,err: - raise Exception,'Err %s while converting cmathml to xml; cmml=%s' % (err,cmml) + except Exception, err: + raise Exception, 'Err %s while converting cmathml to xml; cmml=%s' % (err, cmml) xml = self.fix_greek_in_mathml(xml) self.the_sympy = self.make_sympy(xml[0]) else: @@ -277,36 +287,37 @@ class formula(object): return self.the_sympy def gettag(x): - return re.sub('{http://[^}]+}','',x.tag) + return re.sub('{http://[^}]+}', '', x.tag) # simple math def op_divide(*args): - if not len(args)==2: - raise Exception,'divide given wrong number of arguments!' + if not len(args) == 2: + raise Exception, 'divide given wrong number of arguments!' # print "divide: arg0=%s, arg1=%s" % (args[0],args[1]) - return sympy.Mul(args[0],sympy.Pow(args[1],-1)) + return sympy.Mul(args[0], sympy.Pow(args[1], -1)) + + def op_plus(*args): return args[0] if len(args) == 1 else op_plus(*args[:-1]) + args[-1] - def op_plus(*args): return args[0] if len(args)==1 else op_plus(*args[:-1])+args[-1] - def op_times(*args): return reduce(operator.mul,args) + def op_times(*args): return reduce(operator.mul, args) def op_minus(*args): - if len(args)==1: + if len(args) == 1: return -args[0] - if not len(args)==2: - raise Exception,'minus given wrong number of arguments!' + if not len(args) == 2: + raise Exception, 'minus given wrong number of arguments!' #return sympy.Add(args[0],-args[1]) - return args[0]-args[1] + return args[0] - args[1] - opdict = {'plus': op_plus, - 'divide' : operator.div, - 'times' : op_times, - 'minus' : op_minus, + opdict = {'plus': op_plus, + 'divide': operator.div, + 'times': op_times, + 'minus': op_minus, #'plus': sympy.Add, #'divide' : op_divide, #'times' : sympy.Mul, - 'minus' : op_minus, - 'root' : sympy.sqrt, - 'power' : sympy.Pow, + 'minus': op_minus, + 'root': sympy.sqrt, + 'power': sympy.Pow, 'sin': sympy.sin, 'cos': sympy.cos, } @@ -320,11 +331,11 @@ class formula(object): Parse <msub>, <msup>, <mi>, and <mn> ''' tag = gettag(xml) - if tag=='mn': return xml.text - elif tag=='mi': return xml.text - elif tag=='msub': return '_'.join([parsePresentationMathMLSymbol(y) for y in xml]) - elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml]) - raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag + if tag == 'mn': return xml.text + elif tag == 'mi': return xml.text + elif tag == 'msub': return '_'.join([parsePresentationMathMLSymbol(y) for y in xml]) + elif tag == 'msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml]) + raise Exception, '[parsePresentationMathMLSymbol] unknown tag %s' % tag # parser tree for Content MathML tag = gettag(xml) @@ -332,41 +343,41 @@ class formula(object): # first do compound objects - if tag=='apply': # apply operator + if tag == 'apply': # apply operator opstr = gettag(xml[0]) if opstr in opdict: op = opdict[opstr] - args = [ self.make_sympy(x) for x in xml[1:]] + args = [self.make_sympy(x) for x in xml[1:]] try: res = op(*args) - except Exception,err: + except Exception, err: self.args = args self.op = op - raise Exception,'[formula] error=%s failed to apply %s to args=%s' % (err,opstr,args) + raise Exception, '[formula] error=%s failed to apply %s to args=%s' % (err, opstr, args) return res else: - raise Exception,'[formula]: unknown operator tag %s' % (opstr) + raise Exception, '[formula]: unknown operator tag %s' % (opstr) - elif tag=='list': # square bracket list - if gettag(xml[0])=='matrix': + elif tag == 'list': # square bracket list + if gettag(xml[0]) == 'matrix': return self.make_sympy(xml[0]) else: - return [ self.make_sympy(x) for x in xml ] + return [self.make_sympy(x) for x in xml] - elif tag=='matrix': - return sympy.Matrix([ self.make_sympy(x) for x in xml ]) + elif tag == 'matrix': + return sympy.Matrix([self.make_sympy(x) for x in xml]) - elif tag=='vector': - return [ self.make_sympy(x) for x in xml ] + elif tag == 'vector': + return [self.make_sympy(x) for x in xml] # atoms are below - elif tag=='cn': # number + elif tag == 'cn': # number return sympy.sympify(xml.text) return float(xml.text) - elif tag=='ci': # variable (symbol) - if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'): # subscript or superscript + elif tag == 'ci': # variable (symbol) + if len(xml) > 0 and (gettag(xml[0]) == 'msub' or gettag(xml[0]) == 'msup'): # subscript or superscript usym = parsePresentationMathMLSymbol(xml[0]) sym = sympy.Symbol(str(usym)) else: @@ -374,42 +385,42 @@ class formula(object): if 'hat' in usym: sym = my_sympify(usym) else: - if usym=='i': print "options=",self.options - if usym=='i' and 'imaginary' in self.options: # i = sqrt(-1) - sym = sympy.I + if usym == 'i': print "options=", self.options + if usym == 'i' and 'imaginary' in self.options: # i = sqrt(-1) + sym = sympy.I else: sym = sympy.Symbol(str(usym)) return sym else: # unknown tag - raise Exception,'[formula] unknown tag %s' % tag + raise Exception, '[formula] unknown tag %s' % tag - sympy = property(make_sympy,None,None,'sympy representation') + sympy = property(make_sympy, None, None, 'sympy representation') - def GetContentMathML(self,asciimath,mathml): + def GetContentMathML(self, asciimath, mathml): # URL = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo' URL = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo' if 1: - payload = {'asciiMathInput':asciimath, - 'asciiMathML':mathml, + payload = {'asciiMathInput': asciimath, + 'asciiMathML': mathml, #'asciiMathML':unicode(mathml).encode('utf-8'), } - headers = {'User-Agent':"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"} - r = requests.post(URL,data=payload,headers=headers) + headers = {'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"} + r = requests.post(URL, data=payload, headers=headers) r.encoding = 'utf-8' ret = r.text #print "encoding: ",r.encoding # return ret - + mode = 0 cmathml = [] for k in ret.split('\n'): if 'conversion to Content MathML' in k: mode = 1 continue - if mode==1: + if mode == 1: if '<h3>Maxima Input Form</h3>' in k: mode = 0 continue @@ -420,9 +431,10 @@ class formula(object): # print cmathml #return unicode(cmathml) return cmathml - + #----------------------------------------------------------------------------- + def test1(): xmlstr = ''' <math xmlns="http://www.w3.org/1998/Math/MathML"> @@ -435,6 +447,7 @@ def test1(): ''' return formula(xmlstr) + def test2(): xmlstr = u''' <math xmlns="http://www.w3.org/1998/Math/MathML"> @@ -444,13 +457,14 @@ def test2(): <apply> <times/> <cn>2</cn> - <ci>α</ci> + <ci>α</ci> </apply> </apply> </math> ''' return formula(xmlstr) + def test3(): xmlstr = ''' <math xmlns="http://www.w3.org/1998/Math/MathML"> @@ -467,6 +481,7 @@ def test3(): ''' return formula(xmlstr) + def test4(): xmlstr = u''' <math xmlns="http://www.w3.org/1998/Math/MathML"> @@ -482,6 +497,7 @@ def test4(): ''' return formula(xmlstr) + def test5(): # sum of two matrices xmlstr = u''' <math xmlns="http://www.w3.org/1998/Math/MathML"> @@ -545,6 +561,7 @@ def test5(): # sum of two matrices ''' return formula(xmlstr) + def test6(): # imaginary numbers xmlstr = u''' <math xmlns="http://www.w3.org/1998/Math/MathML"> @@ -555,4 +572,4 @@ def test6(): # imaginary numbers </mstyle> </math> ''' - return formula(xmlstr,options='imaginaryi') + return formula(xmlstr, options='imaginaryi') diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py index 5ea428b1d5f7077ac111d951f64b37a4dc598074..4af012d2cb9b01bddf0cfc68cc2e4c98197c2b64 100644 --- a/lms/lib/symmath/symmath_check.py +++ b/lms/lib/symmath/symmath_check.py @@ -8,7 +8,10 @@ # # Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX -import os, sys, string, re +import os +import sys +import string +import re import traceback from formula import * import logging @@ -20,40 +23,42 @@ log = logging.getLogger(__name__) # # This is one of the main entry points to call. -def symmath_check_simple(expect,ans,adict={},symtab=None,extra_options=None): + +def symmath_check_simple(expect, ans, adict={}, symtab=None, extra_options=None): ''' Check a symbolic mathematical expression using sympy. The input is an ascii string (not MathML) converted to math using sympy.sympify. ''' - options = {'__MATRIX__':False,'__ABC__':False,'__LOWER__':False} + options = {'__MATRIX__': False, '__ABC__': False, '__LOWER__': False} if extra_options: options.update(extra_options) for op in options: # find options in expect string if op in expect: - expect = expect.replace(op,'') + expect = expect.replace(op, '') options[op] = True - expect = expect.replace('__OR__','__or__') # backwards compatibility + expect = expect.replace('__OR__', '__or__') # backwards compatibility if options['__LOWER__']: expect = expect.lower() ans = ans.lower() try: - ret = check(expect,ans, + ret = check(expect, ans, matrix=options['__MATRIX__'], abcsym=options['__ABC__'], symtab=symtab, ) except Exception, err: return {'ok': False, - 'msg': 'Error %s<br/>Failed in evaluating check(%s,%s)' % (err,expect,ans) + 'msg': 'Error %s<br/>Failed in evaluating check(%s,%s)' % (err, expect, ans) } return ret #----------------------------------------------------------------------------- # pretty generic checking function -def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False,do_qubit=True,symtab=None,dosimplify=False): + +def check(expect, given, numerical=False, matrix=False, normphase=False, abcsym=False, do_qubit=True, symtab=None, dosimplify=False): """ Returns dict with @@ -61,51 +66,51 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False 'msg': response message (in HTML) "expect" may have multiple possible acceptable answers, separated by "__OR__" - + """ if "__or__" in expect: # if multiple acceptable answers eset = expect.split('__or__') # then see if any match for eone in eset: - ret = check(eone,given,numerical,matrix,normphase,abcsym,do_qubit,symtab,dosimplify) + ret = check(eone, given, numerical, matrix, normphase, abcsym, do_qubit, symtab, dosimplify) if ret['ok']: return ret return ret flags = {} if "__autonorm__" in expect: - flags['autonorm']=True - expect = expect.replace('__autonorm__','') + flags['autonorm'] = True + expect = expect.replace('__autonorm__', '') matrix = True threshold = 1.0e-3 if "__threshold__" in expect: - (expect,st) = expect.split('__threshold__') + (expect, st) = expect.split('__threshold__') threshold = float(st) - numerical=True + numerical = True - if str(given)=='' and not (str(expect)==''): + if str(given) == '' and not (str(expect) == ''): return {'ok': False, 'msg': ''} try: - xgiven = my_sympify(given,normphase,matrix,do_qubit=do_qubit,abcsym=abcsym,symtab=symtab) - except Exception,err: - return {'ok': False,'msg': 'Error %s<br/> in evaluating your expression "%s"' % (err,given)} + xgiven = my_sympify(given, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab) + except Exception, err: + return {'ok': False, 'msg': 'Error %s<br/> in evaluating your expression "%s"' % (err, given)} try: - xexpect = my_sympify(expect,normphase,matrix,do_qubit=do_qubit,abcsym=abcsym,symtab=symtab) - except Exception,err: - return {'ok': False,'msg': 'Error %s<br/> in evaluating OUR expression "%s"' % (err,expect)} + xexpect = my_sympify(expect, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab) + except Exception, err: + return {'ok': False, 'msg': 'Error %s<br/> in evaluating OUR expression "%s"' % (err, expect)} - if 'autonorm' in flags: # normalize trace of matrices + if 'autonorm' in flags: # normalize trace of matrices try: xgiven /= xgiven.trace() except Exception, err: - return {'ok': False,'msg': 'Error %s<br/> in normalizing trace of your expression %s' % (err,to_latex(xgiven))} + return {'ok': False, 'msg': 'Error %s<br/> in normalizing trace of your expression %s' % (err, to_latex(xgiven))} try: xexpect /= xexpect.trace() except Exception, err: - return {'ok': False,'msg': 'Error %s<br/> in normalizing trace of OUR expression %s' % (err,to_latex(xexpect))} + return {'ok': False, 'msg': 'Error %s<br/> in normalizing trace of OUR expression %s' % (err, to_latex(xexpect))} msg = 'Your expression was evaluated as ' + to_latex(xgiven) # msg += '<br/>Expected ' + to_latex(xexpect) @@ -113,34 +118,35 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False # msg += "<br/>flags=%s" % flags if matrix and numerical: - xgiven = my_evalf(xgiven,chop=True) - dm = my_evalf(sympy.Matrix(xexpect)-sympy.Matrix(xgiven),chop=True) + xgiven = my_evalf(xgiven, chop=True) + dm = my_evalf(sympy.Matrix(xexpect) - sympy.Matrix(xgiven), chop=True) msg += " = " + to_latex(xgiven) - if abs(dm.vec().norm().evalf())<threshold: - return {'ok': True,'msg': msg} + if abs(dm.vec().norm().evalf()) < threshold: + return {'ok': True, 'msg': msg} else: pass #msg += "dm = " + to_latex(dm) + " diff = " + str(abs(dm.vec().norm().evalf())) #msg += "expect = " + to_latex(xexpect) elif dosimplify: - if (sympy.simplify(xexpect)==sympy.simplify(xgiven)): - return {'ok': True,'msg': msg} + if (sympy.simplify(xexpect) == sympy.simplify(xgiven)): + return {'ok': True, 'msg': msg} elif numerical: - if (abs((xexpect-xgiven).evalf(chop=True))<threshold): - return {'ok': True,'msg': msg} - elif (xexpect==xgiven): - return {'ok': True,'msg': msg} + if (abs((xexpect - xgiven).evalf(chop=True)) < threshold): + return {'ok': True, 'msg': msg} + elif (xexpect == xgiven): + return {'ok': True, 'msg': msg} #msg += "<p/>expect='%s', given='%s'" % (expect,given) # debugging # msg += "<p/> dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y'))) - return {'ok': False,'msg': msg } + return {'ok': False, 'msg': msg} #----------------------------------------------------------------------------- # Check function interface, which takes pmathml input # # This is one of the main entry points to call. -def symmath_check(expect,ans,dynamath=None,options=None,debug=None): + +def symmath_check(expect, ans, dynamath=None, options=None, debug=None): ''' Check a symbolic mathematical expression using sympy. The input may be presentation MathML. Uses formula. @@ -160,91 +166,91 @@ def symmath_check(expect,ans,dynamath=None,options=None,debug=None): # parse expected answer try: - fexpect = my_sympify(str(expect),matrix=do_matrix,do_qubit=do_qubit) - except Exception,err: - msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect) - return {'ok':False,'msg':msg} + fexpect = my_sympify(str(expect), matrix=do_matrix, do_qubit=do_qubit) + except Exception, err: + msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err, expect) + return {'ok': False, 'msg': msg} # if expected answer is a number, try parsing provided answer as a number also try: - fans = my_sympify(str(ans),matrix=do_matrix,do_qubit=do_qubit) - except Exception,err: + fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit) + except Exception, err: fans = None - if hasattr(fexpect,'is_number') and fexpect.is_number and fans and hasattr(fans,'is_number') and fans.is_number: - if abs(abs(fans-fexpect)/fexpect)<threshold: - return {'ok':True,'msg':msg} + if hasattr(fexpect, 'is_number') and fexpect.is_number and fans and hasattr(fans, 'is_number') and fans.is_number: + if abs(abs(fans - fexpect) / fexpect) < threshold: + return {'ok': True, 'msg': msg} else: msg += '<p>You entered: %s</p>' % to_latex(fans) - return {'ok':False,'msg':msg} + return {'ok': False, 'msg': msg} - if fexpect==fans: + if fexpect == fans: msg += '<p>You entered: %s</p>' % to_latex(fans) - return {'ok':True,'msg':msg} + return {'ok': True, 'msg': msg} # convert mathml answer to formula try: if dynamath: mmlans = dynamath[0] - except Exception,err: + except Exception, err: mmlans = None if not mmlans: - return {'ok':False,'msg':'[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath} - f = formula(mmlans,options=options) - + return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath} + f = formula(mmlans, options=options) + # get sympy representation of the formula # if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','<') try: fsym = f.sympy msg += '<p>You entered: %s</p>' % to_latex(f.sympy) - except Exception,err: + except Exception, err: log.exception("Error evaluating expression '%s' as a valid equation" % ans) - msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<','<'), + msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<', '<'), ans) if DEBUG: msg += '<hr>' msg += '<p><font color="blue">DEBUG messages:</p>' msg += "<p><pre>%s</pre></p>" % traceback.format_exc() - msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<','<') - msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<','<') + msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<', '<') + msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<', '<') msg += '<hr>' - return {'ok':False,'msg':msg} + return {'ok': False, 'msg': msg} # compare with expected - if hasattr(fexpect,'is_number') and fexpect.is_number: - if hasattr(fsym,'is_number') and fsym.is_number: - if abs(abs(fsym-fexpect)/fexpect)<threshold: - return {'ok':True,'msg':msg} - return {'ok':False,'msg':msg} + if hasattr(fexpect, 'is_number') and fexpect.is_number: + if hasattr(fsym, 'is_number') and fsym.is_number: + if abs(abs(fsym - fexpect) / fexpect) < threshold: + return {'ok': True, 'msg': msg} + return {'ok': False, 'msg': msg} msg += "<p>Expecting a numerical answer!</p>" msg += "<p>given = %s</p>" % repr(ans) msg += "<p>fsym = %s</p>" % repr(fsym) # msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','<') - return {'ok':False,'msg':msg} + return {'ok': False, 'msg': msg} - if fexpect==fsym: - return {'ok':True,'msg':msg} + if fexpect == fsym: + return {'ok': True, 'msg': msg} - if type(fexpect)==list: + if type(fexpect) == list: try: - xgiven = my_evalf(fsym,chop=True) - dm = my_evalf(sympy.Matrix(fexpect)-sympy.Matrix(xgiven),chop=True) - if abs(dm.vec().norm().evalf())<threshold: - return {'ok': True,'msg': msg} + xgiven = my_evalf(fsym, chop=True) + dm = my_evalf(sympy.Matrix(fexpect) - sympy.Matrix(xgiven), chop=True) + if abs(dm.vec().norm().evalf()) < threshold: + return {'ok': True, 'msg': msg} except sympy.ShapeError: msg += "<p>Error - your input vector or matrix has the wrong dimensions" - return {'ok':False,'msg':msg} - except Exception,err: - msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<','<') + return {'ok': False, 'msg': msg} + except Exception, err: + msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<', '<') if DEBUG: msg += "<p/><pre>%s</pre>" % traceback.format_exc() - return {'ok':False,'msg':msg} + return {'ok': False, 'msg': msg} #diff = (fexpect-fsym).simplify() #fsym = fsym.simplify() #fexpect = fexpect.simplify() try: - diff = (fexpect-fsym) - except Exception,err: + diff = (fexpect - fsym) + except Exception, err: diff = None if DEBUG: @@ -252,17 +258,18 @@ def symmath_check(expect,ans,dynamath=None,options=None,debug=None): msg += '<p><font color="blue">DEBUG messages:</p>' msg += "<p>Got: %s</p>" % repr(fsym) # msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','<') - msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)') + msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)') # msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','<') if diff: msg += "<p>Difference: %s</p>" % to_latex(diff) msg += '<hr>' - return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym} + return {'ok': False, 'msg': msg, 'ex': fexpect, 'got': fsym} #----------------------------------------------------------------------------- # tests + def sctest1(): x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))" y = ''' @@ -306,6 +313,5 @@ def sctest1(): </math> '''.strip() z = "1/2(1+(k_e* Q* q)/(m *g *h^2))" - r = sympy_check2(x,z,{'a':z,'a_fromjs':y},'a') + r = sympy_check2(x, z, {'a': z, 'a_fromjs': y}, 'a') return r -