diff --git a/courseware/capa/calc.py b/courseware/capa/calc.py index 41ced03e58..63c5c9de01 100644 --- a/courseware/capa/calc.py +++ b/courseware/capa/calc.py @@ -1,14 +1,17 @@ import copy +import logging import math import operator +import re import numpy +import scipy.constants 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, @@ -23,19 +26,75 @@ 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") -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 ''' + 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 + ''' + # 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) 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, @@ -119,19 +178,22 @@ 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: 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) @@ -147,7 +209,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)") diff --git a/courseware/capa/capa_problem.py b/courseware/capa/capa_problem.py index 73ef168cf0..c5a81e8100 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 @@ -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() @@ -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,10 +81,14 @@ class LoncapaProblem(object): if 'done' in state: self.done = state['done'] +# 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/capa/responsetypes.py b/courseware/capa/responsetypes.py index 93ebac85f9..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 @@ -19,24 +20,36 @@ 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_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 + class numericalresponse(object): def __init__(self, xml, context): 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 = evaluator(dict(),dict(),self.tolerance) + self.tolerance = contextualize_text(self.tolerance_xml, context) 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'} @@ -72,18 +85,31 @@ class customresponse(object): # be handled by capa_problem return {} +class StudentInputError(Exception): + pass + class formularesponse(object): def __init__(self, xml, context): 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 = evaluator(dict(),dict(),self.tolerance) + 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 + 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): @@ -102,10 +128,19 @@ 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 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"} diff --git a/courseware/content_parser.py b/courseware/content_parser.py index 8f6482ccf0..78d435557a 100644 --- a/courseware/content_parser.py +++ b/courseware/content_parser.py @@ -1,16 +1,17 @@ import hashlib import json import logging +import os 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 @@ -49,7 +50,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 @@ -60,7 +61,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 @@ -86,14 +87,20 @@ 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', 'vertical':'id', 'tab':'id', - 'schematic':'id'} - + 'schematic':'id', + 'book' : 'id'} + import courseware.modules + default_ids = courseware.modules.get_default_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])) for elem in elements: @@ -135,23 +142,51 @@ 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 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 - data_template = template_lookup.get_template(filename) - - options = {'dev_content':True} - - tree = etree.XML(data_template.render(**options)) +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") return tree +def course_file(user): + ''' Given a user, return course.xml + ''' + # TODO: Cache. + filename = UserProfile.objects.get(user=user).courseware + + groups = user_groups(user) + + options = {'dev_content':settings.DEV_CONTENT, + 'groups' : groups} + + 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/'): + print filename+" not in "+str(os.listdir(settings.DATA_DIR + '/sections/')) + return None + + options = {'dev_content':settings.DEV_CONTENT, + 'groups' : user_groups(user)} + + tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'sections'))) + 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/management/commands/check_course.py b/courseware/management/commands/check_course.py index 7ca8ef8242..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), @@ -41,6 +42,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/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/module_render.py b/courseware/module_render.py index 3c863684be..92a78528f9 100644 --- a/courseware/module_render.py +++ b/courseware/module_render.py @@ -26,24 +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.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} - 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 @@ -74,18 +60,16 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ajax_url = '/modx/'+module+'/'+id+'/' - id_tag=modx_modules[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=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 @@ -100,7 +84,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 @@ -132,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} diff --git a/courseware/modules/__init__.py b/courseware/modules/__init__.py index e69de29bb2..b20ce3778c 100644 --- a/courseware/modules/__init__.py +++ b/courseware/modules/__init__.py @@ -0,0 +1,69 @@ +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 + +from courseware import content_parser + +# 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) + +#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 + +modx_modules = {} + +# 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): + ''' 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() + +def get_default_ids(): + tags = get_valid_tags() + ids = map(get_module_id, tags) + return dict(zip(tags, ids)) + diff --git a/courseware/modules/capa_module.py b/courseware/modules/capa_module.py index cd740d7dea..17ee6264bd 100644 --- a/courseware/modules/capa_module.py +++ b/courseware/modules/capa_module.py @@ -21,20 +21,23 @@ 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") -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() @@ -78,7 +81,7 @@ class LoncapaModule(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 @@ -91,6 +94,10 @@ class LoncapaModule(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 != "always": + save_button = False + # Check if explanation is available, and if so, give a link explain="" if self.lcp.done and self.explain_available=='attempted': @@ -157,12 +164,12 @@ class LoncapaModule(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) @@ -172,9 +179,13 @@ class LoncapaModule(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) + 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': @@ -250,7 +261,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 @@ -263,7 +274,7 @@ class LoncapaModule(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" @@ -274,22 +285,27 @@ class LoncapaModule(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}) @@ -319,7 +335,7 @@ class LoncapaModule(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." @@ -352,7 +368,7 @@ class LoncapaModule(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 diff --git a/courseware/modules/html_module.py b/courseware/modules/html_module.py index f1ce14ec7e..4194f73e74 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: @@ -23,7 +24,7 @@ class HtmlModule(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/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..02796a9768 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): @@ -65,28 +66,34 @@ class SequentialModule(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) diff --git a/courseware/modules/template_module.py b/courseware/modules/template_module.py new file mode 100644 index 0000000000..d9dbb613f0 --- /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(filename, params, namespace = 'custom_tags') 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..2063c18953 100644 --- a/courseware/modules/video_module.py +++ b/courseware/modules/video_module.py @@ -11,8 +11,8 @@ from x_module import XModule log = logging.getLogger("mitx.courseware.modules") -class VideoModule(XModule): - #id_attribute = 'youtube' +class Module(XModule): + id_attribute = 'youtube' video_time = 0 def handle_ajax(self, dispatch, get): @@ -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..4a379253c4 100644 --- a/courseware/modules/x_module.py +++ b/courseware/modules/x_module.py @@ -8,9 +8,10 @@ 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 - def get_xml_tags(): + @classmethod + def get_xml_tags(c): ''' Tags in the courseware file guaranteed to correspond to the module ''' return [] diff --git a/courseware/tests.py b/courseware/tests.py index 501deb776c..b0d7cad19f 100644 --- a/courseware/tests.py +++ b/courseware/tests.py @@ -1,16 +1,53 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". +import unittest -Replace this with more appropriate tests for your application. -""" +import numpy -from django.test import TestCase +import courseware.modules +import courseware.capa.calc as calc +class ModelsTest(unittest.TestCase): + def setUp(self): + pass + + def test_get_module_class(self): + 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) + 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: + 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) -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/courseware/views.py b/courseware/views.py index 5a63fb3039..b6f2644123 100644 --- a/courseware/views.py +++ b/courseware/views.py @@ -34,6 +34,38 @@ 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 + + # 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 = None, + done = 'i') + 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) def profile(request): ''' User profile. Show username, location, etc, as well as grades . @@ -42,46 +74,48 @@ 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): 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.LoncapaModule(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 @@ -89,10 +123,8 @@ 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={'course':course, - 'section':s.get("name"), - 'chapter':c.get("name"), + + score={'section':s.get("name"), 'scores':scores, 'section_total' : section_total, 'format' : format, @@ -100,7 +132,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 @@ -216,11 +253,12 @@ 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'] } + return render_to_response('profile.html', context) def format_url_params(params): @@ -244,6 +282,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/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.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..b35c9163ca --- /dev/null +++ b/settings.py @@ -0,0 +1,645 @@ +import os +import platform +import sys +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 + +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 = 10000 +# Maximum length of log file before starting a new one. +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")) + +# 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', +) + +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 + +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 + +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/'] + + +MAKO_TEMPLATES.update(ot) + +if MAKO_MODULE_DIR == None: + MAKO_MODULE_DIR = tempfile.mkdtemp('mako') + +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/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/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/settings2/common.py b/settings2/common.py new file mode 100644 index 0000000000..5d5dffb084 --- /dev/null +++ b/settings2/common.py @@ -0,0 +1,348 @@ +""" +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 + +import djcelery +from path import path + +from askbotsettings import LIVESETTINGS_OPTIONS + +################################### 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') +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 ???????? -- +# 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 + +# FIXME: Should we be doing this truncation? +TRACK_MAX_EVENT = 1000 + +GENERATE_PROFILE_SCORES = False + +############################### DJANGO BUILT-INS ############################### +# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here +DEBUG = False +TEMPLATE_DEBUG = False + +# Site info +SITE_ID = 1 +SITE_NAME = "localhost:8000" +CSRF_COOKIE_DOMAIN = '127.0.0.1' +HTTPS = 'on' +#ROOT_URLCONF = 'mitxweb.urls' +ROOT_URLCONF = 'mitx.urls' + +# Email +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu' +DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' +ADMINS = ( + ('MITx Admins', 'admin@mitx.mit.edu'), +) +MANAGERS = ADMINS + +# 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? + +# 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) +) + +print STATICFILES_DIRS + +# 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 +USE_L10N = True + +################################### 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'] +# FIXME: re-enable syslogger later +# 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', + '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 + }, + } +} + +#################################### 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 + +################################### 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 + +CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True +ASKBOT_URL = 'discussion/' +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/' + +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_new_askbot.py b/settings_new_askbot.py deleted file mode 100644 index 50a4929488..0000000000 --- a/settings_new_askbot.py +++ /dev/null @@ -1,392 +0,0 @@ -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 = ( - ('Piotr Mitros', 'staff@csail.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/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() diff --git a/static_template_view/views.py b/static_template_view/views.py index a66a3cd8c5..d3a12a9fdb 100644 --- a/static_template_view/views.py +++ b/static_template_view/views.py @@ -9,20 +9,23 @@ 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', 'copyright.html', - '404.html'] - -print "!!",settings.__dict__ + '404.html', + 'mitx_help.html'] 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'] 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/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'] diff --git a/student/models.py b/student/models.py index 6c614966f9..b9554369bc 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 diff --git a/student/views.py b/student/views.py index 397f32dc39..da82ee3ad1 100644 --- a/student/views.py +++ b/student/views.py @@ -28,27 +28,18 @@ 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. - 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'] @@ -136,9 +127,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: 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) 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) + 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.") 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", {})