Splitting files into subdirectories in preparation for merge

This commit is contained in:
Calen Pennington
2012-03-19 11:10:44 -04:00
parent 9882b30c83
commit 6dc7fd6ea5
131 changed files with 0 additions and 0 deletions

View File

View File

@@ -0,0 +1,12 @@
import uuid
from django.db import models
from django.contrib.auth.models import User
class ServerCircuit(models.Model):
# Later, add owner, who can edit, part of what app, etc.
name = models.CharField(max_length=32, unique=True, db_index=True)
schematic = models.TextField(blank=True)
def __unicode__(self):
return self.name+":"+self.schematic[:8]

View File

@@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View File

@@ -0,0 +1,66 @@
import json
import os
import xml.etree.ElementTree
from django.conf import settings
from django.http import Http404
from django.http import HttpResponse
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
from models import ServerCircuit
def circuit_line(circuit):
''' Returns string for an appropriate input element for a circuit.
TODO: Rename. '''
if not circuit.isalnum():
raise Http404()
try:
sc = ServerCircuit.objects.get(name=circuit)
schematic = sc.schematic
except:
schematic = ''
circuit_line = xml.etree.ElementTree.Element('input')
circuit_line.set('type', 'hidden')
circuit_line.set('class', 'schematic')
circuit_line.set('width', '640')
circuit_line.set('height', '480')
circuit_line.set('name', 'schematic')
circuit_line.set('id', 'schematic_'+circuit)
circuit_line.set('value', schematic) # We do it this way for security -- guarantees users cannot put funny stuff in schematic.
return xml.etree.ElementTree.tostring(circuit_line)
def edit_circuit(request, circuit):
try:
sc = ServerCircuit.objects.get(name=circuit)
except:
sc = None
if not circuit.isalnum():
raise Http404()
response = render_to_response('edit_circuit.html', {'name':circuit,
'circuit_line':circuit_line(circuit)})
response['Cache-Control'] = 'no-cache'
return response
def save_circuit(request, circuit):
if not circuit.isalnum():
raise Http404()
print dict(request.POST)
schematic = request.POST['schematic']
print schematic
try:
sc = ServerCircuit.objects.get(name=circuit)
except:
sc = ServerCircuit()
sc.name = circuit
sc.schematic = schematic
print ":", sc.schematic
sc.save()
json_str = json.dumps({'results': 'success'})
response = HttpResponse(json_str, mimetype='application/json')
response['Cache-Control'] = 'no-cache'
return response

View File

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,220 @@
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, alphanums
default_functions = {'sin' : numpy.sin,
'cos' : numpy.cos,
'tan' : numpy.tan,
'sqrt': numpy.sqrt,
'log10':numpy.log10,
'log2':numpy.log2,
'ln': numpy.log,
'arccos':numpy.arccos,
'arcsin':numpy.arcsin,
'arctan':numpy.arctan,
'abs':numpy.abs
}
default_variables = {'j':numpy.complex(0,1),
'e':numpy.e,
'pi':numpy.pi,
'k':scipy.constants.k,
'c':scipy.constants.c,
'T':298.15,
'q':scipy.constants.e
}
log = logging.getLogger("mitx.courseware.capa")
class UndefinedVariable(Exception):
def raiseself(self):
''' Helper so we can use inside of a lambda '''
raise self
general_whitespace = re.compile('[^\w]+')
def check_variables(string, variables):
''' Confirm the only variables in string are defined.
Pyparsing uses a left-to-right parser, which makes the more
elegant approach pretty hopeless.
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
undefined_variable = achar + Word(alphanums)
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
varnames = varnames | undefined_variable'''
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
bad_variables = list()
for v in possible_variables:
if len(v) == 0:
continue
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
continue
if v not in variables:
bad_variables.append(v)
if len(bad_variables)>0:
raise UndefinedVariable(' '.join(bad_variables))
def evaluator(variables, functions, string, cs=False):
''' Evaluate an expression. Variables are passed as a dictionary
from string to value. Unary functions are passed as a dictionary
from string to function. Variables must be floats.
cs: Case sensitive
TODO: Fix it so we can pass integers and complex numbers in variables dict
'''
# 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,
"*" : operator.mul,
"/" : operator.truediv,
"+" : operator.add,
"-" : operator.sub,
}
# We eliminated extreme ones, since they're rarely used, and potentially
# confusing. They may also conflict with variables if we ever allow e.g.
# 5R instead of 5*R
suffixes={'%':0.01,'k':1e3,'M':1e6,'G':1e9,
'T':1e12,#'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c':1e-2,'m':1e-3,'u':1e-6,
'n':1e-9,'p':1e-12}#,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
def super_float(text):
''' Like float, but with si extensions. 1k goes to 1000'''
if text[-1] in suffixes:
return float(text[:-1])*suffixes[text[-1]]
else:
return float(text)
def number_parse_action(x): # [ '7' ] -> [ 7 ]
return [super_float("".join(x))]
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
x = [e for e in x if type(e) in [float, numpy.float64, numpy.complex]] # Ignore ^
x.reverse()
x=reduce(lambda a,b:b**a, x)
return x
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
if len(x) == 1:
return x[0]
if 0 in x:
return float('nan')
x = [1./e for e in x if type(e) == float] # Ignore ^
return 1./sum(x)
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
total = 0.0
op = ops['+']
for e in x:
if e in set('+-'):
op = ops[e]
else:
total=op(total, e)
return total
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
prod = 1.0
op = ops['*']
for e in x:
if e in set('*/'):
op = ops[e]
else:
prod=op(prod, e)
return prod
def func_parse_action(x):
return [all_functions[x[0]](x[1])]
number_suffix=reduce(lambda a,b:a|b, map(Literal,suffixes.keys()), NoMatch()) # SI suffixes and percent
(dot,minus,plus,times,div,lpar,rpar,exp)=map(Literal,".-+*/()^")
number_part=Word(nums)
inner_number = ( number_part+Optional("."+number_part) ) | ("."+number_part) # 0.33 or 7 or .34
number=Optional(minus | plus)+ inner_number + \
Optional(CaselessLiteral("E")+Optional("-")+number_part)+ \
Optional(number_suffix) # 0.33k or -17
number=number.setParseAction( number_parse_action ) # Convert to number
# Predefine recursive variables
expr = Forward()
factor = Forward()
def sreduce(f, l):
''' Same as reduce, but handle len 1 and len 0 lists sensibly '''
if len(l)==0:
return NoMatch()
if len(l)==1:
return l[0]
return reduce(f, l)
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
# Special case for no variables because of how we understand PyParsing is put together
if len(all_variables)>0:
# We sort the list so that var names (like "e2") match before
# 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: CasedLiteral(x), all_functions.keys()))
function = funcnames+lpar.suppress()+expr+rpar.suppress()
function.setParseAction(func_parse_action)
else:
function = NoMatch()
atom = number | function | varnames | lpar+expr+rpar
factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6
paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k
paritem=paritem.setParseAction(parallel)
term = paritem + ZeroOrMore((times|div)+paritem) # 7 * 5 / 4 - 3
term = term.setParseAction(prod_parse_action)
expr << Optional((plus|minus)) + term + ZeroOrMore((plus|minus)+term) # -5 + 4 - 3
expr=expr.setParseAction(sum_parse_action)
return (expr+stringEnd).parseString(string)[0]
if __name__=='__main__':
variables={'R1':2.0, 'R3':4.0}
functions={'sin':numpy.sin, 'cos':numpy.cos}
print "X",evaluator(variables, functions, "10000||sin(7+5)-6k")
print "X",evaluator(variables, functions, "13")
print evaluator({'R1': 2.0, 'R3':4.0}, {}, "13")
print evaluator({'e1':1,'e2':1.0,'R3':7,'V0':5,'R5':15,'I1':1,'R4':6}, {},"e2")
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
print evaluator({},{}, "-1")
print evaluator({},{}, "-(7+5)")
print evaluator({},{}, "-0.33")
print evaluator({},{}, "-.33")
print evaluator({},{}, "5+7 QWSEKO")

View File

@@ -0,0 +1,286 @@
import copy
import logging
import math
import numpy
import os
import random
import re
import scipy
import struct
from lxml import etree
from lxml.etree import Element
from mako.template import Template
from util import contextualize_text
from inputtypes import textline, schematic
from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse, StudentInputError
import calc
import eia
log = logging.getLogger("mitx.courseware")
response_types = {'numericalresponse':numericalresponse,
'formularesponse':formularesponse,
'customresponse':customresponse,
'schematicresponse':schematicresponse}
entry_types = ['textline', 'schematic']
response_properties = ["responseparam", "answer"]
# How to convert from original XML to HTML
# We should do this with xlst later
html_transforms = {'problem': {'tag':'div'},
"numericalresponse": {'tag':'span'},
"customresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'},
"text": {'tag':'span'}}
global_context={'random':random,
'numpy':numpy,
'math':math,
'scipy':scipy,
'calc':calc,
'eia':eia}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"]
# These should be removed from HTML output, but keeping subelements
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text"]
# These should be transformed
html_special_response = {"textline":textline.render,
"schematic":schematic.render}
class LoncapaProblem(object):
def __init__(self, filename, id=None, state=None, seed=None):
## Initialize class variables from state
self.seed = None
self.student_answers = dict()
self.correct_map = dict()
self.done = False
self.filename = filename
if seed != None:
self.seed = seed
if id:
self.problem_id = id
else:
print "NO ID"
raise Exception("This should never happen (183)")
#self.problem_id = filename
if state:
if 'seed' in state:
self.seed = state['seed']
if 'student_answers' in state:
self.student_answers = state['student_answers']
if 'correct_map' in state:
self.correct_map = state['correct_map']
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()
# Convert startouttext and endouttext to proper <text></text>
# TODO: Do with XML operations
file_text = re.sub("startouttext\s*/","text",file_text)
file_text = re.sub("endouttext\s*/","/text",file_text)
self.tree = etree.XML(file_text)
self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map = self.student_answers)
self.context = self.extract_context(self.tree, seed=self.seed)
def get_state(self):
''' Stored per-user session data neeeded to:
1) Recreate the problem
2) Populate any student answers. '''
return {'seed':self.seed,
'student_answers':self.student_answers,
'correct_map':self.correct_map,
'done':self.done}
def get_max_score(self):
sum = 0
for et in entry_types:
sum = sum + self.tree.xpath('count(//'+et+')')
return int(sum)
def get_score(self):
correct=0
for key in self.correct_map:
if self.correct_map[key] == u'correct':
correct += 1
if (not self.student_answers) or len(self.student_answers)==0:
return {'score':0,
'total':self.get_max_score()}
else:
return {'score':correct,
'total':self.get_max_score()}
def grade_answers(self, answers):
self.student_answers = answers
context=self.extract_context(self.tree)
self.correct_map = dict()
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
grader = response_types[response.tag](response, self.context)
results = grader.grade(answers)
self.correct_map.update(results)
return self.correct_map
def get_question_answers(self):
context=self.extract_context(self.tree)
answer_map = dict()
problems_simple = self.extract_problems(self.tree)
for response in problems_simple:
responder = response_types[response.tag](response, self.context)
results = responder.get_answers()
answer_map.update(results)
for entry in problems_simple.xpath("//"+"|//".join(response_properties+entry_types)):
answer = entry.get('correct_answer')
if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
return answer_map
# ======= Private ========
def extract_context(self, tree, seed = struct.unpack('i', os.urandom(4))[0]): # private
''' Problem XML goes to Python execution context. Runs everything in script tags '''
random.seed(self.seed)
context = dict()
for script in tree.xpath('/problem/script'):
exec script.text in global_context, context
return context
def get_html(self):
return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context)
def extract_html(self, problemtree): # private
''' Helper function for get_html. Recursively converts XML tree to HTML
'''
if problemtree.tag in html_problem_semantics:
return
if problemtree.tag in html_special_response:
status = "unsubmitted"
if problemtree.get('id') in self.correct_map:
status = self.correct_map[problemtree.get('id')]
value = ""
if self.student_answers and problemtree.get('id') in self.student_answers:
value = self.student_answers[problemtree.get('id')]
return html_special_response[problemtree.tag](problemtree, value, status) #TODO
tree=Element(problemtree.tag)
for item in problemtree:
subitems = self.extract_html(item)
if subitems:
for subitem in subitems:
tree.append(subitem)
for (key,value) in problemtree.items():
tree.set(key, value)
tree.text=problemtree.text
tree.tail=problemtree.tail
if problemtree.tag in html_transforms:
tree.tag=html_transforms[problemtree.tag]['tag']
# Reset attributes. Otherwise, we get metadata in HTML
# (e.g. answers)
# TODO: We should remove and not zero them.
# I'm not sure how to do that quickly with lxml
for k in tree.keys():
tree.set(k,"")
# TODO: Fix. This loses Element().tail
#if problemtree.tag in html_skip:
# return tree
return [tree]
def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private
''' Assign IDs to all the responses
Assign sub-IDs to all entries (textline, schematic, etc.)
Annoted correctness and value
In-place transformation
'''
response_id = 1
for response in tree.xpath('//'+"|//".join(response_types)):
response_id_str=self.problem_id+"_"+str(response_id)
response.attrib['id']=response_id_str
if response_id not in correct_map:
correct = 'unsubmitted'
response.attrib['state'] = correct
response_id = response_id + 1
answer_id = 1
for entry in tree.xpath("|".join(['//'+response.tag+'[@id=$id]//'+x for x in entry_types]),
id=response_id_str):
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
entry.attrib['id'] = "%s_%i_%i"%(self.problem_id, response_id, answer_id)
answer_id=answer_id+1
def extract_problems(self, problem_tree):
''' Remove layout from the problem, and give a purified XML tree of just the problems '''
problem_tree=copy.deepcopy(problem_tree)
tree=Element('problem')
for response in problem_tree.xpath("//"+"|//".join(response_types)):
newresponse = copy.copy(response)
for e in newresponse:
newresponse.remove(e)
# copy.copy is needed to make xpath work right. Otherwise, it starts at the root
# of the tree. We should figure out if there's some work-around
for e in copy.copy(response).xpath("//"+"|//".join(response_properties+entry_types)):
newresponse.append(e)
tree.append(newresponse)
return tree
if __name__=='__main__':
problem_id='simpleFormula'
filename = 'simpleFormula.xml'
problem_id='resistor'
filename = 'resistor.xml'
lcp = LoncapaProblem(filename, problem_id)
context = lcp.extract_context(lcp.tree)
problem = lcp.extract_problems(lcp.tree)
print lcp.grade_problems({'resistor_2_1':'1.0','resistor_3_1':'2.0'})
#print lcp.grade_problems({'simpleFormula_2_1':'3*x^3'})
#numericalresponse(problem, context)
#print etree.tostring((lcp.tree))
print '============'
print
#print etree.tostring(lcp.extract_problems(lcp.tree))
print lcp.get_html()
#print extract_context(tree)
# def handle_fr(self, element):
# problem={"answer":self.contextualize_text(answer),
# "type":"formularesponse",
# "tolerance":evaluator({},{},self.contextualize_text(tolerance)),
# "sample_range":dict(zip(variables, sranges)),
# "samples_count": numsamples,
# "id":id,
# self.questions[self.lid]=problem

View File

@@ -0,0 +1,7 @@
E6=[10,15,22,33,47,68]
E12=[10,12,15,18,22,27,33,39,47,56,68,82]
E24=[10,12,15,18,22,27,33,39,47,56,68,82,11,13,16,20,24,30,36,43,51,62,75,91]
E48=[100,121,147,178,215,261,316,383,464,562,681,825,105,127,154,187,226,274,332,402,487,590,715,866,110,133,162,196,237,287,348,422,511,619,750,909,115,140,169,205,249,301,365,442,536,649,787,953]
E96=[100,121,147,178,215,261,316,383,464,562,681,825,102,124,150,182,221,267,324,392,475,576,698,845,105,127,154,187,226,274,332,402,487,590,715,866,107,130,158,191,232,280,340,412,499,604,732,887,110,133,162,196,237,287,348,422,511,619,750,909,113,137,165,200,243,294,357,432,523,634,768,931,115,140,169,205,249,301,365,442,536,649,787,953,118,143,174,210,255,309,374,453,549,665,806,976]
E192=[100,121,147,178,215,261,316,383,464,562,681,825,101,123,149,180,218,264,320,388,470,569,690,835,102,124,150,182,221,267,324,392,475,576,698,845,104,126,152,184,223,271,328,397,481,583,706,856,105,127,154,187,226,274,332,402,487,590,715,866,106,129,156,189,229,277,336,407,493,597,723,876,107,130,158,191,232,280,340,412,499,604,732,887,109,132,160,193,234,284,344,417,505,612,741,898,110,133,162,196,237,287,348,422,511,619,750,909,111,135,164,198,240,291,352,427,517,626,759,920,113,137,165,200,243,294,357,432,523,634,768,931,114,138,167,203,246,298,361,437,530,642,777,942,115,140,169,205,249,301,365,442,536,649,787,953,117,142,172,208,252,305,370,448,542,657,796,965,118,143,174,210,255,309,374,453,549,665,806,976,120,145,176,213,258,312,379,459,556,673,816,988]

View File

@@ -0,0 +1,39 @@
from lxml.etree import Element
from lxml import etree
from mitxmako.shortcuts import render_to_response, render_to_string
class textline(object):
@staticmethod
def render(element, value, state):
eid=element.get('id')
count = int(eid.split('_')[-2])-1 # HACK
context = {'id':eid, 'value':value, 'state':state, 'count':count}
html=render_to_string("textinput.html", context)
return etree.XML(html)
class schematic(object):
@staticmethod
def render(element, value, state):
eid = element.get('id')
height = element.get('height')
width = element.get('width')
parts = element.get('parts')
analyses = element.get('analyses')
initial_value = element.get('initial_value')
submit_analyses = element.get('submit_analyses')
context = {
'id':eid,
'value':value,
'initial_value':initial_value,
'state':state,
'width':width,
'height':height,
'parts':parts,
'analyses':analyses,
'submit_analyses':submit_analyses,
}
html=render_to_string("schematicinput.html", context)
return etree.XML(html)

View File

@@ -0,0 +1,187 @@
import json
import math
import numpy
import random
import scipy
import traceback
from calc import evaluator, UndefinedVariable
from django.conf import settings
from util import contextualize_text
import calc
import eia
# TODO: Should be the same object as in capa_problem
global_context={'random':random,
'numpy':numpy,
'math':math,
'scipy':scipy,
'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 = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
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]
try:
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), self.correct_answer, self.tolerance)
except:
raise StudentInputError('Invalid input -- please use a number only')
if correct:
return {self.answer_id:'correct'}
else:
return {self.answer_id:'incorrect'}
def get_answers(self):
return {self.answer_id:self.correct_answer}
class customresponse(object):
def __init__(self, xml, context):
self.xml = xml
## CRITICAL TODO: Should cover all entrytypes
## NOTE: xpath will look at root of XML tree, not just
## what's in xml. @id=id keeps us in the right customresponse.
self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))
self.context = context
answer = xml.xpath('//*[@id=$id]//answer',
id=xml.get('id'))[0]
answer_src = answer.get('src')
if answer_src != None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
else:
self.code = answer.text
def grade(self, student_answers):
submission = [student_answers[k] for k in sorted(self.answer_ids)]
self.context.update({'submission':submission})
exec self.code in global_context, self.context
return zip(sorted(self.answer_ids), self.context['correct'])
def get_answers(self):
# Since this is explicitly specified in the problem, this will
# 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 = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id'))[0]
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):
variables=self.samples.split('@')[0].split(',')
numsamples=int(self.samples.split('@')[1].split('#')[1])
sranges=zip(*map(lambda x:map(float, x.split(",")),
self.samples.split('@')[1].split('#')[0].split(':')))
ranges=dict(zip(variables, sranges))
correct = True
for i in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict()
for var in ranges:
value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value
student_variables[str(var)] = value
instructor_result = evaluator(instructor_variables,dict(),self.correct_answer, cs = self.case_sensitive)
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(uv.message+" not permitted in answer")
except:
#traceback.print_exc()
raise StudentInputError("Error in formula")
if math.isnan(student_result) or math.isinf(student_result):
return {self.answer_id:"incorrect"}
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return {self.answer_id:"incorrect"}
return {self.answer_id:"correct"}
def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word
keys and all non-numeric values stripped out. All values also
converted to float. Used so we can safely use Python contexts.
'''
d=dict([(k, float(d[k])) for k in d if type(k)==str and \
k.isalnum() and \
(type(d[k]) == float or type(d[k]) == int) ])
return d
def get_answers(self):
return {self.answer_id:self.correct_answer}
class schematicresponse(object):
def __init__(self, xml, context):
self.xml = xml
self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id',
id=xml.get('id'))
self.context = context
answer = xml.xpath('//*[@id=$id]//answer',
id=xml.get('id'))[0]
answer_src = answer.get('src')
if answer_src != None:
self.code = open(settings.DATA_DIR+'src/'+answer_src).read()
else:
self.code = answer.text
def grade(self, student_answers):
submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)]
self.context.update({'submission':submission})
exec self.code in global_context, self.context
return zip(sorted(self.answer_ids), self.context['correct'])
def get_answers(self):
# Since this is explicitly specified in the problem, this will
# be handled by capa_problem
return {}

View File

@@ -0,0 +1,132 @@
import math
import operator
from numpy import eye, array
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
base_units = ['meter', 'gram', 'second', 'ampere', 'kelvin', 'mole', 'cd']
unit_vectors = dict([(base_units[i], eye(len(base_units))[:,i]) for i in range(len(base_units))])
def unit_evaluator(unit_string, units=unit_map):
''' Evaluate an expression. Variables are passed as a dictionary
from string to value. Unary functions are passed as a dictionary
from string to function '''
if string.strip() == "":
return float('nan')
ops = { "^" : operator.pow,
"*" : operator.mul,
"/" : operator.truediv,
}
prefixes={'%':0.01,'k':1e3,'M':1e6,'G':1e9,
'T':1e12,#'P':1e15,'E':1e18,'Z':1e21,'Y':1e24,
'c':1e-2,'m':1e-3,'u':1e-6,
'n':1e-9,'p':1e-12}#,'f':1e-15,'a':1e-18,'z':1e-21,'y':1e-24}
def super_float(text):
''' Like float, but with si extensions. 1k goes to 1000'''
if text[-1] in suffixes:
return float(text[:-1])*suffixes[text[-1]]
else:
return float(text)
def number_parse_action(x): # [ '7' ] -> [ 7 ]
return [super_float("".join(x))]
def exp_parse_action(x): # [ 2 ^ 3 ^ 2 ] -> 512
x = [e for e in x if type(e) == float] # Ignore ^
x.reverse()
x=reduce(lambda a,b:b**a, x)
return x
def parallel(x): # Parallel resistors [ 1 2 ] => 2/3
if len(x) == 1:
return x[0]
if 0 in x:
return float('nan')
x = [1./e for e in x if type(e) == float] # Ignore ^
return 1./sum(x)
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
total = 0.0
op = ops['+']
for e in x:
if e in set('+-'):
op = ops[e]
else:
total=op(total, e)
return total
def prod_parse_action(x): # [ 1 * 2 / 3 ] => 0.66
prod = 1.0
op = ops['*']
for e in x:
if e in set('*/'):
op = ops[e]
else:
prod=op(prod, e)
return prod
def func_parse_action(x):
return [functions[x[0]](x[1])]
number_suffix=reduce(lambda a,b:a|b, map(Literal,suffixes.keys()), NoMatch()) # SI suffixes and percent
(dot,minus,plus,times,div,lpar,rpar,exp)=map(Literal,".-+*/()^")
number_part=Word(nums)
inner_number = ( number_part+Optional("."+number_part) ) | ("."+number_part) # 0.33 or 7 or .34
number=Optional(minus | plus)+ inner_number + \
Optional(CaselessLiteral("E")+Optional("-")+number_part)+ \
Optional(number_suffix) # 0.33k or -17
number=number.setParseAction( number_parse_action ) # Convert to number
# Predefine recursive variables
expr = Forward()
factor = Forward()
def sreduce(f, l):
''' Same as reduce, but handle len 1 and len 0 lists sensibly '''
if len(l)==0:
return NoMatch()
if len(l)==1:
return l[0]
return reduce(f, l)
# Handle variables passed in. E.g. if we have {'R':0.5}, we make the substitution.
# Special case for no variables because of how we understand PyParsing is put together
if len(variables)>0:
varnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), variables.keys()))
varnames.setParseAction(lambda x:map(lambda y:variables[y], x))
else:
varnames=NoMatch()
# Same thing for functions.
if len(functions)>0:
funcnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), functions.keys()))
function = funcnames+lpar.suppress()+expr+rpar.suppress()
function.setParseAction(func_parse_action)
else:
function = NoMatch()
atom = number | varnames | lpar+expr+rpar | function
factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6
paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k
paritem=paritem.setParseAction(parallel)
term = paritem + ZeroOrMore((times|div)+paritem) # 7 * 5 / 4 - 3
term = term.setParseAction(prod_parse_action)
expr << Optional((plus|minus)) + term + ZeroOrMore((plus|minus)+term) # -5 + 4 - 3
expr=expr.setParseAction(sum_parse_action)
return (expr+stringEnd).parseString(string)[0]
if __name__=='__main__':
variables={'R1':2.0, 'R3':4.0}
functions={'sin':math.sin, 'cos':math.cos}
print "X",evaluator(variables, functions, "10000||sin(7+5)-6k")
print "X",evaluator(variables, functions, "13")
print evaluator({'R1': 2.0, 'R3':4.0}, {}, "13")
#
print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5")
print evaluator({},{}, "-1")
print evaluator({},{}, "-(7+5)")
print evaluator({},{}, "-0.33")
print evaluator({},{}, "-.33")
print evaluator({},{}, "5+7 QWSEKO")

View File

@@ -0,0 +1,6 @@
def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context '''
for key in sorted(context, lambda x,y:cmp(len(y),len(x))):
text=text.replace('$'+key, str(context[key]))
return text

View File

@@ -0,0 +1,282 @@
import hashlib
import json
import logging
import os
import re
import sys
import urllib
from datetime import timedelta
from lxml import etree
try: # This lets us do __name__ == ='__main__'
from django.conf import settings
from django.core.cache import cache
from student.models import UserProfile
from student.models import UserTestGroup
from mitxmako.shortcuts import render_to_response, render_to_string
except:
settings = None
''' This file will eventually form an abstraction layer between the
course XML file and the rest of the system.
TODO: Shift everything from xml.dom.minidom to XPath (or XQuery)
'''
class ContentException(Exception):
pass
log = logging.getLogger("mitx.courseware")
timedelta_regex = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
def format_url_params(params):
return [ urllib.quote(string.replace(' ','_')) for string in params ]
def parse_timedelta(time_str):
parts = timedelta_regex.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
def fasthash(string):
m = hashlib.new("md4")
m.update(string)
return "id"+m.hexdigest()
def xpath(xml, query_string, **args):
''' Safe xpath query into an xml tree:
* xml is the tree.
* query_string is the query
* args are the parameters. Substitute for {params}.
We should remove this with the move to lxml.
We should also use lxml argument passing. '''
doc = etree.fromstring(xml)
#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
valid_chars="".join(map(chr, range(ord('a'),ord('z')+1)+range(ord('A'),ord('Z')+1)+range(ord('0'), ord('9')+1)))+"_ "
for e in x:
if e not in valid_chars:
raise Exception("Invalid char in xpath expression. TODO: Escape")
return x
args=dict( ((k, escape(args[k])) for k in args) )
#print args
results = doc.xpath(query_string.format(**args))
return results
def xpath_remove(tree, path):
''' Remove all items matching path from lxml tree. Works in
place.'''
items = tree.xpath(path)
for item in items:
item.getparent().remove(item)
return tree
if __name__=='__main__':
print xpath('<html><problem name="Bob"></problem></html>', '/{search}/problem[@name="{name}"]',
search='html', name="Bob")
def item(l, default="", process=lambda x:x):
if len(l)==0:
return default
elif len(l)==1:
return process(l[0])
else:
raise Exception('Malformed XML')
def id_tag(course):
''' Tag all course elements with unique IDs '''
old_ids = {'video':'youtube',
'problem':'filename',
'sequential':'id',
'html':'filename',
'vertical':'id',
'tab':'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:
if elem.get('id'):
pass
elif elem.get(default_ids[elem.tag]):
new_id = elem.get(default_ids[elem.tag])
new_id = "".join([a for a in new_id if a.isalnum()]) # Convert to alphanumeric
# Without this, a conflict may occur between an hmtl or youtube id
new_id = default_ids[elem.tag] + new_id
elem.set('id', new_id)
else:
elem.set('id', fasthash(etree.tostring(elem)))
def propogate_downward_tag(element, attribute_name, parent_attribute = None):
''' This call is to pass down an attribute to all children. If an element
has this attribute, it will be "inherited" by all of its children. If a
child (A) already has that attribute, A will keep the same attribute and
all of A's children will inherit A's attribute. This is a recursive call.'''
if (parent_attribute == None): #This is the entry call. Select all elements with this attribute
all_attributed_elements = element.xpath("//*[@" + attribute_name +"]")
for attributed_element in all_attributed_elements:
attribute_value = attributed_element.get(attribute_name)
for child_element in attributed_element:
propogate_downward_tag(child_element, attribute_name, attribute_value)
else:
'''The hack below is because we would get _ContentOnlyELements from the
iterator that can't have attributes set. We can't find API for it. If we
ever have an element which subclasses BaseElement, we will not tag it'''
if not element.get(attribute_name) and type(element) == etree._Element:
element.set(attribute_name, parent_attribute)
for child_element in element:
propogate_downward_tag(child_element, attribute_name, parent_attribute)
else:
#This element would have already been found by Xpath, so we return
#for now and trust that this element will get its turn to propogate
#to its children later.
return
def user_groups(user):
# TODO: Rewrite in Django
key = 'user_group_names_{user.id}'.format(user=user)
cache_expiration = 60 * 60 # one hour
# Kill caching on dev machines -- we switch groups a lot
if "dev" not in settings.DEFAULT_GROUPS:
group_names = cache.get(fasthash(key))
else:
group_names = None
if group_names is None:
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
cache.set(fasthash(key), group_names, cache_expiration)
return group_names
# return [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])]
def course_xml_process(tree):
''' Do basic pre-processing of an XML tree. Assign IDs to all
items without. Propagate due dates, grace periods, etc. to child
items.
'''
id_tag(tree)
propogate_downward_tag(tree, "due")
propogate_downward_tag(tree, "graded")
propogate_downward_tag(tree, "graceperiod")
return tree
def course_file(user):
''' Given a user, return course.xml'''
#import logging
#log = logging.getLogger("tracking")
#log.info( "DEBUG: cf:"+str(user) )
filename = UserProfile.objects.get(user=user).courseware # user.profile_cache.courseware
groups = user_groups(user)
options = {'dev_content':settings.DEV_CONTENT,
'groups' : groups}
cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
if "dev" not in settings.DEFAULT_GROUPS:
tree_string = cache.get(fasthash(cache_key))
else:
tree_string = None
if not tree_string:
tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course')))
tree_string = etree.tostring(tree)
cache.set(fasthash(cache_key), tree_string, 60)
else:
tree = etree.XML(tree_string)
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(user, module, id_tag, module_id):
''' Get XML for a module based on module and module_id. Assumes
module occurs once in courseware XML file or hidden section. '''
# Sanitize input
if not module.isalnum():
raise Exception("Module is not alphanumeric")
if not module_id.isalnum():
raise Exception("Module ID is not alphanumeric")
# Generate search
xpath_search='//{module}[(@{id_tag} = "{id}") or (@id = "{id}")]'.format(module=module,
id_tag=id_tag,
id=module_id)
#result_set=doc.xpathEval(xpath_search)
doc = course_file(user)
section_list = (s[:-4] for s in os.listdir(settings.DATA_DIR+'/sections') if s[-4:]=='.xml')
result_set=doc.xpath(xpath_search)
if len(result_set)<1:
for section in section_list:
try:
s = section_file(user, section)
except etree.XMLSyntaxError:
ex= sys.exc_info()
raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")")
result_set = s.xpath(xpath_search)
if len(result_set) != 0:
break
if len(result_set)>1:
print "WARNING: Potentially malformed course file", module, module_id
if len(result_set)==0:
return None
return etree.tostring(result_set[0])
#return result_set[0].serialize()
def toc_from_xml(dom, active_chapter, active_section):
name = dom.xpath('//course/@name')[0]
chapters = dom.xpath('//course[@name=$name]/chapter', name=name)
ch=list()
for c in chapters:
if c.get('name') == 'hidden':
continue
sections=list()
for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section', name=name, chname=c.get('name')):
sections.append({'name':s.get("name") or "",
'format':s.get("subtitle") if s.get("subtitle") else s.get("format") or "",
'due':s.get("due") or "",
'active':(c.get("name")==active_chapter and \
s.get("name")==active_section)})
ch.append({'name':c.get("name"),
'sections':sections,
'active':(c.get("name")==active_chapter)})
return ch

View File

@@ -0,0 +1,228 @@
import logging
import urllib
from lxml import etree
import courseware.content_parser as content_parser
from models import StudentModule
from django.conf import settings
import courseware.modules
from student.models import UserProfile
log = logging.getLogger("mitx.courseware")
def get_grade(user, 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 = 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)
def grade_sheet(student):
dom=content_parser.course_file(student)
course = dom.xpath('//course/@name')[0]
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
responses=StudentModule.objects.filter(student=student)
response_by_id = {}
for response in responses:
response_by_id[response.module_id] = response
total_scores = {}
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:
(correct,total) = get_grade(student, 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
if settings.GENERATE_PROFILE_SCORES:
if total > 1:
correct = random.randrange( max(total-2, 1) , total + 1 )
else:
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
if format and graded_total[1] > 0:
format_scores = total_scores[ format ] if format in total_scores else []
format_scores.append( graded_total + (s.get("name"),) )
total_scores[ format ] = format_scores
score={'section':s.get("name"),
'scores':scores,
'section_total' : section_total,
'format' : format,
'subtitle' : subtitle,
'due' : s.get("due") or "",
'graded' : graded,
}
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
sorted_scores = sorted( enumerate(scores), key=lambda x: -x[1]['percentage'] )
# A list of the indices of the dropped scores
dropped_indices = [score[0] for score in sorted_scores[-drop_count:]]
aggregate_score = 0
for index, score in enumerate(scores):
if index not in dropped_indices:
aggregate_score += score['percentage']
aggregate_score /= len(scores) - drop_count
return aggregate_score, dropped_indices
#Figure the homework scores
homework_scores = total_scores['Homework'] if 'Homework' in total_scores else []
homework_percentages = []
for i in range(12):
if i < len(homework_scores):
percentage = homework_scores[i][0] / float(homework_scores[i][1])
summary = "Homework {0} - {1} - {2:.0%} ({3:g}/{4:g})".format( i + 1, homework_scores[i][2] , percentage, homework_scores[i][0], homework_scores[i][1] )
else:
percentage = 0
summary = "Unreleased Homework {0} - 0% (?/?)".format(i + 1)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "Random Homework - {0:.0%} ({1:g}/{2:g})".format( percentage, points_earned, points_possible )
label = "HW {0:02d}".format(i + 1)
homework_percentages.append( {'percentage': percentage, 'summary': summary, 'label' : label} )
homework_total, homework_dropped_indices = totalWithDrops(homework_percentages, 2)
#Figure the lab scores
lab_scores = total_scores['Lab'] if 'Lab' in total_scores else []
lab_percentages = []
log.debug("lab_scores: {0}".format(lab_scores))
for i in range(12):
if i < len(lab_scores):
percentage = lab_scores[i][0] / float(lab_scores[i][1])
summary = "Lab {0} - {1} - {2:.0%} ({3:g}/{4:g})".format( i + 1, lab_scores[i][2] , percentage, lab_scores[i][0], lab_scores[i][1] )
else:
percentage = 0
summary = "Unreleased Lab {0} - 0% (?/?)".format(i + 1)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "Random Lab - {0:.0%} ({1:g}/{2:g})".format( percentage, points_earned, points_possible )
label = "Lab {0:02d}".format(i + 1)
lab_percentages.append( {'percentage': percentage, 'summary': summary, 'label' : label} )
lab_total, lab_dropped_indices = totalWithDrops(lab_percentages, 2)
#TODO: Pull this data about the midterm and final from the databse. It should be exactly similar to above, but we aren't sure how exams will be done yet.
midterm_score = ('?', '?')
midterm_percentage = 0
final_score = ('?', '?')
final_percentage = 0
if settings.GENERATE_PROFILE_SCORES:
midterm_score = (random.randrange(50, 150), 150)
midterm_percentage = midterm_score[0] / float(midterm_score[1])
final_score = (random.randrange(100, 300), 300)
final_percentage = final_score[0] / float(final_score[1])
grade_summary = [
{
'category': 'Homework',
'subscores' : homework_percentages,
'dropped_indices' : homework_dropped_indices,
'totalscore' : {'score' : homework_total, 'summary' : "Homework Average - {0:.0%}".format(homework_total)},
'totallabel' : 'HW Avg',
'weight' : 0.15,
},
{
'category': 'Labs',
'subscores' : lab_percentages,
'dropped_indices' : lab_dropped_indices,
'totalscore' : {'score' : lab_total, 'summary' : "Lab Average - {0:.0%}".format(lab_total)},
'totallabel' : 'Lab Avg',
'weight' : 0.15,
},
{
'category': 'Midterm',
'totalscore' : {'score' : midterm_percentage, 'summary' : "Midterm - {0:.0%} ({1}/{2})".format(midterm_percentage, midterm_score[0], midterm_score[1])},
'totallabel' : 'Midterm',
'weight' : 0.30,
},
{
'category': 'Final',
'totalscore' : {'score' : final_percentage, 'summary' : "Final - {0:.0%} ({1}/{2})".format(final_percentage, final_score[0], final_score[1])},
'totallabel' : 'Final',
'weight' : 0.40,
}
]
return {'grade_summary' : grade_summary,
'chapters':chapters}

View File

@@ -0,0 +1,55 @@
import os.path
from lxml import etree
from django.core.management.base import BaseCommand
from django.conf import settings
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."
def handle(self, *args, **options):
check = True
sample_user = User.objects.all()[0]
print "Attempting to load courseware"
course = course_file(sample_user)
print "Confirming all problems have alphanumeric names"
for problem in course.xpath('//problem'):
filename = problem.get('filename')
if not filename.isalnum():
print "==============> Invalid (non-alphanumeric) filename", filename
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.modules.modx_modules[module.tag]
# TODO: Abstract this out in render_module.py
try:
instance=module_class(etree.tostring(module),
module.get('id'),
ajax_url='',
state=None,
track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','destroy_js':'','init_js':'','type':'video'})
except:
print "==============> Error in ", etree.tostring(module)
check = False
print "Module render check finished"
sections_dir = settings.DATA_DIR+"sections"
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+")"
# TODO: print "Checking course properly annotated with preprocess.py"
if check:
print 'Courseware passes all checks!'
else:
print "Courseware fails some checks"

View File

@@ -0,0 +1,112 @@
# 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 'StudentModule'
db.create_table('courseware_studentmodule', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('module_type', self.gf('django.db.models.fields.CharField')(default='problem', max_length=32)),
('module_id', self.gf('django.db.models.fields.CharField')(max_length=255)),
('student', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('state', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('grade', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
))
db.send_create_signal('courseware', ['StudentModule'])
# Adding unique constraint on 'StudentModule', fields ['student', 'module_id', 'module_type']
db.create_unique('courseware_studentmodule', ['student_id', 'module_id', 'module_type'])
def backwards(self, orm):
# Removing unique constraint on 'StudentModule', fields ['student', 'module_id', 'module_type']
db.delete_unique('courseware_studentmodule', ['student_id', 'module_id', 'module_type'])
# Deleting model 'StudentModule'
db.delete_table('courseware_studentmodule')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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', 'module_type'),)", 'object_name': 'StudentModule'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'module_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'module_type': ('django.db.models.fields.CharField', [], {'default': "'problem'", 'max_length': '32'}),
'state': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'student': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
}
}
complete_apps = ['courseware']

View File

@@ -0,0 +1,120 @@
# 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 index on 'StudentModule', fields ['created']
db.create_index('courseware_studentmodule', ['created'])
# Adding index on 'StudentModule', fields ['grade']
db.create_index('courseware_studentmodule', ['grade'])
# Adding index on 'StudentModule', fields ['modified']
db.create_index('courseware_studentmodule', ['modified'])
# Adding index on 'StudentModule', fields ['module_type']
db.create_index('courseware_studentmodule', ['module_type'])
# Adding index on 'StudentModule', fields ['module_id']
db.create_index('courseware_studentmodule', ['module_id'])
def backwards(self, orm):
# Removing index on 'StudentModule', fields ['module_id']
db.delete_index('courseware_studentmodule', ['module_id'])
# Removing index on 'StudentModule', fields ['module_type']
db.delete_index('courseware_studentmodule', ['module_type'])
# Removing index on 'StudentModule', fields ['modified']
db.delete_index('courseware_studentmodule', ['modified'])
# Removing index on 'StudentModule', fields ['grade']
db.delete_index('courseware_studentmodule', ['grade'])
# Removing index on 'StudentModule', fields ['created']
db.delete_index('courseware_studentmodule', ['created'])
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', 'module_type'),)", 'object_name': 'StudentModule'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'grade': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': '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']

View File

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

View File

@@ -0,0 +1,92 @@
"""
WE'RE USING MIGRATIONS!
If you make changes to this model, be sure to create an appropriate migration
file and check it in at the same time as your model changes. To do that,
1. Go to the mitx dir
2. ./manage.py schemamigration courseware --auto description_of_your_change
3. Add the migration file created in mitx/courseware/migrations/
ASSUMPTIONS: modules have unique IDs, even across different module_types
"""
from django.db import models
from django.db.models.signals import post_save, post_delete
#from django.core.cache import cache
from django.contrib.auth.models import User
#from cache_toolbox import cache_model, cache_relation
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
class StudentModule(models.Model):
# For a homework problem, contains a JSON
# object consisting of state
MODULE_TYPES = (('problem','problem'),
('video','video'),
('html','html'),
)
## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
module_id = models.CharField(max_length=255, db_index=True) # Filename for homeworks, etc.
student = models.ForeignKey(User, db_index=True)
class Meta:
unique_together = (('student', 'module_id'),)
## Internal state of the object
state = models.TextField(null=True, blank=True)
## Grade, and are we done?
grade = models.FloatField(null=True, blank=True, db_index=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)
# done = models.CharField(max_length=16, choices=DONE_TYPES)
created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True)
def __unicode__(self):
return self.module_type+'/'+self.student.username+"/"+self.module_id+'/'+str(self.state)[:20]
# @classmethod
# def get_with_caching(cls, student, module_id):
# k = cls.key_for(student, module_id)
# student_module = cache.get(k)
# if student_module is None:
# student_module = StudentModule.objects.filter(student=student,
# module_id=module_id)[0]
# # It's possible it really doesn't exist...
# if student_module is not None:
# cache.set(k, student_module, CACHE_TIMEOUT)
# return student_module
@classmethod
def key_for(cls, student, module_id):
return "StudentModule-student_id:{0};module_id:{1}".format(student.id, module_id)
# def clear_cache_by_student_and_module_id(sender, instance, *args, **kwargs):
# k = sender.key_for(instance.student, instance.module_id)
# cache.delete(k)
# def update_cache_by_student_and_module_id(sender, instance, *args, **kwargs):
# k = sender.key_for(instance.student, instance.module_id)
# cache.set(k, instance, CACHE_TIMEOUT)
#post_save.connect(update_cache_by_student_and_module_id, sender=StudentModule, weak=False)
#post_delete.connect(clear_cache_by_student_and_module_id, sender=StudentModule, weak=False)
#cache_model(StudentModule)

View File

@@ -0,0 +1,140 @@
import StringIO
import json
import logging
import os
import sys
import sys
import urllib
import uuid
from lxml import etree
from django.conf import settings
from django.contrib.auth.models import User
from django.core.context_processors import csrf
from django.db import connection
from django.http import Http404
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template import Context
from django.template import Context, loader
from mitxmako.shortcuts import render_to_response, render_to_string
from models import StudentModule
from student.models import UserProfile
import track.views
import courseware.content_parser as content_parser
import courseware.modules
log = logging.getLogger("mitx.courseware")
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
# is broken.
for o in cache:
if o.module_type == module_type and \
o.module_id == module_id:
return o
return None
def make_track_function(request):
def f(event_type, event):
return track.views.server_track(request, event_type, event, page='x_module')
return f
def modx_dispatch(request, module=None, dispatch=None, id=None):
''' Generic view for extensions. '''
if not request.user.is_authenticated():
return redirect('/')
# Grab the student information for the module from the database
s = StudentModule.objects.filter(student=request.user,
module_id=id)
#s = StudentModule.get_with_caching(request.user, id)
if len(s) == 0 or s is None:
log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id))
raise Http404
s = s[0]
oldgrade = s.grade
oldstate = s.state
dispatch=dispatch.split('?')[0]
ajax_url = '/modx/'+module+'/'+id+'/'
# Grab the XML corresponding to the request from course.xml
xml = content_parser.module_xml(request.user, module, 'id', id)
# Create the module
instance=courseware.modules.get_module_class(module)(xml,
id,
ajax_url=ajax_url,
state=oldstate,
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
s.state=instance.get_state()
if instance.get_score():
s.grade=instance.get_score()['score']
if s.grade != oldgrade or s.state != oldstate:
s.save()
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
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=courseware.modules.get_module_class(module_type)
module_id=xml_module.get('id') #module_class.id_attribute) or ""
# Grab state from database
smod = object_cache(module_object_preload,
user,
module_type,
module_id)
if not smod: # If nothing in the database...
state=None
else:
state = smod.state
# Create a new instance
ajax_url = '/modx/'+module_type+'/'+module_id+'/'
instance=module_class(etree.tostring(xml_module),
module_id,
ajax_url=ajax_url,
state=state,
track_function = make_track_function(request),
render_function = lambda x: render_module(user, request, x, module_object_preload))
# If instance wasn't already in the database, create it
if not smod:
smod=StudentModule(student=user,
module_type = module_type,
module_id=module_id,
state=instance.get_state())
smod.save()
module_object_preload.append(smod)
# Grab content
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}
return content
def render_module(user, request, module, module_object_preload):
''' Generic dispatch for internal modules. '''
if module==None :
return {"content":""}
return render_x_module(user, request, module, module_object_preload)

View File

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

View File

@@ -0,0 +1,382 @@
import StringIO
import datetime
import dateutil
import dateutil.parser
import json
import logging
import math
import numpy
import os
import random
import scipy
import struct
import sys
import traceback
from lxml import etree
## TODO: Abstract out from Django
from django.conf import settings
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, StudentInputError
import courseware.content_parser as content_parser
log = logging.getLogger("mitx.courseware")
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.
'''
id_attribute = "filename"
@classmethod
def get_xml_tags(c):
return ["problem"]
def get_state(self):
state = self.lcp.get_state()
state['attempts'] = self.attempts
return json.dumps(state)
def get_score(self):
return self.lcp.get_score()
def max_score(self):
return self.lcp.get_max_score()
def get_html(self):
return render_to_string('problem_ajax.html',
{'id':self.item_id,
'ajax_url':self.ajax_url,
})
def get_init_js(self):
return render_to_string('problem.js',
{'id':self.item_id,
'ajax_url':self.ajax_url,
})
def get_problem_html(self, encapsulate=True):
html = self.lcp.get_html()
content={'name':self.name,
'html':html}
check_button = True
reset_button = True
save_button = True
# If we're after deadline, or user has exhuasted attempts,
# question is read-only.
if self.closed():
check_button = False
reset_button = False
save_button = False
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if self.lcp.done and self.rerandomize == "always":
#print "!"
check_button = False
save_button = False
# User hasn't submitted an answer yet -- we don't want resets
if not self.lcp.done:
reset_button = False
attempts_str = ""
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':
explain=self.explanation
if self.closed() and self.explain_available=='closed':
explain=self.explanation
if len(explain) == 0:
explain = False
html=render_to_string('problem.html',
{'problem' : content,
'id' : self.item_id,
'check_button' : check_button,
'reset_button' : reset_button,
'save_button' : save_button,
'answer_available' : self.answer_available(),
'ajax_url' : self.ajax_url,
'attempts': attempts_str,
'explain': explain
})
if encapsulate:
html = '<div id="main_{id}">'.format(id=self.item_id)+html+"</div>"
return html
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None, meta = None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, track_function, render_function)
self.attempts = 0
self.max_attempts = None
dom2 = etree.fromstring(xml)
self.explanation=content_parser.item(dom2.xpath('/problem/@explain'), default="closed")
self.explain_available=content_parser.item(dom2.xpath('/problem/@explain_available'))
display_due_date_string=content_parser.item(dom2.xpath('/problem/@due'))
if len(display_due_date_string)>0:
self.display_due_date=dateutil.parser.parse(display_due_date_string)
#log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date))
else:
self.display_due_date=None
grace_period_string = content_parser.item(dom2.xpath('/problem/@graceperiod'))
if len(grace_period_string)>0 and self.display_due_date:
self.grace_period = content_parser.parse_timedelta(grace_period_string)
self.close_date = self.display_due_date + self.grace_period
#log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date))
else:
self.grace_period = None
self.close_date = self.display_due_date
self.max_attempts=content_parser.item(dom2.xpath('/problem/@attempts'))
if len(self.max_attempts)>0:
self.max_attempts=int(self.max_attempts)
else:
self.max_attempts=None
self.show_answer=content_parser.item(dom2.xpath('/problem/@showanswer'))
if self.show_answer=="":
self.show_answer="closed"
self.rerandomize=content_parser.item(dom2.xpath('/problem/@rerandomize'))
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)
if state!=None:
state=json.loads(state)
if state!=None and 'attempts' in state:
self.attempts=state['attempts']
self.filename=content_parser.item(dom2.xpath('/problem/@filename'))
filename=settings.DATA_DIR+"/problems/"+self.filename+".xml"
self.name=content_parser.item(dom2.xpath('/problem/@name'))
if self.rerandomize == 'never':
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':
response = self.get_problem(get)
elif False: #self.close_date >
return json.dumps({"error":"Past due date"})
elif dispatch=='problem_check':
response = self.check_problem(get)
elif dispatch=='problem_reset':
response = self.reset_problem(get)
elif dispatch=='problem_save':
response = self.save_problem(get)
elif dispatch=='problem_show':
response = self.get_answer(get)
else:
return "Error"
return response
def closed(self):
''' Is the student still allowed to submit answers? '''
if self.attempts == self.max_attempts:
return True
if self.close_date != None and datetime.datetime.utcnow() > self.close_date:
return True
return False
def answer_available(self):
''' Is the user allowed to see an answer?
'''
if self.show_answer == '':
return False
if self.show_answer == "never":
return False
if self.show_answer == 'attempted' and self.attempts == 0:
return False
if self.show_answer == 'attempted' and self.attempts > 0:
return True
if self.show_answer == 'answered' and self.lcp.done:
return True
if self.show_answer == 'answered' and not self.lcp.done:
return False
if self.show_answer == 'closed' and self.closed():
return True
if self.show_answer == 'closed' and not self.closed():
return False
print "aa", self.show_answer
raise Http404
def get_answer(self, get):
if not self.answer_available():
raise Http404
else:
return json.dumps(self.lcp.get_question_answers())
# Figure out if we should move these to capa_problem?
def get_problem(self, get):
''' Same as get_problem_html -- if we want to reconfirm we
have the right thing e.g. after several AJAX calls.'''
return self.get_problem_html(encapsulate=False)
def check_problem(self, get):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers'''
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['filename'] = self.filename
answers=dict()
# input_resistor_1 ==> resistor_1
for key in get:
answers['_'.join(key.split('_')[1:])]=get[key]
# print "XXX", answers, get
event_info['answers']=answers
# Too late. Cannot submit
if self.closed():
event_info['failure']='closed'
self.tracker('save_problem_check_fail', event_info)
print "cp"
raise Http404
# Problem submitted. Student should reset before checking
# again.
if self.lcp.done and self.rerandomize == "always":
event_info['failure']='unreset'
self.tracker('save_problem_check_fail', event_info)
print "cpdr"
raise Http404
try:
old_state = self.lcp.get_state()
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()
return json.dumps({'success':'Unknown Error'})
self.attempts = self.attempts + 1
self.lcp.done=True
success = 'correct'
for i in correct_map:
if correct_map[i]!='correct':
success = 'incorrect'
js=json.dumps({'correct_map' : correct_map,
'success' : success})
event_info['correct_map']=correct_map
event_info['success']=success
self.tracker('save_problem_check', event_info)
return js
def save_problem(self, get):
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['filename'] = self.filename
answers=dict()
for key in get:
answers['_'.join(key.split('_')[1:])]=get[key]
event_info['answers'] = answers
# Too late. Cannot submit
if self.closed():
event_info['failure']='closed'
self.tracker('save_problem_fail', event_info)
return "Problem is closed"
# Problem submitted. Student should reset before saving
# again.
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."
self.lcp.student_answers=answers
self.tracker('save_problem_fail', event_info)
return json.dumps({'success':True})
def reset_problem(self, get):
''' Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself. '''
event_info = dict()
event_info['old_state']=self.lcp.get_state()
event_info['filename']=self.filename
if self.closed():
event_info['failure']='closed'
self.tracker('reset_problem_fail', event_info)
return "Problem is closed"
if not self.lcp.done:
event_info['failure']='not_done'
self.tracker('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting."
self.lcp.done=False
self.lcp.answers=dict()
self.lcp.correct_map=dict()
self.lcp.student_answers = dict()
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
filename=settings.DATA_DIR+"problems/"+self.filename+".xml"
self.lcp=LoncapaProblem(filename, self.item_id, self.lcp.get_state())
event_info['new_state']=self.lcp.get_state()
self.tracker('reset_problem', event_info)
return json.dumps(self.get_problem_html(encapsulate=False))

View File

@@ -0,0 +1,38 @@
import json
## 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):
id_attribute = 'filename'
def get_state(self):
return json.dumps({ })
@classmethod
def get_xml_tags(c):
return ["html"]
def get_html(self):
if self.filename==None:
xmltree=etree.fromstring(self.xml)
textlist=[xmltree.text]+[etree.tostring(i) for i in xmltree]+[xmltree.tail]
textlist=[i for i in textlist if type(i)==str]
return "".join(textlist)
try:
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})
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)
self.filename = None
filename_l=xmltree.xpath("/html/@filename")
if len(filename_l)>0:
self.filename=str(filename_l[0])

View File

@@ -0,0 +1,24 @@
import json
## 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
class Module(XModule):
id_attribute = 'id'
def get_state(self):
return json.dumps({ })
@classmethod
def get_xml_tags(c):
return ["schematic"]
def get_html(self):
return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id)
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, render_function = None):
XModule.__init__(self, xml, item_id, ajax_url, track_url, state, render_function)

View File

@@ -0,0 +1,121 @@
import json
from lxml import etree
## TODO: Abstract out from Django
from django.http import Http404
from django.conf import settings
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule
# HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
class Module(XModule):
''' Layout module which lays out content in a temporal sequence
'''
id_attribute = 'id'
def get_state(self):
return json.dumps({ 'position':self.position })
@classmethod
def get_xml_tags(c):
return ["sequential", 'tab']
def get_html(self):
self.render()
return self.content
def get_init_js(self):
self.render()
return self.init_js
def get_destroy_js(self):
self.render()
return self.destroy_js
def handle_ajax(self, dispatch, get):
print "GET", get
print "DISPATCH", dispatch
if dispatch=='goto_position':
self.position = int(get['position'])
return json.dumps({'success':True})
raise Http404()
def render(self):
if self.rendered:
return
def j(m):
''' jsonify contents so it can be embedded in a js array
We also need to split </script> tags so they don't break
mid-string'''
if 'init_js' not in m: m['init_js']=""
if 'type' not in m: m['init_js']=""
content=json.dumps(m['content'])
content=content.replace('</script>', '<"+"/script>')
return {'content':content,
"destroy_js":m['destroy_js'],
'init_js':m['init_js'],
'type': m['type']}
## 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.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['type'] = new_class
js=""
params={'items':self.contents,
'id':self.item_id,
'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['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)
self.destroy_js=destroy_js
self.content=render_to_string('seq_module.html',params)
if self.xmltree.tag == 'tab':
params['id'] = 'tab'
self.init_js=js+render_to_string('tab_module.js',params)
self.destroy_js=destroy_js
self.content=render_to_string('tab_module.html',params)
self.rendered = True
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)
self.xmltree=etree.fromstring(xml)
self.position = 1
if state!=None:
state = json.loads(state)
if 'position' in state: self.position = int(state['position'])
self.rendered = False

View File

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

View File

@@ -0,0 +1,35 @@
import json
## 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):
id_attribute = 'id'
def get_state(self):
return json.dumps({ })
@classmethod
def get_xml_tags(c):
return ["vertical"]
def get_html(self):
return render_to_string('vert_module.html',{'items':self.contents})
def get_init_js(self):
return self.init_js_text
def get_destroy_js(self):
return self.destroy_js_text
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)
self.contents=[(e.get("name"),self.render_function(e)) \
for e in xmltree]
self.init_js_text="".join([e[1]['init_js'] for e in self.contents if 'init_js' in e[1]])
self.destroy_js_text="".join([e[1]['destroy_js'] for e in self.contents if 'destroy_js' in e[1]])

View File

@@ -0,0 +1,70 @@
import json
import logging
from lxml import etree
## 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
log = logging.getLogger("mitx.courseware.modules")
class Module(XModule):
id_attribute = 'youtube'
video_time = 0
def handle_ajax(self, dispatch, get):
log.debug(u"GET {0}".format(get))
log.debug(u"DISPATCH {0}".format(dispatch))
if dispatch == 'goto_position':
self.position = int(float(get['position']))
log.debug(u"NEW POSITION {0}".format(self.position))
return json.dumps({'success':True})
raise Http404()
def get_state(self):
log.debug(u"STATE POSITION {0}".format(self.position))
return json.dumps({ 'position':self.position })
@classmethod
def get_xml_tags(c):
'''Tags in the courseware file guaranteed to correspond to the module'''
return ["video"]
def video_list(self):
l = self.youtube.split(',')
l = [i.split(":") for i in l]
return json.dumps(dict(l))
def get_html(self):
return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position,
'name':self.name})
def get_init_js(self):
'''JavaScript code to be run when problem is shown. Be aware
that this may happen several times on the same page
(e.g. student switching tabs). Common functions should be put
in the main course .js files for now. '''
log.debug(u"INIT POSITION {0}".format(self.position))
return render_to_string('video_init.js',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position})
def get_destroy_js(self):
return "videoDestroy(\"{0}\");".format(self.item_id)
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)
self.youtube = etree.XML(xml).get('youtube')
self.name = etree.XML(xml).get('name')
self.position = 0
if state != None:
state = json.loads(state)
if 'position' in state:
self.position = int(float(state['position']))
#log.debug("POSITION IN STATE")
#log.debug(u"LOAD POSITION {0}".format(self.position))

View File

@@ -0,0 +1,57 @@
import courseware.progress
def dummy_track(event_type, event):
pass
class XModule(object):
''' Implements a generic learning module.
Initialized on access with __init__, first time with state=None, and
then with state
'''
id_attribute='id' # An attribute guaranteed to be unique
@classmethod
def get_xml_tags(c):
''' Tags in the courseware file guaranteed to correspond to the module '''
return []
def get_completion(self):
return courseware.progress.completion()
def get_state(self):
return ""
def get_score(self):
return None
def max_score(self):
return None
def get_html(self):
return "Unimplemented"
def get_init_js(self):
''' JavaScript code to be run when problem is shown. Be aware
that this may happen several times on the same page
(e.g. student switching tabs). Common functions should be put
in the main course .js files for now. '''
return ""
def get_destroy_js(self):
return ""
def handle_ajax(self, dispatch, get):
''' dispatch is last part of the URL.
get is a dictionary-like object '''
return ""
def __init__(self, xml, item_id, ajax_url=None, track_url=None, state=None, track_function=None, render_function = None):
''' In most cases, you must pass state or xml'''
self.xml = xml
self.item_id = item_id
self.ajax_url = ajax_url
self.track_url = track_url
self.state = state
self.tracker = track_function
self.render_function = render_function

View File

@@ -0,0 +1,38 @@
class completion(object):
def __init__(self, **d):
self.dict = dict({'duration_total':0,
'duration_watched':0,
'done':True,
'questions_correct':0,
'questions_incorrect':0,
'questions_total':0})
if d:
self.dict.update(d)
def __getitem__(self, key):
return self.dict[key]
def __setitem__(self, key, value):
self.dict[key] = value
def __add__(self, other):
result = dict(self.dict)
for item in ['duration_total',
'duration_watched',
'done',
'questions_correct',
'questions_incorrect',
'questions_total']:
result[item] = result[item]+other.dict[item]
return completion(**result)
def __contains__(self, key):
return key in dict
def __repr__(self):
return repr(self.dict)
if __name__ == '__main__':
dict1=completion(duration_total=5)
dict2=completion(duration_total=7)
print dict1+dict2

View File

@@ -0,0 +1,53 @@
import unittest
import numpy
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 = "<class 'courseware.modules.video_module.Module'>"
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)

View File

View File

@@ -0,0 +1,188 @@
import json
import logging
import os
import random
import sys
import StringIO
import urllib
import uuid
from django.conf import settings
from django.core.context_processors import csrf
from django.contrib.auth.models import User
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
from django.template import Context, loader
from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
from django.db import connection
from django.views.decorators.cache import cache_control
from lxml import etree
from module_render import render_module, modx_dispatch
from models import StudentModule
from student.models import UserProfile
import courseware.content_parser as content_parser
import courseware.modules.capa_module
import courseware.grades as grades
log = logging.getLogger("mitx.courseware")
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments = True))
template_imports={'urllib':urllib}
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request):
if 'course_admin' not in content_parser.user_groups(request.user):
raise Http404
student_objects = User.objects.all()[:100]
student_info = [{'username' :s.username,
'id' : s.id,
'email': s.email,
'grade_info' : grades.grade_sheet(s),
'realname' : UserProfile.objects.get(user = s).name
} for s in student_objects]
return render_to_response('gradebook.html',{'students':student_info})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def profile(request, student_id = None):
''' User profile. Show username, location, etc, as well as grades .
We need to allow the user to change some of these settings .'''
if not request.user.is_authenticated():
return redirect('/')
if student_id == None:
student = request.user
else:
print content_parser.user_groups(request.user)
if 'course_admin' not in content_parser.user_groups(request.user):
raise Http404
student = User.objects.get( id = int(student_id))
user_info = UserProfile.objects.get(user=student) # request.user.profile_cache #
context={'name':user_info.name,
'username':student.username,
'location':user_info.location,
'language':user_info.language,
'email':student.email,
'format_url_params' : content_parser.format_url_params,
'csrf':csrf(request)['csrf_token']
}
context.update(grades.grade_sheet(student))
return render_to_response('profile.html', context)
def render_accordion(request,course,chapter,section):
''' Draws navigation bar. Takes current position in accordion as
parameter. Returns (initialization_javascript, content)'''
if not course:
course = "6.002 Spring 2012"
toc=content_parser.toc_from_xml(content_parser.course_file(request.user), chapter, section)
active_chapter=1
for i in range(len(toc)):
if toc[i]['active']:
active_chapter=i
context=dict([['active_chapter',active_chapter],
['toc',toc],
['course_name',course],
['format_url_params',content_parser.format_url_params],
['csrf',csrf(request)['csrf_token']]] + \
template_imports.items())
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.
'''
user = request.user
if not settings.COURSEWARE_ENABLED or not user.is_authenticated():
return redirect('/')
# Fixes URLs -- we don't get funny encoding characters from spaces
# so they remain readable
## TODO: Properly replace underscores
course=course.replace("_"," ")
chapter=chapter.replace("_"," ")
section=section.replace("_"," ")
# HACK: Force course to 6.002 for now
# Without this, URLs break
if course!="6.002 Spring 2012":
return redirect('/')
#import logging
#log = logging.getLogger("mitx")
#log.info( "DEBUG: "+str(user) )
dom = content_parser.course_file(user)
dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]/*[1]",
course=course, chapter=chapter, section=section)
if len(dom_module) == 0:
module = None
else:
module = dom_module[0]
accordion=render_accordion(request, course, chapter, section)
module_ids = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]//@id",
course=course, chapter=chapter, section=section)
module_object_preload = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
module=render_module(user, request, module, 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

View File

@@ -0,0 +1,9 @@
# Source: django-simplewiki. GPL license.
import os
import sys
# allow mdx_* parsers to be just dropped in the simplewiki folder
module_path = os.path.abspath(os.path.dirname(__file__))
if module_path not in sys.path:
sys.path.append(module_path)

View File

@@ -0,0 +1,62 @@
# Source: django-simplewiki. GPL license.
from django import forms
from django.contrib import admin
from django.utils.translation import ugettext as _
from models import Article, Revision, Permission, ArticleAttachment
class RevisionInline(admin.TabularInline):
model = Revision
extra = 1
class RevisionAdmin(admin.ModelAdmin):
list_display = ('article', '__unicode__', 'revision_date', 'revision_user', 'revision_text')
search_fields = ('article', 'counter')
class AttachmentAdmin(admin.ModelAdmin):
list_display = ('article', '__unicode__', 'uploaded_on', 'uploaded_by')
class ArticleAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = self.cleaned_data
if cleaned_data.get("slug").startswith('_'):
raise forms.ValidationError(_('Slug cannot start with _ character.'
'Reserved for internal use.'))
if not self.instance.pk:
parent = cleaned_data.get("parent")
slug = cleaned_data.get("slug")
if Article.objects.filter(slug__exact=slug, parent__exact=parent):
raise forms.ValidationError(_('Article slug and parent must be '
'unique together.'))
return cleaned_data
class Meta:
model = Article
class ArticleAdmin(admin.ModelAdmin):
list_display = ('created_by', 'slug', 'modified_on', 'parent')
search_fields = ('slug',)
prepopulated_fields = {'slug': ('title',) }
inlines = [RevisionInline]
form = ArticleAdminForm
save_on_top = True
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'current_revision':
# Try to determine the id of the article being edited
id = request.path.split('/')
import re
if len(id) > 0 and re.match(r"\d+", id[-2]):
kwargs["queryset"] = Revision.objects.filter(article=id[-2])
return db_field.formfield(**kwargs)
else:
db_field.editable = False
return db_field.formfield(**kwargs)
return super(ArticleAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
class PermissionAdmin(admin.ModelAdmin):
search_fields = ('article', 'counter')
admin.site.register(Article, ArticleAdmin)
admin.site.register(Revision, RevisionAdmin)
admin.site.register(Permission, PermissionAdmin)
admin.site.register(ArticleAttachment, AttachmentAdmin)

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python
'''
Image Circuit Extension for Python-Markdown
======================================
circuit:name becomes the circuit.
'''
import simplewiki.settings as settings
from mitxmako.shortcuts import render_to_response, render_to_string
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class CircuitExtension(markdown.Extension):
def __init__(self, configs):
for key, value in configs :
self.setConfig(key, value)
def add_inline(self, md, name, klass, re):
pattern = klass(re)
pattern.md = md
pattern.ext = self
md.inlinePatterns.add(name, pattern, "<reference")
def extendMarkdown(self, md, md_globals):
self.add_inline(md, 'circuit', CircuitLink, r'^circuit:(?P<name>[a-zA-Z0-9]*)$')
class CircuitLink(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
name = m.group('name')
if not name.isalnum():
return etree.fromstring("<div>Circuit name must be alphanumeric</div>")
return etree.fromstring(render_to_string('show_circuit.html', {'name':name}))
def makeExtension(configs=None) :
return CircuitExtension(configs=configs)

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python
'''
Image Embedding Extension for Python-Markdown
======================================
Converts lone links to embedded images, provided the file extension is allowed.
Ex:
http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg
becomes
<img src="http://www.ericfehse.net/media/img/ef/blog/django-pony.jpg">
mypic.jpg becomes <img src="/MEDIA_PATH/mypic.jpg">
Requires Python-Markdown 1.6+
'''
import simplewiki.settings as settings
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class ImageExtension(markdown.Extension):
def __init__(self, configs):
for key, value in configs :
self.setConfig(key, value)
def add_inline(self, md, name, klass, re):
pattern = klass(re)
pattern.md = md
pattern.ext = self
md.inlinePatterns.add(name, pattern, "<reference")
def extendMarkdown(self, md, md_globals):
self.add_inline(md, 'image', ImageLink,
r'^(?P<proto>([^:/?#])+://)?(?P<domain>([^/?#]*)/)?(?P<path>[^?#]*\.(?P<ext>[^?#]{3,4}))(?:\?([^#]*))?(?:#(.*))?$')
class ImageLink(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
img = etree.Element('img')
proto = m.group('proto') or "http://"
domain = m.group('domain')
path = m.group('path')
ext = m.group('ext')
# A fixer upper
if ext.lower() in settings.WIKI_IMAGE_EXTENSIONS:
if domain:
src = proto+domain+path
elif path:
# We need a nice way to source local attachments...
src = "/wiki/media/" + path + ".upload"
else:
src = ''
img.set('src', src)
return img
def makeExtension(configs=None) :
return ImageExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@@ -0,0 +1,29 @@
# Source: https://github.com/mayoff/python-markdown-mathjax
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree, AtomicString
except:
from markdown import etree, AtomicString
class MathJaxPattern(markdown.inlinepatterns.Pattern):
def __init__(self):
markdown.inlinepatterns.Pattern.__init__(self, r'(?<!\\)(\$\$?)(.+?)\2')
def handleMatch(self, m):
el = etree.Element('span')
el.text = AtomicString(m.group(2) + m.group(3) + m.group(2))
return el
class MathJaxExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals):
# Needs to come before escape matching because \ is pretty important in LaTeX
md.inlinePatterns.add('mathjax', MathJaxPattern(), '<escape')
def makeExtension(configs=None):
return MathJaxExtension(configs)

View File

@@ -0,0 +1,278 @@
#!/usr/bin/env python
"""
Embeds web videos using URLs. For instance, if a URL to an youtube video is
found in the text submitted to markdown and it isn't enclosed in parenthesis
like a normal link in markdown, then the URL will be swapped with a embedded
youtube video.
All resulting HTML is XHTML Strict compatible.
>>> import markdown
Test Metacafe
>>> s = "http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" height="423" type="application/x-shockwave-flash" width="498"><param name="movie" value="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" /><param name="allowFullScreen" value="true" /></object></p>'
Test Metacafe with arguments
>>> markdown.markdown(s, ['video(metacafe_width=500,metacafe_height=425)'])
u'<p><object data="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" height="425" type="application/x-shockwave-flash" width="500"><param name="movie" value="http://www.metacafe.com/fplayer/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room.swf" /><param name="allowFullScreen" value="true" /></object></p>'
Test Link To Metacafe
>>> s = "[Metacafe link](http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/)"
>>> markdown.markdown(s, ['video'])
u'<p><a href="http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/">Metacafe link</a></p>'
Test Markdown Escaping
>>> s = "\\http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/"
>>> markdown.markdown(s, ['video'])
u'<p>http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/</p>'
>>> s = "`http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/`"
>>> markdown.markdown(s, ['video'])
u'<p><code>http://www.metacafe.com/watch/yt-tZMsrrQCnx8/pycon_2008_django_sprint_room/</code></p>'
Test Youtube
>>> s = "http://www.youtube.com/watch?v=u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" height="344" type="application/x-shockwave-flash" width="425"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" /><param name="allowFullScreen" value="true" /></object></p>'
Test Youtube with argument
>>> markdown.markdown(s, ['video(youtube_width=200,youtube_height=100)'])
u'<p><object data="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" height="100" type="application/x-shockwave-flash" width="200"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&amp;hd=1&amp;fs=1&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1" /><param name="allowFullScreen" value="true" /></object></p>'
Test Youtube Link
>>> s = "[Youtube link](http://www.youtube.com/watch?v=u1mA-0w8XPo&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1)"
>>> markdown.markdown(s, ['video'])
u'<p><a href="http://www.youtube.com/watch?v=u1mA-0w8XPo&amp;feature=PlayList&amp;p=34C6046F7FEACFD3&amp;playnext=1&amp;playnext_from=PL&amp;index=1">Youtube link</a></p>'
Test Dailymotion
>>> s = "http://www.dailymotion.com/relevance/search/ut2004/video/x3kv65_ut2004-ownage_videogames"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.dailymotion.com/swf/x3kv65_ut2004-ownage_videogames" height="405" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.dailymotion.com/swf/x3kv65_ut2004-ownage_videogames" /><param name="allowFullScreen" value="true" /></object></p>'
Test Dailymotion again (Dailymotion and their crazy URLs)
>>> s = "http://www.dailymotion.com/us/video/x8qak3_iron-man-vs-bruce-lee_fun"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.dailymotion.com/swf/x8qak3_iron-man-vs-bruce-lee_fun" height="405" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.dailymotion.com/swf/x8qak3_iron-man-vs-bruce-lee_fun" /><param name="allowFullScreen" value="true" /></object></p>'
Test Yahoo! Video
>>> s = "http://video.yahoo.com/watch/1981791/4769603"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" height="322" type="application/x-shockwave-flash" width="512"><param name="movie" value="http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40" /><param name="allowFullScreen" value="true" /><param name="flashVars" value="id=4769603&amp;vid=1981791" /></object></p>'
Test Veoh Video
>>> s = "http://www.veoh.com/search/videos/q/mario#watch%3De129555XxCZanYD"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=e129555XxCZanYD" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=e129555XxCZanYD" /><param name="allowFullScreen" value="true" /></object></p>'
Test Veoh Video Again (More fun URLs)
>>> s = "http://www.veoh.com/group/BigCatRescuers#watch%3Dv16771056hFtSBYEr"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=v16771056hFtSBYEr" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=v16771056hFtSBYEr" /><param name="allowFullScreen" value="true" /></object></p>'
Test Veoh Video Yet Again (Even more fun URLs)
>>> s = "http://www.veoh.com/browse/videos/category/anime/watch/v181645607JyXPWcQ"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.veoh.com/videodetails2.swf?permalinkId=v181645607JyXPWcQ" height="341" type="application/x-shockwave-flash" width="410"><param name="movie" value="http://www.veoh.com/videodetails2.swf?permalinkId=v181645607JyXPWcQ" /><param name="allowFullScreen" value="true" /></object></p>'
Test Vimeo Video
>>> s = "http://www.vimeo.com/1496152"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" height="321" type="application/x-shockwave-flash" width="400"><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" /><param name="allowFullScreen" value="true" /></object></p>'
Test Vimeo Video with some GET values
>>> s = "http://vimeo.com/1496152?test=test"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" height="321" type="application/x-shockwave-flash" width="400"><param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=1496152&amp;amp;server=vimeo.com" /><param name="allowFullScreen" value="true" /></object></p>'
Test Blip.tv
>>> s = "http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" height="300" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/Pycon-PlenarySprintIntro563.flv" /><param name="allowFullScreen" value="true" /></object></p>'
Test Gametrailers
>>> s = "http://www.gametrailers.com/video/console-comparison-borderlands/58079"
>>> markdown.markdown(s, ['video'])
u'<p><object data="http://www.gametrailers.com/remote_wrap.php?mid=58079" height="392" type="application/x-shockwave-flash" width="480"><param name="movie" value="http://www.gametrailers.com/remote_wrap.php?mid=58079" /><param name="allowFullScreen" value="true" /></object></p>'
"""
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
version = "0.1.6"
class VideoExtension(markdown.Extension):
def __init__(self, configs):
self.config = {
'bliptv_width': ['480', 'Width for Blip.tv videos'],
'bliptv_height': ['300', 'Height for Blip.tv videos'],
'dailymotion_width': ['480', 'Width for Dailymotion videos'],
'dailymotion_height': ['405', 'Height for Dailymotion videos'],
'gametrailers_width': ['480', 'Width for Gametrailers videos'],
'gametrailers_height': ['392', 'Height for Gametrailers videos'],
'metacafe_width': ['498', 'Width for Metacafe videos'],
'metacafe_height': ['423', 'Height for Metacafe videos'],
'veoh_width': ['410', 'Width for Veoh videos'],
'veoh_height': ['341', 'Height for Veoh videos'],
'vimeo_width': ['400', 'Width for Vimeo videos'],
'vimeo_height': ['321', 'Height for Vimeo videos'],
'yahoo_width': ['512', 'Width for Yahoo! videos'],
'yahoo_height': ['322', 'Height for Yahoo! videos'],
'youtube_width': ['425', 'Width for Youtube videos'],
'youtube_height': ['344', 'Height for Youtube videos'],
}
# Override defaults with user settings
for key, value in configs:
self.setConfig(key, value)
def add_inline(self, md, name, klass, re):
pattern = klass(re)
pattern.md = md
pattern.ext = self
md.inlinePatterns.add(name, pattern, "<reference")
def extendMarkdown(self, md, md_globals):
self.add_inline(md, 'bliptv', Bliptv,
r'([^(]|^)http://(\w+\.|)blip.tv/file/get/(?P<bliptvfile>\S+.flv)')
self.add_inline(md, 'dailymotion', Dailymotion,
r'([^(]|^)http://www\.dailymotion\.com/(?P<dailymotionid>\S+)')
self.add_inline(md, 'gametrailers', Gametrailers,
r'([^(]|^)http://www.gametrailers.com/video/[a-z0-9-]+/(?P<gametrailersid>\d+)')
self.add_inline(md, 'metacafe', Metacafe,
r'([^(]|^)http://www\.metacafe\.com/watch/(?P<metacafeid>\S+)/')
self.add_inline(md, 'veoh', Veoh,
r'([^(]|^)http://www\.veoh\.com/\S*(#watch%3D|watch/)(?P<veohid>\w+)')
self.add_inline(md, 'vimeo', Vimeo,
r'([^(]|^)http://(www.|)vimeo\.com/(?P<vimeoid>\d+)\S*')
self.add_inline(md, 'yahoo', Yahoo,
r'([^(]|^)http://video\.yahoo\.com/watch/(?P<yahoovid>\d+)/(?P<yahooid>\d+)')
self.add_inline(md, 'youtube', Youtube,
r'([^(]|^)http://www\.youtube\.com/watch\?\S*v=(?P<youtubeargs>[A-Za-z0-9_&=-]+)\S*')
class Bliptv(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://blip.tv/scripts/flash/showplayer.swf?file=http://blip.tv/file/get/%s' % m.group('bliptvfile')
width = self.ext.config['bliptv_width'][0]
height = self.ext.config['bliptv_height'][0]
return flash_object(url, width, height)
class Dailymotion(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.dailymotion.com/swf/%s' % m.group('dailymotionid').split('/')[-1]
width = self.ext.config['dailymotion_width'][0]
height = self.ext.config['dailymotion_height'][0]
return flash_object(url, width, height)
class Gametrailers(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.gametrailers.com/remote_wrap.php?mid=%s' % \
m.group('gametrailersid').split('/')[-1]
width = self.ext.config['gametrailers_width'][0]
height = self.ext.config['gametrailers_height'][0]
return flash_object(url, width, height)
class Metacafe(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.metacafe.com/fplayer/%s.swf' % m.group('metacafeid')
width = self.ext.config['metacafe_width'][0]
height = self.ext.config['metacafe_height'][0]
return flash_object(url, width, height)
class Veoh(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.veoh.com/videodetails2.swf?permalinkId=%s' % m.group('veohid')
width = self.ext.config['veoh_width'][0]
height = self.ext.config['veoh_height'][0]
return flash_object(url, width, height)
class Vimeo(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://vimeo.com/moogaloop.swf?clip_id=%s&amp;server=vimeo.com' % m.group('vimeoid')
width = self.ext.config['vimeo_width'][0]
height = self.ext.config['vimeo_height'][0]
return flash_object(url, width, height)
class Yahoo(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = "http://d.yimg.com/static.video.yahoo.com/yep/YV_YEP.swf?ver=2.2.40"
width = self.ext.config['yahoo_width'][0]
height = self.ext.config['yahoo_height'][0]
obj = flash_object(url, width, height)
param = etree.Element('param')
param.set('name', 'flashVars')
param.set('value', "id=%s&vid=%s" % (m.group('yahooid'),
m.group('yahoovid')))
obj.append(param)
return obj
class Youtube(markdown.inlinepatterns.Pattern):
def handleMatch(self, m):
url = 'http://www.youtube.com/v/%s' % m.group('youtubeargs')
width = self.ext.config['youtube_width'][0]
height = self.ext.config['youtube_height'][0]
return flash_object(url, width, height)
def flash_object(url, width, height):
obj = etree.Element('object')
obj.set('type', 'application/x-shockwave-flash')
obj.set('width', width)
obj.set('height', height)
obj.set('data', url)
param = etree.Element('param')
param.set('name', 'movie')
param.set('value', url)
obj.append(param)
param = etree.Element('param')
param.set('name', 'allowFullScreen')
param.set('value', 'true')
obj.append(param)
#param = etree.Element('param')
#param.set('name', 'allowScriptAccess')
#param.set('value', 'sameDomain')
#obj.append(param)
return obj
def makeExtension(configs=None) :
return VideoExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python
'''
Wikipath Extension for Python-Markdown
======================================
Converts [Link Name](wiki:ArticleName) to relative links pointing to article. Requires Python-Markdown 2.0+
Basic usage:
>>> import markdown
>>> text = "Some text with a [Link Name](wiki:ArticleName)."
>>> html = markdown.markdown(text, ['wikipath(base_url="/wiki/view/")'])
>>> html
u'<p>Some text with a <a class="wikipath" href="/wiki/view/ArticleName/">Link Name</a>.</p>'
Dependencies:
* [Python 2.3+](http://python.org)
* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/)
'''
import markdown
try:
# Markdown 2.1.0 changed from 2.0.3. We try importing the new version first,
# but import the 2.0.3 version if it fails
from markdown.util import etree
except:
from markdown import etree
class WikiPathExtension(markdown.Extension):
def __init__(self, configs):
# set extension defaults
self.config = {
'base_url' : ['/', 'String to append to beginning or URL.'],
'html_class' : ['wikipath', 'CSS hook. Leave blank for none.']
}
# Override defaults with user settings
for key, value in configs :
# self.config[key][0] = value
self.setConfig(key, value)
def extendMarkdown(self, md, md_globals):
self.md = md
# append to end of inline patterns
WIKI_RE = r'\[(?P<linkTitle>.+?)\]\(wiki:(?P<wikiTitle>[a-zA-Z\d/_-]*)\)'
wikiPathPattern = WikiPath(WIKI_RE, self.config)
wikiPathPattern.md = md
md.inlinePatterns.add('wikipath', wikiPathPattern, "<reference")
class WikiPath(markdown.inlinepatterns.Pattern):
def __init__(self, pattern, config):
markdown.inlinepatterns.Pattern.__init__(self, pattern)
self.config = config
def handleMatch(self, m) :
article_title = m.group('wikiTitle')
if article_title.startswith("/"):
article_title = article_title[1:]
url = self.config['base_url'][0] + article_title
label = m.group('linkTitle')
a = etree.Element('a')
a.set('href', url)
a.text = label
if self.config['html_class'][0]:
a.set('class', self.config['html_class'][0])
return a
def _getMeta(self):
""" Return meta data or config data. """
base_url = self.config['base_url'][0]
html_class = self.config['html_class'][0]
if hasattr(self.md, 'Meta'):
if self.md.Meta.has_key('wiki_base_url'):
base_url = self.md.Meta['wiki_base_url'][0]
if self.md.Meta.has_key('wiki_html_class'):
html_class = self.md.Meta['wiki_html_class'][0]
return base_url, html_class
def makeExtension(configs=None) :
return WikiPathExtension(configs=configs)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@@ -0,0 +1,177 @@
/*
================================================
autosuggest, inquisitor style
================================================
*/
body
{
position: relative;
}
div.autosuggest
{
position: absolute;
background-image: url(img_inquisitor/as_pointer.gif);
background-position: top;
background-repeat: no-repeat;
padding: 10px 0 0 0;
}
div.autosuggest div.as_header,
div.autosuggest div.as_footer
{
position: relative;
height: 6px;
padding: 0 6px;
background-image: url(img_inquisitor/ul_corner_tr.gif);
background-position: top right;
background-repeat: no-repeat;
overflow: hidden;
}
div.autosuggest div.as_footer
{
background-image: url(img_inquisitor/ul_corner_br.gif);
}
div.autosuggest div.as_header div.as_corner,
div.autosuggest div.as_footer div.as_corner
{
position: absolute;
top: 0;
left: 0;
height: 6px;
width: 6px;
background-image: url(img_inquisitor/ul_corner_tl.gif);
background-position: top left;
background-repeat: no-repeat;
}
div.autosuggest div.as_footer div.as_corner
{
background-image: url(img_inquisitor/ul_corner_bl.gif);
}
div.autosuggest div.as_header div.as_bar,
div.autosuggest div.as_footer div.as_bar
{
height: 6px;
overflow: hidden;
background-color: #333;
}
div.autosuggest ul
{
list-style: none;
margin: 0 0 -4px 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
div.autosuggest ul li
{
color: #ccc;
padding: 0;
margin: 0 4px 4px;
text-align: left;
}
div.autosuggest ul li a
{
color: #ccc;
display: block;
text-decoration: none;
background-color: transparent;
text-shadow: #000 0px 0px 5px;
position: relative;
padding: 0;
width: 100%;
}
div.autosuggest ul li a:hover
{
background-color: #444;
}
div.autosuggest ul li.as_highlight a:hover
{
background-color: #1B5CCD;
}
div.autosuggest ul li a span
{
display: block;
padding: 3px 6px;
font-weight: bold;
}
div.autosuggest ul li a span small
{
font-weight: normal;
color: #999;
}
div.autosuggest ul li.as_highlight a span small
{
color: #ccc;
}
div.autosuggest ul li.as_highlight a
{
color: #fff;
background-color: #1B5CCD;
background-image: url(img_inquisitor/hl_corner_br.gif);
background-position: bottom right;
background-repeat: no-repeat;
}
div.autosuggest ul li.as_highlight a span
{
background-image: url(img_inquisitor/hl_corner_bl.gif);
background-position: bottom left;
background-repeat: no-repeat;
}
div.autosuggest ul li a .tl,
div.autosuggest ul li a .tr
{
background-image: transparent;
background-repeat: no-repeat;
width: 6px;
height: 6px;
position: absolute;
top: 0;
padding: 0;
margin: 0;
}
div.autosuggest ul li a .tr
{
right: 0;
}
div.autosuggest ul li.as_highlight a .tl
{
left: 0;
background-image: url(img_inquisitor/hl_corner_tl.gif);
background-position: bottom left;
}
div.autosuggest ul li.as_highlight a .tr
{
right: 0;
background-image: url(img_inquisitor/hl_corner_tr.gif);
background-position: bottom right;
}
div.autosuggest ul li.as_warning
{
font-weight: bold;
text-align: center;
}
div.autosuggest ul em
{
font-style: normal;
color: #6EADE7;
}

View File

@@ -0,0 +1,281 @@
body
{
font-family: 'Lucida Sans', 'Sans';
}
a img
{
border: 0;
}
div#wiki_article a {
color: #06d;
text-decoration: none;
}
div#wiki_article a:hover {
color: #f82;
text-decoration: underline;
}
hr
{
background-color: #def;
height: 2px;
border: 0;
}
div#wiki_article .toc a
{
color: #025
}
div#wiki_article p
{
/* font-size: 90%; looks funny when combined with lists/tables */
line-height: 140%;
}
div#wiki_article h1
{
font-size: 200%;
font-weight: normal;
color: #048;
}
div#wiki_article h2
{
font-size: 150%;
font-weight: normal;
color: #025;
}
div#wiki_article h3
{
font-size: 120%;
font-weight: bold;
color: #000;
}
table
{
border: 1px solid black;
border-collapse: collapse;
margin: 12px;
}
table tr.dark
{
background-color: #F3F3F3;
}
table thead tr
{
background-color: #def;
border-bottom: 2px solid black;
}
table td, th
{
padding: 6px 10px 6px 10px;
border: 1px solid black;
}
table thead th
{
padding-bottom: 8px;
padding-top: 8px;
}
div#wiki_panel
{
float: right;
}
div.wiki_box
{
width: 230px;
padding: 10px;
color: #fff;
font-size: 80%;
}
div.wiki_box div.wiki_box_contents
{ background-color: #222;
padding: 5px 10px;}
div.wiki_box div.wiki_box_header,
div.wiki_box div.wiki_box_footer
{
position: relative;
height: 6px;
padding: 0 6px;
background-image: url(../img/box_corner_tr.gif);
background-position: top right;
background-repeat: no-repeat;
overflow: hidden;
}
div.wiki_box div.wiki_box_footer
{
background-image: url(../img/box_corner_br.gif);
}
div.wiki_box div.wiki_box_header div.wiki_box_corner,
div.wiki_box div.wiki_box_footer div.wiki_box_corner
{
position: absolute;
top: 0;
left: 0;
height: 6px;
width: 6px;
background-image: url(../img/box_corner_tl.gif);
background-position: top left;
background-repeat: no-repeat;
}
div.wiki_box div.wiki_box_footer div.wiki_box_corner
{
background-image: url(../img/box_corner_bl.gif);
}
div.wiki_box div.wiki_box_header div.wiki_box_bar,
div.wiki_box div.wiki_box_footer div.wiki_box_bar
{
height: 6px;
overflow: hidden;
background-color: #222;
}
div.wiki_box a
{
color: #acf;
}
div.wiki_box p
{
margin: 5px 0;
}
div.wiki_box ul
{
padding-left: 20px;
margin-left: 0;
}
div.wiki_box div.wiki_box_title
{
margin-bottom: 5px;
font-size: 140%;
}
form#wiki_revision #id_contents
{
width:500px;
height: 400px;
font-family: monospace;
}
form#wiki_revision #id_title
{
width: 500px;
}
form#wiki_revision #id_revision_text
{
width: 500px;
}
table#wiki_revision_table
{
border: none;
border-collapse: collapse;
padding-right: 250px;
}
table#wiki_revision_table th
{
border: none;
text-align: left;
vertical-align: top;
}
table#wiki_revision_table td
{
border: none;
}
table#wiki_history_table
{
border-collapse: collapse;
border-spacing: 0;
padding-right: 250px;
}
table#wiki_history_table th#modified
{
width: 220px;
}
table#wiki_history_table td
{
border: none;
}
table#wiki_history_table tbody tr
{
border-bottom: 1px solid black;
}
table#wiki_history_table tbody td
{
vertical-align: top;
padding: 5px;
}
table#wiki_history_table tfoot td
{
border: none;
}
table#wiki_history_table tbody td.diff
{
font-family: monospace;
overflow: hidden;
border-left: 1px dotted black;
border-right: 1px dotted black;
}
table#wiki_history_table th
{
text-align: left;
}
div#wiki_attach_progress_container
{
background-color: #333;
width: 100%;
height: 20px;
display: none;
}
div#wiki_attach_progress
{
width: 25%;
background-color: #999;
}
blockquote {
margin-top: 15px;
margin-bottom: 15px;
margin-left: 50px;
padding-left: 15px;
border-left: 3px solid #666;
color: #999;
max-width: 400px ;
}
blockquote p {
margin-top: 8px;
margin-bottom: 8px;
}

View File

@@ -0,0 +1,6 @@
div#wiki_panel
{
display:none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

View File

@@ -0,0 +1,961 @@
/**
* author: Timothy Groves - http://www.brandspankingnew.net
* version: 1.2 - 2006-11-17
* 1.3 - 2006-12-04
* 2.0 - 2007-02-07
*
*/
var useBSNns;
if (useBSNns)
{
if (typeof(bsn) == "undefined")
bsn = {}
_bsn = bsn;
}
else
{
_bsn = this;
}
if (typeof(_bsn.Autosuggest) == "undefined")
_bsn.Autosuggest = {}
_bsn.AutoSuggest = function (fldID, param)
{
// no DOM - give up!
//
if (!document.getElementById)
return false;
// get field via DOM
//
this.fld = _bsn.DOM.getElement(fldID);
if (!this.fld)
return false;
// init variables
//
this.sInput = "";
this.nInputChars = 0;
this.aSuggestions = [];
this.iHighlighted = 0;
// parameters object
//
this.oP = (param) ? param : {};
// defaults
//
if (!this.oP.minchars) this.oP.minchars = 1;
if (!this.oP.method) this.oP.meth = "get";
if (!this.oP.varname) this.oP.varname = "input";
if (!this.oP.className) this.oP.className = "autosuggest";
if (!this.oP.timeout) this.oP.timeout = 2500;
if (!this.oP.delay) this.oP.delay = 500;
if (!this.oP.offsety) this.oP.offsety = -5;
if (!this.oP.shownoresults) this.oP.shownoresults = true;
if (!this.oP.noresults) this.oP.noresults = "No results!";
if (!this.oP.maxheight && this.oP.maxheight !== 0) this.oP.maxheight = 250;
if (!this.oP.cache && this.oP.cache != false) this.oP.cache = true;
// set keyup handler for field
// and prevent autocomplete from client
//
var pointer = this;
// NOTE: not using addEventListener because UpArrow fired twice in Safari
//_bsn.DOM.addEvent( this.fld, 'keyup', function(ev){ return pointer.onKeyPress(ev); } );
this.fld.onkeypress = function(ev){ return pointer.onKeyPress(ev); }
this.fld.onkeyup = function(ev){ return pointer.onKeyUp(ev); }
this.fld.setAttribute("autocomplete","off");
}
_bsn.AutoSuggest.prototype.onKeyPress = function(ev)
{
var key = (window.event) ? window.event.keyCode : ev.keyCode;
// set responses to keydown events in the field
// this allows the user to use the arrow keys to scroll through the results
// ESCAPE clears the list
// TAB sets the current highlighted value
//
var RETURN = 13;
var TAB = 9;
var ESC = 27;
var bubble = true;
switch(key)
{
case RETURN:
this.setHighlightedValue();
bubble = false;
break;
case ESC:
this.clearSuggestions();
break;
}
return bubble;
}
_bsn.AutoSuggest.prototype.onKeyUp = function(ev)
{
var key = (window.event) ? window.event.keyCode : ev.keyCode;
// set responses to keydown events in the field
// this allows the user to use the arrow keys to scroll through the results
// ESCAPE clears the list
// TAB sets the current highlighted value
//
var ARRUP = 38;
var ARRDN = 40;
var bubble = true;
switch(key)
{
case ARRUP:
this.changeHighlight(key);
bubble = false;
break;
case ARRDN:
this.changeHighlight(key);
bubble = false;
break;
default:
this.getSuggestions(this.fld.value);
}
return bubble;
}
_bsn.AutoSuggest.prototype.getSuggestions = function (val)
{
// if input stays the same, do nothing
//
if (val == this.sInput)
return false;
// input length is less than the min required to trigger a request
// reset input string
// do nothing
//
if (val.length < this.oP.minchars)
{
this.sInput = "";
return false;
}
// if caching enabled, and user is typing (ie. length of input is increasing)
// filter results out of aSuggestions from last request
//
if (val.length>this.nInputChars && this.aSuggestions.length && this.oP.cache)
{
var arr = [];
for (var i=0;i<this.aSuggestions.length;i++)
{
if (this.aSuggestions[i].value.substr(0,val.length).toLowerCase() == val.toLowerCase())
arr.push( this.aSuggestions[i] );
}
this.sInput = val;
this.nInputChars = val.length;
this.aSuggestions = arr;
this.createList(this.aSuggestions);
return false;
}
else
// do new request
//
{
this.sInput = val;
this.nInputChars = val.length;
var pointer = this;
clearTimeout(this.ajID);
this.ajID = setTimeout( function() { pointer.doAjaxRequest() }, this.oP.delay );
}
return false;
}
_bsn.AutoSuggest.prototype.doAjaxRequest = function ()
{
var pointer = this;
// create ajax request
var url = this.oP.script+this.oP.varname+"="+escape(this.fld.value);
var meth = this.oP.meth;
var onSuccessFunc = function (req) { pointer.setSuggestions(req) };
var onErrorFunc = function (status) { alert("AJAX error: "+status); };
var myAjax = new _bsn.Ajax();
myAjax.makeRequest( url, meth, onSuccessFunc, onErrorFunc );
}
_bsn.AutoSuggest.prototype.setSuggestions = function (req)
{
this.aSuggestions = [];
if (this.oP.json)
{
var jsondata = eval('(' + req.responseText + ')');
for (var i=0;i<jsondata.results.length;i++)
{
this.aSuggestions.push( { 'id':jsondata.results[i].id, 'value':jsondata.results[i].value, 'info':jsondata.results[i].info } );
}
}
else
{
var xml = req.responseXML;
// traverse xml
//
var results = xml.getElementsByTagName('results')[0].childNodes;
for (var i=0;i<results.length;i++)
{
if (results[i].hasChildNodes())
this.aSuggestions.push( { 'id':results[i].getAttribute('id'), 'value':results[i].childNodes[0].nodeValue, 'info':results[i].getAttribute('info') } );
}
}
this.idAs = "as_"+this.fld.id;
this.createList(this.aSuggestions);
}
_bsn.AutoSuggest.prototype.createList = function(arr)
{
var pointer = this;
// get rid of old list
// and clear the list removal timeout
//
_bsn.DOM.removeElement(this.idAs);
this.killTimeout();
// create holding div
//
var div = _bsn.DOM.createElement("div", {id:this.idAs, className:this.oP.className});
var hcorner = _bsn.DOM.createElement("div", {className:"as_corner"});
var hbar = _bsn.DOM.createElement("div", {className:"as_bar"});
var header = _bsn.DOM.createElement("div", {className:"as_header"});
header.appendChild(hcorner);
header.appendChild(hbar);
div.appendChild(header);
// create and populate ul
//
var ul = _bsn.DOM.createElement("ul", {id:"as_ul"});
// loop throught arr of suggestions
// creating an LI element for each suggestion
//
for (var i=0;i<arr.length;i++)
{
// format output with the input enclosed in a EM element
// (as HTML, not DOM)
//
var val = arr[i].value;
var st = val.toLowerCase().indexOf( this.sInput.toLowerCase() );
var output = val.substring(0,st) + "<em>" + val.substring(st, st+this.sInput.length) + "</em>" + val.substring(st+this.sInput.length);
var span = _bsn.DOM.createElement("span", {}, output, true);
if (arr[i].info != "")
{
var br = _bsn.DOM.createElement("br", {});
span.appendChild(br);
var small = _bsn.DOM.createElement("small", {}, arr[i].info);
span.appendChild(small);
}
var a = _bsn.DOM.createElement("a", { href:"#" });
var tl = _bsn.DOM.createElement("span", {className:"tl"}, " ");
var tr = _bsn.DOM.createElement("span", {className:"tr"}, " ");
a.appendChild(tl);
a.appendChild(tr);
a.appendChild(span);
a.name = i+1;
a.onclick = function () { pointer.setHighlightedValue(); return false; }
a.onmouseover = function () { pointer.setHighlight(this.name); }
var li = _bsn.DOM.createElement( "li", {}, a );
ul.appendChild( li );
}
// no results
//
if (arr.length == 0)
{
var li = _bsn.DOM.createElement( "li", {className:"as_warning"}, this.oP.noresults );
ul.appendChild( li );
}
div.appendChild( ul );
var fcorner = _bsn.DOM.createElement("div", {className:"as_corner"});
var fbar = _bsn.DOM.createElement("div", {className:"as_bar"});
var footer = _bsn.DOM.createElement("div", {className:"as_footer"});
footer.appendChild(fcorner);
footer.appendChild(fbar);
div.appendChild(footer);
// get position of target textfield
// position holding div below it
// set width of holding div to width of field
//
var pos = _bsn.DOM.getPos(this.fld);
div.style.left = pos.x + "px";
div.style.top = ( pos.y + this.fld.offsetHeight + this.oP.offsety ) + "px";
div.style.width = this.fld.offsetWidth + "px";
// set mouseover functions for div
// when mouse pointer leaves div, set a timeout to remove the list after an interval
// when mouse enters div, kill the timeout so the list won't be removed
//
div.onmouseover = function(){ pointer.killTimeout() }
div.onmouseout = function(){ pointer.resetTimeout() }
// add DIV to document
//
document.getElementsByTagName("body")[0].appendChild(div);
// currently no item is highlighted
//
this.iHighlighted = 0;
// remove list after an interval
//
var pointer = this;
this.toID = setTimeout(function () { pointer.clearSuggestions() }, this.oP.timeout);
}
_bsn.AutoSuggest.prototype.changeHighlight = function(key)
{
var list = _bsn.DOM.getElement("as_ul");
if (!list)
return false;
var n;
if (key == 40)
n = this.iHighlighted + 1;
else if (key == 38)
n = this.iHighlighted - 1;
if (n > list.childNodes.length)
n = list.childNodes.length;
if (n < 1)
n = 1;
this.setHighlight(n);
}
_bsn.AutoSuggest.prototype.setHighlight = function(n)
{
var list = _bsn.DOM.getElement("as_ul");
if (!list)
return false;
if (this.iHighlighted > 0)
this.clearHighlight();
this.iHighlighted = Number(n);
list.childNodes[this.iHighlighted-1].className = "as_highlight";
this.killTimeout();
}
_bsn.AutoSuggest.prototype.clearHighlight = function()
{
var list = _bsn.DOM.getElement("as_ul");
if (!list)
return false;
if (this.iHighlighted > 0)
{
list.childNodes[this.iHighlighted-1].className = "";
this.iHighlighted = 0;
}
}
_bsn.AutoSuggest.prototype.setHighlightedValue = function ()
{
if (this.iHighlighted)
{
this.sInput = this.fld.value = this.aSuggestions[ this.iHighlighted-1 ].value;
// move cursor to end of input (safari)
//
this.fld.focus();
if (this.fld.selectionStart)
this.fld.setSelectionRange(this.sInput.length, this.sInput.length);
this.clearSuggestions();
// pass selected object to callback function, if exists
//
if (typeof(this.oP.callback) == "function")
this.oP.callback( this.aSuggestions[this.iHighlighted-1] );
}
}
_bsn.AutoSuggest.prototype.killTimeout = function()
{
clearTimeout(this.toID);
}
_bsn.AutoSuggest.prototype.resetTimeout = function()
{
clearTimeout(this.toID);
var pointer = this;
this.toID = setTimeout(function () { pointer.clearSuggestions() }, 1000);
}
_bsn.AutoSuggest.prototype.clearSuggestions = function ()
{
this.killTimeout();
var ele = _bsn.DOM.getElement(this.idAs);
var pointer = this;
if (ele)
{
var fade = new _bsn.Fader(ele,1,0,250,function () { _bsn.DOM.removeElement(pointer.idAs) });
}
}
// AJAX PROTOTYPE _____________________________________________
if (typeof(_bsn.Ajax) == "undefined")
_bsn.Ajax = {}
_bsn.Ajax = function ()
{
this.req = {};
this.isIE = false;
}
_bsn.Ajax.prototype.makeRequest = function (url, meth, onComp, onErr)
{
if (meth != "POST")
meth = "GET";
this.onComplete = onComp;
this.onError = onErr;
var pointer = this;
// branch for native XMLHttpRequest object
if (window.XMLHttpRequest)
{
this.req = new XMLHttpRequest();
this.req.onreadystatechange = function () { pointer.processReqChange() };
this.req.open("GET", url, true); //
this.req.send(null);
// branch for IE/Windows ActiveX version
}
else if (window.ActiveXObject)
{
this.req = new ActiveXObject("Microsoft.XMLHTTP");
if (this.req)
{
this.req.onreadystatechange = function () { pointer.processReqChange() };
this.req.open(meth, url, true);
this.req.send();
}
}
}
_bsn.Ajax.prototype.processReqChange = function()
{
// only if req shows "loaded"
if (this.req.readyState == 4) {
// only if "OK"
if (this.req.status == 200)
{
this.onComplete( this.req );
} else {
this.onError( this.req.status );
}
}
}
// DOM PROTOTYPE _____________________________________________
if (typeof(_bsn.DOM) == "undefined")
_bsn.DOM = {}
_bsn.DOM.createElement = function ( type, attr, cont, html )
{
var ne = document.createElement( type );
if (!ne)
return false;
for (var a in attr)
ne[a] = attr[a];
if (typeof(cont) == "string" && !html)
ne.appendChild( document.createTextNode(cont) );
else if (typeof(cont) == "string" && html)
ne.innerHTML = cont;
else if (typeof(cont) == "object")
ne.appendChild( cont );
return ne;
}
_bsn.DOM.clearElement = function ( id )
{
var ele = this.getElement( id );
if (!ele)
return false;
while (ele.childNodes.length)
ele.removeChild( ele.childNodes[0] );
return true;
}
_bsn.DOM.removeElement = function ( ele )
{
var e = this.getElement(ele);
if (!e)
return false;
else if (e.parentNode.removeChild(e))
return true;
else
return false;
}
_bsn.DOM.replaceContent = function ( id, cont, html )
{
var ele = this.getElement( id );
if (!ele)
return false;
this.clearElement( ele );
if (typeof(cont) == "string" && !html)
ele.appendChild( document.createTextNode(cont) );
else if (typeof(cont) == "string" && html)
ele.innerHTML = cont;
else if (typeof(cont) == "object")
ele.appendChild( cont );
}
_bsn.DOM.getElement = function ( ele )
{
if (typeof(ele) == "undefined")
{
return false;
}
else if (typeof(ele) == "string")
{
var re = document.getElementById( ele );
if (!re)
return false;
else if (typeof(re.appendChild) != "undefined" ) {
return re;
} else {
return false;
}
}
else if (typeof(ele.appendChild) != "undefined")
return ele;
else
return false;
}
_bsn.DOM.appendChildren = function ( id, arr )
{
var ele = this.getElement( id );
if (!ele)
return false;
if (typeof(arr) != "object")
return false;
for (var i=0;i<arr.length;i++)
{
var cont = arr[i];
if (typeof(cont) == "string")
ele.appendChild( document.createTextNode(cont) );
else if (typeof(cont) == "object")
ele.appendChild( cont );
}
}
_bsn.DOM.getPos = function ( ele )
{
var ele = this.getElement(ele);
var obj = ele;
var curleft = 0;
if (obj.offsetParent)
{
while (obj.offsetParent)
{
curleft += obj.offsetLeft
obj = obj.offsetParent;
}
}
else if (obj.x)
curleft += obj.x;
var obj = ele;
var curtop = 0;
if (obj.offsetParent)
{
while (obj.offsetParent)
{
curtop += obj.offsetTop
obj = obj.offsetParent;
}
}
else if (obj.y)
curtop += obj.y;
return {x:curleft, y:curtop}
}
// FADER PROTOTYPE _____________________________________________
if (typeof(_bsn.Fader) == "undefined")
_bsn.Fader = {}
_bsn.Fader = function (ele, from, to, fadetime, callback)
{
if (!ele)
return false;
this.ele = ele;
this.from = from;
this.to = to;
this.callback = callback;
this.nDur = fadetime;
this.nInt = 50;
this.nTime = 0;
var p = this;
this.nID = setInterval(function() { p._fade() }, this.nInt);
}
_bsn.Fader.prototype._fade = function()
{
this.nTime += this.nInt;
var ieop = Math.round( this._tween(this.nTime, this.from, this.to, this.nDur) * 100 );
var op = ieop / 100;
if (this.ele.filters) // internet explorer
{
try
{
this.ele.filters.item("DXImageTransform.Microsoft.Alpha").opacity = ieop;
} catch (e) {
// If it is not set initially, the browser will throw an error. This will set it if it is not set yet.
this.ele.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity='+ieop+')';
}
}
else // other browsers
{
this.ele.style.opacity = op;
}
if (this.nTime == this.nDur)
{
clearInterval( this.nID );
if (this.callback != undefined)
this.callback();
}
}
_bsn.Fader.prototype._tween = function(t,b,c,d)
{
return b + ( (c-b) * (t/d) );
}

View File

@@ -0,0 +1,367 @@
import difflib
import os
from django import forms
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import signals
from django.utils.translation import ugettext_lazy as _
from markdown import markdown
from settings import *
class ShouldHaveExactlyOneRootSlug(Exception):
pass
class Article(models.Model):
"""Wiki article referring to Revision model for actual content.
'slug' and 'parent' field should be maintained centrally, since users
aren't allowed to change them, anyways.
"""
title = models.CharField(max_length=512, verbose_name=_('Article title'),
blank=False)
slug = models.SlugField(max_length=100, verbose_name=_('slug'),
help_text=_('Letters, numbers, underscore and hyphen.'),
blank=True)
created_by = models.ForeignKey(User, verbose_name=_('Created by'), blank=True, null=True)
created_on = models.DateTimeField(auto_now_add = 1)
modified_on = models.DateTimeField(auto_now_add = 1)
parent = models.ForeignKey('self', verbose_name=_('Parent article slug'),
help_text=_('Affects URL structure and possibly inherits permissions'),
null=True, blank=True)
locked = models.BooleanField(default=False, verbose_name=_('Locked for editing'))
permissions = models.ForeignKey('Permission', verbose_name=_('Permissions'),
blank=True, null=True,
help_text=_('Permission group'))
current_revision = models.OneToOneField('Revision', related_name='current_rev',
blank=True, null=True, editable=True)
related = models.ManyToManyField('self', verbose_name=_('Related articles'), symmetrical=True,
help_text=_('Sets a symmetrical relation other articles'),
blank=True, null=True)
def attachments(self):
return ArticleAttachment.objects.filter(article__exact = self)
@classmethod
def get_root(cls):
"""Return the root article, which should ALWAYS exist..
except the very first time the wiki is loaded, in which
case the user is prompted to create this article."""
try:
return Article.objects.filter(slug__exact = "")[0]
except:
raise ShouldHaveExactlyOneRootSlug()
def get_url(self):
"""Return the Wiki URL for an article"""
url = self.slug + "/"
if self.parent_id:
parent_url = cache.get("wiki_url-" + str(self.parent_id))
if parent_url is None:
parent_url = self.parent.get_url()
url = parent_url + url
cache.set("wiki_url-" + str(self.id), url, 60*60)
return url
def get_abs_url(self):
"""Return the absolute path for an article. This is necessary in cases
where the template system isn't used for generating URLs..."""
# TODO: Remove and create a reverse() lookup.
return WIKI_BASE + self.get_url()
@models.permalink
def get_absolute_url(self):
return ('wiki_view', [self.get_url()])
@classmethod
def get_url_reverse(cls, path, article, return_list=[]):
"""Lookup a URL and return the corresponding set of articles
in the path."""
if path == []:
return return_list + [article]
# Lookup next child in path
try:
a = Article.objects.get(parent__exact = article, slug__exact=str(path[0]))
return cls.get_url_reverse(path[1:], a, return_list+[article])
except Exception, e:
return None
def can_read(self, user):
""" Check read permissions and return True/False."""
if user.is_superuser:
return True
if self.permissions:
perms = self.permissions.can_read.all()
return perms.count() == 0 or (user in perms)
else:
return self.parent.can_read(user) if self.parent else True
def can_write(self, user):
""" Check write permissions and return True/False."""
if user.is_superuser:
return True
if self.permissions:
perms = self.permissions.can_write.all()
return perms.count() == 0 or (user in perms)
else:
return self.parent.can_write(user) if self.parent else True
def can_write_l(self, user):
"""Check write permissions and locked status"""
if user.is_superuser:
return True
return not self.locked and self.can_write(user)
def can_attach(self, user):
return self.can_write_l(user) and (WIKI_ALLOW_ANON_ATTACHMENTS or not user.is_anonymous())
def __unicode__(self):
if self.slug == '' and not self.parent:
return unicode(_('Root article'))
else:
return self.get_url()
class Meta:
unique_together = (('slug', 'parent'),)
verbose_name = _('Article')
verbose_name_plural = _('Articles')
def get_attachment_filepath(instance, filename):
"""Store file, appending new extension for added security"""
dir_ = WIKI_ATTACHMENTS + instance.article.get_url()
dir_ = '/'.join(filter(lambda x: x!='', dir_.split('/')))
if not os.path.exists(WIKI_ATTACHMENTS_ROOT + dir_):
os.makedirs(WIKI_ATTACHMENTS_ROOT + dir_)
return dir_ + '/' + filename + '.upload'
class ArticleAttachment(models.Model):
article = models.ForeignKey(Article, verbose_name=_('Article'))
file = models.FileField(max_length=255, upload_to=get_attachment_filepath, verbose_name=_('Attachment'))
uploaded_by = models.ForeignKey(User, blank=True, verbose_name=_('Uploaded by'), null=True)
uploaded_on = models.DateTimeField(auto_now_add = True, verbose_name=_('Upload date'))
def download_url(self):
return reverse('wiki_view_attachment', args=(self.article.get_url(), self.filename()))
def filename(self):
return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
def get_size(self):
try:
size = self.file.size
except OSError:
size = 0
return size
def filename(self):
return '.'.join(self.file.name.split('/')[-1].split('.')[:-1])
def is_image(self):
fname = self.filename().split('.')
if len(fname) > 1 and fname[-1].lower() in WIKI_IMAGE_EXTENSIONS:
return True
return False
def get_thumb(self):
return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE)
def get_thumb_small(self):
return self.get_thumb_impl(*WIKI_IMAGE_THUMB_SIZE_SMALL)
def mk_thumbs(self):
self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE, **{'force':True})
self.mk_thumb(*WIKI_IMAGE_THUMB_SIZE_SMALL, **{'force':True})
def mk_thumb(self, width, height, force=False):
"""Requires Python Imaging Library (PIL)"""
if not self.get_size():
return False
if not self.is_image():
return False
base_path = os.path.dirname(self.file.path)
orig_name = self.filename().split('.')
thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
thumb_filepath = "%s%s%s" % (base_path, os.sep, thumb_filename)
if force or not os.path.exists(thumb_filepath):
try:
import Image
img = Image.open(self.file.path)
img.thumbnail((width,height), Image.ANTIALIAS)
img.save(thumb_filepath)
except IOError:
return False
return True
def get_thumb_impl(self, width, height):
"""Requires Python Imaging Library (PIL)"""
if not self.get_size():
return False
if not self.is_image():
return False
self.mk_thumb(width, height)
orig_name = self.filename().split('.')
thumb_filename = "%s__thumb__%d_%d.%s" % ('.'.join(orig_name[:-1]), width, height, orig_name[-1])
thumb_url = settings.MEDIA_URL + WIKI_ATTACHMENTS + self.article.get_url() +'/' + thumb_filename
return thumb_url
def __unicode__(self):
return self.filename()
class Revision(models.Model):
article = models.ForeignKey(Article, verbose_name=_('Article'))
revision_text = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Description of change'))
revision_user = models.ForeignKey(User, verbose_name=_('Modified by'),
blank=True, null=True, related_name='wiki_revision_user')
revision_date = models.DateTimeField(auto_now_add = True, verbose_name=_('Revision date'))
contents = models.TextField(verbose_name=_('Contents (Use MarkDown format)'))
contents_parsed = models.TextField(editable=False, blank=True, null=True)
counter = models.IntegerField(verbose_name=_('Revision#'), default=1, editable=False)
previous_revision = models.ForeignKey('self', blank=True, null=True, editable=False)
# Deleted has three values. 0 is normal, non-deleted. 1 is if it was deleted by a normal user. It should
# be a NEW revision, so that it appears in the history. 2 is a special flag that can be applied or removed
# from a normal revision. It means it has been admin-deleted, and can only been seen by an admin. It doesn't
# show up in the history.
deleted = models.IntegerField(verbose_name=_('Deleted group'), default=0)
def get_user(self):
return self.revision_user if self.revision_user else _('Anonymous')
# Called after the deleted fied has been changed (between 0 and 2). This bypasses the normal checks put in
# save that update the revision or reject the save if contents haven't changed
def adminSetDeleted(self, deleted):
self.deleted = deleted
super(Revision, self).save()
def save(self, **kwargs):
# Check if contents have changed... if not, silently ignore save
if self.article and self.article.current_revision:
if self.deleted == 0 and self.article.current_revision.contents == self.contents:
return
else:
import datetime
self.article.modified_on = datetime.datetime.now()
self.article.save()
# Increment counter according to previous revision
previous_revision = Revision.objects.filter(article=self.article).order_by('-counter')
if previous_revision.count() > 0:
if previous_revision.count() > previous_revision[0].counter:
self.counter = previous_revision.count() + 1
else:
self.counter = previous_revision[0].counter + 1
else:
self.counter = 1
if (self.article.current_revision and self.article.current_revision.deleted == 0):
self.previous_revision = self.article.current_revision
# Create pre-parsed contents - no need to parse on-the-fly
ext = WIKI_MARKDOWN_EXTENSIONS
ext += ["wikipath(base_url=%s)" % reverse('wiki_view', args=('/',))]
print ext
self.contents_parsed = markdown(self.contents,
extensions=ext,
safe_mode='escape',)
super(Revision, self).save(**kwargs)
def delete(self, **kwargs):
"""If a current revision is deleted, then regress to the previous
revision or insert a stub, if no other revisions are available"""
article = self.article
if article.current_revision == self:
prev_revision = Revision.objects.filter(article__exact = article,
pk__not = self.pk).order_by('-counter')
if prev_revision:
article.current_revision = prev_revision[0]
article.save()
else:
r = Revision(article=article,
revision_user = article.created_by)
r.contents = unicode(_('Auto-generated stub'))
r.revision_text= unicode(_('Auto-generated stub'))
r.save()
article.current_revision = r
article.save()
super(Revision, self).delete(**kwargs)
def get_diff(self):
if (self.deleted == 1):
yield "Article Deletion"
return
if self.previous_revision:
previous = self.previous_revision.contents.splitlines(1)
else:
previous = []
# Todo: difflib.HtmlDiff would look pretty for our history pages!
diff = difflib.unified_diff(previous, self.contents.splitlines(1))
# let's skip the preamble
diff.next(); diff.next(); diff.next()
for d in diff:
yield d
def __unicode__(self):
return "r%d" % self.counter
class Meta:
verbose_name = _('article revision')
verbose_name_plural = _('article revisions')
class Permission(models.Model):
permission_name = models.CharField(max_length = 255, verbose_name=_('Permission name'))
can_write = models.ManyToManyField(User, blank=True, null=True, related_name='write',
help_text=_('Select none to grant anonymous access.'))
can_read = models.ManyToManyField(User, blank=True, null=True, related_name='read',
help_text=_('Select none to grant anonymous access.'))
def __unicode__(self):
return self.permission_name
class Meta:
verbose_name = _('Article permission')
verbose_name_plural = _('Article permissions')
class RevisionForm(forms.ModelForm):
contents = forms.CharField(label=_('Contents'), widget=forms.Textarea(attrs={'rows':8, 'cols':50}))
class Meta:
model = Revision
fields = ['contents', 'revision_text']
class RevisionFormWithTitle(forms.ModelForm):
title = forms.CharField(label=_('Title'))
class Meta:
model = Revision
fields = ['title', 'contents', 'revision_text']
class CreateArticleForm(RevisionForm):
title = forms.CharField(label=_('Title'))
class Meta:
model = Revision
fields = ['title', 'contents',]
def set_revision(sender, *args, **kwargs):
"""Signal handler to ensure that a new revision is always chosen as the
current revision - automatically. It simplifies stuff greatly. Also
stores previous revision for diff-purposes"""
instance = kwargs['instance']
created = kwargs['created']
if created and instance.article:
instance.article.current_revision = instance
instance.article.save()
signals.post_save.connect(set_revision, Revision)

View File

@@ -0,0 +1,111 @@
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
# Default settings.. overwrite in your own settings.py
# Planned feature.
WIKI_USE_MARKUP_WIDGET = True
####################
# LOGIN PROTECTION #
####################
# Before setting the below parameters, please note that permissions can
# be set in the django permission system on individual articles and their
# child articles. In this way you can add a user group and give them
# special permissions, be it on the root article or some other. Permissions
# are inherited on lower levels.
# Adds standard django login protection for viewing
WIKI_REQUIRE_LOGIN_VIEW = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_VIEW',
True)
# Adds standard django login protection for editing
WIKI_REQUIRE_LOGIN_EDIT = getattr(settings, 'SIMPLE_WIKI_REQUIRE_LOGIN_EDIT',
True)
####################
# ATTACHMENTS #
####################
# This should be a directory that's writable for the web server.
# It's relative to the MEDIA_ROOT.
WIKI_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS',
'simplewiki/attachments/')
# If false, attachments will completely disappear
WIKI_ALLOW_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ATTACHMENTS',
False)
# If WIKI_REQUIRE_LOGIN_EDIT is False, then attachments can still be disallowed
WIKI_ALLOW_ANON_ATTACHMENTS = getattr(settings, 'SIMPLE_WIKI_ALLOW_ANON_ATTACHMENTS', False)
# Attachments are automatically stored with a dummy extension and delivered
# back to the user with their original extension.
# This setting does not add server security, but might add user security
# if set -- or force users to use standard formats, which might also
# be a good idea.
# Example: ('pdf', 'doc', 'gif', 'jpeg', 'jpg', 'png')
WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS',
None)
# At the moment this variable should not be modified, because
# it breaks compatibility with the normal Django FileField and uploading
# from the admin interface.
WIKI_ATTACHMENTS_ROOT = settings.MEDIA_ROOT
# Bytes! Default: 1 MB.
WIKI_ATTACHMENTS_MAX = getattr(settings, 'SIMPLE_WIKI_ATTACHMENTS_MAX',
1 * 1024 * 1024)
# Allow users to edit titles of pages
# (warning! titles are not maintained in the revision system.)
WIKI_ALLOW_TITLE_EDIT = getattr(settings, 'SIMPLE_WIKI_ALLOW_TITLE_EDIT', False)
# Global context processors
# These are appended to TEMPLATE_CONTEXT_PROCESSORS in your Django settings
# whenever the wiki is in use. It can be used as a simple, but effective
# way of extending simplewiki without touching original code (and thus keeping
# everything easily maintainable)
WIKI_CONTEXT_PREPROCESSORS = getattr(settings, 'SIMPLE_WIKI_CONTEXT_PREPROCESSORS',
())
####################
# AESTHETICS #
####################
# List of extensions to be used by Markdown. Custom extensions (i.e., with file
# names of mdx_*.py) can be dropped into the simplewiki (or project) directory
# and then added to this list to be utilized. Wiki is enabled automatically.
#
# For more information, see
# http://www.freewisdom.org/projects/python-markdown/Available_Extensions
WIKI_MARKDOWN_EXTENSIONS = getattr(settings, 'SIMPLE_WIKI_MARKDOWN_EXTENSIONS',
['footnotes',
'tables',
'headerid',
'fenced_code',
'def_list',
#'codehilite', #This was throwing errors
'abbr',
'toc',
'mathjax',
'video', # In-line embedding for YouTube, etc.
'circuit',
])
WIKI_IMAGE_EXTENSIONS = getattr(settings,
'SIMPLE_WIKI_IMAGE_EXTENSIONS',
('jpg','jpeg','gif','png','tiff','bmp'))
# Planned features
WIKI_PAGE_WIDTH = getattr(settings,
'SIMPLE_WIKI_PAGE_WIDTH', "100%")
WIKI_PAGE_ALIGN = getattr(settings,
'SIMPLE_WIKI_PAGE_ALIGN', "center")
WIKI_IMAGE_THUMB_SIZE = getattr(settings,
'SIMPLE_WIKI_IMAGE_THUMB_SIZE', (200,150))
WIKI_IMAGE_THUMB_SIZE_SMALL = getattr(settings,
'SIMPLE_WIKI_IMAGE_THUMB_SIZE_SMALL', (100,100))

View File

@@ -0,0 +1,18 @@
from django import template
from django.conf import settings
from django.template.defaultfilters import stringfilter
from django.utils.http import urlquote as django_urlquote
from simplewiki.settings import *
register = template.Library()
@register.filter()
def prepend_media_url(value):
"""Prepend user defined media root to url"""
return settings.MEDIA_URL + value
@register.filter()
def urlquote(value):
"""Prepend user defined media root to url"""
return django_urlquote(value)

View File

@@ -0,0 +1,23 @@
"""
This file demonstrates two different styles of tests (one doctest and one
unittest). These will both pass when you run "manage.py test".
Replace these with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.failUnlessEqual(1 + 1, 2)
__test__ = {"doctest": """
Another way to test that 1 + 1 is equal to 2.
>>> 1 + 1 == 2
True
"""}

View File

@@ -0,0 +1,20 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('',
url(r'^$', 'simplewiki.views.root_redirect', name='wiki_root'),
url(r'^view(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.view', name='wiki_view'),
url(r'^view_revision/([0-9]*)(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.view_revision', name='wiki_view_revision'),
url(r'^edit(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.edit', name='wiki_edit'),
url(r'^create(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.create', name='wiki_create'),
url(r'^history(/[a-zA-Z\d/_-]*)/([0-9]*)/?$', 'simplewiki.views.history', name='wiki_history'),
url(r'^search_related(/[a-zA-Z\d/_-]*)/?$', 'simplewiki.views.search_add_related', name='search_related'),
url(r'^random/?$', 'simplewiki.views.random_article', name='wiki_random'),
url(r'^revision_feed/([0-9]*)/?$', 'simplewiki.views.revision_feed', name='wiki_revision_feed'),
url(r'^search/?$', 'simplewiki.views.search_articles', name='wiki_search_articles'),
url(r'^list/?$', 'simplewiki.views.search_articles', name='wiki_list_articles'), #Just an alias for the search, but you usually don't submit a search term
# url(r'^/?([a-zA-Z\d/_-]*)/_related/add/$', 'simplewiki.views.add_related', name='add_related'),
# url(r'^/?([a-zA-Z\d/_-]*)/_related/remove/(\d+)$', 'simplewiki.views.remove_related', name='wiki_remove_relation'),
# url(r'^/?([a-zA-Z\d/_-]*)/_add_attachment/$', 'simplewiki.views_attachments.add_attachment', name='add_attachment'),
# url(r'^/?([a-zA-Z\d/_-]*)/_view_attachment/(.+)?$', 'simplewiki.views_attachments.view_attachment', name='wiki_view_attachment'),
# url(r'^(.*)$', 'simplewiki.views.encode_err', name='wiki_encode_err')
)

View File

@@ -0,0 +1,800 @@
# Markdown: Syntax
[TOC]
## Overview
### Philosophy
Markdown is intended to be as easy-to-read and easy-to-write as is feasible.
Readability, however, is emphasized above all else. A Markdown-formatted
document should be publishable as-is, as plain text, without looking
like it's been marked up with tags or formatting instructions. While
Markdown's syntax has been influenced by several existing text-to-HTML
filters -- including [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4],
[Grutatext] [5], and [EtText] [6] -- the single biggest source of
inspiration for Markdown's syntax is the format of plain text email.
[1]: http://docutils.sourceforge.net/mirror/setext.html
[2]: http://www.aaronsw.com/2002/atx/
[3]: http://textism.com/tools/textile/
[4]: http://docutils.sourceforge.net/rst.html
[5]: http://www.triptico.com/software/grutatxt.html
[6]: http://ettext.taint.org/doc/
To this end, Markdown's syntax is comprised entirely of punctuation
characters, which punctuation characters have been carefully chosen so
as to look like what they mean. E.g., asterisks around a word actually
look like \*emphasis\*. Markdown lists look like, well, lists. Even
blockquotes look like quoted passages of text, assuming you've ever
used email.
### Automatic Escaping for Special Characters
In HTML, there are two characters that demand special treatment: `<`
and `&`. Left angle brackets are used to start tags; ampersands are
used to denote HTML entities. If you want to use them as literal
characters, you must escape them as entities, e.g. `&lt;`, and
`&amp;`.
Ampersands in particular are bedeviling for web writers. If you want to
write about 'AT&T', you need to write '`AT&amp;T`'. You even need to
escape ampersands within URLs. Thus, if you want to link to:
http://images.google.com/images?num=30&q=larry+bird
you need to encode the URL as:
http://images.google.com/images?num=30&amp;q=larry+bird
in your anchor tag `href` attribute. Needless to say, this is easy to
forget, and is probably the single most common source of HTML validation
errors in otherwise well-marked-up web sites.
Markdown allows you to use these characters naturally, taking care of
all the necessary escaping for you. If you use an ampersand as part of
an HTML entity, it remains unchanged; otherwise it will be translated
into `&amp;`.
So, if you want to include a copyright symbol in your article, you can write:
&copy;
and Markdown will leave it alone. But if you write:
AT&T
Markdown will translate it to:
AT&amp;T
Similarly, because Markdown supports [inline HTML](#html), if you use
angle brackets as delimiters for HTML tags, Markdown will treat them as
such. But if you write:
4 < 5
Markdown will translate it to:
4 &lt; 5
However, inside Markdown code spans and blocks, angle brackets and
ampersands are *always* encoded automatically. This makes it easy to use
Markdown to write about HTML code. (As opposed to raw HTML, which is a
terrible format for writing about HTML syntax, because every single `<`
and `&` in your example code needs to be escaped.)
* * *
## Block Elements
### Paragraphs and Line Breaks
A paragraph is simply one or more consecutive lines of text, separated
by one or more blank lines. (A blank line is any line that looks like a
blank line -- a line containing nothing but spaces or tabs is considered
blank.) Normal paragraphs should not be indented with spaces or tabs.
The implication of the "one or more consecutive lines of text" rule is
that Markdown supports "hard-wrapped" text paragraphs. This differs
significantly from most other text-to-HTML formatters (including Movable
Type's "Convert Line Breaks" option) which translate every line break
character in a paragraph into a `<br />` tag.
When you *do* want to insert a `<br />` break tag using Markdown, you
end a line with two or more spaces, then type return.
Yes, this takes a tad more effort to create a `<br />`, but a simplistic
"every line break is a `<br />`" rule wouldn't work for Markdown.
Markdown's email-style [blockquoting][bq] and multi-paragraph [list items][l]
work best -- and look better -- when you format them with hard breaks.
[bq]: #blockquote
[l]: #list
### Headers
Markdown supports two styles of headers, [Setext] [1] and [atx] [2].
Setext-style headers are "underlined" using equal signs (for first-level
headers) and dashes (for second-level headers). For example:
This is an H1
=============
This is an H2
-------------
This is an H3
_____________
Any number of underlining `=`'s or `-`'s will work.
Atx-style headers use 1-6 hash characters at the start of the line,
corresponding to header levels 1-6. For example:
# This is an H1
## This is an H2
###### This is an H6
Optionally, you may "close" atx-style headers. This is purely
cosmetic -- you can use this if you think it looks better. The
closing hashes don't even need to match the number of hashes
used to open the header. (The number of opening hashes
determines the header level.) :
# This is an H1 #
## This is an H2 ##
### This is an H3 ######
### Blockquotes
Markdown uses email-style `>` characters for blockquoting. If you're
familiar with quoting passages of text in an email message, then you
know how to create a blockquote in Markdown. It looks best if you hard
wrap the text and put a `>` before every line:
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
> consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
> Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
>
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
> id sem consectetuer libero luctus adipiscing.
Markdown allows you to be lazy and only put the `>` before the first
line of a hard-wrapped paragraph:
> This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
> Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.
Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by
adding additional levels of `>`:
> This is the first level of quoting.
>
> > This is nested blockquote.
>
> Back to the first level.
Blockquotes can contain other Markdown elements, including headers, lists,
and code blocks:
> ## This is a header.
>
> 1. This is the first list item.
> 2. This is the second list item.
>
> Here's some example code:
>
> return shell_exec("echo $input | $markdown_script");
Any decent text editor should make email-style quoting easy. For
example, with BBEdit, you can make a selection and choose Increase
Quote Level from the Text menu.
### Lists
Markdown supports ordered (numbered) and unordered (bulleted) lists.
Unordered lists use asterisks, pluses, and hyphens -- interchangably
-- as list markers:
* Red
* Green
* Blue
is equivalent to:
+ Red
+ Green
+ Blue
and:
- Red
- Green
- Blue
Ordered lists use numbers followed by periods:
1. Bird
2. McHale
3. Parish
It's important to note that the actual numbers you use to mark the
list have no effect on the HTML output Markdown produces. The HTML
Markdown produces from the above list is:
<ol>
<li>Bird</li>
<li>McHale</li>
<li>Parish</li>
</ol>
If you instead wrote the list in Markdown like this:
1. Bird
1. McHale
1. Parish
or even:
3. Bird
1. McHale
8. Parish
you'd get the exact same HTML output. The point is, if you want to,
you can use ordinal numbers in your ordered Markdown lists, so that
the numbers in your source match the numbers in your published HTML.
But if you want to be lazy, you don't have to.
If you do use lazy list numbering, however, you should still start the
list with the number 1. At some point in the future, Markdown may support
starting ordered lists at an arbitrary number.
List markers typically start at the left margin, but may be indented by
up to three spaces. List markers must be followed by one or more spaces
or a tab.
To make lists look nice, you can wrap items with hanging indents:
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
viverra nec, fringilla in, laoreet vitae, risus.
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
Suspendisse id sem consectetuer libero luctus adipiscing.
But if you want to be lazy, you don't have to:
* Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
viverra nec, fringilla in, laoreet vitae, risus.
* Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
Suspendisse id sem consectetuer libero luctus adipiscing.
If list items are separated by blank lines, Markdown will wrap the
items in `<p>` tags in the HTML output. For example, this input:
* Bird
* Magic
will turn into:
<ul>
<li>Bird</li>
<li>Magic</li>
</ul>
But this:
* Bird
* Magic
will turn into:
<ul>
<li><p>Bird</p></li>
<li><p>Magic</p></li>
</ul>
List items may consist of multiple paragraphs. Each subsequent
paragraph in a list item must be indented by either 4 spaces
or one tab:
1. This is a list item with two paragraphs. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
sit amet velit.
2. Suspendisse id sem consectetuer libero luctus adipiscing.
It looks nice if you indent every line of the subsequent
paragraphs, but here again, Markdown will allow you to be
lazy:
* This is a list item with two paragraphs.
This is the second paragraph in the list item. You're
only required to indent the first line. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit.
* Another item in the same list.
To put a blockquote within a list item, the blockquote's `>`
delimiters need to be indented:
* A list item with a blockquote:
> This is a blockquote
> inside a list item.
To put a code block within a list item, the code block needs
to be indented *twice* -- 8 spaces or two tabs:
* A list item with a code block:
<code goes here>
It's worth noting that it's possible to trigger an ordered list by
accident, by writing something like this:
1986. What a great season.
In other words, a *number-period-space* sequence at the beginning of a
line. To avoid this, you can backslash-escape the period:
1986\. What a great season.
### Code Blocks
Pre-formatted code blocks are used for writing about programming or
markup source code. Rather than forming normal paragraphs, the lines
of a code block are interpreted literally. Markdown wraps a code block
in both `<pre>` and `<code>` tags.
To produce a code block in Markdown, simply indent every line of the
block by at least 4 spaces or 1 tab. For example, given this input:
This is a normal paragraph:
This is a code block.
Markdown will generate:
<p>This is a normal paragraph:</p>
<pre><code>This is a code block.
</code></pre>
One level of indentation -- 4 spaces or 1 tab -- is removed from each
line of the code block. For example, this:
Here is an example of AppleScript:
tell application "Foo"
beep
end tell
will turn into:
<p>Here is an example of AppleScript:</p>
<pre><code>tell application "Foo"
beep
end tell
</code></pre>
A code block continues until it reaches a line that is not indented
(or the end of the article).
Within a code block, ampersands (`&`) and angle brackets (`<` and `>`)
are automatically converted into HTML entities. This makes it very
easy to include example HTML source code using Markdown -- just paste
it and indent it, and Markdown will handle the hassle of encoding the
ampersands and angle brackets. For example, this:
<div class="footer">
&copy; 2004 Foo Corporation
</div>
will turn into:
<pre><code>&lt;div class="footer"&gt;
&amp;copy; 2004 Foo Corporation
&lt;/div&gt;
</code></pre>
Regular Markdown syntax is not processed within code blocks. E.g.,
asterisks are just literal asterisks within a code block. This means
it's also easy to use Markdown to write about Markdown's own syntax.
### Horizontal Rules
You can produce a horizontal rule tag (`<hr />`) by placing three or
more hyphens, asterisks, or underscores on a line by themselves. If you
wish, you may use spaces between the hyphens or asterisks. Each of the
following lines will produce a horizontal rule:
* * *
***
*****
- - -
---------------------------------------
## Span Elements
### Links
Markdown supports two style of links: *inline* and *reference*.
In both styles, the link text is delimited by [square brackets].
To create an inline link, use a set of regular parentheses immediately
after the link text's closing square bracket. Inside the parentheses,
put the URL where you want the link to point, along with an *optional*
title for the link, surrounded in quotes. For example:
This is [an example](http://example.com/ "Title") inline link.
[This link](http://example.net/) has no title attribute.
Will produce:
<p>This is <a href="http://example.com/" title="Title">
an example</a> inline link.</p>
<p><a href="http://example.net/">This link</a> has no
title attribute.</p>
If you're referring to a local resource on the same server, you can
use relative paths:
See my [About](/about/) page for details.
Reference-style links use a second set of square brackets, inside
which you place a label of your choosing to identify the link:
This is [an example][id] reference-style link.
You can optionally use a space to separate the sets of brackets:
This is [an example] [id] reference-style link.
Then, anywhere in the document, you define your link label like this,
on a line by itself:
[id]: http://example.com/ "Optional Title Here"
That is:
* Square brackets containing the link identifier (optionally
indented from the left margin using up to three spaces);
* followed by a colon;
* followed by one or more spaces (or tabs);
* followed by the URL for the link;
* optionally followed by a title attribute for the link, enclosed
in double or single quotes, or enclosed in parentheses.
The following three link definitions are equivalent:
[foo]: http://example.com/ "Optional Title Here"
[foo]: http://example.com/ 'Optional Title Here'
[foo]: http://example.com/ (Optional Title Here)
**Note:** There is a known bug in Markdown.pl 1.0.1 which prevents
single quotes from being used to delimit link titles.
The link URL may, optionally, be surrounded by angle brackets:
[id]: <http://example.com/> "Optional Title Here"
You can put the title attribute on the next line and use extra spaces
or tabs for padding, which tends to look better with longer URLs:
[id]: http://example.com/longish/path/to/resource/here
"Optional Title Here"
Link definitions are only used for creating links during Markdown
processing, and are stripped from your document in the HTML output.
Link definition names may consist of letters, numbers, spaces, and
punctuation -- but they are *not* case sensitive. E.g. these two
links:
[link text][a]
[link text][A]
are equivalent.
The *implicit link name* shortcut allows you to omit the name of the
link, in which case the link text itself is used as the name.
Just use an empty set of square brackets -- e.g., to link the word
"Google" to the google.com web site, you could simply write:
[Google][]
And then define the link:
[Google]: http://google.com/
Because link names may contain spaces, this shortcut even works for
multiple words in the link text:
Visit [Daring Fireball][] for more information.
And then define the link:
[Daring Fireball]: http://daringfireball.net/
Link definitions can be placed anywhere in your Markdown document. I
tend to put them immediately after each paragraph in which they're
used, but if you want, you can put them all at the end of your
document, sort of like footnotes.
Here's an example of reference links in action:
I get 10 times more traffic from [Google] [1] than from
[Yahoo] [2] or [MSN] [3].
[1]: http://google.com/ "Google"
[2]: http://search.yahoo.com/ "Yahoo Search"
[3]: http://search.msn.com/ "MSN Search"
Using the implicit link name shortcut, you could instead write:
I get 10 times more traffic from [Google][] than from
[Yahoo][] or [MSN][].
[google]: http://google.com/ "Google"
[yahoo]: http://search.yahoo.com/ "Yahoo Search"
[msn]: http://search.msn.com/ "MSN Search"
Both of the above examples will produce the following HTML output:
<p>I get 10 times more traffic from <a href="http://google.com/"
title="Google">Google</a> than from
<a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a>
or <a href="http://search.msn.com/" title="MSN Search">MSN</a>.</p>
For comparison, here is the same paragraph written using
Markdown's inline link style:
I get 10 times more traffic from [Google](http://google.com/ "Google")
than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or
[MSN](http://search.msn.com/ "MSN Search").
The point of reference-style links is not that they're easier to
write. The point is that with reference-style links, your document
source is vastly more readable. Compare the above examples: using
reference-style links, the paragraph itself is only 81 characters
long; with inline-style links, it's 176 characters; and as raw HTML,
it's 234 characters. In the raw HTML, there's more markup than there
is text.
With Markdown's reference-style links, a source document much more
closely resembles the final output, as rendered in a browser. By
allowing you to move the markup-related metadata out of the paragraph,
you can add links without interrupting the narrative flow of your
prose.
### Emphasis
Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
emphasis. Text wrapped with one `*` or `_` will be wrapped with an
HTML `<em>` tag; double `*`'s or `_`'s will be wrapped with an HTML
`<strong>` tag. E.g., this input:
*single asterisks*
_single underscores_
**double asterisks**
__double underscores__
will produce:
<em>single asterisks</em>
<em>single underscores</em>
<strong>double asterisks</strong>
<strong>double underscores</strong>
You can use whichever style you prefer; the lone restriction is that
the same character must be used to open and close an emphasis span.
Emphasis can be used in the middle of a word:
un*frigging*believable
But if you surround an `*` or `_` with spaces, it'll be treated as a
literal asterisk or underscore.
To produce a literal asterisk or underscore at a position where it
would otherwise be used as an emphasis delimiter, you can backslash
escape it:
\*this text is surrounded by literal asterisks\*
### Code
To indicate a span of code, wrap it with backtick quotes (`` ` ``).
Unlike a pre-formatted code block, a code span indicates code within a
normal paragraph. For example:
Use the `printf()` function.
will produce:
<p>Use the <code>printf()</code> function.</p>
To include a literal backtick character within a code span, you can use
multiple backticks as the opening and closing delimiters:
``There is a literal backtick (`) here.``
which will produce this:
<p><code>There is a literal backtick (`) here.</code></p>
The backtick delimiters surrounding a code span may include spaces --
one after the opening, one before the closing. This allows you to place
literal backtick characters at the beginning or end of a code span:
A single backtick in a code span: `` ` ``
A backtick-delimited string in a code span: `` `foo` ``
will produce:
<p>A single backtick in a code span: <code>`</code></p>
<p>A backtick-delimited string in a code span: <code>`foo`</code></p>
With a code span, ampersands and angle brackets are encoded as HTML
entities automatically, which makes it easy to include example HTML
tags. Markdown will turn this:
Please don't use any `<blink>` tags.
into:
<p>Please don't use any <code>&lt;blink&gt;</code> tags.</p>
You can write this:
`&#8212;` is the decimal-encoded equivalent of `&mdash;`.
to produce:
<p><code>&amp;#8212;</code> is the decimal-encoded
equivalent of <code>&amp;mdash;</code>.</p>
### Images
Admittedly, it's fairly difficult to devise a "natural" syntax for
placing images into a plain text document format.
Markdown uses an image syntax that is intended to resemble the syntax
for links, allowing for two styles: *inline* and *reference*.
Inline image syntax looks like this:
![Alt text](/path/to/img.jpg)
![Alt text](/path/to/img.jpg "Optional title")
That is:
* An exclamation mark: `!`;
* followed by a set of square brackets, containing the `alt`
attribute text for the image;
* followed by a set of parentheses, containing the URL or path to
the image, and an optional `title` attribute enclosed in double
or single quotes.
Reference-style image syntax looks like this:
![Alt text][id]
Where "id" is the name of a defined image reference. Image references
are defined using syntax identical to link references:
[id]: url/to/image "Optional title attribute"
As of this writing, Markdown has no syntax for specifying the
dimensions of an image; if this is important to you, you can simply
use regular HTML `<img>` tags.
## Miscellaneous
### Automatic Links
Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this:
<http://example.com/>
Markdown will turn this into:
<a href="http://example.com/">http://example.com/</a>
Automatic links for email addresses work similarly, except that
Markdown will also perform a bit of randomized decimal and hex
entity-encoding to help obscure your address from address-harvesting
spambots. For example, Markdown will turn this:
<address@example.com>
into something like this:
<a href="&#x6D;&#x61;i&#x6C;&#x74;&#x6F;:&#x61;&#x64;&#x64;&#x72;&#x65;
&#115;&#115;&#64;&#101;&#120;&#x61;&#109;&#x70;&#x6C;e&#x2E;&#99;&#111;
&#109;">&#x61;&#x64;&#x64;&#x72;&#x65;&#115;&#115;&#64;&#101;&#120;&#x61;
&#109;&#x70;&#x6C;e&#x2E;&#99;&#111;&#109;</a>
which will render in a browser as a clickable link to "address@example.com".
(This sort of entity-encoding trick will indeed fool many, if not
most, address-harvesting bots, but it definitely won't fool all of
them. It's better than nothing, but an address published in this way
will probably eventually start receiving spam.)
### Backslash Escapes
Markdown allows you to use backslash escapes to generate literal
characters which would otherwise have special meaning in Markdown's
formatting syntax. For example, if you wanted to surround a word
with literal asterisks (instead of an HTML `<em>` tag), you can use
backslashes before the asterisks, like this:
\*literal asterisks\*
Markdown provides backslash escapes for the following characters:
\ backslash
` backtick
* asterisk
_ underscore
{} curly braces
[] square brackets
() parentheses
# hash mark
+ plus sign
- minus sign (hyphen)
. dot
! exclamation mark

View File

@@ -0,0 +1,557 @@
# -*- coding: utf-8 -*-
import types
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.context_processors import csrf
from django.core.urlresolvers import get_callable
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseServerError, HttpResponseForbidden, HttpResponseNotAllowed
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.template import Context
from django.template import RequestContext, Context, loader
from django.utils import simplejson
from django.utils.translation import ugettext_lazy as _
from mitxmako.shortcuts import render_to_response, render_to_string
from mako.lookup import TemplateLookup
from mako.template import Template
import mitxmako.middleware
from models import * # TODO: Clean up
from settings import *
def view(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True, check_deleted=True)
if perm_err:
return perm_err
d = {'wiki_article': article,
'wiki_article_revision':article.current_revision,
'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'wiki_current_revision_deleted' : not (article.current_revision.deleted == 0),
'wiki_title' : article.title + " - MITX 6.002x Wiki"
}
d.update(csrf(request))
return render_to_response('simplewiki_view.html', d)
def view_revision(request, revision_number, wiki_url, revision=None):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
try:
revision = Revision.objects.get(counter=int(revision_number), article=article)
except:
d = {'wiki_article': article,
'wiki_err_norevision': revision_number,}
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
perm_err = check_permissions(request, article, check_read=True, check_deleted=True, revision=revision)
if perm_err:
return perm_err
d = {'wiki_article': article,
'wiki_article_revision':revision,
'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'wiki_current_revision_deleted' : not (revision.deleted == 0),
}
d.update(csrf(request))
return render_to_response('simplewiki_view.html', d)
def root_redirect(request):
if not request.user.is_authenticated():
return redirect('/')
try:
root = Article.get_root()
except:
err = not_found(request, '/')
return err
return HttpResponseRedirect(reverse('wiki_view', args=(root.get_url())))
def create(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
url_path = get_url_path(wiki_url)
if url_path != [] and url_path[0].startswith('_'):
d = {'wiki_err_keyword': True,
'wiki_url': '/'.join(url_path) }
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
# Lookup path
try:
# Ensure that the path exists...
root = Article.get_root()
# Remove root slug if present in path
if url_path and root.slug == url_path[0]:
url_path = url_path[1:]
path = Article.get_url_reverse(url_path[:-1], root)
if not path:
d = {'wiki_err_noparent': True,
'wiki_url_parent': '/'.join(url_path[:-1]) }
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
perm_err = check_permissions(request, path[-1], check_locked=False, check_write=True, check_deleted=True)
if perm_err:
return perm_err
# Ensure doesn't already exist
article = Article.get_url_reverse(url_path, root)
if article:
return HttpResponseRedirect(reverse('wiki_view', args=(article[-1].get_url(),)))
# TODO: Somehow this doesnt work...
#except ShouldHaveExactlyOneRootSlug, (e):
except:
if Article.objects.filter(parent=None).count() > 0:
return HttpResponseRedirect(reverse('wiki_view', args=('/',)))
# Root not found...
path = []
url_path = [""]
if request.method == 'POST':
f = CreateArticleForm(request.POST)
if f.is_valid():
article = Article()
article.slug = url_path[-1]
if not request.user.is_anonymous():
article.created_by = request.user
article.title = f.cleaned_data.get('title')
if path != []:
article.parent = path[-1]
a = article.save()
new_revision = f.save(commit=False)
if not request.user.is_anonymous():
new_revision.revision_user = request.user
new_revision.article = article
new_revision.save()
import django.db as db
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
else:
f = CreateArticleForm(initial={'title':request.GET.get('wiki_article_name', url_path[-1]),
'contents':_('Headline\n===\n\n')})
d = {'wiki_form': f,
'wiki_write': True,
}
d.update(csrf(request))
return render_to_response('simplewiki_create.html', d)
def edit(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
# Check write permissions
perm_err = check_permissions(request, article, check_write=True, check_locked=True, check_deleted=False)
if perm_err:
return perm_err
if WIKI_ALLOW_TITLE_EDIT:
EditForm = RevisionFormWithTitle
else:
EditForm = RevisionForm
if request.method == 'POST':
f = EditForm(request.POST)
if f.is_valid():
new_revision = f.save(commit=False)
new_revision.article = article
if request.POST.__contains__('delete'):
if (article.current_revision.deleted == 1): #This article has already been deleted. Redirect
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
new_revision.contents = ""
new_revision.deleted = 1
elif not new_revision.get_diff():
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
if not request.user.is_anonymous():
new_revision.revision_user = request.user
new_revision.save()
if WIKI_ALLOW_TITLE_EDIT:
new_revision.article.title = f.cleaned_data['title']
new_revision.article.save()
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
else:
startContents = article.current_revision.contents if (article.current_revision.deleted == 0) else 'Headline\n===\n\n'
f = EditForm({'contents': startContents, 'title': article.title})
d = {'wiki_form': f,
'wiki_write': True,
'wiki_article': article,
'wiki_title' : article.title,
'wiki_attachments_write': article.can_attach(request.user),
}
d.update(csrf(request))
return render_to_response('simplewiki_edit.html', d)
def history(request, wiki_url, page=1):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True, check_deleted=False)
if perm_err:
print "returned error " , perm_err
return perm_err
page_size = 10
try:
p = int(page)
except ValueError:
p = 1
history = Revision.objects.filter(article__exact = article).order_by('-counter').select_related('previous_revision__counter', 'revision_user', 'wiki_article')
if request.method == 'POST':
if request.POST.__contains__('revision'): #They selected a version, but they can be either deleting or changing the version
perm_err = check_permissions(request, article, check_write=True, check_locked=True)
if perm_err:
return perm_err
redirectURL = reverse('wiki_view', args=(article.get_url(),))
try:
r = int(request.POST['revision'])
revision = Revision.objects.get(id=r)
if request.POST.__contains__('change'):
article.current_revision = revision
article.save()
elif request.POST.__contains__('view'):
redirectURL = reverse('wiki_view_revision', args=(revision.counter, article.get_url(),))
#The rese of these are admin functions
elif request.POST.__contains__('delete') and request.user.is_superuser:
if (revision.deleted == 0):
revision.adminSetDeleted(2)
elif request.POST.__contains__('restore') and request.user.is_superuser:
if (revision.deleted == 2):
revision.adminSetDeleted(0)
elif request.POST.__contains__('delete_all') and request.user.is_superuser:
Revision.objects.filter(article__exact = article, deleted = 0).update(deleted = 2)
elif request.POST.__contains__('lock_article'):
print "changing locked article " , article.locked
article.locked = not article.locked
print "changed locked article " , article.locked
article.save()
except:
pass
finally:
return HttpResponseRedirect(redirectURL)
#
#
# <input type="submit" name="delete" value="Delete revision"/>
# <input type="submit" name="restore" value="Restore revision"/>
# <input type="submit" name="delete_all" value="Delete all revisions">
# %else:
# <input type="submit" name="delete_article" value="Delete all revisions">
#
page_count = (history.count()+(page_size-1)) / page_size
if p > page_count:
p = 1
beginItem = (p-1) * page_size
next_page = p + 1 if page_count > p else None
prev_page = p - 1 if p > 1 else None
d = {'wiki_page': p,
'wiki_next_page': next_page,
'wiki_prev_page': prev_page,
'wiki_write': article.can_write_l(request.user),
'wiki_attachments_write': article.can_attach(request.user),
'wiki_article': article,
'wiki_title': article.title,
'wiki_history': history[beginItem:beginItem+page_size],
'show_delete_revision' : request.user.is_superuser,}
d.update(csrf(request))
return render_to_response('simplewiki_history.html', d)
def revision_feed(request, page=1):
if not request.user.is_superuser:
return redirect('/')
page_size = 10
try:
p = int(page)
except ValueError:
p = 1
history = Revision.objects.order_by('-revision_date').select_related('revision_user', 'article', 'previous_revision')
page_count = (history.count()+(page_size-1)) / page_size
if p > page_count:
p = 1
beginItem = (p-1) * page_size
next_page = p + 1 if page_count > p else None
prev_page = p - 1 if p > 1 else None
d = {'wiki_page': p,
'wiki_next_page': next_page,
'wiki_prev_page': prev_page,
'wiki_history': history[beginItem:beginItem+page_size],
'show_delete_revision' : request.user.is_superuser,}
d.update(csrf(request))
return render_to_response('simplewiki_revision_feed.html', d)
def search_articles(request):
if not request.user.is_authenticated():
return redirect('/')
# blampe: We should check for the presence of other popular django search
# apps and use those if possible. Only fall back on this as a last resort.
# Adding some context to results (eg where matches were) would also be nice.
# todo: maybe do some perm checking here
if request.method == 'POST':
querystring = request.POST['value'].strip()
else:
querystring = ""
results = Article.objects.all()
if request.user.is_superuser:
results = results.order_by('current_revision__deleted')
else:
results = results.filter(current_revision__deleted = 0)
if querystring:
for queryword in querystring.split():
# Basic negation is as fancy as we get right now
if queryword[0] == '-' and len(queryword) > 1:
results._search = lambda x: results.exclude(x)
queryword = queryword[1:]
else:
results._search = lambda x: results.filter(x)
results = results._search(Q(current_revision__contents__icontains = queryword) | \
Q(title__icontains = queryword))
results = results.select_related('current_revision__deleted')
results = sorted(results, key=lambda article: (article.current_revision.deleted, article.get_url().lower()) )
if len(results) == 1 and querystring:
return HttpResponseRedirect(reverse('wiki_view', args=(results[0].get_url(),)))
else:
d = {'wiki_search_results': results,
'wiki_search_query': querystring,}
d.update(csrf(request))
return render_to_response('simplewiki_searchresults.html', d)
def search_add_related(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True)
if perm_err:
return perm_err
search_string = request.GET.get('query', None)
self_pk = request.GET.get('self', None)
if search_string:
results = []
related = Article.objects.filter(title__istartswith = search_string)
others = article.related.all()
if self_pk:
related = related.exclude(pk=self_pk)
if others:
related = related.exclude(related__in = others)
related = related.order_by('title')[:10]
for item in related:
results.append({'id': str(item.id),
'value': item.title,
'info': item.get_url()})
else:
results = []
json = simplejson.dumps({'results': results})
return HttpResponse(json, mimetype='application/json')
def add_related(request, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_write=True, check_locked=True)
if perm_err:
return perm_err
try:
related_id = request.POST['id']
rel = Article.objects.get(id=related_id)
has_already = article.related.filter(id=related_id).count()
if has_already == 0 and not rel == article:
article.related.add(rel)
article.save()
except:
pass
finally:
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def remove_related(request, wiki_url, related_id):
if not request.user.is_authenticated():
return redirect('/')
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_write=True, check_locked=True)
if perm_err:
return perm_err
try:
rel_id = int(related_id)
rel = Article.objects.get(id=rel_id)
article.related.remove(rel)
article.save()
except:
pass
finally:
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def random_article(request):
if not request.user.is_authenticated():
return redirect('/')
from random import randint
num_arts = Article.objects.count()
article = Article.objects.all()[randint(0, num_arts-1)]
return HttpResponseRedirect(reverse('wiki_view', args=(article.get_url(),)))
def encode_err(request, url):
d = {'wiki_err_encode': True}
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
def not_found(request, wiki_url):
if not request.user.is_authenticated():
return redirect('/')
"""Generate a NOT FOUND message for some URL"""
d = {'wiki_err_notfound': True,
'wiki_url': wiki_url}
d.update(csrf(request))
return render_to_response('simplewiki_error.html', d)
def get_url_path(url):
"""Return a list of all actual elements of a url, safely ignoring
double-slashes (//) """
return filter(lambda x: x!='', url.split('/'))
def fetch_from_url(request, url):
"""Analyze URL, returning the article and the articles in its path
If something goes wrong, return an error HTTP response"""
err = None
article = None
path = None
url_path = get_url_path(url)
try:
root = Article.get_root()
except:
err = not_found(request, '/')
return (article, path, err)
if url_path and root.slug == url_path[0]:
url_path = url_path[1:]
path = Article.get_url_reverse(url_path, root)
if not path:
err = not_found(request, '/' + '/'.join(url_path))
else:
article = path[-1]
return (article, path, err)
def check_permissions(request, article, check_read=False, check_write=False, check_locked=False, check_deleted=False, revision = None):
read_err = check_read and not article.can_read(request.user)
write_err = check_write and not article.can_write(request.user)
locked_err = check_locked and article.locked
if revision == None:
revision = article.current_revision
deleted_err = check_deleted and not (revision.deleted == 0)
if (request.user.is_superuser):
deleted_err = False
locked_err = False
if read_err or write_err or locked_err or deleted_err:
d = {'wiki_article': article,
'wiki_err_noread': read_err,
'wiki_err_nowrite': write_err,
'wiki_err_locked': locked_err,
'wiki_err_deleted': deleted_err,}
d.update(csrf(request))
# TODO: Make this a little less jarring by just displaying an error
# on the current page? (no such redirect happens for an anon upload yet)
# benjaoming: I think this is the nicest way of displaying an error, but
# these errors shouldn't occur, but rather be prevented on the other pages.
return render_to_response('simplewiki_error.html', d)
else:
return None
####################
# LOGIN PROTECTION #
####################
if WIKI_REQUIRE_LOGIN_VIEW:
view = login_required(view)
history = login_required(history)
# search_related = login_required(search_related)
# wiki_encode_err = login_required(wiki_encode_err)
if WIKI_REQUIRE_LOGIN_EDIT:
create = login_required(create)
edit = login_required(edit)
add_related = login_required(add_related)
remove_related = login_required(remove_related)
if WIKI_CONTEXT_PREPROCESSORS:
settings.TEMPLATE_CONTEXT_PROCESSORS = settings.TEMPLATE_CONTEXT_PROCESSORS + WIKI_CONTEXT_PREPROCESSORS

View File

@@ -0,0 +1,146 @@
import os
from django.contrib.auth.decorators import login_required
from django.core.servers.basehttp import FileWrapper
from django.db.models.fields.files import FieldFile
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404
from django.template import loader, Context
from settings import * # TODO: Clean up
from models import Article, ArticleAttachment, get_attachment_filepath
from views import not_found, check_permissions, get_url_path, fetch_from_url
from simplewiki.settings import WIKI_ALLOW_ANON_ATTACHMENTS
def add_attachment(request, wiki_url):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_write=True, check_locked=True)
if perm_err:
return perm_err
if not WIKI_ALLOW_ATTACHMENTS or (not WIKI_ALLOW_ANON_ATTACHMENTS and request.user.is_anonymous()):
return HttpResponseForbidden()
if request.method == 'POST':
if request.FILES.__contains__('attachment'):
attachment = ArticleAttachment()
if not request.user.is_anonymous():
attachment.uploaded_by = request.user
attachment.article = article
file = request.FILES['attachment']
file_rel_path = get_attachment_filepath(attachment, file.name)
chunk_size = request.upload_handlers[0].chunk_size
filefield = FieldFile(attachment, attachment.file, file_rel_path)
attachment.file = filefield
file_path = WIKI_ATTACHMENTS_ROOT + attachment.file.name
if not request.POST.__contains__('overwrite') and os.path.exists(file_path):
c = Context({'overwrite_warning' : True,
'wiki_article': article,
'filename': file.name})
t = loader.get_template('simplewiki_updateprogressbar.html')
return HttpResponse(t.render(c))
if file.size > WIKI_ATTACHMENTS_MAX:
c = Context({'too_big' : True,
'max_size': WIKI_ATTACHMENTS_MAX,
'wiki_article': article,
'file': file})
t = loader.get_template('simplewiki_updateprogressbar.html')
return HttpResponse(t.render(c))
def get_extension(fname):
return attachment.file.name.split('.')[-2]
if WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS and not \
get_extension(attachment.file.name) in WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS:
c = Context({'extension_err' : True,
'extensions': WIKI_ATTACHMENTS_ALLOWED_EXTENSIONS,
'wiki_article': article,
'file': file})
t = loader.get_template('simplewiki_updateprogressbar.html')
return HttpResponse(t.render(c))
# Remove existing attachments
# TODO: Move this until AFTER having removed file.
# Current problem is that Django's FileField delete() method
# automatically deletes files
for a in article.attachments():
if file_rel_path == a.file.name:
a.delete()
def receive_file():
destination = open(file_path, 'wb+')
size = file.size
cnt = 0
c = Context({'started' : True,})
t = loader.get_template('simplewiki_updateprogressbar.html')
yield t.render(c)
for chunk in file.chunks():
cnt += 1
destination.write(chunk)
c = Context({'progress_width' : (cnt*chunk_size) / size,
'wiki_article': article,})
t = loader.get_template('simplewiki_updateprogressbar.html')
yield t.render(c)
c = Context({'finished' : True,
'wiki_article': article,})
t = loader.get_template('simplewiki_updateprogressbar.html')
destination.close()
attachment.save()
yield t.render(c)
return HttpResponse(receive_file())
return HttpResponse('')
# Taken from http://www.djangosnippets.org/snippets/365/
def send_file(request, filepath):
"""
Send a file through Django without loading the whole file into
memory at once. The FileWrapper will turn the file object into an
iterator for chunks of 8KB.
"""
filename = filepath
wrapper = FileWrapper(file(filename))
response = HttpResponse(wrapper, content_type='text/plain')
response['Content-Length'] = os.path.getsize(filename)
return response
def view_attachment(request, wiki_url, file_name):
(article, path, err) = fetch_from_url(request, wiki_url)
if err:
return err
perm_err = check_permissions(request, article, check_read=True)
if perm_err:
return perm_err
attachment = None
for a in article.attachments():
if get_attachment_filepath(a, file_name) == a.file.name:
attachment = a
if attachment:
filepath = WIKI_ATTACHMENTS_ROOT + attachment.file.name
if os.path.exists(filepath):
return send_file(request, filepath)
raise Http404()
####################
# LOGIN PROTECTION #
####################
if WIKI_REQUIRE_LOGIN_VIEW:
view_attachment = login_required(view_attachment)
if WIKI_REQUIRE_LOGIN_EDIT or not WIKI_ALLOW_ANON_ATTACHMENTS:
add_attachment = login_required(add_attachment)

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View File

@@ -0,0 +1,47 @@
# View for semi-static templatized content.
#
# List of valid templates is explicitly managed for (short-term)
# security reasons.
from mitxmako.shortcuts import render_to_response, render_to_string
from django.shortcuts import redirect
from django.core.context_processors import csrf
from django.conf import settings
#valid_templates=['index.html', 'staff.html', 'info.html', 'credits.html']
valid_templates=['index.html',
'tos.html',
'privacy.html',
'honor.html',
'copyright.html',
'404.html',
'mitx_help.html']
if settings.STATIC_GRAB:
valid_templates = valid_templates+['server-down.html',
'server-error.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']
if template in valid_templates:
return render_to_response(template, {'error' : '',
'csrf': csrf_token })
else:
return redirect('/')
valid_auth_templates=['help.html']
def auth_index(request, template):
if not request.user.is_authenticated():
return redirect('/')
if template in valid_auth_templates:
return render_to_response(template,{})
else:
return redirect('/')

View File

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View File

@@ -0,0 +1,15 @@
# Create your views here.
import os
from django.conf import settings
from django.http import Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
def index(request, page=0):
if not request.user.is_authenticated():
return redirect('/')
return render_to_response('staticbook.html',{'page':int(page)})
def index_shifted(request, page):
return index(request, int(page)+24)

View File

View File

@@ -0,0 +1,99 @@
import os.path
from lxml import etree
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
from student.models import UserTestGroup
import random
import sys
import datetime
import json
middleware.MakoMiddleware()
def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
in [0,1], return the associated group (in the above case, return
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
'''
sum = 0
for (g,p) in groups:
sum = sum + p
if sum > v:
return g
return g # For round-off errors
class Command(BaseCommand):
help = \
''' Assign users to test groups. Takes a list
of groups:
a:0.3,b:0.4,c:0.3 file.txt "Testing something"
Will assign each user to group a, b, or c with
probability 0.3, 0.4, 0.3. Probabilities must
add up to 1.
Will log what happened to file.txt.
'''
def handle(self, *args, **options):
if len(args) != 3:
print "Invalid number of options"
sys.exit(-1)
# Extract groups from string
group_strs = [x.split(':') for x in args[0].split(',')]
groups = [(group,float(value)) for group,value in group_strs]
print "Groups", groups
## Confirm group probabilities add up to 1
total = sum(zip(*groups)[1])
print "Total:", total
if abs(total-1)>0.01:
print "Total not 1"
sys.exit(-1)
## Confirm groups don't already exist
for group in dict(groups):
if UserTestGroup.objects.filter(name=group).count() != 0:
print group, "already exists!"
sys.exit(-1)
group_objects = {}
f = open(args[1],"a+")
## Create groups
for group in dict(groups):
utg = UserTestGroup()
utg.name=group
utg.description = json.dumps({"description":args[2]},
{"time":datetime.datetime.utcnow().isoformat()})
group_objects[group]=utg
group_objects[group].save()
## Assign groups
users = list(User.objects.all())
count = 0
for user in users:
if count % 1000 == 0:
print count
count = count + 1
v = random.uniform(0,1)
group = group_from_value(groups,v)
group_objects[group].users.add(user)
f.write("Assigned user {name} ({id}) to {group}\n".format(name=user.username,
id=user.id,
group=group))
## Save groups
for group in group_objects:
group_objects[group].save()
f.close()
# python manage.py assigngroups summary_test:0.3,skip_summary_test:0.7 log.txt "Do previews of future materials help?"
# python manage.py assigngroups skip_capacitor:0.3,capacitor:0.7 log.txt "Do we show capacitor in linearity tutorial?"

View File

@@ -0,0 +1,23 @@
import os.path
from lxml import etree
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
class Command(BaseCommand):
help = \
''' Extract an e-mail list of all active students. '''
def handle(self, *args, **options):
#text = open(args[0]).read()
#subject = open(args[1]).read()
users = User.objects.all()
for user in users:
if user.is_active:
print user.email

View File

@@ -0,0 +1,27 @@
import os.path
from lxml import etree
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
class Command(BaseCommand):
help = \
'''Sends an e-mail to all users. Takes a single
parameter -- name of e-mail template -- located
in templates/email. Adds a .txt for the message
body, and an _subject.txt for the subject. '''
def handle(self, *args, **options):
#text = open(args[0]).read()
#subject = open(args[1]).read()
users = User.objects.all()
text = middleware.lookup['main'].get_template('email/'+args[0]+".txt").render()
subject = middleware.lookup['main'].get_template('email/'+args[0]+"_subject.txt").render().strip()
for user in users:
if user.is_active:
user.email_user(subject, text)

View File

@@ -0,0 +1,64 @@
import os.path
import time
from lxml import etree
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
from django.core.mail import send_mass_mail
import sys
import datetime
middleware.MakoMiddleware()
def chunks(l, n):
""" Yield successive n-sized chunks from l.
"""
for i in xrange(0, len(l), n):
yield l[i:i+n]
class Command(BaseCommand):
help = \
'''Sends an e-mail to all users in a text file.
E.g.
manage.py userlist.txt message logfile.txt rate
userlist.txt -- list of all users
message -- prefix for template with message
logfile.txt -- where to log progress
rate -- messages per second
'''
log_file = None
def hard_log(self, text):
self.log_file.write(datetime.datetime.utcnow().isoformat()+' -- '+text+'\n')
def handle(self, *args, **options):
global log_file
(user_file, message_base, logfilename, ratestr) = args
users = [u.strip() for u in open(user_file).readlines()]
message = middleware.lookup['main'].get_template('emails/'+message_base+"_body.txt").render()
subject = middleware.lookup['main'].get_template('emails/'+message_base+"_subject.txt").render().strip()
rate = int(ratestr)
self.log_file = open(logfilename, "a+", buffering = 0)
i=0
for users in chunks(users, rate):
emails = [ (subject, message, settings.DEFAULT_FROM_EMAIL, [u]) for u in users ]
self.hard_log(" ".join(users))
send_mass_mail( emails, fail_silently = False )
time.sleep(1)
print datetime.datetime.utcnow().isoformat(), i
i = i+len(users)
# Emergency interruptor
if os.path.exists("/tmp/stopemails.txt"):
self.log_file.close()
sys.exit(-1)
self.log_file.close()

View File

@@ -0,0 +1,38 @@
import os.path
from lxml import etree
from django.core.management.base import BaseCommand
from django.conf import settings
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
import json
from student.models import UserProfile
middleware.MakoMiddleware()
class Command(BaseCommand):
help = \
''' Extract full user information into a JSON file.
Pass a single filename.'''
def handle(self, *args, **options):
f = open(args[0],'w')
#text = open(args[0]).read()
#subject = open(args[1]).read()
users = User.objects.all()
l = []
for user in users:
up = UserProfile.objects.get(user = user)
d = { 'username':user.username,
'email':user.email,
'is_active':user.is_active,
'joined':user.date_joined.isoformat(),
'name':up.name,
'language':up.language,
'location':up.location}
l.append(d)
json.dump(l,f)
f.close()

View File

@@ -0,0 +1,121 @@
# 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 'UserProfile'
db.create_table('auth_userprofile', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True)),
('name', self.gf('django.db.models.fields.TextField')(blank=True)),
('language', self.gf('django.db.models.fields.TextField')(blank=True)),
('location', self.gf('django.db.models.fields.TextField')(blank=True)),
('meta', self.gf('django.db.models.fields.TextField')(blank=True)),
('courseware', self.gf('django.db.models.fields.TextField')(default='course.xml', blank=True)),
))
db.send_create_signal('student', ['UserProfile'])
# Adding model 'Registration'
db.create_table('auth_registration', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True)),
('activation_key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32, db_index=True)),
))
db.send_create_signal('student', ['Registration'])
def backwards(self, orm):
# Deleting model 'UserProfile'
db.delete_table('auth_userprofile')
# Deleting model 'Registration'
db.delete_table('auth_registration')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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.TextField', [], {'default': "'course.xml'", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
}
}
complete_apps = ['student']

View File

@@ -0,0 +1,143 @@
# 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):
# Changing field 'UserProfile.name'
db.alter_column('auth_userprofile', 'name', self.gf('django.db.models.fields.CharField')(max_length=255))
# Adding index on 'UserProfile', fields ['name']
db.create_index('auth_userprofile', ['name'])
# Changing field 'UserProfile.language'
db.alter_column('auth_userprofile', 'language', self.gf('django.db.models.fields.CharField')(max_length=255))
# Adding index on 'UserProfile', fields ['language']
db.create_index('auth_userprofile', ['language'])
# Changing field 'UserProfile.courseware'
db.alter_column('auth_userprofile', 'courseware', self.gf('django.db.models.fields.CharField')(max_length=255))
# Changing field 'UserProfile.meta'
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.CharField')(max_length=255))
# Changing field 'UserProfile.location'
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.CharField')(max_length=255))
# Adding index on 'UserProfile', fields ['location']
db.create_index('auth_userprofile', ['location'])
def backwards(self, orm):
# Removing index on 'UserProfile', fields ['location']
db.delete_index('auth_userprofile', ['location'])
# Removing index on 'UserProfile', fields ['language']
db.delete_index('auth_userprofile', ['language'])
# Removing index on 'UserProfile', fields ['name']
db.delete_index('auth_userprofile', ['name'])
# Changing field 'UserProfile.name'
db.alter_column('auth_userprofile', 'name', self.gf('django.db.models.fields.TextField')())
# Changing field 'UserProfile.language'
db.alter_column('auth_userprofile', 'language', self.gf('django.db.models.fields.TextField')())
# Changing field 'UserProfile.courseware'
db.alter_column('auth_userprofile', 'courseware', self.gf('django.db.models.fields.TextField')())
# Changing field 'UserProfile.meta'
db.alter_column('auth_userprofile', 'meta', self.gf('django.db.models.fields.TextField')())
# Changing field 'UserProfile.location'
db.alter_column('auth_userprofile', 'location', self.gf('django.db.models.fields.TextField')())
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'})
}
}
complete_apps = ['student']

View File

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

View File

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

View File

@@ -0,0 +1,130 @@
"""
WE'RE USING MIGRATIONS!
If you make changes to this model, be sure to create an appropriate migration
file and check it in at the same time as your model changes. To do that,
1. Go to the mitx dir
2. ./manage.py schemamigration user --auto description_of_your_change
3. Add the migration file created in mitx/courseware/migrations/
"""
import uuid
from django.db import models
from django.contrib.auth.models import User
import json
#from cache_toolbox import cache_model, cache_relation
class UserProfile(models.Model):
class Meta:
db_table = "auth_userprofile"
## CRITICAL TODO/SECURITY
# Sanitize all fields.
# This is not visible to other users, but could introduce holes later
user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile')
name = models.CharField(blank=True, max_length=255, db_index=True)
language = models.CharField(blank=True, max_length=255, db_index=True)
location = models.CharField(blank=True, max_length=255, db_index=True)
meta = models.CharField(blank=True, max_length=255) # JSON dictionary for future expansion
courseware = models.CharField(blank=True, max_length=255, default='course.xml')
def get_meta():
try:
js = json.reads(self.meta)
except:
js = dict()
return json
def set_meta(js):
self.meta = json.dumps(js)
## TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group
class UserTestGroup(models.Model):
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
registration profile is created when the user creates an
account, but that account is inactive. Once the user clicks
on the activation key, it becomes active. '''
class Meta:
db_table = "auth_registration"
user = models.ForeignKey(User, unique=True)
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
def register(self, user):
# MINOR TODO: Switch to crypto-secure key
self.activation_key=uuid.uuid4().hex
self.user=user
self.save()
def activate(self):
self.user.is_active = True
self.user.save()
#self.delete()
class PendingNameChange(models.Model):
user = models.OneToOneField(User, unique=True, db_index=True)
new_name = models.CharField(blank=True, max_length=255)
rationale = models.CharField(blank=True, max_length=1024)
class PendingEmailChange(models.Model):
user = models.OneToOneField(User, unique=True, db_index=True)
new_email = models.CharField(blank=True, max_length=255, db_index=True)
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
#cache_relation(User.profile)
#### Helper methods for use from python manage.py shell.
def get_user(email):
u = User.objects.get(email = email)
up = UserProfile.objects.get(user = u)
return u,up
def user_info(email):
u,up = get_user(email)
print "User id", u.id
print "Username", u.username
print "E-mail", u.email
print "Name", up.name
print "Location", up.location
print "Language", up.language
return u,up
def change_email(old_email, new_email):
u = User.objects.get(email = old_email)
u.email = new_email
u.save()
def change_name(email, new_name):
u,up = get_user(email)
up.name = new_name
up.save()
def user_count():
return User.objects.all().count()
def active_user_count():
return User.objects.filter(is_active = True).count()
def create_group(name, description):
utg = UserTestGroup()
utg.name = name
utg.description = description
utg.save()
def add_user_to_group(group, user):
utg = UserTestGroup.objects.get(name = group)
utg.users.add(User.objects.get(username = user))
utg.save()
def remove_user_from_group(group, user):
utg = UserTestGroup.objects.get(name = group)
utg.users.remove(User.objects.get(username = user))
utg.save()

View File

@@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

417
djangoapps/student/views.py Normal file
View File

@@ -0,0 +1,417 @@
import json
import logging
import random
import string
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.core.context_processors import csrf
from django.core.mail import send_mail
from django.core.validators import validate_email, validate_slug
from django.db import connection
from django.http import HttpResponse, Http404
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
from models import Registration, UserProfile
from django_future.csrf import ensure_csrf_cookie
log = logging.getLogger("mitx.user")
def csrf_token(context):
''' A csrf token that can be included in a form.
'''
csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED':
return ''
return u'<div style="display:none"><input type="hidden" name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token)
@ensure_csrf_cookie
def index(request):
''' Redirects to main page -- info page if user authenticated, or marketing if not
'''
if settings.COURSEWARE_ENABLED and request.user.is_authenticated():
return redirect('/info')
else:
csrf_token = csrf(request)['csrf_token']
# TODO: Clean up how 'error' is done.
return render_to_response('index.html', {'csrf': csrf_token })
# Need different levels of logging
@ensure_csrf_cookie
def login_user(request, error=""):
''' AJAX request to log in the user. '''
if 'email' not in request.POST or 'password' not in request.POST:
return HttpResponse(json.dumps({'success':False,
'error': 'Invalid login'})) # TODO: User error message
email = request.POST['email']
password = request.POST['password']
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
log.warning("Login failed - Unknown user email: {0}".format(email))
return HttpResponse(json.dumps({'success':False,
'error': 'Invalid login'})) # TODO: User error message
username = user.username
user = authenticate(username=username, password=password)
if user is None:
log.warning("Login failed - password for {0} is invalid".format(email))
return HttpResponse(json.dumps({'success':False,
'error': 'Invalid login'}))
if user is not None and user.is_active:
try:
login(request, user)
if request.POST['remember'] == 'true':
request.session.set_expiry(None) # or change to 604800 for 7 days
log.debug("Setting user session to never expire")
else:
request.session.set_expiry(0)
except Exception as e:
log.critical("Login failed - Could not create session. Is memcached running?")
log.exception(e)
log.info("Login success - {0} ({1})".format(username, email))
return HttpResponse(json.dumps({'success':True}))
log.warning("Login failed - Account not active for user {0}".format(username))
return HttpResponse(json.dumps({'success':False,
'error': 'Account not active. Check your e-mail.'}))
@ensure_csrf_cookie
def logout_user(request):
''' HTTP request to log in the user. Redirects to marketing page'''
logout(request)
# print len(connection.queries), connection.queries
return redirect('/')
@ensure_csrf_cookie
def change_setting(request):
if not request.user.is_authenticated():
return redirect('/')
up = UserProfile.objects.get(user=request.user) #request.user.profile_cache
if 'location' in request.POST:
# print "loc"
up.location=request.POST['location']
if 'language' in request.POST:
# print "lang"
up.language=request.POST['language']
up.save()
return HttpResponse(json.dumps({'success':True,
'language':up.language,
'location':up.location,}))
@ensure_csrf_cookie
def create_account(request, post_override=None):
js={'success':False}
post_vars = post_override if post_override else request.POST
# Confirm we have a properly formed request
for a in ['username', 'email', 'password', 'location', 'language', 'name']:
if a not in post_vars:
js['value']="Error (401 {field}). E-mail us.".format(field=a)
return HttpResponse(json.dumps(js))
if post_vars['honor_code']!=u'true':
js['value']="To enroll, you must follow the honor code.".format(field=a)
return HttpResponse(json.dumps(js))
if post_vars['terms_of_service']!=u'true':
js['value']="You must accept the terms of service.".format(field=a)
return HttpResponse(json.dumps(js))
# Confirm appropriate fields are there.
# TODO: Check e-mail format is correct.
# 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', 'name', 'password', 'terms_of_service', 'honor_code']:
if len(post_vars[a])<2:
error_str = {'username' : 'Username of length 2 or greater',
'email' : 'Properly formatted e-mail',
'name' : 'Your legal name ',
'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:
validate_email(post_vars['email'])
except:
js['value']="Valid e-mail is required.".format(field=a)
return HttpResponse(json.dumps(js))
try:
validate_slug(post_vars['username'])
except:
js['value']="Username should only consist of A-Z and 0-9.".format(field=a)
return HttpResponse(json.dumps(js))
# Confirm username and e-mail are unique. TODO: This should be in a transaction
if len(User.objects.filter(username=post_vars['username']))>0:
js['value']="An account with this username already exists."
return HttpResponse(json.dumps(js))
if len(User.objects.filter(email=post_vars['email']))>0:
js['value']="An account with this e-mail already exists."
return HttpResponse(json.dumps(js))
u=User(username=post_vars['username'],
email=post_vars['email'],
is_active=False)
u.set_password(post_vars['password'])
r=Registration()
# TODO: Rearrange so that if part of the process fails, the whole process fails.
# Right now, we can have e.g. no registration e-mail sent out and a zombie account
u.save()
r.register(u)
up = UserProfile(user=u)
up.name=post_vars['name']
up.language=post_vars['language']
up.location=post_vars['location']
up.save()
d={'name':post_vars['name'],
'key':r.activation_key,
'site':settings.SITE_NAME}
subject = render_to_string('emails/activation_email_subject.txt',d)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
message = render_to_string('emails/activation_email.txt',d)
try:
if not settings.GENERATE_RANDOM_USER_CREDENTIALS:
res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
except:
js['value']='Could not send activation e-mail.'
return HttpResponse(json.dumps(js))
js={'success':True,
'value':render_to_string('registration/reg_complete.html', {'email':post_vars['email'],
'csrf':csrf(request)['csrf_token']})}
# print len(connection.queries), connection.queries
return HttpResponse(json.dumps(js), mimetype="application/json")
def create_random_account(create_account_function):
def id_generator(size=6, chars=string.ascii_uppercase + string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
def inner_create_random_account(request):
post_override= {'username' : "random_" + id_generator(),
'email' : id_generator(size=10, chars=string.ascii_lowercase) + "_dummy_test@mitx.mit.edu",
'password' : id_generator(),
'location' : id_generator(size=5, chars=string.ascii_uppercase),
'language' : id_generator(size=5, chars=string.ascii_uppercase) + "ish",
'name' : id_generator(size=5, chars=string.ascii_lowercase) + " " + id_generator(size=7, chars=string.ascii_lowercase),
'honor_code' : u'true',
'terms_of_service' : u'true',}
# print "Creating random account: " , post_override
return create_account_function(request, post_override = post_override)
return inner_create_random_account
if settings.GENERATE_RANDOM_USER_CREDENTIALS:
create_account = create_random_account(create_account)
@ensure_csrf_cookie
def activate_account(request, key):
''' When link in activation e-mail is clicked
'''
r=Registration.objects.filter(activation_key=key)
if len(r)==1:
if not r[0].user.is_active:
r[0].activate()
resp = render_to_response("activation_complete.html",{'csrf':csrf(request)['csrf_token']})
return resp
resp = render_to_response("activation_active.html",{'csrf':csrf(request)['csrf_token']})
return resp
if len(r)==0:
return render_to_response("activation_invalid.html",{'csrf':csrf(request)['csrf_token']})
return HttpResponse("Unknown error. Please e-mail us to let us know how it happened.")
@ensure_csrf_cookie
def password_reset(request):
''' Attempts to send a password reset e-mail. '''
if request.method != "POST":
raise Http404
form = PasswordResetForm(request.POST)
if form.is_valid():
form.save( use_https = request.is_secure(),
from_email = settings.DEFAULT_FROM_EMAIL,
request = request )
return HttpResponse(json.dumps({'success':True,
'value': render_to_string('registration/password_reset_done.html', {})}))
else:
return HttpResponse(json.dumps({'success':False,
'error': 'Invalid e-mail'}))
@ensure_csrf_cookie
def reactivation_email(request):
''' Send an e-mail to reactivate a deactivated account, or to
resend an activation e-mail '''
email = request.POST['email']
try:
user = User.objects.get(email = 'email')
except: # TODO: Type of exception
return HttpResponse(json.dumps({'success':False,
'error': 'No inactive user with this e-mail exists'}))
if user.is_active:
return HttpResponse(json.dumps({'success':False,
'error': 'User is already active'}))
reg = Registration.objects.get(user = user)
reg.register(user)
d={'name':UserProfile.get(user = user).name,
'key':r.activation_key,
'site':settings.SITE_NAME}
subject = render_to_string('reactivation_email_subject.txt',d)
subject = ''.join(subject.splitlines())
message = render_to_string('reactivation_email.txt',d)
res=u.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
return HttpResponse(json.dumps({'success':True}))
@ensure_csrf_cookie
def change_email_request(request):
''' AJAX call from the profile page. User wants a new e-mail.
'''
## Make sure it checks for existing e-mail conflicts
if not request.user.is_authenticated:
raise Http404
user = request.user
if not user.check_password(request.POST['password']):
return HttpResponse(json.dumps({'success':False,
'error':'Invalid password'}))
new_email = request.POST['new_email']
if len(User.objects.filter(email = new_email)) != 0:
## CRITICAL TODO: Handle case for e-mails
return HttpResponse(json.dumps({'success':False,
'error':'An account with this e-mail already exists.'}))
pec_list = PendingEmailChange.objects.filter(user = request.user)
if len(pec_list) == 0:
pec = PendingEmailChange()
pec.user = user
else :
pec = pec_list[0]
pec.new_email = request.POST['new_email']
pec.activation_key = uuid.uuid4().hex
pec.save()
if pec.new_email == user.email:
pec.delete()
return HttpResponse(json.dumps({'success':False,
'error':'Old email is the same as the new email.'}))
d = {'site':settings.SITE_NAME,
'key':pec.activation_key,
'old_email' : user.email,
'new_email' : pec.email}
subject = render_to_string('emails/email_change_subject.txt',d)
message = render_to_string('emails/email_change.txt',d)
res=send_email(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.email])
return HttpResponse(json.dumps({'success':True}))
@ensure_csrf_cookie
def confirm_email_change(request, key):
''' User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update
'''
try:
pec=PendingEmailChange.objects.get(activation_key=key)
except:
return render_to_response("email_invalid_key.html")
subject = render_to_string('emails/confirm_email_change_subject.txt',d)
subject = ''.join(subject.splitlines())
message = render_to_string('emails/confirm_email_change.txt',d)
user = pec.user
user.email_user(subject, message, DEFAULT_FROM_EMAIL)
up = UserProfile.objects.get( user = user )
meta = up.get_meta()
if 'old_emails' not in meta:
meta['old_emails'] = []
meta['old_emails'].append(user.email)
up.set_meta(meta)
up.save()
user.email = pec.new_email
user.save()
pec.delete()
return render_to_response("email_change_successful.html")
@ensure_csrf_cookie
def change_name_request(request):
if not request.user.is_authenticated:
raise Http404
pnc = PendingNameChange()
pnc.user = request.User
pnc.new_name = request.POST['new_name']
pnc.rationale = request.POST['rationale']
pnc.save()
return HttpResponse(json.dumps({'success':True}))
@ensure_csrf_cookie
def change_name_list(request):
if not request.user.is_staff:
raise Http404
changes = list(PendingNameChange.objects.all())
json = [{'new_name': c.new_name,
'rationale':c.rationale,
'old_name':UserProfile.Objects.get(username=c.user).name,
'email':c.user.email,
'id':c.id} for c in changes]
return render_to_response('name_changes.html', json)
@ensure_csrf_cookie
def change_name_reject(request):
''' Course staff clicks 'reject' on a given name change '''
if not request.user.is_staff:
raise Http404
pnc = PendingNameChange.objects.get(id = int(request.POST['id']))
pnc.delete()
return HttpResponse(json.dumps({'success':True}))
@ensure_csrf_cookie
def change_name_accept(request):
''' Course staff clicks 'accept' on a given name change '''
pnc = PendingNameChange.objects.get(id = int(request.POST['id']))
u = pnc.user
up = UserProfile.objects.get(user=u)
up.name = pnc.name
up.save()
pnc.delete()
return HttpResponse(json.dumps({'success':True}))

View File

Some files were not shown because too many files have changed in this diff Show More