Splitting files into subdirectories in preparation for merge
0
djangoapps/circuit/__init__.py
Normal file
12
djangoapps/circuit/models.py
Normal 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]
|
||||
16
djangoapps/circuit/tests.py
Normal 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)
|
||||
66
djangoapps/circuit/views.py
Normal 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
|
||||
|
||||
0
djangoapps/courseware/__init__.py
Normal file
1
djangoapps/courseware/capa/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
220
djangoapps/courseware/capa/calc.py
Normal 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")
|
||||
286
djangoapps/courseware/capa/capa_problem.py
Normal 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
|
||||
7
djangoapps/courseware/capa/eia.py
Normal 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]
|
||||
|
||||
39
djangoapps/courseware/capa/inputtypes.py
Normal 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)
|
||||
|
||||
|
||||
187
djangoapps/courseware/capa/responsetypes.py
Normal 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 {}
|
||||
132
djangoapps/courseware/capa/unit.py
Normal 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")
|
||||
6
djangoapps/courseware/capa/util.py
Normal 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
|
||||
282
djangoapps/courseware/content_parser.py
Normal 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
|
||||
|
||||
228
djangoapps/courseware/grades.py
Normal 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}
|
||||
|
||||
|
||||
0
djangoapps/courseware/management/__init__.py
Normal file
55
djangoapps/courseware/management/commands/check_course.py
Normal 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"
|
||||
112
djangoapps/courseware/migrations/0001_initial.py
Normal 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']
|
||||
120
djangoapps/courseware/migrations/0002_add_indexes.py
Normal 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']
|
||||
116
djangoapps/courseware/migrations/0003_done_grade_cache.py
Normal 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']
|
||||
0
djangoapps/courseware/migrations/__init__.py
Normal file
92
djangoapps/courseware/models.py
Normal 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)
|
||||
|
||||
140
djangoapps/courseware/module_render.py
Normal 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)
|
||||
69
djangoapps/courseware/modules/__init__.py
Normal 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))
|
||||
|
||||
382
djangoapps/courseware/modules/capa_module.py
Normal 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))
|
||||
38
djangoapps/courseware/modules/html_module.py
Normal 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])
|
||||
24
djangoapps/courseware/modules/schematic_module.py
Normal 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)
|
||||
|
||||
121
djangoapps/courseware/modules/seq_module.py
Normal 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
|
||||
29
djangoapps/courseware/modules/template_module.py
Normal 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')
|
||||
35
djangoapps/courseware/modules/vertical_module.py
Normal 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]])
|
||||
70
djangoapps/courseware/modules/video_module.py
Normal 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))
|
||||
57
djangoapps/courseware/modules/x_module.py
Normal 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
|
||||
|
||||
38
djangoapps/courseware/progress.py
Normal 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
|
||||
53
djangoapps/courseware/tests.py
Normal 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)
|
||||
|
||||
0
djangoapps/courseware/urls.py
Normal file
188
djangoapps/courseware/views.py
Normal 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
|
||||
9
djangoapps/simplewiki/__init__.py
Normal 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)
|
||||
62
djangoapps/simplewiki/admin.py
Normal 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)
|
||||
45
djangoapps/simplewiki/mdx_circuit.py
Executable 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)
|
||||
69
djangoapps/simplewiki/mdx_image.py
Executable 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()
|
||||
29
djangoapps/simplewiki/mdx_mathjax.py
Normal 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)
|
||||
|
||||
|
||||
278
djangoapps/simplewiki/mdx_video.py
Executable 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&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" height="344" type="application/x-shockwave-flash" width="425"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&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&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&index=1" height="100" type="application/x-shockwave-flash" width="200"><param name="movie" value="http://www.youtube.com/v/u1mA-0w8XPo&hd=1&fs=1&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&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&feature=PlayList&p=34C6046F7FEACFD3&playnext=1&playnext_from=PL&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&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;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;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;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;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&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()
|
||||
92
djangoapps/simplewiki/mdx_wikipath.py
Executable 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()
|
||||
177
djangoapps/simplewiki/media/css/autosuggest_inquisitor.css
Normal 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;
|
||||
}
|
||||
281
djangoapps/simplewiki/media/css/base.css
Normal 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;
|
||||
}
|
||||
6
djangoapps/simplewiki/media/css/base_print.css
Normal file
@@ -0,0 +1,6 @@
|
||||
div#wiki_panel
|
||||
{
|
||||
display:none;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/as_pointer.gif
Normal file
|
After Width: | Height: | Size: 66 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/hl_corner_bl.gif
Normal file
|
After Width: | Height: | Size: 73 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/hl_corner_br.gif
Normal file
|
After Width: | Height: | Size: 73 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/hl_corner_tl.gif
Normal file
|
After Width: | Height: | Size: 73 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/hl_corner_tr.gif
Normal file
|
After Width: | Height: | Size: 73 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/ul_corner_bl.gif
Normal file
|
After Width: | Height: | Size: 49 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/ul_corner_br.gif
Normal file
|
After Width: | Height: | Size: 49 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/ul_corner_tl.gif
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
djangoapps/simplewiki/media/css/img_inquisitor/ul_corner_tr.gif
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
djangoapps/simplewiki/media/img/box_corner_bl.gif
Normal file
|
After Width: | Height: | Size: 49 B |
BIN
djangoapps/simplewiki/media/img/box_corner_br.gif
Normal file
|
After Width: | Height: | Size: 49 B |
BIN
djangoapps/simplewiki/media/img/box_corner_tl.gif
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
djangoapps/simplewiki/media/img/box_corner_tr.gif
Normal file
|
After Width: | Height: | Size: 50 B |
BIN
djangoapps/simplewiki/media/img/delete.gif
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
djangoapps/simplewiki/media/img/delete_grey.gif
Normal file
|
After Width: | Height: | Size: 130 B |
961
djangoapps/simplewiki/media/js/bsn.AutoSuggest_c_2.0.js
Normal 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) );
|
||||
}
|
||||
367
djangoapps/simplewiki/models.py
Normal 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)
|
||||
111
djangoapps/simplewiki/settings.py
Normal 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))
|
||||
0
djangoapps/simplewiki/templatetags/__init__.py
Normal file
18
djangoapps/simplewiki/templatetags/simplewiki_utils.py
Normal 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)
|
||||
23
djangoapps/simplewiki/tests.py
Normal 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
|
||||
"""}
|
||||
|
||||
20
djangoapps/simplewiki/urls.py
Normal 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')
|
||||
)
|
||||
800
djangoapps/simplewiki/usage.txt
Normal 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. `<`, and
|
||||
`&`.
|
||||
|
||||
Ampersands in particular are bedeviling for web writers. If you want to
|
||||
write about 'AT&T', you need to write '`AT&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&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 `&`.
|
||||
|
||||
So, if you want to include a copyright symbol in your article, you can write:
|
||||
|
||||
©
|
||||
|
||||
and Markdown will leave it alone. But if you write:
|
||||
|
||||
AT&T
|
||||
|
||||
Markdown will translate it to:
|
||||
|
||||
AT&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 < 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">
|
||||
© 2004 Foo Corporation
|
||||
</div>
|
||||
|
||||
will turn into:
|
||||
|
||||
<pre><code><div class="footer">
|
||||
&copy; 2004 Foo Corporation
|
||||
</div>
|
||||
</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><blink></code> tags.</p>
|
||||
|
||||
You can write this:
|
||||
|
||||
`—` is the decimal-encoded equivalent of `—`.
|
||||
|
||||
to produce:
|
||||
|
||||
<p><code>&#8212;</code> is the decimal-encoded
|
||||
equivalent of <code>&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:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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="mailto:addre
|
||||
ss@example.co
|
||||
m">address@exa
|
||||
mple.com</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
|
||||
|
||||
557
djangoapps/simplewiki/views.py
Normal 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
|
||||
146
djangoapps/simplewiki/views_attachments.py
Normal 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)
|
||||
0
djangoapps/static_template_view/__init__.py
Normal file
3
djangoapps/static_template_view/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
16
djangoapps/static_template_view/tests.py
Normal 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)
|
||||
47
djangoapps/static_template_view/views.py
Normal 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('/')
|
||||
0
djangoapps/staticbook/__init__.py
Normal file
3
djangoapps/staticbook/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
16
djangoapps/staticbook/tests.py
Normal 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)
|
||||
15
djangoapps/staticbook/views.py
Normal 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)
|
||||
0
djangoapps/student/__init__.py
Normal file
0
djangoapps/student/management/__init__.py
Normal file
0
djangoapps/student/management/commands/__init__.py
Normal file
99
djangoapps/student/management/commands/assigngroups.py
Normal 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?"
|
||||
23
djangoapps/student/management/commands/emaillist.py
Normal 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
|
||||
27
djangoapps/student/management/commands/massemail.py
Normal 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)
|
||||
64
djangoapps/student/management/commands/massemailtxt.py
Normal 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()
|
||||
38
djangoapps/student/management/commands/userinfo.py
Normal 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()
|
||||
121
djangoapps/student/migrations/0001_initial.py
Normal 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']
|
||||
@@ -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']
|
||||
124
djangoapps/student/migrations/0003_auto__add_usertestgroup.py
Normal 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']
|
||||
106
djangoapps/student/migrations/0004_add_email_index.py
Normal 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']
|
||||
0
djangoapps/student/migrations/__init__.py
Normal file
130
djangoapps/student/models.py
Normal 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()
|
||||
16
djangoapps/student/tests.py
Normal 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
@@ -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}))
|
||||
|
||||