From 7218cf4326a36e3ae9c1fdb356e2e2fe490028b6 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 5 Feb 2012 01:28:01 -0500 Subject: [PATCH 01/53] AB Test Framework --HG-- branch : AB Testing --- courseware/content_parser.py | 7 +- .../0003_auto__add_usertestgroup.py | 124 ++++++++++++++++++ student/models.py | 4 + 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 student/migrations/0003_auto__add_usertestgroup.py diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 8f6482ccf0..e766ccf47e 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -11,6 +11,7 @@ from mako.lookup import TemplateLookup try: # This lets us do __name__ == ='__main__' from django.conf import settings from student.models import UserProfile + from student.models import UserTestGroup except: settings = None @@ -143,7 +144,11 @@ def course_file(user): filename = UserProfile.objects.get(user=user).courseware data_template = template_lookup.get_template(filename) - options = {'dev_content':True} + # TODO: Rewrite in Django + groups = [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])] + + options = {'dev_content':True, + 'groups' : groups} tree = etree.XML(data_template.render(**options)) id_tag(tree) diff --git a/student/migrations/0003_auto__add_usertestgroup.py b/student/migrations/0003_auto__add_usertestgroup.py new file mode 100644 index 0000000000..4765d63578 --- /dev/null +++ b/student/migrations/0003_auto__add_usertestgroup.py @@ -0,0 +1,124 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'UserTestGroup' + db.create_table('student_usertestgroup', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)), + ('description', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('student', ['UserTestGroup']) + + # Adding M2M table for field users on 'UserTestGroup' + db.create_table('student_usertestgroup_users', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('usertestgroup', models.ForeignKey(orm['student.usertestgroup'], null=False)), + ('user', models.ForeignKey(orm['auth.user'], null=False)) + )) + 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'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'meta': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] diff --git a/student/models.py b/student/models.py index 39e0738c72..8242a174b8 100644 --- a/student/models.py +++ b/student/models.py @@ -27,6 +27,10 @@ class UserProfile(models.Model): meta = models.CharField(blank=True, max_length=255) # JSON dictionary for future expansion courseware = models.CharField(blank=True, max_length=255, default='course.xml') +class UserTestGroup(models.Model): + users = models.ManyToManyField(User, db_index=True) + 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 From 18fe892f4f8a37f8ac0aed80d5f13e416ea619d3 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 7 Feb 2012 14:39:43 -0500 Subject: [PATCH 02/53] have our catch-all exception handling push a 500 error --HG-- branch : AB Testing --- util/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/util/middleware.py b/util/middleware.py index fe73e67c7b..342fff1790 100644 --- a/util/middleware.py +++ b/util/middleware.py @@ -1,7 +1,7 @@ import logging from django.conf import settings -from django.http import HttpResponse +from django.http import HttpResponseServerError log = logging.getLogger("mitx") @@ -12,4 +12,4 @@ class ExceptionLoggingMiddleware(object): if not settings.TEMPLATE_DEBUG: def process_exception(self, request, exception): log.exception(exception) - return HttpResponse("Server Error - Please try again later.") + return HttpResponseServerError("Server Error - Please try again later.") From fd7f229ae1b86a3de568a7aa717c45ed3a05c8c2 Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Thu, 9 Feb 2012 14:55:20 -0500 Subject: [PATCH 03/53] Cleaning up profile to output better lists --HG-- branch : mitx-profilecleanup --- courseware/views.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/courseware/views.py b/courseware/views.py index 24ee4982f7..49d1aab59c 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -41,19 +41,19 @@ def profile(request): return redirect('/') dom=content_parser.course_file(request.user) - hw=[] course = dom.xpath('//course/@name')[0] - chapters = dom.xpath('//course[@name=$course]/chapter', course=course) + xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course) responses=StudentModule.objects.filter(student=request.user) response_by_id = {} for response in responses: response_by_id[response.module_id] = response - - + + total_scores = {} - - for c in chapters: + chapters=[] + for c in xmlChapters: + sections = [] chname=c.get('name') for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section', course=course, chname=chname): @@ -89,9 +89,7 @@ def profile(request): format_scores.append( graded_total ) total_scores[ format ] = format_scores - score={'course':course, - 'section':s.get("name"), - 'chapter':c.get("name"), + score={'section':s.get("name"), 'scores':scores, 'section_total' : section_total, 'format' : format, @@ -99,7 +97,12 @@ def profile(request): 'due' : s.get("due") or "", 'graded' : graded, } - hw.append(score) + sections.append(score) + + chapters.append({'course':course, + 'chapter' : c.get("name"), + 'sections' : sections,}) + def totalWithDrops(scores, drop_count): #Note that this key will sort the list descending @@ -186,7 +189,7 @@ def profile(request): 'location':user_info.location, 'language':user_info.language, 'email':request.user.email, - 'homeworks':hw, + 'chapters':chapters, 'format_url_params' : format_url_params, 'grade_summary' : grade_summary, 'csrf':csrf(request)['csrf_token'] From dae8420456280fdf1f0971301986995c21fd8027 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 10 Feb 2012 13:38:44 -0500 Subject: [PATCH 04/53] Support for static pages added --- static_template_view/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/static_template_view/views.py b/static_template_view/views.py index a66a3cd8c5..0d0fd3d54b 100644 --- a/static_template_view/views.py +++ b/static_template_view/views.py @@ -9,8 +9,7 @@ from django.core.context_processors import csrf from django.conf import settings #valid_templates=['index.html', 'staff.html', 'info.html', 'credits.html'] -valid_templates=['mitx_global.html', - 'index.html', +valid_templates=['index.html', 'tos.html', 'privacy.html', 'honor.html', @@ -22,7 +21,12 @@ print "!!",settings.__dict__ if settings.STATIC_GRAB: valid_templates = valid_templates+['server-down.html', 'server-error.html' - 'server-overloaded.html'] + 'server-overloaded.html', + 'mitx_global.html', + 'mitx-overview.html', + '6002x-faq.html', + '6002x-press-release.html' + ] def index(request, template): csrf_token = csrf(request)['csrf_token'] From 091a45dc0cd61434e30385c5be8686808c6b8bdf Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Fri, 10 Feb 2012 16:57:01 -0500 Subject: [PATCH 05/53] Removed random debug code --- static_template_view/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/static_template_view/views.py b/static_template_view/views.py index 0d0fd3d54b..0f15dd14ae 100644 --- a/static_template_view/views.py +++ b/static_template_view/views.py @@ -16,8 +16,6 @@ valid_templates=['index.html', 'copyright.html', '404.html'] -print "!!",settings.__dict__ - if settings.STATIC_GRAB: valid_templates = valid_templates+['server-down.html', 'server-error.html' From a50d3d7f91a07db15992d142cbd9876f09495121 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Fri, 10 Feb 2012 18:58:35 -0500 Subject: [PATCH 06/53] changed ADMIN email to admin@mitx.mit.edu --- settings_new_askbot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings_new_askbot.py b/settings_new_askbot.py index 4e56a07cd6..0158d69821 100644 --- a/settings_new_askbot.py +++ b/settings_new_askbot.py @@ -55,7 +55,7 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( - ('Piotr Mitros', 'staff@csail.mit.edu'), + ('MITx Admins', 'admin@mitx.mit.edu'), ) MANAGERS = ADMINS From 39011ce011909dfa030753d6b2815394ce3cab15 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 Feb 2012 00:01:19 -0500 Subject: [PATCH 07/53] Book module works --HG-- branch : pmitros-mod-template --- courseware/content_parser.py | 5 +++-- courseware/management/commands/check_course.py | 1 + courseware/module_render.py | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 8f6482ccf0..a3143724f9 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -92,7 +92,8 @@ def id_tag(course): 'html':'filename', 'vertical':'id', 'tab':'id', - 'schematic':'id'} + 'schematic':'id', + 'book' : 'id'} # Tag elements with unique IDs elements = course.xpath("|".join(['//'+c for c in default_ids])) @@ -143,7 +144,7 @@ def course_file(user): filename = UserProfile.objects.get(user=user).courseware data_template = template_lookup.get_template(filename) - options = {'dev_content':True} + options = {'dev_content':settings.DEV_CONTENT} tree = etree.XML(data_template.render(**options)) id_tag(tree) diff --git a/courseware/management/commands/check_course.py b/courseware/management/commands/check_course.py index 7ca8ef8242..b020529ffb 100644 --- a/courseware/management/commands/check_course.py +++ b/courseware/management/commands/check_course.py @@ -41,6 +41,7 @@ class Command(BaseCommand): if os.path.exists(sections_dir): print "Checking all section includes are valid XML" for f in os.listdir(sections_dir): + print f etree.parse(sections_dir+'/'+f) else: print "Skipping check of include files -- no section includes dir ("+sections_dir+")" diff --git a/courseware/module_render.py b/courseware/module_render.py index 3c863684be..5c1c136155 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -30,6 +30,7 @@ import courseware.modules.capa_module import courseware.modules.html_module import courseware.modules.schematic_module import courseware.modules.seq_module +import courseware.modules.template_module import courseware.modules.vertical_module import courseware.modules.video_module @@ -44,6 +45,9 @@ modx_modules={'problem':courseware.modules.capa_module.LoncapaModule, 'tab':courseware.modules.seq_module.SequentialModule, 'schematic':courseware.modules.schematic_module.SchematicModule} +for f in os.listdir(settings.DATA_DIR+'/custom_tags'): + modx_modules[f] = courseware.modules.template_module.TemplateModule + def object_cache(cache, user, module_type, module_id): # We don't look up on user -- all queries include user # Additional lookup would require a DB hit the way Django From cbc020785a7e7403ca1095cdf3b8eff1bc5579dd Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 Feb 2012 08:20:43 -0500 Subject: [PATCH 08/53] Semi-generic registration mechanism for modules. Still need to clean up references in content_parser --HG-- branch : pmitros-mod-template --- courseware/module_render.py | 36 ++++++------------------ courseware/modules/__init__.py | 38 ++++++++++++++++++++++++++ courseware/modules/capa_module.py | 7 +++-- courseware/modules/html_module.py | 7 +++-- courseware/modules/schematic_module.py | 7 +++-- courseware/modules/seq_module.py | 5 ++-- courseware/modules/vertical_module.py | 7 +++-- courseware/modules/video_module.py | 7 +++-- courseware/modules/x_module.py | 3 +- 9 files changed, 73 insertions(+), 44 deletions(-) diff --git a/courseware/module_render.py b/courseware/module_render.py index 5c1c136155..1f4e784b2e 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -26,28 +26,10 @@ import track.views import courseware.content_parser as content_parser -import courseware.modules.capa_module -import courseware.modules.html_module -import courseware.modules.schematic_module -import courseware.modules.seq_module -import courseware.modules.template_module -import courseware.modules.vertical_module -import courseware.modules.video_module +import courseware.modules log = logging.getLogger("mitx.courseware") -## TODO: Add registration mechanism -modx_modules={'problem':courseware.modules.capa_module.LoncapaModule, - 'video':courseware.modules.video_module.VideoModule, - 'html':courseware.modules.html_module.HtmlModule, - 'vertical':courseware.modules.vertical_module.VerticalModule, - 'sequential':courseware.modules.seq_module.SequentialModule, - 'tab':courseware.modules.seq_module.SequentialModule, - 'schematic':courseware.modules.schematic_module.SchematicModule} - -for f in os.listdir(settings.DATA_DIR+'/custom_tags'): - modx_modules[f] = courseware.modules.template_module.TemplateModule - def object_cache(cache, user, module_type, module_id): # We don't look up on user -- all queries include user # Additional lookup would require a DB hit the way Django @@ -78,18 +60,18 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ajax_url = '/modx/'+module+'/'+id+'/' - id_tag=modx_modules[module].id_attribute + id_tag=courseware.modules.get_module_class(module.id_attribute) # Grab the XML corresponding to the request from course.xml xml = content_parser.module_xml(content_parser.course_file(request.user), module, id_tag, id) # Create the module - instance=modx_modules[module](xml, - s.module_id, - ajax_url=ajax_url, - state=s.state, - track_function = make_track_function(request), - render_function = None) + instance=courseware.modules.get_module_class(module)(xml, + s.module_id, + ajax_url=ajax_url, + state=s.state, + track_function = make_track_function(request), + render_function = None) # Let the module handle the AJAX ajax_return=instance.handle_ajax(dispatch, request.POST) # Save the state back to the database @@ -104,7 +86,7 @@ def render_x_module(user, request, xml_module, module_object_preload): ''' Generic module for extensions. This renders to HTML. ''' # Check if problem has an instance in DB module_type=xml_module.tag - module_class=modx_modules[module_type] + module_class=courseware.modules.get_module_class(module_type) module_id=xml_module.get('id') #module_class.id_attribute) or "" # Grab state from database diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index e69de29bb2..4150088c22 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -0,0 +1,38 @@ +import os +import os.path + +from django.conf import settings + +import capa_module +import html_module +import schematic_module +import seq_module +import template_module +import vertical_module +import video_module + +# Import all files in modules directory, excluding backups (# and . in name) +# and __init__ +# +# Stick them in a list +modx_module_list = [] + +for f in os.listdir(os.path.dirname(__file__)): + if f!='__init__.py' and \ + f[-3:] == ".py" and \ + "." not in f[:-3] \ + and '#' not in f: + mod_path = 'courseware.modules.'+f[:-3] + mod = __import__(mod_path, fromlist = "courseware.modules") + if 'Module' in mod.__dict__: + modx_module_list.append(mod) + +# Convert list to a dictionary for lookup by tag +modx_modules = {} +for module in modx_module_list: + for tag in module.Module.get_xml_tags(): + modx_modules[tag] = module.Module + +def get_module_class(tag): + return modx_modules[tag] + diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index cd740d7dea..a9846abf58 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -26,15 +26,18 @@ import courseware.content_parser as content_parser log = logging.getLogger("mitx.courseware") -class LoncapaModule(XModule): +class Module(XModule): ''' Interface between capa_problem and x_module. Originally a hack meant to be refactored out, but it seems to be serving a useful prupose now. We can e.g .destroy and create the capa_problem on a reset. ''' - xml_tags = ["problem"] + id_attribute = "filename" + @classmethod + def get_xml_tags(c): + return ["problem"] def get_state(self): state = self.lcp.get_state() diff --git a/courseware/modules/html_module.py b/courseware/modules/html_module.py index f1ce14ec7e..92e29df938 100644 --- a/courseware/modules/html_module.py +++ b/courseware/modules/html_module.py @@ -7,14 +7,15 @@ from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule from lxml import etree -class HtmlModule(XModule): +class Module(XModule): id_attribute = 'filename' def get_state(self): return json.dumps({ }) - def get_xml_tags(): - return "html" + @classmethod + def get_xml_tags(c): + return ["html"] def get_html(self): if self.filename==None: diff --git a/courseware/modules/schematic_module.py b/courseware/modules/schematic_module.py index 0312321b4d..e253f1acc6 100644 --- a/courseware/modules/schematic_module.py +++ b/courseware/modules/schematic_module.py @@ -6,14 +6,15 @@ from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule -class SchematicModule(XModule): +class Module(XModule): id_attribute = 'id' def get_state(self): return json.dumps({ }) - def get_xml_tags(): - return "schematic" + @classmethod + def get_xml_tags(c): + return ["schematic"] def get_html(self): return ''.format(item_id=self.item_id) diff --git a/courseware/modules/seq_module.py b/courseware/modules/seq_module.py index 33c4088486..6ec74feace 100644 --- a/courseware/modules/seq_module.py +++ b/courseware/modules/seq_module.py @@ -13,7 +13,7 @@ from x_module import XModule # OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem'] -class SequentialModule(XModule): +class Module(XModule): ''' Layout module which lays out content in a temporal sequence ''' id_attribute = 'id' @@ -21,7 +21,8 @@ class SequentialModule(XModule): def get_state(self): return json.dumps({ 'position':self.position }) - def get_xml_tags(): + @classmethod + def get_xml_tags(c): return ["sequential", 'tab'] def get_html(self): diff --git a/courseware/modules/vertical_module.py b/courseware/modules/vertical_module.py index 6939c75cc9..c068cb9a76 100644 --- a/courseware/modules/vertical_module.py +++ b/courseware/modules/vertical_module.py @@ -7,14 +7,15 @@ from mitxmako.shortcuts import render_to_response, render_to_string from x_module import XModule from lxml import etree -class VerticalModule(XModule): +class Module(XModule): id_attribute = 'id' def get_state(self): return json.dumps({ }) - def get_xml_tags(): - return "vertical" + @classmethod + def get_xml_tags(c): + return ["vertical"] def get_html(self): return render_to_string('vert_module.html',{'items':self.contents}) diff --git a/courseware/modules/video_module.py b/courseware/modules/video_module.py index b7e1e211dc..518413946c 100644 --- a/courseware/modules/video_module.py +++ b/courseware/modules/video_module.py @@ -11,7 +11,7 @@ from x_module import XModule log = logging.getLogger("mitx.courseware.modules") -class VideoModule(XModule): +class Module(XModule): #id_attribute = 'youtube' video_time = 0 @@ -28,9 +28,10 @@ class VideoModule(XModule): log.debug(u"STATE POSITION {0}".format(self.position)) return json.dumps({ 'position':self.position }) - def get_xml_tags(): + @classmethod + def get_xml_tags(c): '''Tags in the courseware file guaranteed to correspond to the module''' - return "video" + return ["video"] def video_list(self): l = self.youtube.split(',') diff --git a/courseware/modules/x_module.py b/courseware/modules/x_module.py index 4b45c4c7fc..1a2154256c 100644 --- a/courseware/modules/x_module.py +++ b/courseware/modules/x_module.py @@ -10,7 +10,8 @@ class XModule(object): ''' id_attribute='name' # An attribute guaranteed to be unique - def get_xml_tags(): + @classmethod + def get_xml_tags(c): ''' Tags in the courseware file guaranteed to correspond to the module ''' return [] From b274c2e26cc1685a4f2ec50afc17165810a3b0aa Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 Feb 2012 09:31:03 -0500 Subject: [PATCH 09/53] Added basic test case --HG-- branch : pmitros-mod-template --- courseware/modules/__init__.py | 11 +++++++++++ courseware/tests.py | 22 +++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index 4150088c22..db8b1abf2d 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -34,5 +34,16 @@ for module in modx_module_list: modx_modules[tag] = module.Module def get_module_class(tag): + ''' Given an XML tag (e.g. 'video'), return + the associated module (e.g. video_module.Module). + ''' return modx_modules[tag] +def get_module_id(tag): + ''' Given an XML tag (e.g. 'video'), return + the default ID for that module (e.g. 'youtube_id') + ''' + return modx_modules[tag].id_attribute + +def get_valid_tags(): + return modx_modules.keys() diff --git a/courseware/tests.py b/courseware/tests.py index 501deb776c..03bdbb7ecf 100644 --- a/courseware/tests.py +++ b/courseware/tests.py @@ -1,16 +1,12 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". +import unittest +import courseware.modules -Replace this with more appropriate tests for your application. -""" +class ModelsTest(unittest.TestCase): + def setUp(self): + pass -from django.test import TestCase + def test_get_module_class(self): + vc = courseware.modules.get_module_class('video') + vc_str = "" + self.assertEqual(str(vc), vc_str) - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) From 33013a0b150f5c2f4c0b7e716ca2b53dd53a9c15 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 Feb 2012 15:07:15 -0500 Subject: [PATCH 10/53] Test cases, basic framework for moving id tags out of content_parser --HG-- branch : pmitros-mod-template --- courseware/content_parser.py | 9 ++++++++- courseware/modules/__init__.py | 8 ++++++++ courseware/modules/video_module.py | 2 +- courseware/modules/x_module.py | 2 +- courseware/tests.py | 24 ++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index a3143724f9..e49c7b1b9f 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -8,6 +8,8 @@ from lxml import etree from mako.template import Template from mako.lookup import TemplateLookup +#import courseware.modules + try: # This lets us do __name__ == ='__main__' from django.conf import settings from student.models import UserProfile @@ -94,7 +96,12 @@ def id_tag(course): 'tab':'id', 'schematic':'id', 'book' : 'id'} - +# TODO: +# alt_ids = courseware.modules.get_default_ids() + +# print default_ids, alt_ids +# print default_ids == alt_ids + # Tag elements with unique IDs elements = course.xpath("|".join(['//'+c for c in default_ids])) for elem in elements: diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index db8b1abf2d..ed80aeaa86 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -11,6 +11,8 @@ import template_module import vertical_module import video_module +from courseware import content_parser + # Import all files in modules directory, excluding backups (# and . in name) # and __init__ # @@ -47,3 +49,9 @@ def get_module_id(tag): def get_valid_tags(): return modx_modules.keys() + +def get_default_ids(): + tags = get_valid_tags() + ids = map(get_module_id, tags) + return dict(zip(tags, ids)) + diff --git a/courseware/modules/video_module.py b/courseware/modules/video_module.py index 518413946c..2063c18953 100644 --- a/courseware/modules/video_module.py +++ b/courseware/modules/video_module.py @@ -12,7 +12,7 @@ from x_module import XModule log = logging.getLogger("mitx.courseware.modules") class Module(XModule): - #id_attribute = 'youtube' + id_attribute = 'youtube' video_time = 0 def handle_ajax(self, dispatch, get): diff --git a/courseware/modules/x_module.py b/courseware/modules/x_module.py index 1a2154256c..4a379253c4 100644 --- a/courseware/modules/x_module.py +++ b/courseware/modules/x_module.py @@ -8,7 +8,7 @@ class XModule(object): Initialized on access with __init__, first time with state=None, and then with state ''' - id_attribute='name' # An attribute guaranteed to be unique + id_attribute='id' # An attribute guaranteed to be unique @classmethod def get_xml_tags(c): diff --git a/courseware/tests.py b/courseware/tests.py index 03bdbb7ecf..66a7ef181e 100644 --- a/courseware/tests.py +++ b/courseware/tests.py @@ -1,5 +1,9 @@ import unittest + +import numpy + import courseware.modules +import courseware.capa.calc as calc class ModelsTest(unittest.TestCase): def setUp(self): @@ -9,4 +13,24 @@ class ModelsTest(unittest.TestCase): vc = courseware.modules.get_module_class('video') vc_str = "" self.assertEqual(str(vc), vc_str) + video_id = courseware.modules.get_default_ids()['video'] + self.assertEqual(video_id, 'youtube') + def test_calc(self): + variables={'R1':2.0, 'R3':4.0} + functions={'sin':numpy.sin, 'cos':numpy.cos} + + self.assertEqual(calc.evaluator(variables, functions, "10000||sin(7+5)-6k"), 4000.0) + 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) + exception_happened = False + try: + evaluator({},{}, "5+7 QWSEKO") + except: + exception_happened = True + self.assertTrue(exception_happened) + From cc28d828b0ec51121034dbec74c6313c9716f1d5 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sat, 11 Feb 2012 18:00:12 -0500 Subject: [PATCH 11/53] Added template_module.py, which I omitted a few commits ago --HG-- branch : pmitros-mod-template --- courseware/modules/template_module.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 courseware/modules/template_module.py diff --git a/courseware/modules/template_module.py b/courseware/modules/template_module.py new file mode 100644 index 0000000000..d760a23ec2 --- /dev/null +++ b/courseware/modules/template_module.py @@ -0,0 +1,29 @@ +import json +import os + +## TODO: Abstract out from Django +from django.conf import settings +from mitxmako.shortcuts import render_to_response, render_to_string + +from x_module import XModule +from lxml import etree + +class Module(XModule): + def get_state(self): + return json.dumps({ }) + + @classmethod + def get_xml_tags(c): + tags = os.listdir(settings.DATA_DIR+'/custom_tags') + return tags + + def get_html(self): + return self.html + + def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None): + XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function) + xmltree = etree.fromstring(xml) + filename = xmltree.tag + params = dict(xmltree.items()) + print params + self.html = render_to_string('custom_tags/'+filename, params) From d22c0cd5fb9a74a49cc3f0da610dce2b3bf724b1 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 12 Feb 2012 09:48:06 -0500 Subject: [PATCH 12/53] Legal name required, better error messages --- student/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/student/views.py b/student/views.py index 397f32dc39..e59a3796a2 100644 --- a/student/views.py +++ b/student/views.py @@ -136,9 +136,15 @@ def create_account(request, post_override=None): # TODO: Confirm e-mail is not from a generic domain (mailinator, etc.)? Not sure if # this is a good idea # TODO: Check password is sane - for a in ['username', 'email', 'password', 'terms_of_service', 'honor_code']: + for a in ['username', 'email', 'name', 'password', 'terms_of_service', 'honor_code']: if len(post_vars[a])<2: - js['value']="{field} is required.".format(field=a) + 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]) return HttpResponse(json.dumps(js)) try: From a4dc01509a764a962566e8a958843823c684f457 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 00:30:22 -0500 Subject: [PATCH 13/53] Bug fix. Not very tested --HG-- branch : tolerance_bug --- courseware/capa/responsetypes.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/courseware/capa/responsetypes.py b/courseware/capa/responsetypes.py index 93ebac85f9..382e0f4191 100644 --- a/courseware/capa/responsetypes.py +++ b/courseware/capa/responsetypes.py @@ -19,6 +19,19 @@ global_context={'random':random, 'calc':calc, 'eia':eia} + +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 + ''' + relative = "%" in tol + if relative: + tolerance = evaluator(dict(),dict(),tol[:-1]) * 0.01 + tolerance = tolerance * max(abs(v1), abs(v2)) + else: + tolerance = evaluator(dict(),dict(),tol) + return abs(v1-v2) <= tolerance + class numericalresponse(object): def __init__(self, xml, context): self.xml = xml @@ -27,16 +40,15 @@ class numericalresponse(object): self.tolerance = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', id=xml.get('id'))[0] self.tolerance = contextualize_text(self.tolerance, context) - self.tolerance = evaluator(dict(),dict(),self.tolerance) self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', id=xml.get('id'))[0] def grade(self, student_answers): ''' Display HTML for a numeric response ''' student_answer = student_answers[self.answer_id] - error = abs(evaluator(dict(),dict(),student_answer) - self.correct_answer) - allowed_error = abs(self.correct_answer*self.tolerance) - if error <= allowed_error: + correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), self.correct_answer, self.tolerance) + + if correct: return {self.answer_id:'correct'} else: return {self.answer_id:'incorrect'} @@ -80,7 +92,6 @@ class formularesponse(object): self.tolerance = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', id=xml.get('id'))[0] self.tolerance = contextualize_text(self.tolerance, context) - self.tolerance = evaluator(dict(),dict(),self.tolerance) self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', id=xml.get('id'))[0] self.context = context @@ -105,7 +116,7 @@ class formularesponse(object): student_result = evaluator(student_variables,dict(),student_answers[self.answer_id]) if math.isnan(student_result) or math.isinf(student_result): return {self.answer_id:"incorrect"} - if abs( student_result - instructor_result ) > self.tolerance: + if not compare_with_tolerance(student_result, instructor_result, self.tolerance): return {self.answer_id:"incorrect"} return {self.answer_id:"correct"} From fca78b83ba7dcd2cc7161269b5856d958d3b0d4f Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 08:10:32 -0500 Subject: [PATCH 14/53] Relative tolerance, minor clean-up, as per Dave --HG-- branch : tolerance_bug --- courseware/capa/responsetypes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/courseware/capa/responsetypes.py b/courseware/capa/responsetypes.py index 382e0f4191..7f2b61e997 100644 --- a/courseware/capa/responsetypes.py +++ b/courseware/capa/responsetypes.py @@ -26,8 +26,8 @@ def compare_with_tolerance(v1, v2, tol): ''' relative = "%" in tol if relative: - tolerance = evaluator(dict(),dict(),tol[:-1]) * 0.01 - tolerance = tolerance * max(abs(v1), abs(v2)) + 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 @@ -37,9 +37,9 @@ class numericalresponse(object): self.xml = xml self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = float(self.correct_answer) - self.tolerance = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', + self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', id=xml.get('id'))[0] - self.tolerance = contextualize_text(self.tolerance, context) + self.tolerance = contextualize_text(self.tolerance_xml, context) self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', id=xml.get('id'))[0] @@ -89,9 +89,9 @@ class formularesponse(object): self.xml = xml self.correct_answer = contextualize_text(xml.get('answer'), context) self.samples = contextualize_text(xml.get('samples'), context) - self.tolerance = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', + self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', id=xml.get('id'))[0] - self.tolerance = contextualize_text(self.tolerance, context) + self.tolerance = contextualize_text(self.tolerance_xml, context) self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', id=xml.get('id'))[0] self.context = context From 126c1f982c78b28201ba716185af2b1b38f1c715 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 13:10:14 -0500 Subject: [PATCH 15/53] Removed extra print statement --- courseware/modules/capa_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index cd740d7dea..3161664a9e 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -250,7 +250,7 @@ class LoncapaModule(XModule): for key in get: answers['_'.join(key.split('_')[1:])]=get[key] - print "XXX", answers, get +# print "XXX", answers, get event_info['answers']=answers From caa2ed9195d04d1e51acf27835f30cf9aac8a309 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 16:36:48 -0500 Subject: [PATCH 16/53] Removed comments and obsolet error codes --- student/views.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/student/views.py b/student/views.py index e59a3796a2..f5ab302775 100644 --- a/student/views.py +++ b/student/views.py @@ -32,23 +32,14 @@ def index(request): else: csrf_token = csrf(request)['csrf_token'] # TODO: Clean up how 'error' is done. - return render_to_response('index.html', {'error' : '', - 'csrf': csrf_token }) + return render_to_response('index.html', {'csrf': csrf_token }) -# def courseinfo(request): -# if request.user.is_authenticated(): -# return redirect('/courseware') -# else: -# csrf_token = csrf(request)['csrf_token'] -# # TODO: Clean up how 'error' is done. -# return render_to_response('courseinfo.html', {'error' : '', -# 'csrf': csrf_token }) - # Need different levels of logging @ensure_csrf_cookie def login_user(request, error=""): if 'email' not in request.POST or 'password' not in request.POST: - return render_to_response('login.html', {'error':error.replace('+',' ')}) + return HttpResponse(json.dumps({'success':False, + 'error': 'Invalid login'})) # TODO: User error message email = request.POST['email'] password = request.POST['password'] From 7c5d93a51462320d544cb4c79a33088c03c2faf9 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 16:46:01 -0500 Subject: [PATCH 17/53] Commented out debug print statements for possible merge --HG-- branch : pmitros-mod-template --- courseware/content_parser.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index e49c7b1b9f..c296e30f3d 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -8,8 +8,6 @@ from lxml import etree from mako.template import Template from mako.lookup import TemplateLookup -#import courseware.modules - try: # This lets us do __name__ == ='__main__' from django.conf import settings from student.models import UserProfile @@ -51,7 +49,7 @@ def xpath(xml, query_string, **args): We should remove this with the move to lxml. We should also use lxml argument passing. ''' doc = etree.fromstring(xml) - print type(doc) + #print type(doc) def escape(x): # TODO: This should escape the string. For now, we just assume it's made of valid characters. # Couldn't figure out how to escape for lxml in a few quick Googles @@ -62,7 +60,7 @@ def xpath(xml, query_string, **args): return x args=dict( ((k, escape(args[k])) for k in args) ) - print args + #print args results = doc.xpath(query_string.format(**args)) return results @@ -88,7 +86,7 @@ def item(l, default="", process=lambda x:x): def id_tag(course): ''' Tag all course elements with unique IDs ''' - default_ids = {'video':'youtube', + old_ids = {'video':'youtube', 'problem':'filename', 'sequential':'id', 'html':'filename', @@ -96,11 +94,11 @@ def id_tag(course): 'tab':'id', 'schematic':'id', 'book' : 'id'} -# TODO: -# alt_ids = courseware.modules.get_default_ids() + import courseware.modules + default_ids = courseware.modules.get_default_ids() -# print default_ids, alt_ids -# print default_ids == alt_ids + #print default_ids, old_ids + #print default_ids == old_ids # Tag elements with unique IDs elements = course.xpath("|".join(['//'+c for c in default_ids])) From 44e124c46f3a0be52488f8bb059bfed603a3a9cc Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 17:06:16 -0500 Subject: [PATCH 18/53] Removed file system hackery --HG-- branch : pmitros-mod-template --- courseware/modules/__init__.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index ed80aeaa86..4c528144d2 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -17,17 +17,21 @@ from courseware import content_parser # and __init__ # # Stick them in a list -modx_module_list = [] +# modx_module_list = [] -for f in os.listdir(os.path.dirname(__file__)): - if f!='__init__.py' and \ - f[-3:] == ".py" and \ - "." not in f[:-3] \ - and '#' not in f: - mod_path = 'courseware.modules.'+f[:-3] - mod = __import__(mod_path, fromlist = "courseware.modules") - if 'Module' in mod.__dict__: - modx_module_list.append(mod) +# for f in os.listdir(os.path.dirname(__file__)): +# if f!='__init__.py' and \ +# f[-3:] == ".py" and \ +# "." not in f[:-3] \ +# and '#' not in f: +# mod_path = 'courseware.modules.'+f[:-3] +# mod = __import__(mod_path, fromlist = "courseware.modules") +# if 'Module' in mod.__dict__: +# modx_module_list.append(mod) + +#print modx_module_list +modx_module_list = [capa_module, html_module, schematic_module, seq_module, template_module, vertical_module, video_module] +#print modx_module_list # Convert list to a dictionary for lookup by tag modx_modules = {} From 9ab2f591302451359f6c16bf98c7d7b895ddcc42 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 17:07:31 -0500 Subject: [PATCH 19/53] Removed debug print code for merge --HG-- branch : pmitros-mod-template --- courseware/modules/template_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courseware/modules/template_module.py b/courseware/modules/template_module.py index d760a23ec2..11778348e3 100644 --- a/courseware/modules/template_module.py +++ b/courseware/modules/template_module.py @@ -25,5 +25,5 @@ class Module(XModule): xmltree = etree.fromstring(xml) filename = xmltree.tag params = dict(xmltree.items()) - print params +# print params self.html = render_to_string('custom_tags/'+filename, params) From 20ada529f6bee67aa9ff8ee9e53dcc474b285fde Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 17:10:52 -0500 Subject: [PATCH 20/53] Help file added to static views --- static_template_view/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static_template_view/views.py b/static_template_view/views.py index 0f15dd14ae..d3a12a9fdb 100644 --- a/static_template_view/views.py +++ b/static_template_view/views.py @@ -14,7 +14,8 @@ valid_templates=['index.html', 'privacy.html', 'honor.html', 'copyright.html', - '404.html'] + '404.html', + 'mitx_help.html'] if settings.STATIC_GRAB: valid_templates = valid_templates+['server-down.html', From 6e9c320a5a839d023be8001a017f8eef638d93c3 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 13 Feb 2012 18:05:23 -0500 Subject: [PATCH 21/53] Major bug -- using obsolete ID type in modx_render. Minor bug -- profile page broken --- courseware/module_render.py | 4 +--- courseware/views.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/courseware/module_render.py b/courseware/module_render.py index 1f4e784b2e..c0752f4ba9 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -60,10 +60,8 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ajax_url = '/modx/'+module+'/'+id+'/' - id_tag=courseware.modules.get_module_class(module.id_attribute) - # Grab the XML corresponding to the request from course.xml - xml = content_parser.module_xml(content_parser.course_file(request.user), module, id_tag, id) + xml = content_parser.module_xml(content_parser.course_file(request.user), module, 'id', id) # Create the module instance=courseware.modules.get_module_class(module)(xml, diff --git a/courseware/views.py b/courseware/views.py index 24ee4982f7..b82d6c23a1 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -71,7 +71,7 @@ def profile(request): if response.grade!=None: correct=response.grade - total=courseware.modules.capa_module.LoncapaModule(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + total=courseware.modules.capa_module.Module(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? scores.append((int(correct),total, graded )) From 8174e4969081c388e01ad4a7767cc07b5c0a70d1 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 14 Feb 2012 11:49:47 -0500 Subject: [PATCH 22/53] echo change in 1a68a3047124 to remove outdated call to modules.get_module_class --HG-- branch : pmitros-mod-template --- courseware/capa/calc.py | 6 ++++++ courseware/module_render.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/courseware/capa/calc.py b/courseware/capa/calc.py index 41ced03e58..dc8653cf21 100644 --- a/courseware/capa/calc.py +++ b/courseware/capa/calc.py @@ -1,4 +1,5 @@ import copy +import logging import math import operator @@ -27,10 +28,15 @@ default_variables = {'j':numpy.complex(0,1), } +log = logging.getLogger("mitx.courseware.capa") + def evaluator(variables, functions, string): ''' Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary from string to function ''' + log.debug(u"Evaluating: {0} with vars: {1}, funcs: {2}" + .format(string, variables, functions)) + all_variables = copy.copy(default_variables) all_variables.update(variables) all_functions = copy.copy(default_functions) diff --git a/courseware/module_render.py b/courseware/module_render.py index 1f4e784b2e..bf62585d65 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -60,10 +60,10 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ajax_url = '/modx/'+module+'/'+id+'/' - id_tag=courseware.modules.get_module_class(module.id_attribute) + # id_tag=courseware.modules.get_module_class(module) # Grab the XML corresponding to the request from course.xml - xml = content_parser.module_xml(content_parser.course_file(request.user), module, id_tag, id) + xml = content_parser.module_xml(content_parser.course_file(request.user), module, 'id', id) # Create the module instance=courseware.modules.get_module_class(module)(xml, From ed9f2f421b1475b2f9a24b7a395005fe562bbf3d Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 14 Feb 2012 12:10:09 -0500 Subject: [PATCH 23/53] sort keys so evaluation takes longer terms first - vars like 'e2' will be substituted in before mathematical constants like 'e' --- courseware/capa/calc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/courseware/capa/calc.py b/courseware/capa/calc.py index 41ced03e58..be44050888 100644 --- a/courseware/capa/calc.py +++ b/courseware/capa/calc.py @@ -119,7 +119,10 @@ def evaluator(variables, functions, string): # 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: - varnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), all_variables.keys())) + # 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: CaselessLiteral(x), all_variables_keys)) varnames.setParseAction(lambda x:map(lambda y:all_variables[y], x)) else: varnames=NoMatch() From a5b9372e4f47a71bcb5f3eccb158df37668adf10 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 14 Feb 2012 16:50:18 -0500 Subject: [PATCH 24/53] document inability to run evaluator with non-float variables --- courseware/capa/calc.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/courseware/capa/calc.py b/courseware/capa/calc.py index be44050888..c900b21dd2 100644 --- a/courseware/capa/calc.py +++ b/courseware/capa/calc.py @@ -1,4 +1,5 @@ import copy +import logging import math import operator @@ -26,11 +27,19 @@ default_variables = {'j':numpy.complex(0,1), 'e':numpy.complex(numpy.e) } +log = logging.getLogger("mitx.courseware.capa") def evaluator(variables, functions, string): ''' Evaluate an expression. Variables are passed as a dictionary from string to value. Unary functions are passed as a dictionary - from string to function ''' + from string to function. Variables must be floats. + + TODO: Fix it so we can pass integers and complex numbers in variables dict + ''' + # log.debug("variables: {0}".format(variables)) + # log.debug("functions: {0}".format(functions)) + # log.debug("string: {0}".format(string)) + all_variables = copy.copy(default_variables) all_variables.update(variables) all_functions = copy.copy(default_functions) @@ -150,7 +159,9 @@ if __name__=='__main__': 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({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5") print evaluator({},{}, "-1") print evaluator({},{}, "-(7+5)") From d9466219150e79adcc6da59465edb231624b57fb Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Tue, 14 Feb 2012 17:40:46 -0500 Subject: [PATCH 25/53] Fixed course check --- courseware/management/commands/check_course.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/courseware/management/commands/check_course.py b/courseware/management/commands/check_course.py index b020529ffb..4d0b9840ab 100644 --- a/courseware/management/commands/check_course.py +++ b/courseware/management/commands/check_course.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import User from mitx.courseware.content_parser import course_file import mitx.courseware.module_render +import mitx.courseware.modules class Command(BaseCommand): help = "Does basic validity tests on course.xml." @@ -24,7 +25,7 @@ class Command(BaseCommand): check = False print "Confirming all modules render. Nothing should print during this step. " for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'): - module_class=mitx.courseware.module_render.modx_modules[module.tag] + module_class=mitx.courseware.modules.modx_modules[module.tag] # TODO: Abstract this out in render_module.py try: instance=module_class(etree.tostring(module), From c02502b34b9135d7082d60f357f15bf29f1c76b3 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Thu, 16 Feb 2012 13:36:09 -0500 Subject: [PATCH 26/53] New constant for calculator and new tests --- courseware/capa/calc.py | 8 +++++++- courseware/tests.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/courseware/capa/calc.py b/courseware/capa/calc.py index c900b21dd2..571f4991d5 100644 --- a/courseware/capa/calc.py +++ b/courseware/capa/calc.py @@ -4,6 +4,7 @@ import math import operator import numpy +import scipy.constants from pyparsing import Word, alphas, nums, oneOf, Literal from pyparsing import ZeroOrMore, OneOrMore, StringStart @@ -24,7 +25,12 @@ default_functions = {'sin' : numpy.sin, 'abs':numpy.abs } default_variables = {'j':numpy.complex(0,1), - 'e':numpy.complex(numpy.e) + '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") diff --git a/courseware/tests.py b/courseware/tests.py index 66a7ef181e..39edcc0e72 100644 --- a/courseware/tests.py +++ b/courseware/tests.py @@ -27,6 +27,9 @@ class ModelsTest(unittest.TestCase): 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) exception_happened = False try: evaluator({},{}, "5+7 QWSEKO") From 93011db4e38fd5ddb3b8ab4ff6e5104188bd254b Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Thu, 16 Feb 2012 14:55:27 -0500 Subject: [PATCH 27/53] Staff can see problem XML --- courseware/module_render.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/courseware/module_render.py b/courseware/module_render.py index c0752f4ba9..92a78528f9 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -116,7 +116,10 @@ def render_x_module(user, request, xml_module, module_object_preload): smod.save() # This may be optional (at least in the case of no instance in the dB) module_object_preload.append(smod) # Grab content - content = {'content':instance.get_html(), + content = instance.get_html() + if user.is_staff: + content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module)}) + content = {'content':content, "destroy_js":instance.get_destroy_js(), 'init_js':instance.get_init_js(), 'type':module_type} From 085cc17178658af9df18bc72ebad6ec9b8810a38 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Thu, 16 Feb 2012 19:52:52 -0500 Subject: [PATCH 28/53] Caching problem maximum scores in profile (~10x performance boost in that section of the code) --HG-- branch : profile-cache --- .../migrations/0003_done_grade_cache.py | 116 ++++++++++++++++++ courseware/models.py | 9 +- courseware/views.py | 48 +++++--- 3 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 courseware/migrations/0003_done_grade_cache.py diff --git a/courseware/migrations/0003_done_grade_cache.py b/courseware/migrations/0003_done_grade_cache.py new file mode 100644 index 0000000000..f2fedcf1a8 --- /dev/null +++ b/courseware/migrations/0003_done_grade_cache.py @@ -0,0 +1,116 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Removing unique constraint on 'StudentModule', fields ['module_id', 'module_type', 'student'] + db.delete_unique('courseware_studentmodule', ['module_id', 'module_type', 'student_id']) + + # Adding field 'StudentModule.max_grade' + db.add_column('courseware_studentmodule', 'max_grade', self.gf('django.db.models.fields.FloatField')(null=True, blank=True), keep_default=False) + + # Adding field 'StudentModule.done' + db.add_column('courseware_studentmodule', 'done', self.gf('django.db.models.fields.CharField')(default='na', max_length=8, db_index=True), keep_default=False) + + # 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']) + + # Deleting field 'StudentModule.max_grade' + db.delete_column('courseware_studentmodule', 'max_grade') + + # Deleting field 'StudentModule.done' + db.delete_column('courseware_studentmodule', 'done') + + # 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'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'courseware.studentmodule': { + 'Meta': {'unique_together': "(('student', 'module_id'),)", 'object_name': 'StudentModule'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'done': ('django.db.models.fields.CharField', [], {'default': "'na'", 'max_length': '8', 'db_index': 'True'}), + 'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'max_grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'module_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32', 'db_index': 'True'}), + 'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['courseware'] diff --git a/courseware/models.py b/courseware/models.py index f713da3195..a41e58eb81 100644 --- a/courseware/models.py +++ b/courseware/models.py @@ -31,8 +31,13 @@ class StudentModule(models.Model): ## Grade, and are we done? grade = models.FloatField(null=True, blank=True, db_index=True) - #max_grade = models.FloatField(null=True, blank=True) - + max_grade = models.FloatField(null=True, blank=True) + DONE_TYPES = (('na','NOT_APPLICABLE'), + ('f','FINISHED'), + ('i','INCOMPLETE'), + ) + done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True) + # DONE_TYPES = (('done','DONE'), # Finished # ('incomplete','NOTDONE'), # Not finished # ('na','NA')) # Not applicable (e.g. vertical) diff --git a/courseware/views.py b/courseware/views.py index fbdd7fceab..b3253af592 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -33,6 +33,22 @@ etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, template_imports={'urllib':urllib} +def get_grade(request, problem, cache): + id = problem.get('id') + correct = 0 + if id in cache: + response = cache[id] + if response.grade!=None: + correct=response.grade + if response.max_grade != None: + total = response.max_grade + else: + total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + response.max_grade = total + response.save() + + return (correct, total) + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def profile(request): ''' User profile. Show username, location, etc, as well as grades . @@ -59,28 +75,30 @@ def profile(request): course=course, chname=chname): problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem', course=course, chname=chname, section=s.get('name')) - + graded = True if s.get('graded') == "true" else False scores=[] if len(problems)>0: for p in problems: - id = p.get('id') - correct = 0 - if id in response_by_id: - response = response_by_id[id] - if response.grade!=None: - correct=response.grade - - total=courseware.modules.capa_module.Module(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + (correct,total) = get_grade(request, p, response_by_id) + # id = p.get('id') + # correct = 0 + # if id in response_by_id: + # response = response_by_id[id] + # if response.grade!=None: + # correct=response.grade + + # total=courseware.modules.capa_module.Module(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + # print correct, total scores.append((int(correct),total, graded )) - - + + section_total = (sum([score[0] for score in scores]), sum([score[1] for score in scores])) - + graded_total = (sum([score[0] for score in scores if score[2]]), sum([score[1] for score in scores if score[2]])) - + #Add the graded total to total_scores format = s.get('format') if s.get('format') else "" subtitle = s.get('subtitle') if s.get('subtitle') else format @@ -88,7 +106,7 @@ def profile(request): format_scores = total_scores[ format ] if format in total_scores else [] format_scores.append( graded_total ) total_scores[ format ] = format_scores - + score={'section':s.get("name"), 'scores':scores, 'section_total' : section_total, @@ -98,7 +116,7 @@ def profile(request): 'graded' : graded, } sections.append(score) - + chapters.append({'course':course, 'chapter' : c.get("name"), 'sections' : sections,}) From b2f3a75d861479b6bce4022ef228dbdfe2b7a174 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 19 Feb 2012 12:18:19 -0500 Subject: [PATCH 29/53] Working on case where id not in cache. Committing to merge with mainline --HG-- branch : profile-cache --- courseware/views.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/courseware/views.py b/courseware/views.py index b3253af592..4369816444 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -34,18 +34,33 @@ etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, template_imports={'urllib':urllib} def get_grade(request, problem, cache): + ## HACK: assumes max score is fixed per problem id = problem.get('id') correct = 0 + print ",", if id in cache: + print "In cache" response = cache[id] if response.grade!=None: correct=response.grade - if response.max_grade != None: + if id in cache and response.max_grade != None: total = response.max_grade - else: - total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? - response.max_grade = total - response.save() + else: #if id in cache and response.max_grade == None: + total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() + # response.max_grade = total + # response.save() + # else: # if id not in cache + # total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() + # module = StudentModule(module_type = 'problem', + # module_id = id, + # student = request.user, + # state = None, + # grade = 0, + # max_grade = total, + # done = 'i') + # total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() + # module.save() + # cache[id] = module return (correct, total) From 7bea9382465a728bc8a960e8824873103fbf9f8f Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 19 Feb 2012 12:21:57 -0500 Subject: [PATCH 30/53] Handling case of response not in cache explicitly --HG-- branch : profile-cache --- courseware/views.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/courseware/views.py b/courseware/views.py index b4dc0683dd..c313cc43e3 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -38,30 +38,28 @@ def get_grade(request, problem, cache): ## HACK: assumes max score is fixed per problem id = problem.get('id') correct = 0 - print ",", if id in cache: - print "In cache" response = cache[id] if response.grade!=None: correct=response.grade if id in cache and response.max_grade != None: total = response.max_grade - else: #if id in cache and response.max_grade == None: + elif id in cache and response.max_grade == None: total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() - # response.max_grade = total - # response.save() - # else: # if id not in cache - # total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() - # module = StudentModule(module_type = 'problem', - # module_id = id, - # student = request.user, - # state = None, - # grade = 0, - # max_grade = total, - # done = 'i') - # total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() - # module.save() - # cache[id] = module + response.max_grade = total + response.save() + else: # if id not in cache + total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() + module = StudentModule(module_type = 'problem', + module_id = id, + student = request.user, + state = None, + grade = 0, + max_grade = total, + done = 'i') + total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() + module.save() + cache[id] = module return (correct, total) From ab0f07ebd179426910f9f851fb5af4b5e5ff5c5d Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 19 Feb 2012 12:30:08 -0500 Subject: [PATCH 31/53] Code cleaned up to be slighlty more explicit --HG-- branch : profile-cache --- courseware/views.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/courseware/views.py b/courseware/views.py index c313cc43e3..2556cb44bb 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -38,29 +38,32 @@ def get_grade(request, problem, cache): ## HACK: assumes max score is fixed per problem id = problem.get('id') correct = 0 - if id in cache: - response = cache[id] - if response.grade!=None: - correct=response.grade - if id in cache and response.max_grade != None: - total = response.max_grade - elif id in cache and response.max_grade == None: - total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() - response.max_grade = total - response.save() - else: # if id not in cache - total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() - module = StudentModule(module_type = 'problem', + + # If the ID is not in the cache, add the item + if id not in cache: + module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__? module_id = id, student = request.user, state = None, grade = 0, - max_grade = total, + max_grade = None, done = 'i') - total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() - module.save() cache[id] = module + # Grab the # correct from cache + if id in cache: + response = cache[id] + if response.grade!=None: + correct=response.grade + + # Grab max grade from cache, or if it doesn't exist, compute and save to DB + if id in cache and response.max_grade != None: + total = response.max_grade + else: + total=courseware.modules.capa_module.Module(etree.tostring(problem), "id").max_score() + response.max_grade = total + response.save() + return (correct, total) @cache_control(no_cache=True, no_store=True, must_revalidate=True) @@ -255,6 +258,7 @@ def profile(request): 'grade_summary' : grade_summary, 'csrf':csrf(request)['csrf_token'] } + return render_to_response('profile.html', context) def format_url_params(params): From cb27c471ffe65683a6d20a252f29e2cfd9149598 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 19 Feb 2012 17:03:46 -0500 Subject: [PATCH 32/53] Capa problems handle case sensitivity and give better errors --HG-- branch : pmitros-capa-fixes --- courseware/capa/calc.py | 54 ++++++++++++++++++++++++++++--- courseware/capa/capa_problem.py | 2 +- courseware/capa/responsetypes.py | 28 ++++++++++++++-- courseware/modules/capa_module.py | 21 +++++++----- courseware/tests.py | 18 +++++++++-- 5 files changed, 105 insertions(+), 18 deletions(-) diff --git a/courseware/capa/calc.py b/courseware/capa/calc.py index 571f4991d5..63c5c9de01 100644 --- a/courseware/capa/calc.py +++ b/courseware/capa/calc.py @@ -2,6 +2,7 @@ import copy import logging import math import operator +import re import numpy import scipy.constants @@ -10,7 +11,7 @@ from pyparsing import Word, alphas, nums, oneOf, Literal from pyparsing import ZeroOrMore, OneOrMore, StringStart from pyparsing import StringEnd, Optional, Forward from pyparsing import CaselessLiteral, Group, StringEnd -from pyparsing import NoMatch, stringEnd +from pyparsing import NoMatch, stringEnd, alphanums default_functions = {'sin' : numpy.sin, 'cos' : numpy.cos, @@ -35,10 +36,40 @@ default_variables = {'j':numpy.complex(0,1), log = logging.getLogger("mitx.courseware.capa") -def evaluator(variables, functions, string): +class UndefinedVariable(Exception): + def raiseself(self): + ''' Helper so we can use inside of a lambda ''' + raise self + + +general_whitespace = re.compile('[^\w]+') +def check_variables(string, variables): + ''' Confirm the only variables in string are defined. + + Pyparsing uses a left-to-right parser, which makes the more + 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 + 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 + continue + if v not in variables: + bad_variables.append(v) + 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 from string to function. Variables must be floats. + cs: Case sensitive TODO: Fix it so we can pass integers and complex numbers in variables dict ''' @@ -51,6 +82,19 @@ def evaluator(variables, functions, string): all_functions = copy.copy(default_functions) all_functions.update(functions) + if not cs: + string_cs = string.lower() + for v in all_variables.keys(): + all_variables[v.lower()]=all_variables[v] + for f in all_functions.keys(): + all_functions[f.lower()]=all_functions[f] + CasedLiteral = CaselessLiteral + else: + string_cs = string + CasedLiteral = Literal + + check_variables(string_cs, set(all_variables.keys()+all_functions.keys())) + if string.strip() == "": return float('nan') ops = { "^" : operator.pow, @@ -137,19 +181,19 @@ def evaluator(variables, functions, string): # 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: CaselessLiteral(x), all_variables_keys)) + 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: CaselessLiteral(x), all_functions.keys())) + 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 | varnames | lpar+expr+rpar | function + 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) diff --git a/courseware/capa/capa_problem.py b/courseware/capa/capa_problem.py index 73ef168cf0..e9778008c1 100644 --- a/courseware/capa/capa_problem.py +++ b/courseware/capa/capa_problem.py @@ -15,7 +15,7 @@ from mako.template import Template from util import contextualize_text from inputtypes import textline, schematic -from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse +from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse, StudentInputError import calc import eia diff --git a/courseware/capa/responsetypes.py b/courseware/capa/responsetypes.py index 7f2b61e997..8809d935b1 100644 --- a/courseware/capa/responsetypes.py +++ b/courseware/capa/responsetypes.py @@ -3,8 +3,9 @@ import math import numpy import random import scipy +import traceback -from calc import evaluator +from calc import evaluator, UndefinedVariable from django.conf import settings from util import contextualize_text @@ -84,6 +85,9 @@ class customresponse(object): # be handled by capa_problem return {} +class StudentInputError(Exception): + pass + class formularesponse(object): def __init__(self, xml, context): self.xml = xml @@ -95,6 +99,17 @@ class formularesponse(object): self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', id=xml.get('id'))[0] self.context = context + ts = xml.get('type') + if ts == None: + typeslist = [] + else: + typeslist = ts.split(',') + if 'ci' in typeslist: # Case insensitive + self.case_sensitive = False + elif 'cs' in typeslist: # Case sensitive + self.case_sensitive = True + else: # Default + self.case_sensitive = False def grade(self, student_answers): @@ -113,7 +128,16 @@ class formularesponse(object): instructor_variables[str(var)] = value student_variables[str(var)] = value instructor_result = evaluator(instructor_variables,dict(),self.correct_answer) - student_result = evaluator(student_variables,dict(),student_answers[self.answer_id]) + try: + #print student_variables,dict(),student_answers[self.answer_id] + student_result = evaluator(student_variables,dict(), + student_answers[self.answer_id], + cs = self.case_sensitive) + except UndefinedVariable as uv: + raise StudentInputError('Undefined: '+uv.message) + except: + #traceback.print_exc() + raise StudentInputError("Syntax Error") if math.isnan(student_result) or math.isinf(student_result): return {self.answer_id:"incorrect"} if not compare_with_tolerance(student_result, instructor_result, self.tolerance): diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 378c322c42..08cb81d7bd 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -21,7 +21,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string from django.http import Http404 from x_module import XModule -from courseware.capa.capa_problem import LoncapaProblem +from courseware.capa.capa_problem import LoncapaProblem, StudentInputError import courseware.content_parser as content_parser log = logging.getLogger("mitx.courseware") @@ -277,22 +277,27 @@ class Module(XModule): lcp_id = self.lcp.problem_id filename = self.lcp.filename correct_map = self.lcp.grade_answers(answers) + except StudentInputError as inst: + self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state) + traceback.print_exc() +# print {'error':sys.exc_info(), +# 'answers':answers, +# 'seed':self.lcp.seed, +# 'filename':self.lcp.filename} + return json.dumps({'success':inst.message}) except: self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state) traceback.print_exc() - print {'error':sys.exc_info(), - 'answers':answers, - 'seed':self.lcp.seed, - 'filename':self.lcp.filename} - return json.dumps({'success':'syntax'}) + return json.dumps({'success':'Unknown Error'}) + self.attempts = self.attempts + 1 self.lcp.done=True - success = 'finished' + success = 'correct' for i in correct_map: if correct_map[i]!='correct': - success = 'errors' + success = 'incorrect' js=json.dumps({'correct_map' : correct_map, 'success' : success}) diff --git a/courseware/tests.py b/courseware/tests.py index 39edcc0e72..b0d7cad19f 100644 --- a/courseware/tests.py +++ b/courseware/tests.py @@ -32,8 +32,22 @@ class ModelsTest(unittest.TestCase): self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001) exception_happened = False try: - evaluator({},{}, "5+7 QWSEKO") + calc.evaluator({},{}, "5+7 QWSEKO") except: exception_happened = True self.assertTrue(exception_happened) - + + 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: + calc.evaluator(variables, functions, "r1*r3", cs=True) + except: + exception_happened = True + self.assertTrue(exception_happened) + From 622c4e3427e442dfb2f91f40409fe2aa67635123 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 19 Feb 2012 17:27:21 -0500 Subject: [PATCH 33/53] Eliminated save button where not relevant --HG-- branch : pmitros-capa-fixes --- courseware/modules/capa_module.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 08cb81d7bd..366d0463e4 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -94,6 +94,10 @@ class Module(XModule): if self.max_attempts != None: attempts_str = " ({a}/{m})".format(a=self.attempts, m=self.max_attempts) + # We don't need a "save" button if infinite number of attempts and non-randomized + if self.max_attempts == None and self.rerandomize == False: + save_button = False + # Check if explanation is available, and if so, give a link explain="" if self.lcp.done and self.explain_available=='attempted': From 0d9a24e4f4048580a7c1df2c750bd8ec11606f6e Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Sun, 19 Feb 2012 20:12:34 -0500 Subject: [PATCH 34/53] Templates broken into namespaces --HG-- branch : pmitros-mod-template --- courseware/content_parser.py | 9 ++------- courseware/modules/html_module.py | 2 +- courseware/modules/template_module.py | 2 +- mitxmako/middleware.py | 22 ++++++++++------------ mitxmako/shortcuts.py | 8 ++++---- settings_new_askbot.py | 16 +++++++++++++--- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 61e65caf71..7fb7a8a298 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -5,13 +5,12 @@ import re from datetime import timedelta from lxml import etree -from mako.template import Template -from mako.lookup import TemplateLookup try: # This lets us do __name__ == ='__main__' from django.conf import settings from student.models import UserProfile from student.models import UserTestGroup + from mitxmako.shortcuts import render_to_response, render_to_string except: settings = None @@ -142,13 +141,9 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None): #to its children later. return -template_lookup = TemplateLookup(directories = [settings.DATA_DIR], - module_directory = settings.MAKO_MODULE_DIR) - def course_file(user): # TODO: Cache. filename = UserProfile.objects.get(user=user).courseware - data_template = template_lookup.get_template(filename) # TODO: Rewrite in Django groups = [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])] @@ -156,7 +151,7 @@ def course_file(user): options = {'dev_content':settings.DEV_CONTENT, 'groups' : groups} - tree = etree.XML(data_template.render(**options)) + tree = etree.XML(render_to_string(filename, options, namespace = 'course')) id_tag(tree) propogate_downward_tag(tree, "due") propogate_downward_tag(tree, "graded") diff --git a/courseware/modules/html_module.py b/courseware/modules/html_module.py index 92e29df938..4194f73e74 100644 --- a/courseware/modules/html_module.py +++ b/courseware/modules/html_module.py @@ -24,7 +24,7 @@ class Module(XModule): textlist=[i for i in textlist if type(i)==str] return "".join(textlist) try: - filename=settings.DATA_DIR+"html/"+self.filename+".xml" + filename=settings.DATA_DIR+"html/"+self.filename return open(filename).read() except: # For backwards compatibility. TODO: Remove return render_to_string(self.filename, {'id': self.item_id}) diff --git a/courseware/modules/template_module.py b/courseware/modules/template_module.py index 11778348e3..ff548dea34 100644 --- a/courseware/modules/template_module.py +++ b/courseware/modules/template_module.py @@ -26,4 +26,4 @@ class Module(XModule): filename = xmltree.tag params = dict(xmltree.items()) # print params - self.html = render_to_string('custom_tags/'+filename, params) + self.html = render_to_string('custom_tags/'+filename, params, namespace = 'custom_tags') diff --git a/mitxmako/middleware.py b/mitxmako/middleware.py index 481d1db672..857ae29541 100644 --- a/mitxmako/middleware.py +++ b/mitxmako/middleware.py @@ -17,33 +17,31 @@ import tempfile from django.template import RequestContext requestcontext = None -lookup = None +lookup = {} class MakoMiddleware(object): def __init__(self): """Setup mako variables and lookup object""" from django.conf import settings # Set all mako variables based on django settings - global template_dirs, output_encoding, module_directory, encoding_errors - directories = getattr(settings, 'MAKO_TEMPLATE_DIRS', settings.TEMPLATE_DIRS) - + template_locations = settings.MAKO_TEMPLATES module_directory = getattr(settings, 'MAKO_MODULE_DIR', None) + if module_directory is None: module_directory = tempfile.mkdtemp() - output_encoding = getattr(settings, 'MAKO_OUTPUT_ENCODING', 'utf-8') - encoding_errors = getattr(settings, 'MAKO_ENCODING_ERRORS', 'replace') - - global lookup - lookup = TemplateLookup(directories=directories, + for location in template_locations: + lookup[location] = TemplateLookup(directories=template_locations[location], module_directory=module_directory, - output_encoding=output_encoding, - encoding_errors=encoding_errors, + output_encoding='utf-8', + input_encoding='utf-8', + encoding_errors='replace', ) + import mitxmako mitxmako.lookup = lookup def process_request (self, request): global requestcontext requestcontext = RequestContext(request) -# print requestcontext + diff --git a/mitxmako/shortcuts.py b/mitxmako/shortcuts.py index 261cee5b41..862f8b96bb 100644 --- a/mitxmako/shortcuts.py +++ b/mitxmako/shortcuts.py @@ -20,7 +20,7 @@ from django.conf import settings from mitxmako.middleware import requestcontext -def render_to_string(template_name, dictionary, context_instance=None): +def render_to_string(template_name, dictionary, context_instance=None, namespace='main'): context_instance = context_instance or Context(dictionary) # add dictionary to context_instance context_instance.update(dictionary or {}) @@ -31,12 +31,12 @@ def render_to_string(template_name, dictionary, context_instance=None): for d in context_instance: context_dictionary.update(d) # fetch and render template - template = middleware.lookup.get_template(template_name) + template = middleware.lookup[namespace].get_template(template_name) return template.render(**context_dictionary) -def render_to_response(template_name, dictionary, context_instance=None, **kwargs): +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 lookup.get_template(args[0]).render with the passed arguments. """ - return HttpResponse(render_to_string(template_name, dictionary, context_instance), **kwargs) + return HttpResponse(render_to_string(template_name, dictionary, context_instance, namespace), **kwargs) diff --git a/settings_new_askbot.py b/settings_new_askbot.py index 561abaef3a..6efecdaf48 100644 --- a/settings_new_askbot.py +++ b/settings_new_askbot.py @@ -148,12 +148,11 @@ MAXLOG = 500 LOG_DIR = "/tmp/" MAKO_MODULE_DIR = None +MAKO_TEMPLATES = {} + # Make sure we execute correctly regardless of where we're called from execfile(os.path.join(BASE_DIR, "settings.py")) -if MAKO_MODULE_DIR == None: - MAKO_MODULE_DIR = tempfile.mkdtemp('mako') - # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error. @@ -388,5 +387,16 @@ ASKBOT_CSS_DEVEL = True BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" CELERY_ALWAYS_EAGER = True +ot = MAKO_TEMPLATES +MAKO_TEMPLATES['course'] = [DATA_DIR] +MAKO_TEMPLATES['custom_tags'] = [DATA_DIR+'/custom_tags'] +MAKO_TEMPLATES['main'] = [BASE_DIR+'/templates/'] + + +MAKO_TEMPLATES.update(ot) + +if MAKO_MODULE_DIR == None: + MAKO_MODULE_DIR = tempfile.mkdtemp('mako') + djcelery.setup_loader() From 1befa20b1260236ea6f79a655e5261311d679be0 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 07:08:22 -0500 Subject: [PATCH 35/53] Last commit tested and debugged --HG-- branch : pmitros-mod-template --- courseware/modules/template_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courseware/modules/template_module.py b/courseware/modules/template_module.py index ff548dea34..d9dbb613f0 100644 --- a/courseware/modules/template_module.py +++ b/courseware/modules/template_module.py @@ -26,4 +26,4 @@ class Module(XModule): filename = xmltree.tag params = dict(xmltree.items()) # print params - self.html = render_to_string('custom_tags/'+filename, params, namespace = 'custom_tags') + self.html = render_to_string(filename, params, namespace = 'custom_tags') From e57fc19045bb9488ca010a3f37d986bb076d852b Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 07:16:22 -0500 Subject: [PATCH 36/53] Including namespace for sections --HG-- branch : pmitros-mod-template --- settings_new_askbot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/settings_new_askbot.py b/settings_new_askbot.py index 6efecdaf48..496e1ad79c 100644 --- a/settings_new_askbot.py +++ b/settings_new_askbot.py @@ -389,6 +389,7 @@ CELERY_ALWAYS_EAGER = True ot = MAKO_TEMPLATES MAKO_TEMPLATES['course'] = [DATA_DIR] +MAKO_TEMPLATES['sections'] = [DATA_DIR+'/sections'] MAKO_TEMPLATES['custom_tags'] = [DATA_DIR+'/custom_tags'] MAKO_TEMPLATES['main'] = [BASE_DIR+'/templates/'] From 7bd509b7ed4fdbcb34d4e6def809bbe54487cdaa Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 11:29:12 -0500 Subject: [PATCH 37/53] Beginning to merge new settings file --HG-- branch : pmitros-dormsbee-settings2 rename : settings_new_askbot.py => settings.py --- courseware/modules/capa_module.py | 2 +- settings.py | 393 +++++++++++++- settings2/README.txt | 4 + settings2/__init__.py | 0 settings2/aws.py | 18 + settings_new_askbot.py => settings2/common.py | 480 +++++++++--------- settings2/dev.py | 28 + settings2/devplus.py | 35 ++ settings2/loadtest.py | 3 + settings2/staging.py | 13 + settings_old_askbot.py | 245 --------- 11 files changed, 730 insertions(+), 491 deletions(-) mode change 120000 => 100644 settings.py create mode 100644 settings2/README.txt create mode 100644 settings2/__init__.py create mode 100644 settings2/aws.py rename settings_new_askbot.py => settings2/common.py (51%) create mode 100644 settings2/dev.py create mode 100644 settings2/devplus.py create mode 100644 settings2/loadtest.py create mode 100644 settings2/staging.py delete mode 100644 settings_old_askbot.py diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 378c322c42..8f3cc3fe89 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -175,7 +175,7 @@ class Module(XModule): self.attempts=state['attempts'] self.filename=content_parser.item(dom2.xpath('/problem/@filename')) - filename=settings.DATA_DIR+"problems/"+self.filename+".xml" + filename=settings.DATA_DIR+"/problems/"+self.filename+".xml" self.name=content_parser.item(dom2.xpath('/problem/@name')) self.lcp=LoncapaProblem(filename, self.item_id, state) diff --git a/settings.py b/settings.py deleted file mode 120000 index f42725e7f1..0000000000 --- a/settings.py +++ /dev/null @@ -1 +0,0 @@ -settings_new_askbot.py \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000000..561abaef3a --- /dev/null +++ b/settings.py @@ -0,0 +1,392 @@ +import os +import platform +import sys +import tempfile + +import djcelery + +# Configuration option for when we want to grab server error pages +STATIC_GRAB = False +DEV_CONTENT = True + +LIB_URL = '/static/lib/' +LIB_URL = 'https://mitxstatic.s3.amazonaws.com/js/' +BOOK_URL = '/static/book/' +BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' + +# Feature Flags. These should be set to false until they are ready to deploy, and then eventually flag mechanisms removed +GENERATE_PROFILE_SCORES = False # If this is true, random scores will be generated for the purpose of debugging the profile graphs + +# Our parent dir (mitx_all) is the BASE_DIR +BASE_DIR = os.path.abspath(os.path.join(__file__, "..", "..")) + +COURSEWARE_ENABLED = True +ASKBOT_ENABLED = True +CSRF_COOKIE_DOMAIN = '127.0.0.1' + +# Defaults to be overridden +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +SITE_NAME = "localhost:8000" + +DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu' +DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' + +GENERATE_RANDOM_USER_CREDENTIALS = False + +WIKI_REQUIRE_LOGIN_EDIT = True +WIKI_REQUIRE_LOGIN_VIEW = True + +PERFSTATS = False + +HTTPS = 'on' + +MEDIA_URL = '' +MEDIA_ROOT = '' + +# S3BotoStorage insists on a timeout for uploaded assets. We should make it +# permanent instead, but rather than trying to figure out exactly where that +# setting is, I'm just bumping the expiration time to something absurd (100 +# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3 +# in the global settings.py +AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years + +# Needed for Askbot +# Deployed machines: Move to S3 +DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('MITx Admins', 'admin@mitx.mit.edu'), +) + +MANAGERS = ADMINS + +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +TIME_ZONE = 'America/New_York' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'track.middleware.TrackMiddleware', + 'mitxmako.middleware.MakoMiddleware', + #'debug_toolbar.middleware.DebugToolbarMiddleware', +) + +ROOT_URLCONF = 'mitx.urls' + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'courseware', + 'student', + 'django.contrib.humanize', + 'static_template_view', + 'staticbook', + 'simplewiki', + 'track', + 'circuit', + 'perfstats', + 'util', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) + +#TRACK_DIR = None +DEBUG_TRACK_LOG = False +# Maximum length of a tracking string. We don't want e.g. a file upload in our log +TRACK_MAX_EVENT = 1000 +# Maximum length of log file before starting a new one. +MAXLOG = 500 + +LOG_DIR = "/tmp/" +MAKO_MODULE_DIR = None + +# Make sure we execute correctly regardless of where we're called from +execfile(os.path.join(BASE_DIR, "settings.py")) + +if MAKO_MODULE_DIR == None: + MAKO_MODULE_DIR = tempfile.mkdtemp('mako') + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. + +pid = os.getpid() +hostname = platform.node().split(".")[0] +SYSLOG_ADDRESS = ('syslog.m.i4x.org', 514) + +handlers = ['console'] +if not DEBUG: + handlers.append('syslogger') + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters' : { + 'standard' : { + 'format' : '%(asctime)s %(levelname)s %(process)d [%(name)s] %(filename)s:%(lineno)d - %(message)s', + }, + 'syslog_format' : { + 'format' : '[%(name)s] %(levelname)s [' + hostname + ' %(process)d] [%(filename)s:%(lineno)d] - %(message)s', + }, + 'raw' : { + 'format' : '%(message)s', + } + }, + 'handlers' : { + 'console' : { + 'level' : 'DEBUG' if DEBUG else 'INFO', + 'class' : 'logging.StreamHandler', + 'formatter' : 'standard', + 'stream' : sys.stdout, + }, + 'console_err' : { + 'level' : 'ERROR', + 'class' : 'logging.StreamHandler', + 'formatter' : 'standard', + 'stream' : sys.stderr, + }, + 'syslogger' : { + 'level' : 'INFO', + 'class' : 'logging.handlers.SysLogHandler', + 'address' : SYSLOG_ADDRESS, + 'formatter' : 'syslog_format', + }, + 'mail_admins' : { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler', + }, + }, + 'loggers' : { + 'django' : { + 'handlers' : handlers + ['mail_admins'], + 'propagate' : True, + 'level' : 'INFO' + }, + 'tracking' : { + 'handlers' : [] if DEBUG else ['syslogger'], # handlers, + 'level' : 'DEBUG', + 'propagate' : False, + }, + 'root' : { + 'handlers' : handlers, + 'level' : 'DEBUG', + 'propagate' : False + }, + 'mitx' : { + 'handlers' : handlers, + 'level' : 'DEBUG', + 'propagate' : False + }, + } +} + + + +if PERFSTATS : + MIDDLEWARE_CLASSES = ( 'perfstats.middleware.ProfileMiddleware',) + MIDDLEWARE_CLASSES + +if 'TRACK_DIR' not in locals(): + TRACK_DIR = BASE_DIR+'/track_dir/' +if 'STATIC_ROOT' not in locals(): + STATIC_ROOT = BASE_DIR+'/staticroot/' +if 'DATA_DIR' not in locals(): + DATA_DIR = BASE_DIR+'/data/' +if 'TEXTBOOK_DIR' not in locals(): + TEXTBOOK_DIR = BASE_DIR+'/textbook/' + +if 'TEMPLATE_DIRS' not in locals(): + TEMPLATE_DIRS = ( + BASE_DIR+'/templates/', + DATA_DIR+'/templates', + TEXTBOOK_DIR, + ) + +if 'STATICFILES_DIRS' not in locals(): + STATICFILES_DIRS = ( + BASE_DIR+'/3rdParty/static', + BASE_DIR+'/static', + ) + + +if 'ASKBOT_EXTRA_SKINS_DIR' not in locals(): + ASKBOT_EXTRA_SKINS_DIR = BASE_DIR+'/askbot-devel/askbot/skins' +if 'ASKBOT_DIR' not in locals(): + ASKBOT_DIR = BASE_DIR+'/askbot-devel' + +sys.path.append(ASKBOT_DIR) +import askbot +import site + +STATICFILES_DIRS = STATICFILES_DIRS + ( ASKBOT_DIR+'/askbot/skins',) + +ASKBOT_ROOT = os.path.dirname(askbot.__file__) + +# Needed for Askbot +# Deployed machines: Move to S3 +if MEDIA_ROOT == '': + MEDIA_ROOT = ASKBOT_DIR+'/askbot/upfiles' +if MEDIA_URL == '': + MEDIA_URL = '/discussion/upfiles/' + +site.addsitedir(os.path.join(os.path.dirname(askbot.__file__), 'deps')) +TEMPLATE_LOADERS = TEMPLATE_LOADERS + ('askbot.skins.loaders.filesystem_load_template_source',) + +MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ( + 'util.middleware.ExceptionLoggingMiddleware', + 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', + 'askbot.middleware.forum_mode.ForumModeMiddleware', + 'askbot.middleware.cancel.CancelActionMiddleware', + 'django.middleware.transaction.TransactionMiddleware', + #'debug_toolbar.middleware.DebugToolbarMiddleware', + 'askbot.middleware.view_log.ViewLogMiddleware', + 'askbot.middleware.spaceless.SpacelessMiddleware', + # 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', +) + +FILE_UPLOAD_TEMP_DIR = os.path.join(os.path.dirname(__file__), 'tmp').replace('\\','/') +FILE_UPLOAD_HANDLERS = ( + 'django.core.files.uploadhandler.MemoryFileUploadHandler', + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', +) +ASKBOT_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') +ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes +# ASKBOT_FILE_UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') + +PROJECT_ROOT = os.path.dirname(__file__) + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.request', + 'askbot.context.application_settings', + #'django.core.context_processors.i18n', + 'askbot.user_messages.context_processors.user_messages',#must be before auth + 'django.core.context_processors.auth', #this is required for admin + 'django.core.context_processors.csrf', #necessary for csrf protection +) + +INSTALLED_APPS = INSTALLED_APPS + ( + 'django.contrib.sitemaps', + 'django.contrib.admin', + 'south', + 'askbot.deps.livesettings', + 'askbot', + #'keyedcache', # TODO: Main askbot tree has this installed, but we get intermittent errors if we include it. + 'robots', + 'django_countries', + 'djcelery', + 'djkombu', + 'followit', +) + +# askbot livesettings +LIVESETTINGS_OPTIONS = { + 1: { + 'SETTINGS' : { + 'FORUM_DATA_RULES' : { + 'MIN_TITLE_LENGTH' : 1, + 'MIN_QUESTION_BODY_LENGTH' : 1, + 'MIN_ANSWER_BODY_LENGTH' : 1, + + # 'ENABLE_VIDEO_EMBEDDING' : True, + # + # Enabling video requires forked version of markdown + # pip uninstall markdown2 + # pip install -e git+git://github.com/andryuha/python-markdown2.git#egg=markdown2 + }, + 'MIN_REP' : { + 'MIN_REP_TO_VOTE_UP' : 1, + 'MIN_REP_TO_VOTE_DOWN' : 1, + 'MIN_REP_TO_ANSWER_OWN_QUESTION' : 1, + 'MIN_REP_TO_ACCEPT_OWN_ANSWER' : 1, + 'MIN_REP_TO_FLAG_OFFENSIVE' : 1, + 'MIN_REP_TO_LEAVE_COMMENTS' : 1, + 'MIN_REP_TO_CLOSE_OWN_QUESTIONS' : 1, + 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS' : 1, + 'MIN_REP_TO_REOPEN_OWN_QUESTIONS' : 1, + 'MIN_REP_TO_EDIT_WIKI' : 1, + 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS' : 100, + 'MIN_REP_TO_UPLOAD_FILES' : 1, + }, + 'SOCIAL_SHARING' : { + 'ENABLE_SHARING_TWITTER' : False, + 'ENABLE_SHARING_FACEBOOK' : False, + 'ENABLE_SHARING_LINKEDIN' : False, + 'ENABLE_SHARING_IDENTICA' : False, + 'ENABLE_SHARING_GOOGLE' : False, + }, + 'USER_SETTINGS' : { + 'EDITABLE_SCREEN_NAME' : False, + 'EDITABLE_EMAIL' : False, + 'ALLOW_ADD_REMOVE_LOGIN_METHODS' : False, + 'ENABLE_GRAVATAR' : False, + } + } + }, +} + + +CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True +ASKBOT_URL = 'discussion/' +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/' + +# ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/') +ALLOW_UNICODE_SLUGS = False +ASKBOT_USE_STACKEXCHANGE_URLS = False #mimic url scheme of stackexchange +ASKBOT_CSS_DEVEL = True + +# Celery Settings +BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" +CELERY_ALWAYS_EAGER = True + +djcelery.setup_loader() + diff --git a/settings2/README.txt b/settings2/README.txt new file mode 100644 index 0000000000..64a4c910df --- /dev/null +++ b/settings2/README.txt @@ -0,0 +1,4 @@ +Transitional for moving to new settings scheme. + +To use: +django-admin runserver --settings=settings2.dev --pythonpath="." diff --git a/settings2/__init__.py b/settings2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/settings2/aws.py b/settings2/aws.py new file mode 100644 index 0000000000..138f52c255 --- /dev/null +++ b/settings2/aws.py @@ -0,0 +1,18 @@ +from common import * + +EMAIL_BACKEND = 'django_ses.SESBackend' +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' +DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' + +CSRF_COOKIE_DOMAIN = '.mitx.mit.edu' +LIB_URL = 'https://mitxstatic.s3.amazonaws.com/js/' +BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + } +} + +DEBUG = False +TEMPLATE_DEBUG = False diff --git a/settings_new_askbot.py b/settings2/common.py similarity index 51% rename from settings_new_askbot.py rename to settings2/common.py index 561abaef3a..386f3b0fa8 100644 --- a/settings_new_askbot.py +++ b/settings2/common.py @@ -1,172 +1,172 @@ +""" +This is the common settings file, intended to set sane defaults. If you have a +piece of configuration that's dependent on a set of feature flags being set, +then create a function that returns the calculated value based on the value of +MITX_FEATURES[...]. That classes that extend this one can change the feature +configuration in an environment specific config file and re-calculate those +values. + +We should make a method that calls all these config methods so that you just +make one call at the end of your site-specific dev file and it reset all the +dependent variables (like INSTALLED_APPS) for you. + +TODO: +1. Right now our treatment of static content in general and in particular + course-specific static content is haphazard. +2. We should have a more disciplined approach to feature flagging, even if it + just means that we stick them in a dict called MITX_FEATURES. +3. We need to handle configuration for multiple courses. This could be as + multiple sites, but we do need a way to map their data assets. +""" import os import platform import sys import tempfile +from path import path import djcelery +################################### FEATURES ################################### +COURSEWARE_ENABLED = True +ASKBOT_ENABLED = True +GENERATE_RANDOM_USER_CREDENTIALS = False +PERFSTATS = False + +# Features +MITX_FEATURES = { + 'SAMPLE' : False +} + +############################# SET PATH INFORMATION ############################# +PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitxweb +ENV_ROOT = PROJECT_ROOT.dirname() # virtualenv dir /mitxweb is in +#ASKBOT_ROOT = ENV_ROOT / "3rdparty" / "askbot-devel" +ASKBOT_ROOT = ENV_ROOT / "askbot-devel" +#COURSES_ROOT = ENV_ROOT / "courses" +COURSES_ROOT = ENV_ROOT / "data" + +# FIXME: code shouldn't expect trailing "/" +# FIXME: To support multiple courses, we should walk the courses dir at startup +#DATA_DIR = COURSES_ROOT / "6002x" / "" +DATA_DIR = COURSES_ROOT + +#print DATA_DIR, COURSES_ROOT, ASKBOT_ROOT, ENV_ROOT, PROJECT_ROOT + +sys.path.append(ENV_ROOT) +sys.path.append(ASKBOT_ROOT) +sys.path.append(ASKBOT_ROOT / "askbot" / "deps") +sys.path.append(PROJECT_ROOT / 'djangoapps') +sys.path.append(PROJECT_ROOT / 'lib') + +################################## MITXWEB ##################################### +# This is where we stick our compiled template files +MAKO_MODULE_DIR = tempfile.mkdtemp('mako') +TEXTBOOK_DIR = ENV_ROOT / "books" / "circuits_agarwal_lang" + +# FIXME ???????? -- +# We should have separate S3 staged URLs in case we need to make changes to +# these assets and test them. +LIB_URL = '/static/lib/' +# LIB_URL = 'https://mitxstatic.s3.amazonaws.com/js/' # For AWS deploys + +# Dev machines shouldn't need the book +# BOOK_URL = '/static/book/' +BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' # For AWS deploys + +# FIXME ??????? What are these exactly? # Configuration option for when we want to grab server error pages STATIC_GRAB = False DEV_CONTENT = True -LIB_URL = '/static/lib/' -LIB_URL = 'https://mitxstatic.s3.amazonaws.com/js/' -BOOK_URL = '/static/book/' -BOOK_URL = 'https://mitxstatic.s3.amazonaws.com/book_images/' +# FIXME: Should we be doing this truncation? +TRACK_MAX_EVENT = 1000 -# Feature Flags. These should be set to false until they are ready to deploy, and then eventually flag mechanisms removed -GENERATE_PROFILE_SCORES = False # If this is true, random scores will be generated for the purpose of debugging the profile graphs +GENERATE_PROFILE_SCORES = False -# Our parent dir (mitx_all) is the BASE_DIR -BASE_DIR = os.path.abspath(os.path.join(__file__, "..", "..")) +############################### DJANGO BUILT-INS ############################### +# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here +DEBUG = False +TEMPLATE_DEBUG = False -COURSEWARE_ENABLED = True -ASKBOT_ENABLED = True -CSRF_COOKIE_DOMAIN = '127.0.0.1' - -# Defaults to be overridden -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Site info +SITE_ID = 1 SITE_NAME = "localhost:8000" +CSRF_COOKIE_DOMAIN = '127.0.0.1' +HTTPS = 'on' +ROOT_URLCONF = 'mitxweb.urls' +# Email +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu' DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' - -GENERATE_RANDOM_USER_CREDENTIALS = False - -WIKI_REQUIRE_LOGIN_EDIT = True -WIKI_REQUIRE_LOGIN_VIEW = True - -PERFSTATS = False - -HTTPS = 'on' - -MEDIA_URL = '' -MEDIA_ROOT = '' - -# S3BotoStorage insists on a timeout for uploaded assets. We should make it -# permanent instead, but rather than trying to figure out exactly where that -# setting is, I'm just bumping the expiration time to something absurd (100 -# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3 -# in the global settings.py -AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years - -# Needed for Askbot -# Deployed machines: Move to S3 -DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - ADMINS = ( ('MITx Admins', 'admin@mitx.mit.edu'), ) - MANAGERS = ADMINS -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -TIME_ZONE = 'America/New_York' +# Static content +STATIC_URL = '/static/' +ADMIN_MEDIA_PREFIX = '/static/admin/' +STATIC_ROOT = ENV_ROOT / "staticfiles" # FIXME: Should this and uploads be moved out of the repo? -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en' +# FIXME: We should iterate through the courses we have, adding the static +# contents for each of them. +STATICFILES_DIRS = ( + # FIXME: Need to add entries for book, data/images, etc. +# PROJECT_ROOT / "static", + ENV_ROOT / "static", + ASKBOT_ROOT / "askbot" / "skins", +# ("circuits", DATA_DIR / "images"), +# ("handouts", DATA_DIR / "handouts"), +# ("subs", DATA_DIR / "subs"), +# ("book", TEXTBOOK_DIR) +) -SITE_ID = 1 +print STATICFILES_DIRS -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. +# Templates +TEMPLATE_DIRS = ( + ENV_ROOT / "templates", +# PROJECT_ROOT / "templates", +# DATA_DIR / "problems", +) + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.request', + 'askbot.context.application_settings', + #'django.core.context_processors.i18n', + 'askbot.user_messages.context_processors.user_messages',#must be before auth + 'django.core.context_processors.auth', #this is required for admin + 'django.core.context_processors.csrf', #necessary for csrf protection +) + +# Storage +DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +MEDIA_ROOT = ENV_ROOT / "uploads" +MEDIA_URL = "/discussion/upfiles/" +FILE_UPLOAD_TEMP_DIR = os.path.join(os.path.dirname(__file__), 'tmp').replace('\\','/') +FILE_UPLOAD_HANDLERS = ( + 'django.core.files.uploadhandler.MemoryFileUploadHandler', + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', +) + +# Locale/Internationalization +TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale USE_L10N = True -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'track.middleware.TrackMiddleware', - 'mitxmako.middleware.MakoMiddleware', - #'debug_toolbar.middleware.DebugToolbarMiddleware', -) - -ROOT_URLCONF = 'mitx.urls' - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'courseware', - 'student', - 'django.contrib.humanize', - 'static_template_view', - 'staticbook', - 'simplewiki', - 'track', - 'circuit', - 'perfstats', - 'util', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', -) - -#TRACK_DIR = None -DEBUG_TRACK_LOG = False -# Maximum length of a tracking string. We don't want e.g. a file upload in our log -TRACK_MAX_EVENT = 1000 -# Maximum length of log file before starting a new one. -MAXLOG = 500 - -LOG_DIR = "/tmp/" -MAKO_MODULE_DIR = None - -# Make sure we execute correctly regardless of where we're called from -execfile(os.path.join(BASE_DIR, "settings.py")) - -if MAKO_MODULE_DIR == None: - MAKO_MODULE_DIR = tempfile.mkdtemp('mako') - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. - -pid = os.getpid() +################################### LOGGING #################################### +# Might want to rewrite this to use logger code and push more things to the root +# logger. +pid = os.getpid() # So we can log which process is creating the log hostname = platform.node().split(".")[0] SYSLOG_ADDRESS = ('syslog.m.i4x.org', 514) handlers = ['console'] -if not DEBUG: - handlers.append('syslogger') +# FIXME: re-enable syslogger later +# if not DEBUG: +# handlers.append('syslogger') LOGGING = { 'version': 1, @@ -184,7 +184,7 @@ LOGGING = { }, 'handlers' : { 'console' : { - 'level' : 'DEBUG' if DEBUG else 'INFO', + 'level' : 'DEBUG', 'class' : 'logging.StreamHandler', 'formatter' : 'standard', 'stream' : sys.stdout, @@ -230,104 +230,28 @@ LOGGING = { } } +#################################### AWS ####################################### +# S3BotoStorage insists on a timeout for uploaded assets. We should make it +# permanent instead, but rather than trying to figure out exactly where that +# setting is, I'm just bumping the expiration time to something absurd (100 +# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3 +# in the global settings.py +AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years - -if PERFSTATS : - MIDDLEWARE_CLASSES = ( 'perfstats.middleware.ProfileMiddleware',) + MIDDLEWARE_CLASSES - -if 'TRACK_DIR' not in locals(): - TRACK_DIR = BASE_DIR+'/track_dir/' -if 'STATIC_ROOT' not in locals(): - STATIC_ROOT = BASE_DIR+'/staticroot/' -if 'DATA_DIR' not in locals(): - DATA_DIR = BASE_DIR+'/data/' -if 'TEXTBOOK_DIR' not in locals(): - TEXTBOOK_DIR = BASE_DIR+'/textbook/' - -if 'TEMPLATE_DIRS' not in locals(): - TEMPLATE_DIRS = ( - BASE_DIR+'/templates/', - DATA_DIR+'/templates', - TEXTBOOK_DIR, - ) - -if 'STATICFILES_DIRS' not in locals(): - STATICFILES_DIRS = ( - BASE_DIR+'/3rdParty/static', - BASE_DIR+'/static', - ) - - -if 'ASKBOT_EXTRA_SKINS_DIR' not in locals(): - ASKBOT_EXTRA_SKINS_DIR = BASE_DIR+'/askbot-devel/askbot/skins' -if 'ASKBOT_DIR' not in locals(): - ASKBOT_DIR = BASE_DIR+'/askbot-devel' - -sys.path.append(ASKBOT_DIR) -import askbot -import site - -STATICFILES_DIRS = STATICFILES_DIRS + ( ASKBOT_DIR+'/askbot/skins',) - -ASKBOT_ROOT = os.path.dirname(askbot.__file__) - -# Needed for Askbot -# Deployed machines: Move to S3 -if MEDIA_ROOT == '': - MEDIA_ROOT = ASKBOT_DIR+'/askbot/upfiles' -if MEDIA_URL == '': - MEDIA_URL = '/discussion/upfiles/' - -site.addsitedir(os.path.join(os.path.dirname(askbot.__file__), 'deps')) -TEMPLATE_LOADERS = TEMPLATE_LOADERS + ('askbot.skins.loaders.filesystem_load_template_source',) - -MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ( - 'util.middleware.ExceptionLoggingMiddleware', - 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', - 'askbot.middleware.forum_mode.ForumModeMiddleware', - 'askbot.middleware.cancel.CancelActionMiddleware', - 'django.middleware.transaction.TransactionMiddleware', - #'debug_toolbar.middleware.DebugToolbarMiddleware', - 'askbot.middleware.view_log.ViewLogMiddleware', - 'askbot.middleware.spaceless.SpacelessMiddleware', - # 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', -) - -FILE_UPLOAD_TEMP_DIR = os.path.join(os.path.dirname(__file__), 'tmp').replace('\\','/') -FILE_UPLOAD_HANDLERS = ( - 'django.core.files.uploadhandler.MemoryFileUploadHandler', - 'django.core.files.uploadhandler.TemporaryFileUploadHandler', -) +################################### ASKBOT ##################################### +ASKBOT_EXTRA_SKINS_DIR = ASKBOT_ROOT / "askbot" / "skins" ASKBOT_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') -ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes -# ASKBOT_FILE_UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') +ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 # result in bytes -PROJECT_ROOT = os.path.dirname(__file__) +CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True +ASKBOT_URL = 'discussion/' +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/' -TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.request', - 'askbot.context.application_settings', - #'django.core.context_processors.i18n', - 'askbot.user_messages.context_processors.user_messages',#must be before auth - 'django.core.context_processors.auth', #this is required for admin - 'django.core.context_processors.csrf', #necessary for csrf protection -) +ALLOW_UNICODE_SLUGS = False +ASKBOT_USE_STACKEXCHANGE_URLS = False # mimic url scheme of stackexchange +ASKBOT_CSS_DEVEL = True -INSTALLED_APPS = INSTALLED_APPS + ( - 'django.contrib.sitemaps', - 'django.contrib.admin', - 'south', - 'askbot.deps.livesettings', - 'askbot', - #'keyedcache', # TODO: Main askbot tree has this installed, but we get intermittent errors if we include it. - 'robots', - 'django_countries', - 'djcelery', - 'djkombu', - 'followit', -) - -# askbot livesettings LIVESETTINGS_OPTIONS = { 1: { 'SETTINGS' : { @@ -368,25 +292,93 @@ LIVESETTINGS_OPTIONS = { 'EDITABLE_EMAIL' : False, 'ALLOW_ADD_REMOVE_LOGIN_METHODS' : False, 'ENABLE_GRAVATAR' : False, - } - } + }, + }, }, } - -CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True -ASKBOT_URL = 'discussion/' -LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/' - -# ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/') -ALLOW_UNICODE_SLUGS = False -ASKBOT_USE_STACKEXCHANGE_URLS = False #mimic url scheme of stackexchange -ASKBOT_CSS_DEVEL = True - # Celery Settings BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" CELERY_ALWAYS_EAGER = True - djcelery.setup_loader() +################################# SIMPLEWIKI ################################### +WIKI_REQUIRE_LOGIN_EDIT = True +WIKI_REQUIRE_LOGIN_VIEW = True + +################################# Middleware ################################### +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'askbot.skins.loaders.filesystem_load_template_source', + # 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'util.middleware.ExceptionLoggingMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'track.middleware.TrackMiddleware', + 'mitxmako.middleware.MakoMiddleware', + # 'debug_toolbar.middleware.DebugToolbarMiddleware', + 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', + 'askbot.middleware.forum_mode.ForumModeMiddleware', + 'askbot.middleware.cancel.CancelActionMiddleware', + 'django.middleware.transaction.TransactionMiddleware', + 'askbot.middleware.view_log.ViewLogMiddleware', + 'askbot.middleware.spaceless.SpacelessMiddleware', + # 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', +) + +################################### APPS ####################################### +def installed_apps(): + """If you want to get a different set of INSTALLED_APPS out of this, you'll + have to set ASKBOT_ENABLED and COURSEWARE_ENABLED to True/False and call + this method. We can't just take these as params because other pieces of the + code check fo the value of these constants. + """ + # We always install these + STANDARD_APPS = ['django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.humanize', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'track', + 'util'] + COURSEWARE_APPS = ['circuit', + 'courseware', + 'student', + 'static_template_view', + 'staticbook', + 'simplewiki', + 'perfstats'] + ASKBOT_APPS = ['django.contrib.sitemaps', + 'django.contrib.admin', + 'south', + 'askbot.deps.livesettings', + 'askbot', + 'keyedcache', + 'robots', + 'django_countries', + 'djcelery', + 'djkombu', + 'followit'] + + return tuple(STANDARD_APPS + + (COURSEWARE_APPS if COURSEWARE_ENABLED else []) + + (ASKBOT_APPS if ASKBOT_ENABLED else [])) + +INSTALLED_APPS = installed_apps() diff --git a/settings2/dev.py b/settings2/dev.py new file mode 100644 index 0000000000..925f9ac02f --- /dev/null +++ b/settings2/dev.py @@ -0,0 +1,28 @@ +""" +This config file runs the simplest dev environment using sqlite, and db-based +sessions. +""" +from common import * + +CSRF_COOKIE_DOMAIN = 'localhost' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ENV_ROOT / "db" / "mitx.db", + } +} + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' + +DEBUG = True +TEMPLATE_DEBUG = False + +# This is disabling ASKBOT, but not properly overwriting INSTALLED_APPS. ??? +# It's because our ASKBOT_ENABLED here is actually shadowing the real one. +# +# ASKBOT_ENABLED = True +# MITX_FEATURES['SAMPLE'] = True # Switch to this system so we get around the shadowing +# +# INSTALLED_APPS = installed_apps() diff --git a/settings2/devplus.py b/settings2/devplus.py new file mode 100644 index 0000000000..9cc9a67779 --- /dev/null +++ b/settings2/devplus.py @@ -0,0 +1,35 @@ +""" +This config file tries to mimic the production environment more closely than the +normal dev.py. It assumes you're running a local instance of MySQL 5.1 and that +you're running memcached. +""" +from common import * + +CSRF_COOKIE_DOMAIN = 'localhost' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'wwc', # Or path to database file if using sqlite3. + 'USER': 'root', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '127.0.0.1', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '3306', # Set to empty string for default. Not used with sqlite3. + } +} + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + } +} + + +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' + +DEBUG = True +TEMPLATE_DEBUG = True \ No newline at end of file diff --git a/settings2/loadtest.py b/settings2/loadtest.py new file mode 100644 index 0000000000..172fbcc125 --- /dev/null +++ b/settings2/loadtest.py @@ -0,0 +1,3 @@ +from common import * + +GENERATE_RANDOM_USER_CREDENTIALS = True diff --git a/settings2/staging.py b/settings2/staging.py new file mode 100644 index 0000000000..0c7dba41b6 --- /dev/null +++ b/settings2/staging.py @@ -0,0 +1,13 @@ +from aws import * + +# Staging specific overrides +SITE_NAME = "staging.mitx.mit.edu" +AWS_STORAGE_BUCKET_NAME = 'mitx_askbot_stage' +CACHES['default']['LOCATION'] = ['***REMOVED***', + '***REMOVED***'] + +### Secure Data Below Here ### +SECRET_KEY = "" +AWS_ACCESS_KEY_ID = "" +AWS_SECRET_ACCESS_KEY = "" + diff --git a/settings_old_askbot.py b/settings_old_askbot.py deleted file mode 100644 index b711440d65..0000000000 --- a/settings_old_askbot.py +++ /dev/null @@ -1,245 +0,0 @@ -if 'COURSEWARE_ENABLED' not in locals(): - COURSEWARE_ENABLED = True -if 'ASKBOT_ENABLED' not in locals(): - ASKBOT_ENABLED = True -if not COURSEWARE_ENABLED: - ASKBOT_ENABLED = False - -# Defaults to be overridden -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -SITE_NAME = "localhost:8000" - -DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu' -DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' - -# For testing the login system -GENERATE_RANDOM_USER_CREDENTIALS = False - -WIKI_REQUIRE_LOGIN_EDIT = True -WIKI_REQUIRE_LOGIN_VIEW = True - -PERFSTATS = False - -HTTPS = 'on' - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('Piotr Mitros', 'staff@csail.mit.edu'), -) - -MANAGERS = ADMINS - -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -#MEDIA_ROOT = '' -#MEDIA_URL = '' - -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'track.middleware.TrackMiddleware', - 'mitxmako.middleware.MakoMiddleware', - #'debug_toolbar.middleware.DebugToolbarMiddleware', -) - -ROOT_URLCONF = 'mitx.urls' - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'courseware', - 'auth', - 'django.contrib.humanize', - 'static_template_view', - 'staticbook', - 'simplewiki', - 'track', - 'circuit', - 'perfstats', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -#TRACK_DIR = None -DEBUG_TRACK_LOG = False -# Maximum length of a tracking string. We don't want e.g. a file upload in our log -TRACK_MAX_EVENT = 1000 -# Maximum length of log file before starting a new one. -MAXLOG = 500 - -execfile("../settings.py") - -if PERFSTATS : - MIDDLEWARE_CLASSES = ( 'perfstats.middleware.ProfileMiddleware',) + MIDDLEWARE_CLASSES - -if 'TRACK_DIR' not in locals(): - TRACK_DIR = BASE_DIR+'/track_dir/' -if 'ASKBOT_EXTRA_SKINS_DIR' not in locals(): - ASKBOT_EXTRA_SKINS_DIR = BASE_DIR+'/askbot/skins' -if 'ASKBOT_DIR' not in locals(): - ASKBOT_DIR = BASE_DIR -if 'STATIC_ROOT' not in locals(): - STATIC_ROOT = BASE_DIR+'/staticroot/' -if 'DATA_DIR' not in locals(): - DATA_DIR = BASE_DIR+'/data/' -if 'TEXTBOOK_DIR' not in locals(): - TEXTBOOK_DIR = BASE_DIR+'/textbook/' - -if 'TEMPLATE_DIRS' not in locals(): - TEMPLATE_DIRS = ( - BASE_DIR+'/templates/', - DATA_DIR+'/templates', - TEXTBOOK_DIR, - ) - -if 'STATICFILES_DIRS' not in locals(): - STATICFILES_DIRS = ( - BASE_DIR+'/3rdParty/static', - BASE_DIR+'/static' - ) - -if ASKBOT_ENABLED: - import sys - sys.path.append(ASKBOT_DIR) - import os - import askbot - import site - site.addsitedir(os.path.join(os.path.dirname(askbot.__file__), 'deps')) - TEMPLATE_LOADERS = TEMPLATE_LOADERS + ('askbot.skins.loaders.filesystem_load_template_source',) - - MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + ( - 'askbot.middleware.anon_user.ConnectToSessionMessagesMiddleware', - 'askbot.middleware.pagesize.QuestionsPageSizeMiddleware', - 'askbot.middleware.cancel.CancelActionMiddleware', - 'django.middleware.transaction.TransactionMiddleware', - 'askbot.middleware.view_log.ViewLogMiddleware', - 'askbot.middleware.spaceless.SpacelessMiddleware', - 'askbot.middleware.forum_mode.ForumModeMiddleware', - ) - - FILE_UPLOAD_TEMP_DIR = os.path.join( - os.path.dirname(__file__), - 'tmp' - ).replace('\\','/') - FILE_UPLOAD_HANDLERS = ( - 'django.core.files.uploadhandler.MemoryFileUploadHandler', - 'django.core.files.uploadhandler.TemporaryFileUploadHandler', - ) - ASKBOT_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') - ASKBOT_MAX_UPLOAD_FILE_SIZE = 1024 * 1024 #result in bytes - ASKBOT_FILE_UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') - DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' - - PROJECT_ROOT = os.path.dirname(__file__) - - TEMPLATE_CONTEXT_PROCESSORS = ( - 'django.core.context_processors.request', - 'askbot.context.application_settings', - #'django.core.context_processors.i18n', - 'askbot.user_messages.context_processors.user_messages',#must be before auth - 'django.core.context_processors.auth', #this is required for admin - 'django.core.context_processors.csrf', #necessary for csrf protection - ) - - INSTALLED_APPS = INSTALLED_APPS + ( - 'django.contrib.sitemaps', - 'django.contrib.admin', - 'south', - 'askbot.deps.livesettings', - 'askbot', - #'keyedcache', # TODO: Main askbot tree has this installed, but we get intermittent errors if we include it. - 'robots', - 'django_countries', - 'djcelery', - 'djkombu', - 'followit', - ) - - CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True - ASKBOT_URL = 'discussion/' - LOGIN_REDIRECT_URL = '/' - LOGIN_URL = '/' - - ASKBOT_UPLOADED_FILES_URL = '%s%s' % (ASKBOT_URL, 'upfiles/') - ALLOW_UNICODE_SLUGS = False - ASKBOT_USE_STACKEXCHANGE_URLS = False #mimic url scheme of stackexchange - ASKBOT_CSS_DEVEL = True - - #Celery Settings - BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" - CELERY_ALWAYS_EAGER = True - - import djcelery - djcelery.setup_loader() From 0888eed9066eee49dbe9669c797e835d1060d3de Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 13:06:13 -0500 Subject: [PATCH 38/53] Post-merge cleanup --- settings2/common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/settings2/common.py b/settings2/common.py index 386f3b0fa8..6e9c98b41c 100644 --- a/settings2/common.py +++ b/settings2/common.py @@ -61,6 +61,12 @@ sys.path.append(PROJECT_ROOT / 'lib') ################################## MITXWEB ##################################### # This is where we stick our compiled template files MAKO_MODULE_DIR = tempfile.mkdtemp('mako') +MAKO_TEMPLATES = {} +MAKO_TEMPLATES['course'] = [DATA_DIR] +MAKO_TEMPLATES['sections'] = [DATA_DIR+'/sections'] +MAKO_TEMPLATES['custom_tags'] = [DATA_DIR+'/custom_tags'] +MAKO_TEMPLATES['main'] = [ENV_ROOT+'/templates/'] + TEXTBOOK_DIR = ENV_ROOT / "books" / "circuits_agarwal_lang" # FIXME ???????? -- From 9812fda559c810352557c46b67279789231a6806 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 15:46:31 -0500 Subject: [PATCH 39/53] Minor bug when restarting server with custom tags This patch has limited testing w.r.t. whether the bug is completely gone, but it appears to have no negative side effects, and fix at least one case. --- courseware/modules/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index 4c528144d2..b20ce3778c 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -33,16 +33,24 @@ from courseware import content_parser modx_module_list = [capa_module, html_module, schematic_module, seq_module, template_module, vertical_module, video_module] #print modx_module_list -# Convert list to a dictionary for lookup by tag modx_modules = {} -for module in modx_module_list: - for tag in module.Module.get_xml_tags(): - modx_modules[tag] = module.Module + +# Convert list to a dictionary for lookup by tag +def update_modules(): + global modx_modules + modx_modules = dict() + for module in modx_module_list: + for tag in module.Module.get_xml_tags(): + modx_modules[tag] = module.Module + +update_modules() def get_module_class(tag): ''' Given an XML tag (e.g. 'video'), return the associated module (e.g. video_module.Module). ''' + if tag not in modx_modules: + update_modules() return modx_modules[tag] def get_module_id(tag): From 086523e0fab797bd79d6f36e98acad808913ed24 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 16:10:55 -0500 Subject: [PATCH 40/53] Bug in urls file --- settings2/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/settings2/common.py b/settings2/common.py index 6e9c98b41c..e700dea793 100644 --- a/settings2/common.py +++ b/settings2/common.py @@ -99,7 +99,8 @@ SITE_ID = 1 SITE_NAME = "localhost:8000" CSRF_COOKIE_DOMAIN = '127.0.0.1' HTTPS = 'on' -ROOT_URLCONF = 'mitxweb.urls' +#ROOT_URLCONF = 'mitxweb.urls' +ROOT_URLCONF = 'mitx.urls' # Email EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' From f83c4927201526c65d4ea7d787f05db6f9bb3a56 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 16:35:12 -0500 Subject: [PATCH 41/53] per-section rendering works, if unclean --HG-- branch : pmitros-section --- courseware/content_parser.py | 26 ++++++++++++++++++++++-- courseware/views.py | 38 ++++++++++++++++++++++++++++++++++-- urls.py | 2 ++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 7fb7a8a298..7e2ef9ca8e 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -1,6 +1,7 @@ import hashlib import json import logging +import os import re from datetime import timedelta @@ -141,12 +142,15 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None): #to its children later. return +def user_groups(user): + # TODO: Rewrite in Django + return [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])] + def course_file(user): # TODO: Cache. filename = UserProfile.objects.get(user=user).courseware - # TODO: Rewrite in Django - groups = [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])] + groups = user_groups(user) options = {'dev_content':settings.DEV_CONTENT, 'groups' : groups} @@ -158,6 +162,24 @@ def course_file(user): propogate_downward_tag(tree, "graceperiod") return tree +def section_file(user, section): + filename = section+".xml" + + if filename not in os.listdir(settings.DATA_DIR + '/sections/'): + print filename+" not in "+str(os.listdir(settings.DATA_DIR + '/sections/')) + return None + + options = {'dev_content':settings.DEV_CONTENT, + 'groups' : user_groups(user)} + + tree = etree.XML(render_to_string(filename, options, namespace = 'sections')) + id_tag(tree) + propogate_downward_tag(tree, "due") + propogate_downward_tag(tree, "graded") + propogate_downward_tag(tree, "graceperiod") + return tree + + def module_xml(coursefile, module, id_tag, module_id): ''' Get XML for a module based on module and module_id. Assumes module occurs once in courseware XML file.. ''' diff --git a/courseware/views.py b/courseware/views.py index c02e0e6459..42e59ee06e 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -71,8 +71,8 @@ def profile(request): response = response_by_id[id] if response.grade!=None: correct=response.grade - - total=courseware.modules.capa_module.Module(etree.tostring(p), "id").max_score() # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + # TODO: Add state. Not useful now, but maybe someday problems will have randomized max scores? + total=courseware.modules.capa_module.Module(etree.tostring(p), "id").max_score() scores.append((int(correct),total, graded )) @@ -247,6 +247,40 @@ def render_accordion(request,course,chapter,section): return {'init_js':render_to_string('accordion_init.js',context), 'content':render_to_string('accordion.html',context)} +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def render_section(request, section): + ''' TODO: Consolidate with index + ''' + user = request.user + if not settings.COURSEWARE_ENABLED or not user.is_authenticated(): + return redirect('/') + +# try: + dom = content_parser.section_file(user, section) + #except: + # raise Http404 + + accordion=render_accordion(request, '', '', '') + + module_ids = dom.xpath("//@id") + + module_object_preload = list(StudentModule.objects.filter(student=user, + module_id__in=module_ids)) + + module=render_module(user, request, dom, module_object_preload) + + if 'init_js' not in module: + module['init_js']='' + + context={'init':accordion['init_js']+module['init_js'], + 'accordion':accordion['content'], + 'content':module['content'], + 'csrf':csrf(request)['csrf_token']} + + result = render_to_response('courseware.html', context) + return result + + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def index(request, course="6.002 Spring 2012", chapter="Using the System", section="Hints"): ''' Displays courseware accordion, and any associated content. diff --git a/urls.py b/urls.py index e876f4098f..6e882e1580 100644 --- a/urls.py +++ b/urls.py @@ -40,6 +40,7 @@ if settings.COURSEWARE_ENABLED: url(r'^courseware/(?P[^/]*)/(?P[^/]*)/(?P
[^/]*)/$', 'courseware.views.index', name="courseware_section"), url(r'^courseware/(?P[^/]*)/(?P[^/]*)/$', 'courseware.views.index', name="courseware_chapter"), url(r'^courseware/(?P[^/]*)/$', 'courseware.views.index', name="courseware_course"), + url(r'^section/(?P
[^/]*)/$', 'courseware.views.render_section'), url(r'^modx/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)$', 'courseware.views.modx_dispatch'), #reset_problem'), url(r'^profile$', 'courseware.views.profile'), url(r'^change_setting$', 'student.views.change_setting'), @@ -63,3 +64,4 @@ if settings.ASKBOT_ENABLED: ) urlpatterns = patterns(*urlpatterns) + From 2416587c19b33261b7074767848ca6f8cc1f5c8b Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 16:47:53 -0500 Subject: [PATCH 42/53] Minor cleanup --HG-- branch : pmitros-section --- courseware/content_parser.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 7e2ef9ca8e..536436629d 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -146,7 +146,19 @@ def user_groups(user): # TODO: Rewrite in Django return [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])] +def course_xml_process(tree): + ''' Do basic pre-processing of an XML tree. Assign IDs to all + items without. Propagate due dates, grace periods, etc. to child + items. + ''' + id_tag(tree) + propogate_downward_tag(tree, "due") + propogate_downward_tag(tree, "graded") + propogate_downward_tag(tree, "graceperiod") + def course_file(user): + ''' Given a user, return course.xml + ''' # TODO: Cache. filename = UserProfile.objects.get(user=user).courseware @@ -155,14 +167,12 @@ def course_file(user): options = {'dev_content':settings.DEV_CONTENT, 'groups' : groups} - tree = etree.XML(render_to_string(filename, options, namespace = 'course')) - id_tag(tree) - propogate_downward_tag(tree, "due") - propogate_downward_tag(tree, "graded") - propogate_downward_tag(tree, "graceperiod") + tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course'))) return tree def section_file(user, section): + ''' Given a user and the name of a section, return that section + ''' filename = section+".xml" if filename not in os.listdir(settings.DATA_DIR + '/sections/'): @@ -172,11 +182,7 @@ def section_file(user, section): options = {'dev_content':settings.DEV_CONTENT, 'groups' : user_groups(user)} - tree = etree.XML(render_to_string(filename, options, namespace = 'sections')) - id_tag(tree) - propogate_downward_tag(tree, "due") - propogate_downward_tag(tree, "graded") - propogate_downward_tag(tree, "graceperiod") + tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'sections'))) return tree From 024da20b482db60b3b614d83e7a057f7b2882051 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 20 Feb 2012 20:58:47 -0500 Subject: [PATCH 43/53] Subtitle support working --HG-- branch : pmitros-subtitles --- courseware/modules/seq_module.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/courseware/modules/seq_module.py b/courseware/modules/seq_module.py index 6ec74feace..02796a9768 100644 --- a/courseware/modules/seq_module.py +++ b/courseware/modules/seq_module.py @@ -66,28 +66,34 @@ class Module(XModule): ## Returns a set of all types of all sub-children child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree] - self.contents=[(e.get("name"),j(self.render_function(e))) \ - for e in self.xmltree] + self.titles = json.dumps(["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") != None]) \ + for e in self.xmltree]) + + self.contents = [j(self.render_function(e)) \ + for e in self.xmltree] + + print self.titles for (content, element_class) in zip(self.contents, child_classes): new_class = 'other' for c in class_priority: if c in element_class: new_class = c - content[1]['type'] = new_class + content['type'] = new_class js="" params={'items':self.contents, 'id':self.item_id, - 'position': self.position} + 'position': self.position, + 'titles':self.titles} # TODO/BUG: Destroy JavaScript should only be called for the active view # This calls it for all the views # # To fix this, we'd probably want to have some way of assigning unique # IDs to sequences. - destroy_js="".join([e[1]['destroy_js'] for e in self.contents if 'destroy_js' in e[1]]) + destroy_js="".join([e['destroy_js'] for e in self.contents if 'destroy_js' in e]) if self.xmltree.tag == 'sequential': self.init_js=js+render_to_string('seq_module.js',params) From 2a5ba5d0c7fb84e376cc315bf11862b60256f825 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 21 Feb 2012 14:43:05 -0500 Subject: [PATCH 44/53] Force askbot config to draw all config values from askbotsettings.py, and not hit the database --- settings.py | 51 +------ settings2/askbotsettings.py | 287 ++++++++++++++++++++++++++++++++++++ settings2/common.py | 49 +----- 3 files changed, 293 insertions(+), 94 deletions(-) create mode 100644 settings2/askbotsettings.py diff --git a/settings.py b/settings.py index 496e1ad79c..51c59bbecf 100644 --- a/settings.py +++ b/settings.py @@ -5,6 +5,8 @@ import tempfile import djcelery +from settings2.askbotsettings import LIVESETTINGS_OPTIONS + # Configuration option for when we want to grab server error pages STATIC_GRAB = False DEV_CONTENT = True @@ -318,7 +320,7 @@ INSTALLED_APPS = INSTALLED_APPS + ( 'south', 'askbot.deps.livesettings', 'askbot', - #'keyedcache', # TODO: Main askbot tree has this installed, but we get intermittent errors if we include it. + 'keyedcache', # TODO: Main askbot tree has this installed, but we get intermittent errors if we include it. 'robots', 'django_countries', 'djcelery', @@ -326,53 +328,6 @@ INSTALLED_APPS = INSTALLED_APPS + ( 'followit', ) -# askbot livesettings -LIVESETTINGS_OPTIONS = { - 1: { - 'SETTINGS' : { - 'FORUM_DATA_RULES' : { - 'MIN_TITLE_LENGTH' : 1, - 'MIN_QUESTION_BODY_LENGTH' : 1, - 'MIN_ANSWER_BODY_LENGTH' : 1, - - # 'ENABLE_VIDEO_EMBEDDING' : True, - # - # Enabling video requires forked version of markdown - # pip uninstall markdown2 - # pip install -e git+git://github.com/andryuha/python-markdown2.git#egg=markdown2 - }, - 'MIN_REP' : { - 'MIN_REP_TO_VOTE_UP' : 1, - 'MIN_REP_TO_VOTE_DOWN' : 1, - 'MIN_REP_TO_ANSWER_OWN_QUESTION' : 1, - 'MIN_REP_TO_ACCEPT_OWN_ANSWER' : 1, - 'MIN_REP_TO_FLAG_OFFENSIVE' : 1, - 'MIN_REP_TO_LEAVE_COMMENTS' : 1, - 'MIN_REP_TO_CLOSE_OWN_QUESTIONS' : 1, - 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS' : 1, - 'MIN_REP_TO_REOPEN_OWN_QUESTIONS' : 1, - 'MIN_REP_TO_EDIT_WIKI' : 1, - 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS' : 100, - 'MIN_REP_TO_UPLOAD_FILES' : 1, - }, - 'SOCIAL_SHARING' : { - 'ENABLE_SHARING_TWITTER' : False, - 'ENABLE_SHARING_FACEBOOK' : False, - 'ENABLE_SHARING_LINKEDIN' : False, - 'ENABLE_SHARING_IDENTICA' : False, - 'ENABLE_SHARING_GOOGLE' : False, - }, - 'USER_SETTINGS' : { - 'EDITABLE_SCREEN_NAME' : False, - 'EDITABLE_EMAIL' : False, - 'ALLOW_ADD_REMOVE_LOGIN_METHODS' : False, - 'ENABLE_GRAVATAR' : False, - } - } - }, -} - - CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True ASKBOT_URL = 'discussion/' LOGIN_REDIRECT_URL = '/' diff --git a/settings2/askbotsettings.py b/settings2/askbotsettings.py new file mode 100644 index 0000000000..8b04726a00 --- /dev/null +++ b/settings2/askbotsettings.py @@ -0,0 +1,287 @@ +# askbot livesettings +LIVESETTINGS_OPTIONS = { + 1: { + 'DB' : False, + 'SETTINGS' : { + 'ACCESS_CONTROL' : { + 'ASKBOT_CLOSED_FORUM_MODE' : True, + }, + 'BADGES' : { + 'DISCIPLINED_BADGE_MIN_UPVOTES' : 3, + 'PEER_PRESSURE_BADGE_MIN_DOWNVOTES' : 3, + 'TEACHER_BADGE_MIN_UPVOTES' : 1, + 'NICE_ANSWER_BADGE_MIN_UPVOTES' : 2, + 'GOOD_ANSWER_BADGE_MIN_UPVOTES' : 3, + 'GREAT_ANSWER_BADGE_MIN_UPVOTES' : 5, + 'NICE_QUESTION_BADGE_MIN_UPVOTES' : 2, + 'GOOD_QUESTION_BADGE_MIN_UPVOTES' : 3, + 'GREAT_QUESTION_BADGE_MIN_UPVOTES' : 5, + 'POPULAR_QUESTION_BADGE_MIN_VIEWS' : 150, + 'NOTABLE_QUESTION_BADGE_MIN_VIEWS' : 250, + 'FAMOUS_QUESTION_BADGE_MIN_VIEWS' : 500, + 'SELF_LEARNER_BADGE_MIN_UPVOTES' : 1, + 'CIVIC_DUTY_BADGE_MIN_VOTES' : 100, + 'ENLIGHTENED_BADGE_MIN_UPVOTES' : 3, + 'ASSOCIATE_EDITOR_BADGE_MIN_EDITS' : 20, + 'COMMENTATOR_BADGE_MIN_COMMENTS' : 10, + 'ENTHUSIAST_BADGE_MIN_DAYS' : 30, + 'FAVORITE_QUESTION_BADGE_MIN_STARS' : 3, + 'GURU_BADGE_MIN_UPVOTES' : 5, + 'NECROMANCER_BADGE_MIN_DELAY' : 30, + 'NECROMANCER_BADGE_MIN_UPVOTES' : 1, + 'STELLAR_QUESTION_BADGE_MIN_STARS' : 5, + 'TAXONOMIST_BADGE_MIN_USE_COUNT' : 10, + }, + 'EMAIL' : { + 'EMAIL_SUBJECT_PREFIX' : u'[Django] ', + 'EMAIL_UNIQUE' : True, + 'EMAIL_VALIDATION' : False, + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_M_AND_C' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ALL' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ANS' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ASK' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_SEL' : u'w', + 'ENABLE_UNANSWERED_REMINDERS' : False, + 'DAYS_BEFORE_SENDING_UNANSWERED_REMINDER' : 1, + 'UNANSWERED_REMINDER_FREQUENCY' : 1, + 'MAX_UNANSWERED_REMINDERS' : 5, + 'ENABLE_ACCEPT_ANSWER_REMINDERS' : False, + 'DAYS_BEFORE_SENDING_ACCEPT_ANSWER_REMINDER' : 3, + 'ACCEPT_ANSWER_REMINDER_FREQUENCY' : 3, + 'MAX_ACCEPT_ANSWER_REMINDERS' : 5, + 'ANONYMOUS_USER_EMAIL' : u'anonymous@askbot.org', + 'ALLOW_ASKING_BY_EMAIL' : False, + 'REPLACE_SPACE_WITH_DASH_IN_EMAILED_TAGS' : True, + 'MAX_ALERTS_PER_EMAIL' : 7, + }, + 'EMBEDDABLE_WIDGETS' : { + 'QUESTIONS_WIDGET_CSS' : u"\nbody {\n overflow: hidden;\n}\n#container {\n width: 200px;\n height: 350px;\n}\nul {\n list-style: none;\n padding: 5px;\n margin: 5px;\n}\nli {\n border-bottom: #CCC 1px solid;\n padding-bottom: 5px;\n padding-top: 5px;\n}\nli:last-child {\n border: none;\n}\na {\n text-decoration: none;\n color: #464646;\n font-family: 'Yanone Kaffeesatz', sans-serif;\n font-size: 15px;\n}\n", + 'QUESTIONS_WIDGET_FOOTER' : u"\n\n", + 'QUESTIONS_WIDGET_HEADER' : u'', + 'QUESTIONS_WIDGET_MAX_QUESTIONS' : 7, + }, + 'EXTERNAL_KEYS' : { + 'RECAPTCHA_KEY' : u'', + 'RECAPTCHA_SECRET' : u'', + 'FACEBOOK_KEY' : u'', + 'FACEBOOK_SECRET' : u'', + 'HOW_TO_CHANGE_LDAP_PASSWORD' : u'', + 'IDENTICA_KEY' : u'', + 'IDENTICA_SECRET' : u'', + 'GOOGLE_ANALYTICS_KEY' : u'', + 'GOOGLE_SITEMAP_CODE' : u'', + 'LDAP_PROVIDER_NAME' : u'', + 'LDAP_URL' : u'', + 'LINKEDIN_KEY' : u'', + 'LINKEDIN_SECRET' : u'', + 'TWITTER_KEY' : u'', + 'TWITTER_SECRET' : u'', + 'USE_LDAP_FOR_PASSWORD_LOGIN' : False, + 'USE_RECAPTCHA' : False, + }, + 'FLATPAGES' : { + 'FORUM_ABOUT' : u'', + 'FORUM_FAQ' : u'', + 'FORUM_PRIVACY' : u'', + }, + 'FORUM_DATA_RULES' : { + 'MIN_TITLE_LENGTH' : 1, + 'MIN_QUESTION_BODY_LENGTH' : 1, + 'MIN_ANSWER_BODY_LENGTH' : 1, + 'WIKI_ON' : True, + 'ALLOW_ASK_ANONYMOUSLY' : True, + 'ALLOW_POSTING_BEFORE_LOGGING_IN' : True, + 'ALLOW_SWAPPING_QUESTION_WITH_ANSWER' : False, + 'MAX_TAG_LENGTH' : 20, + 'MIN_TITLE_LENGTH' : 1, + 'MIN_QUESTION_BODY_LENGTH' : 1, + 'MIN_ANSWER_BODY_LENGTH' : 1, + 'MANDATORY_TAGS' : u'', + 'FORCE_LOWERCASE_TAGS' : False, + 'TAG_LIST_FORMAT' : u'list', + 'USE_WILDCARD_TAGS' : False, + 'MAX_COMMENTS_TO_SHOW' : 5, + 'MAX_COMMENT_LENGTH' : 300, + 'USE_TIME_LIMIT_TO_EDIT_COMMENT' : True, + 'MINUTES_TO_EDIT_COMMENT' : 10, + 'SAVE_COMMENT_ON_ENTER' : True, + 'MIN_SEARCH_WORD_LENGTH' : 4, + 'DECOUPLE_TEXT_QUERY_FROM_SEARCH_STATE' : False, + 'MAX_TAGS_PER_POST' : 5, + 'DEFAULT_QUESTIONS_PAGE_SIZE' : u'30', + 'UNANSWERED_QUESTION_MEANING' : u'NO_ACCEPTED_ANSWERS', + + # Enabling video requires forked version of markdown + # pip uninstall markdown2 + # pip install -e git+git://github.com/andryuha/python-markdown2.git#egg=markdown2 + 'ENABLE_VIDEO_EMBEDDING' : False, + }, + 'GENERAL_SKIN_SETTINGS' : { + 'CUSTOM_CSS' : u'', + 'CUSTOM_FOOTER' : u'', + 'CUSTOM_HEADER' : u'', + 'CUSTOM_HTML_HEAD' : u'', + 'CUSTOM_JS' : u'', + 'SITE_FAVICON' : u'/images/favicon.gif', + 'SITE_LOGO_URL' : u'/images/logo.gif', + 'SHOW_LOGO' : False, + 'LOCAL_LOGIN_ICON' : u'/images/pw-login.gif', + 'ALWAYS_SHOW_ALL_UI_FUNCTIONS' : False, + 'ASKBOT_DEFAULT_SKIN' : u'default', + 'USE_CUSTOM_HTML_HEAD' : False, + 'FOOTER_MODE' : u'default', + 'USE_CUSTOM_CSS' : False, + 'USE_CUSTOM_JS' : False, + }, + 'LEADING_SIDEBAR' : { + 'ENABLE_LEADING_SIDEBAR' : False, + 'LEADING_SIDEBAR' : u'', + }, + 'LOGIN_PROVIDERS' : { + 'PASSWORD_REGISTER_SHOW_PROVIDER_BUTTONS' : True, + 'SIGNIN_ALWAYS_SHOW_LOCAL_LOGIN' : True, + 'SIGNIN_AOL_ENABLED' : True, + 'SIGNIN_BLOGGER_ENABLED' : True, + 'SIGNIN_CLAIMID_ENABLED' : True, + 'SIGNIN_FACEBOOK_ENABLED' : True, + 'SIGNIN_FLICKR_ENABLED' : True, + 'SIGNIN_GOOGLE_ENABLED' : True, + 'SIGNIN_IDENTI.CA_ENABLED' : True, + 'SIGNIN_LINKEDIN_ENABLED' : True, + 'SIGNIN_LIVEJOURNAL_ENABLED' : True, + 'SIGNIN_LOCAL_ENABLED' : True, + 'SIGNIN_OPENID_ENABLED' : True, + 'SIGNIN_TECHNORATI_ENABLED' : True, + 'SIGNIN_TWITTER_ENABLED' : True, + 'SIGNIN_VERISIGN_ENABLED' : True, + 'SIGNIN_VIDOOP_ENABLED' : True, + 'SIGNIN_WORDPRESS_ENABLED' : True, + 'SIGNIN_WORDPRESS_SITE_ENABLED' : False, + 'SIGNIN_YAHOO_ENABLED' : True, + 'WORDPRESS_SITE_ICON' : u'/images/logo.gif', + 'WORDPRESS_SITE_URL' : '', + }, + 'LICENSE_SETTINGS' : { + 'LICENSE_ACRONYM' : u'cc-by-sa', + 'LICENSE_LOGO_URL' : u'/images/cc-by-sa.png', + 'LICENSE_TITLE' : u'Creative Commons Attribution Share Alike 3.0', + 'LICENSE_URL' : 'http://creativecommons.org/licenses/by-sa/3.0/legalcode', + 'LICENSE_USE_LOGO' : True, + 'LICENSE_USE_URL' : True, + 'USE_LICENSE' : True, + }, + 'MARKUP' : { + 'MARKUP_CODE_FRIENDLY' : False, + 'ENABLE_MATHJAX' : False, # FIXME: Test with this enabled + 'MATHJAX_BASE_URL' : u'', + 'ENABLE_AUTO_LINKING' : False, + 'AUTO_LINK_PATTERNS' : u'', + 'AUTO_LINK_URLS' : u'', + }, + 'MIN_REP' : { + 'MIN_REP_TO_ACCEPT_OWN_ANSWER' : 1, + 'MIN_REP_TO_ANSWER_OWN_QUESTION' : 1, + 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS' : 100, + 'MIN_REP_TO_CLOSE_OWN_QUESTIONS' : 1, + 'MIN_REP_TO_DELETE_OTHERS_COMMENTS' : 2000, + 'MIN_REP_TO_DELETE_OTHERS_POSTS' : 5000, + 'MIN_REP_TO_EDIT_OTHERS_POSTS' : 2000, + 'MIN_REP_TO_EDIT_WIKI' : 1, + 'MIN_REP_TO_FLAG_OFFENSIVE' : 1, + 'MIN_REP_TO_HAVE_STRONG_URL' : 250, + 'MIN_REP_TO_LEAVE_COMMENTS' : 1, + 'MIN_REP_TO_LOCK_POSTS' : 4000, + 'MIN_REP_TO_REOPEN_OWN_QUESTIONS' : 1, + 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS' : 1, + 'MIN_REP_TO_UPLOAD_FILES' : 1, + 'MIN_REP_TO_VIEW_OFFENSIVE_FLAGS' : 2000, + 'MIN_REP_TO_VOTE_DOWN' : 1, + 'MIN_REP_TO_VOTE_UP' : 1, + }, + 'QA_SITE_SETTINGS' : { + 'APP_COPYRIGHT' : u'Copyright Askbot, 2010-2011.', + 'APP_DESCRIPTION' : u'Open source question and answer forum written in Python and Django', + 'APP_KEYWORDS' : u'Askbot,CNPROG,forum,community', + 'APP_SHORT_NAME' : u'Askbot', + 'APP_TITLE' : u'Askbot: Open Source Q&A Forum', + 'APP_URL' : u'http://askbot.org', + 'FEEDBACK_SITE_URL' : u'', + 'ENABLE_GREETING_FOR_ANON_USER' : True, + 'GREETING_FOR_ANONYMOUS_USER' : u'First time here? Check out the FAQ!', + }, + 'REP_CHANGES' : { + 'MAX_REP_GAIN_PER_USER_PER_DAY' : 200, + 'REP_GAIN_FOR_ACCEPTING_ANSWER' : 2, + 'REP_GAIN_FOR_CANCELING_DOWNVOTE' : 1, + 'REP_GAIN_FOR_RECEIVING_ANSWER_ACCEPTANCE' : 15, + 'REP_GAIN_FOR_RECEIVING_DOWNVOTE_CANCELATION' : 2, + 'REP_GAIN_FOR_RECEIVING_UPVOTE' : 10, + 'REP_LOSS_FOR_CANCELING_ANSWER_ACCEPTANCE' : -2, + 'REP_LOSS_FOR_DOWNVOTING' : -2, + 'REP_LOSS_FOR_RECEIVING_CANCELATION_OF_ANSWER_ACCEPTANCE' : -5, + 'REP_LOSS_FOR_RECEIVING_DOWNVOTE' : -1, + 'REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION' : -100, + 'REP_LOSS_FOR_RECEIVING_FLAG' : -2, + 'REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION' : -30, + 'REP_LOSS_FOR_RECEIVING_UPVOTE_CANCELATION' : -10, + }, + 'SOCIAL_SHARING' : { + 'ENABLE_SHARING_TWITTER' : False, + 'ENABLE_SHARING_FACEBOOK' : False, + 'ENABLE_SHARING_LINKEDIN' : False, + 'ENABLE_SHARING_IDENTICA' : False, + 'ENABLE_SHARING_GOOGLE' : False, + }, + 'SIDEBAR_MAIN' : { + 'SIDEBAR_MAIN_AVATAR_LIMIT' : 16, + 'SIDEBAR_MAIN_FOOTER' : u'', + 'SIDEBAR_MAIN_HEADER' : u'', + 'SIDEBAR_MAIN_SHOW_AVATARS' : True, + 'SIDEBAR_MAIN_SHOW_TAGS' : True, + 'SIDEBAR_MAIN_SHOW_TAG_SELECTOR' : True, + }, + 'SIDEBAR_PROFILE' : { + 'SIDEBAR_PROFILE_FOOTER' : u'', + 'SIDEBAR_PROFILE_HEADER' : u'', + }, + 'SIDEBAR_QUESTION' : { + 'SIDEBAR_QUESTION_FOOTER' : u'', + 'SIDEBAR_QUESTION_HEADER' : u'', + 'SIDEBAR_QUESTION_SHOW_META' : True, + 'SIDEBAR_QUESTION_SHOW_RELATED' : True, + 'SIDEBAR_QUESTION_SHOW_TAGS' : True, + }, + 'SITE_MODES' : { + 'ACTIVATE_BOOTSTRAP_MODE' : False, + }, + 'SKIN_COUNTER_SETTINGS' : { + + }, + 'SPAM_AND_MODERATION' : { + 'AKISMET_API_KEY' : u'', + 'USE_AKISMET' : False, + }, + 'USER_SETTINGS' : { + 'EDITABLE_SCREEN_NAME' : False, + 'EDITABLE_EMAIL' : False, + 'ALLOW_ADD_REMOVE_LOGIN_METHODS' : False, + 'ENABLE_GRAVATAR' : False, + 'GRAVATAR_TYPE' : u'identicon', + 'NAME_OF_ANONYMOUS_USER' : u'', + 'DEFAULT_AVATAR_URL' : u'/images/nophoto.png', + 'MIN_USERNAME_LENGTH' : 1, + 'ALLOW_ACCOUNT_RECOVERY_BY_EMAIL' : True, + }, + 'VOTE_RULES' : { + 'MAX_VOTES_PER_USER_PER_DAY' : 30, + 'MAX_FLAGS_PER_USER_PER_DAY' : 5, + 'MIN_DAYS_FOR_STAFF_TO_ACCEPT_ANSWER' : 7, + 'MIN_DAYS_TO_ANSWER_OWN_QUESTION' : 0, + 'MIN_FLAGS_TO_DELETE_POST' : 5, + 'MIN_FLAGS_TO_HIDE_POST' : 3, + 'MAX_DAYS_TO_CANCEL_VOTE' : 1, + 'VOTES_LEFT_WARNING_THRESHOLD' : 5, + }, + }, + }, +} diff --git a/settings2/common.py b/settings2/common.py index e700dea793..5d5dffb084 100644 --- a/settings2/common.py +++ b/settings2/common.py @@ -23,8 +23,10 @@ import platform import sys import tempfile -from path import path import djcelery +from path import path + +from askbotsettings import LIVESETTINGS_OPTIONS ################################### FEATURES ################################### COURSEWARE_ENABLED = True @@ -259,51 +261,6 @@ ALLOW_UNICODE_SLUGS = False ASKBOT_USE_STACKEXCHANGE_URLS = False # mimic url scheme of stackexchange ASKBOT_CSS_DEVEL = True -LIVESETTINGS_OPTIONS = { - 1: { - 'SETTINGS' : { - 'FORUM_DATA_RULES' : { - 'MIN_TITLE_LENGTH' : 1, - 'MIN_QUESTION_BODY_LENGTH' : 1, - 'MIN_ANSWER_BODY_LENGTH' : 1, - - # 'ENABLE_VIDEO_EMBEDDING' : True, - # - # Enabling video requires forked version of markdown - # pip uninstall markdown2 - # pip install -e git+git://github.com/andryuha/python-markdown2.git#egg=markdown2 - }, - 'MIN_REP' : { - 'MIN_REP_TO_VOTE_UP' : 1, - 'MIN_REP_TO_VOTE_DOWN' : 1, - 'MIN_REP_TO_ANSWER_OWN_QUESTION' : 1, - 'MIN_REP_TO_ACCEPT_OWN_ANSWER' : 1, - 'MIN_REP_TO_FLAG_OFFENSIVE' : 1, - 'MIN_REP_TO_LEAVE_COMMENTS' : 1, - 'MIN_REP_TO_CLOSE_OWN_QUESTIONS' : 1, - 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS' : 1, - 'MIN_REP_TO_REOPEN_OWN_QUESTIONS' : 1, - 'MIN_REP_TO_EDIT_WIKI' : 1, - 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS' : 100, - 'MIN_REP_TO_UPLOAD_FILES' : 1, - }, - 'SOCIAL_SHARING' : { - 'ENABLE_SHARING_TWITTER' : False, - 'ENABLE_SHARING_FACEBOOK' : False, - 'ENABLE_SHARING_LINKEDIN' : False, - 'ENABLE_SHARING_IDENTICA' : False, - 'ENABLE_SHARING_GOOGLE' : False, - }, - 'USER_SETTINGS' : { - 'EDITABLE_SCREEN_NAME' : False, - 'EDITABLE_EMAIL' : False, - 'ALLOW_ADD_REMOVE_LOGIN_METHODS' : False, - 'ENABLE_GRAVATAR' : False, - }, - }, - }, -} - # Celery Settings BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" CELERY_ALWAYS_EAGER = True From 7e79e9f34ab257d84a37af7b2bb9b3017b6e9a43 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Tue, 21 Feb 2012 15:09:45 -0500 Subject: [PATCH 45/53] emergency move of askbot settings back into settings.py so staging doesn't blow up --- settings.py | 289 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 288 insertions(+), 1 deletion(-) diff --git a/settings.py b/settings.py index 51c59bbecf..e8f4342168 100644 --- a/settings.py +++ b/settings.py @@ -5,7 +5,7 @@ import tempfile import djcelery -from settings2.askbotsettings import LIVESETTINGS_OPTIONS +# from settings2.askbotsettings import LIVESETTINGS_OPTIONS # Configuration option for when we want to grab server error pages STATIC_GRAB = False @@ -338,6 +338,293 @@ ALLOW_UNICODE_SLUGS = False ASKBOT_USE_STACKEXCHANGE_URLS = False #mimic url scheme of stackexchange ASKBOT_CSS_DEVEL = True +LIVESETTINGS_OPTIONS = { + 1: { + 'DB' : False, + 'SETTINGS' : { + 'ACCESS_CONTROL' : { + 'ASKBOT_CLOSED_FORUM_MODE' : True, + }, + 'BADGES' : { + 'DISCIPLINED_BADGE_MIN_UPVOTES' : 3, + 'PEER_PRESSURE_BADGE_MIN_DOWNVOTES' : 3, + 'TEACHER_BADGE_MIN_UPVOTES' : 1, + 'NICE_ANSWER_BADGE_MIN_UPVOTES' : 2, + 'GOOD_ANSWER_BADGE_MIN_UPVOTES' : 3, + 'GREAT_ANSWER_BADGE_MIN_UPVOTES' : 5, + 'NICE_QUESTION_BADGE_MIN_UPVOTES' : 2, + 'GOOD_QUESTION_BADGE_MIN_UPVOTES' : 3, + 'GREAT_QUESTION_BADGE_MIN_UPVOTES' : 5, + 'POPULAR_QUESTION_BADGE_MIN_VIEWS' : 150, + 'NOTABLE_QUESTION_BADGE_MIN_VIEWS' : 250, + 'FAMOUS_QUESTION_BADGE_MIN_VIEWS' : 500, + 'SELF_LEARNER_BADGE_MIN_UPVOTES' : 1, + 'CIVIC_DUTY_BADGE_MIN_VOTES' : 100, + 'ENLIGHTENED_BADGE_MIN_UPVOTES' : 3, + 'ASSOCIATE_EDITOR_BADGE_MIN_EDITS' : 20, + 'COMMENTATOR_BADGE_MIN_COMMENTS' : 10, + 'ENTHUSIAST_BADGE_MIN_DAYS' : 30, + 'FAVORITE_QUESTION_BADGE_MIN_STARS' : 3, + 'GURU_BADGE_MIN_UPVOTES' : 5, + 'NECROMANCER_BADGE_MIN_DELAY' : 30, + 'NECROMANCER_BADGE_MIN_UPVOTES' : 1, + 'STELLAR_QUESTION_BADGE_MIN_STARS' : 5, + 'TAXONOMIST_BADGE_MIN_USE_COUNT' : 10, + }, + 'EMAIL' : { + 'EMAIL_SUBJECT_PREFIX' : u'[Django] ', + 'EMAIL_UNIQUE' : True, + 'EMAIL_VALIDATION' : False, + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_M_AND_C' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ALL' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ANS' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ASK' : u'w', + 'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_SEL' : u'w', + 'ENABLE_UNANSWERED_REMINDERS' : False, + 'DAYS_BEFORE_SENDING_UNANSWERED_REMINDER' : 1, + 'UNANSWERED_REMINDER_FREQUENCY' : 1, + 'MAX_UNANSWERED_REMINDERS' : 5, + 'ENABLE_ACCEPT_ANSWER_REMINDERS' : False, + 'DAYS_BEFORE_SENDING_ACCEPT_ANSWER_REMINDER' : 3, + 'ACCEPT_ANSWER_REMINDER_FREQUENCY' : 3, + 'MAX_ACCEPT_ANSWER_REMINDERS' : 5, + 'ANONYMOUS_USER_EMAIL' : u'anonymous@askbot.org', + 'ALLOW_ASKING_BY_EMAIL' : False, + 'REPLACE_SPACE_WITH_DASH_IN_EMAILED_TAGS' : True, + 'MAX_ALERTS_PER_EMAIL' : 7, + }, + 'EMBEDDABLE_WIDGETS' : { + 'QUESTIONS_WIDGET_CSS' : u"\nbody {\n overflow: hidden;\n}\n#container {\n width: 200px;\n height: 350px;\n}\nul {\n list-style: none;\n padding: 5px;\n margin: 5px;\n}\nli {\n border-bottom: #CCC 1px solid;\n padding-bottom: 5px;\n padding-top: 5px;\n}\nli:last-child {\n border: none;\n}\na {\n text-decoration: none;\n color: #464646;\n font-family: 'Yanone Kaffeesatz', sans-serif;\n font-size: 15px;\n}\n", + 'QUESTIONS_WIDGET_FOOTER' : u"\n\n", + 'QUESTIONS_WIDGET_HEADER' : u'', + 'QUESTIONS_WIDGET_MAX_QUESTIONS' : 7, + }, + 'EXTERNAL_KEYS' : { + 'RECAPTCHA_KEY' : u'', + 'RECAPTCHA_SECRET' : u'', + 'FACEBOOK_KEY' : u'', + 'FACEBOOK_SECRET' : u'', + 'HOW_TO_CHANGE_LDAP_PASSWORD' : u'', + 'IDENTICA_KEY' : u'', + 'IDENTICA_SECRET' : u'', + 'GOOGLE_ANALYTICS_KEY' : u'', + 'GOOGLE_SITEMAP_CODE' : u'', + 'LDAP_PROVIDER_NAME' : u'', + 'LDAP_URL' : u'', + 'LINKEDIN_KEY' : u'', + 'LINKEDIN_SECRET' : u'', + 'TWITTER_KEY' : u'', + 'TWITTER_SECRET' : u'', + 'USE_LDAP_FOR_PASSWORD_LOGIN' : False, + 'USE_RECAPTCHA' : False, + }, + 'FLATPAGES' : { + 'FORUM_ABOUT' : u'', + 'FORUM_FAQ' : u'', + 'FORUM_PRIVACY' : u'', + }, + 'FORUM_DATA_RULES' : { + 'MIN_TITLE_LENGTH' : 1, + 'MIN_QUESTION_BODY_LENGTH' : 1, + 'MIN_ANSWER_BODY_LENGTH' : 1, + 'WIKI_ON' : True, + 'ALLOW_ASK_ANONYMOUSLY' : True, + 'ALLOW_POSTING_BEFORE_LOGGING_IN' : True, + 'ALLOW_SWAPPING_QUESTION_WITH_ANSWER' : False, + 'MAX_TAG_LENGTH' : 20, + 'MIN_TITLE_LENGTH' : 1, + 'MIN_QUESTION_BODY_LENGTH' : 1, + 'MIN_ANSWER_BODY_LENGTH' : 1, + 'MANDATORY_TAGS' : u'', + 'FORCE_LOWERCASE_TAGS' : False, + 'TAG_LIST_FORMAT' : u'list', + 'USE_WILDCARD_TAGS' : False, + 'MAX_COMMENTS_TO_SHOW' : 5, + 'MAX_COMMENT_LENGTH' : 300, + 'USE_TIME_LIMIT_TO_EDIT_COMMENT' : True, + 'MINUTES_TO_EDIT_COMMENT' : 10, + 'SAVE_COMMENT_ON_ENTER' : True, + 'MIN_SEARCH_WORD_LENGTH' : 4, + 'DECOUPLE_TEXT_QUERY_FROM_SEARCH_STATE' : False, + 'MAX_TAGS_PER_POST' : 5, + 'DEFAULT_QUESTIONS_PAGE_SIZE' : u'30', + 'UNANSWERED_QUESTION_MEANING' : u'NO_ACCEPTED_ANSWERS', + + # Enabling video requires forked version of markdown + # pip uninstall markdown2 + # pip install -e git+git://github.com/andryuha/python-markdown2.git#egg=markdown2 + 'ENABLE_VIDEO_EMBEDDING' : False, + }, + 'GENERAL_SKIN_SETTINGS' : { + 'CUSTOM_CSS' : u'', + 'CUSTOM_FOOTER' : u'', + 'CUSTOM_HEADER' : u'', + 'CUSTOM_HTML_HEAD' : u'', + 'CUSTOM_JS' : u'', + 'SITE_FAVICON' : u'/images/favicon.gif', + 'SITE_LOGO_URL' : u'/images/logo.gif', + 'SHOW_LOGO' : False, + 'LOCAL_LOGIN_ICON' : u'/images/pw-login.gif', + 'ALWAYS_SHOW_ALL_UI_FUNCTIONS' : False, + 'ASKBOT_DEFAULT_SKIN' : u'default', + 'USE_CUSTOM_HTML_HEAD' : False, + 'FOOTER_MODE' : u'default', + 'USE_CUSTOM_CSS' : False, + 'USE_CUSTOM_JS' : False, + }, + 'LEADING_SIDEBAR' : { + 'ENABLE_LEADING_SIDEBAR' : False, + 'LEADING_SIDEBAR' : u'', + }, + 'LOGIN_PROVIDERS' : { + 'PASSWORD_REGISTER_SHOW_PROVIDER_BUTTONS' : True, + 'SIGNIN_ALWAYS_SHOW_LOCAL_LOGIN' : True, + 'SIGNIN_AOL_ENABLED' : True, + 'SIGNIN_BLOGGER_ENABLED' : True, + 'SIGNIN_CLAIMID_ENABLED' : True, + 'SIGNIN_FACEBOOK_ENABLED' : True, + 'SIGNIN_FLICKR_ENABLED' : True, + 'SIGNIN_GOOGLE_ENABLED' : True, + 'SIGNIN_IDENTI.CA_ENABLED' : True, + 'SIGNIN_LINKEDIN_ENABLED' : True, + 'SIGNIN_LIVEJOURNAL_ENABLED' : True, + 'SIGNIN_LOCAL_ENABLED' : True, + 'SIGNIN_OPENID_ENABLED' : True, + 'SIGNIN_TECHNORATI_ENABLED' : True, + 'SIGNIN_TWITTER_ENABLED' : True, + 'SIGNIN_VERISIGN_ENABLED' : True, + 'SIGNIN_VIDOOP_ENABLED' : True, + 'SIGNIN_WORDPRESS_ENABLED' : True, + 'SIGNIN_WORDPRESS_SITE_ENABLED' : False, + 'SIGNIN_YAHOO_ENABLED' : True, + 'WORDPRESS_SITE_ICON' : u'/images/logo.gif', + 'WORDPRESS_SITE_URL' : '', + }, + 'LICENSE_SETTINGS' : { + 'LICENSE_ACRONYM' : u'cc-by-sa', + 'LICENSE_LOGO_URL' : u'/images/cc-by-sa.png', + 'LICENSE_TITLE' : u'Creative Commons Attribution Share Alike 3.0', + 'LICENSE_URL' : 'http://creativecommons.org/licenses/by-sa/3.0/legalcode', + 'LICENSE_USE_LOGO' : True, + 'LICENSE_USE_URL' : True, + 'USE_LICENSE' : True, + }, + 'MARKUP' : { + 'MARKUP_CODE_FRIENDLY' : False, + 'ENABLE_MATHJAX' : False, # FIXME: Test with this enabled + 'MATHJAX_BASE_URL' : u'', + 'ENABLE_AUTO_LINKING' : False, + 'AUTO_LINK_PATTERNS' : u'', + 'AUTO_LINK_URLS' : u'', + }, + 'MIN_REP' : { + 'MIN_REP_TO_ACCEPT_OWN_ANSWER' : 1, + 'MIN_REP_TO_ANSWER_OWN_QUESTION' : 1, + 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS' : 100, + 'MIN_REP_TO_CLOSE_OWN_QUESTIONS' : 1, + 'MIN_REP_TO_DELETE_OTHERS_COMMENTS' : 2000, + 'MIN_REP_TO_DELETE_OTHERS_POSTS' : 5000, + 'MIN_REP_TO_EDIT_OTHERS_POSTS' : 2000, + 'MIN_REP_TO_EDIT_WIKI' : 1, + 'MIN_REP_TO_FLAG_OFFENSIVE' : 1, + 'MIN_REP_TO_HAVE_STRONG_URL' : 250, + 'MIN_REP_TO_LEAVE_COMMENTS' : 1, + 'MIN_REP_TO_LOCK_POSTS' : 4000, + 'MIN_REP_TO_REOPEN_OWN_QUESTIONS' : 1, + 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS' : 1, + 'MIN_REP_TO_UPLOAD_FILES' : 1, + 'MIN_REP_TO_VIEW_OFFENSIVE_FLAGS' : 2000, + 'MIN_REP_TO_VOTE_DOWN' : 1, + 'MIN_REP_TO_VOTE_UP' : 1, + }, + 'QA_SITE_SETTINGS' : { + 'APP_COPYRIGHT' : u'Copyright Askbot, 2010-2011.', + 'APP_DESCRIPTION' : u'Open source question and answer forum written in Python and Django', + 'APP_KEYWORDS' : u'Askbot,CNPROG,forum,community', + 'APP_SHORT_NAME' : u'Askbot', + 'APP_TITLE' : u'Askbot: Open Source Q&A Forum', + 'APP_URL' : u'http://askbot.org', + 'FEEDBACK_SITE_URL' : u'', + 'ENABLE_GREETING_FOR_ANON_USER' : True, + 'GREETING_FOR_ANONYMOUS_USER' : u'First time here? Check out the FAQ!', + }, + 'REP_CHANGES' : { + 'MAX_REP_GAIN_PER_USER_PER_DAY' : 200, + 'REP_GAIN_FOR_ACCEPTING_ANSWER' : 2, + 'REP_GAIN_FOR_CANCELING_DOWNVOTE' : 1, + 'REP_GAIN_FOR_RECEIVING_ANSWER_ACCEPTANCE' : 15, + 'REP_GAIN_FOR_RECEIVING_DOWNVOTE_CANCELATION' : 2, + 'REP_GAIN_FOR_RECEIVING_UPVOTE' : 10, + 'REP_LOSS_FOR_CANCELING_ANSWER_ACCEPTANCE' : -2, + 'REP_LOSS_FOR_DOWNVOTING' : -2, + 'REP_LOSS_FOR_RECEIVING_CANCELATION_OF_ANSWER_ACCEPTANCE' : -5, + 'REP_LOSS_FOR_RECEIVING_DOWNVOTE' : -1, + 'REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION' : -100, + 'REP_LOSS_FOR_RECEIVING_FLAG' : -2, + 'REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION' : -30, + 'REP_LOSS_FOR_RECEIVING_UPVOTE_CANCELATION' : -10, + }, + 'SOCIAL_SHARING' : { + 'ENABLE_SHARING_TWITTER' : False, + 'ENABLE_SHARING_FACEBOOK' : False, + 'ENABLE_SHARING_LINKEDIN' : False, + 'ENABLE_SHARING_IDENTICA' : False, + 'ENABLE_SHARING_GOOGLE' : False, + }, + 'SIDEBAR_MAIN' : { + 'SIDEBAR_MAIN_AVATAR_LIMIT' : 16, + 'SIDEBAR_MAIN_FOOTER' : u'', + 'SIDEBAR_MAIN_HEADER' : u'', + 'SIDEBAR_MAIN_SHOW_AVATARS' : True, + 'SIDEBAR_MAIN_SHOW_TAGS' : True, + 'SIDEBAR_MAIN_SHOW_TAG_SELECTOR' : True, + }, + 'SIDEBAR_PROFILE' : { + 'SIDEBAR_PROFILE_FOOTER' : u'', + 'SIDEBAR_PROFILE_HEADER' : u'', + }, + 'SIDEBAR_QUESTION' : { + 'SIDEBAR_QUESTION_FOOTER' : u'', + 'SIDEBAR_QUESTION_HEADER' : u'', + 'SIDEBAR_QUESTION_SHOW_META' : True, + 'SIDEBAR_QUESTION_SHOW_RELATED' : True, + 'SIDEBAR_QUESTION_SHOW_TAGS' : True, + }, + 'SITE_MODES' : { + 'ACTIVATE_BOOTSTRAP_MODE' : False, + }, + 'SKIN_COUNTER_SETTINGS' : { + + }, + 'SPAM_AND_MODERATION' : { + 'AKISMET_API_KEY' : u'', + 'USE_AKISMET' : False, + }, + 'USER_SETTINGS' : { + 'EDITABLE_SCREEN_NAME' : False, + 'EDITABLE_EMAIL' : False, + 'ALLOW_ADD_REMOVE_LOGIN_METHODS' : False, + 'ENABLE_GRAVATAR' : False, + 'GRAVATAR_TYPE' : u'identicon', + 'NAME_OF_ANONYMOUS_USER' : u'', + 'DEFAULT_AVATAR_URL' : u'/images/nophoto.png', + 'MIN_USERNAME_LENGTH' : 1, + 'ALLOW_ACCOUNT_RECOVERY_BY_EMAIL' : True, + }, + 'VOTE_RULES' : { + 'MAX_VOTES_PER_USER_PER_DAY' : 30, + 'MAX_FLAGS_PER_USER_PER_DAY' : 5, + 'MIN_DAYS_FOR_STAFF_TO_ACCEPT_ANSWER' : 7, + 'MIN_DAYS_TO_ANSWER_OWN_QUESTION' : 0, + 'MIN_FLAGS_TO_DELETE_POST' : 5, + 'MIN_FLAGS_TO_HIDE_POST' : 3, + 'MAX_DAYS_TO_CANCEL_VOTE' : 1, + 'VOTES_LEFT_WARNING_THRESHOLD' : 5, + }, + }, + }, +} + # Celery Settings BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport" CELERY_ALWAYS_EAGER = True From d99676432b2947dc5c1aa55215a720a7d8afedf7 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Tue, 21 Feb 2012 18:27:37 -0500 Subject: [PATCH 46/53] Rerandomize patch -- untested --HG-- branch : pmitros-rerandomize --- courseware/capa/capa_problem.py | 5 ++++- courseware/modules/capa_module.py | 28 ++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/courseware/capa/capa_problem.py b/courseware/capa/capa_problem.py index e9778008c1..7bb3bc3d22 100644 --- a/courseware/capa/capa_problem.py +++ b/courseware/capa/capa_problem.py @@ -53,7 +53,7 @@ html_special_response = {"textline":textline.render, "schematic":schematic.render} class LoncapaProblem(object): - def __init__(self, filename, id=None, state=None): + def __init__(self, filename, id=None, state=None, seed=None): ## Initialize class variables from state self.seed = None self.student_answers = dict() @@ -78,6 +78,9 @@ class LoncapaProblem(object): if 'done' in state: self.done = state['done'] + if seed != None: + self.seed = seed + # TODO: Does this deplete the Linux entropy pool? Is this fast enough? if not self.seed: self.seed=struct.unpack('i', os.urandom(4))[0] diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 0e24434da2..7952a73a04 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -81,7 +81,7 @@ class Module(XModule): # User submitted a problem, and hasn't reset. We don't want # more submissions. - if self.lcp.done and self.rerandomize: + if self.lcp.done and self.rerandomize == "always": #print "!" check_button = False save_button = False @@ -95,7 +95,7 @@ class Module(XModule): attempts_str = " ({a}/{m})".format(a=self.attempts, m=self.max_attempts) # We don't need a "save" button if infinite number of attempts and non-randomized - if self.max_attempts == None and self.rerandomize == False: + if self.max_attempts == None and self.rerandomize != "always": save_button = False # Check if explanation is available, and if so, give a link @@ -164,12 +164,12 @@ class Module(XModule): self.show_answer="closed" self.rerandomize=content_parser.item(dom2.xpath('/problem/@rerandomize')) - if self.rerandomize=="": - self.rerandomize=True - elif self.rerandomize=="false": - self.rerandomize=False - elif self.rerandomize=="true": - self.rerandomize=True + if self.rerandomize=="" or self.rerandomize=="always" or self.rerandomize=="true": + self.rerandomize="always" + elif self.rerandomize=="false" or self.rerandomize=="per_student": + self.rerandomize="per_student" + elif self.rerandomize=="never": + self.rerandomize="never" else: raise Exception("Invalid rerandomize attribute "+self.rerandomize) @@ -181,7 +181,11 @@ class Module(XModule): self.filename=content_parser.item(dom2.xpath('/problem/@filename')) filename=settings.DATA_DIR+"/problems/"+self.filename+".xml" self.name=content_parser.item(dom2.xpath('/problem/@name')) - self.lcp=LoncapaProblem(filename, self.item_id, state) + if self.rerandomize == 'Never': + seed = 1 + else: + seed = None + self.lcp=LoncapaProblem(filename, self.item_id, state, seed = seed) def handle_ajax(self, dispatch, get): if dispatch=='problem_get': @@ -270,7 +274,7 @@ class Module(XModule): # Problem submitted. Student should reset before checking # again. - if self.lcp.done and self.rerandomize: + if self.lcp.done and self.rerandomize == "always": event_info['failure']='unreset' self.tracker('save_problem_check_fail', event_info) print "cpdr" @@ -331,7 +335,7 @@ class Module(XModule): # Problem submitted. Student should reset before saving # again. - if self.lcp.done and self.rerandomize: + if self.lcp.done and self.rerandomize == "always": event_info['failure']='done' self.tracker('save_problem_fail', event_info) return "Problem needs to be reset prior to save." @@ -364,7 +368,7 @@ class Module(XModule): self.lcp.student_answers = dict() - if self.rerandomize: + if self.rerandomize == "always": self.lcp.context=dict() self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. self.lcp.seed=None From 3037a0c8ab685563f7f76ef851bd1d4a945ad88c Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Tue, 21 Feb 2012 18:36:36 -0500 Subject: [PATCH 47/53] Fixed bad commit --- courseware/content_parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 536436629d..78d435557a 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -155,6 +155,7 @@ def course_xml_process(tree): propogate_downward_tag(tree, "due") propogate_downward_tag(tree, "graded") propogate_downward_tag(tree, "graceperiod") + return tree def course_file(user): ''' Given a user, return course.xml From c52727b0e0fb241d8211900975d3b69fe5a1bd57 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 22 Feb 2012 11:01:27 -0500 Subject: [PATCH 48/53] add migration for putting an index on auth_user.email --- student/migrations/0004_add_email_index.py | 106 +++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 student/migrations/0004_add_email_index.py diff --git a/student/migrations/0004_add_email_index.py b/student/migrations/0004_add_email_index.py new file mode 100644 index 0000000000..c95e36ae96 --- /dev/null +++ b/student/migrations/0004_add_email_index.py @@ -0,0 +1,106 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + 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'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}), + 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}), + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}), + 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}), + 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}), + 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'meta': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] From 7b27b0164224e5c2044e4a5572592c8267497505 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 22 Feb 2012 13:58:03 -0500 Subject: [PATCH 49/53] Default view now info --- student/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/student/views.py b/student/views.py index f5ab302775..da82ee3ad1 100644 --- a/student/views.py +++ b/student/views.py @@ -28,7 +28,7 @@ def csrf_token(context): @ensure_csrf_cookie def index(request): if settings.COURSEWARE_ENABLED and request.user.is_authenticated(): - return redirect('/courseware') + return redirect('/info') else: csrf_token = csrf(request)['csrf_token'] # TODO: Clean up how 'error' is done. From c9bbc54238b21cb1c1975f03c80f445d890db2ac Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 22 Feb 2012 14:37:51 -0500 Subject: [PATCH 50/53] Rerandomize per_student/always/never tested and working --HG-- branch : pmitros-rerandomize --- courseware/capa/capa_problem.py | 8 ++++++-- courseware/modules/capa_module.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/courseware/capa/capa_problem.py b/courseware/capa/capa_problem.py index 7bb3bc3d22..c5a81e8100 100644 --- a/courseware/capa/capa_problem.py +++ b/courseware/capa/capa_problem.py @@ -61,6 +61,9 @@ class LoncapaProblem(object): self.done = False self.filename = filename + if seed != None: + self.seed = seed + if id: self.problem_id = id else: @@ -78,13 +81,14 @@ class LoncapaProblem(object): if 'done' in state: self.done = state['done'] - if seed != None: - self.seed = seed +# print self.seed # TODO: Does this deplete the Linux entropy pool? Is this fast enough? if not self.seed: self.seed=struct.unpack('i', os.urandom(4))[0] +# print filename, self.seed, seed + ## Parse XML file #log.debug(u"LoncapaProblem() opening file {0}".format(filename)) file_text = open(filename).read() diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index 7952a73a04..17ee6264bd 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -181,7 +181,7 @@ class Module(XModule): self.filename=content_parser.item(dom2.xpath('/problem/@filename')) filename=settings.DATA_DIR+"/problems/"+self.filename+".xml" self.name=content_parser.item(dom2.xpath('/problem/@name')) - if self.rerandomize == 'Never': + if self.rerandomize == 'never': seed = 1 else: seed = None From e18074024e697e3ad6bb7cd977aa7ccad56c43e7 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 22 Feb 2012 15:49:38 -0500 Subject: [PATCH 51/53] Missing authentication on /info --- util/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/util/views.py b/util/views.py index 83d525708a..a479277444 100644 --- a/util/views.py +++ b/util/views.py @@ -51,4 +51,7 @@ def send_feedback(request): def info(request): ''' Info page (link from main header) ''' + if not request.user.is_authenticated(): + return redirect('/') + return render_to_response("info.html", {}) From 09cc7ec35dfe848186852d2901c8613d1bdce49a Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 22 Feb 2012 21:15:25 -0500 Subject: [PATCH 52/53] Tracking now records time --- track/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/track/views.py b/track/views.py index a06baa6212..6b7256c756 100644 --- a/track/views.py +++ b/track/views.py @@ -1,6 +1,7 @@ import json import logging import os +import datetime # Create your views here. from django.http import HttpResponse @@ -39,6 +40,7 @@ def user_track(request): "event" : request.GET['event'], "agent" : agent, "page" : request.GET['page'], + "time": datetime.datetime.utcnow().isoformat(), } log_event(event) return HttpResponse('success') @@ -62,5 +64,6 @@ def server_track(request, event_type, event, page=None): "event" : event, "agent" : agent, "page" : page, + "time": datetime.datetime.utcnow().isoformat(), } log_event(event) From 49f86241ed7cab4c607c67678d9354ae0006942d Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 22 Feb 2012 21:21:44 -0500 Subject: [PATCH 53/53] Maximum tracking event is now 10k. We were losing ends of event on the main server with big sessions --- settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.py b/settings.py index e8f4342168..b35c9163ca 100644 --- a/settings.py +++ b/settings.py @@ -143,7 +143,7 @@ INSTALLED_APPS = ( #TRACK_DIR = None DEBUG_TRACK_LOG = False # Maximum length of a tracking string. We don't want e.g. a file upload in our log -TRACK_MAX_EVENT = 1000 +TRACK_MAX_EVENT = 10000 # Maximum length of log file before starting a new one. MAXLOG = 500