Merge branch 'master' into move_settings

Conflicts:
	djangoapps/student/views.py
	urls.py
This commit is contained in:
David Ormsbee
2012-04-02 14:26:05 -04:00
50 changed files with 1533 additions and 407 deletions

View File

@@ -5,6 +5,7 @@ import operator
import re
import numpy
import numbers
import scipy.constants
from pyparsing import Word, alphas, nums, oneOf, Literal
@@ -121,7 +122,7 @@ def evaluator(variables, functions, string, cs=False):
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 = [e for e in x if isinstance(e, numbers.Number)] # Ignore ^
x.reverse()
x=reduce(lambda a,b:b**a, x)
return x
@@ -130,7 +131,7 @@ def evaluator(variables, functions, string, cs=False):
return x[0]
if 0 in x:
return float('nan')
x = [1./e for e in x if type(e) == float] # Ignore ^
x = [1./e for e in x if isinstance(e, numbers.Number)] # Ignore ||
return 1./sum(x)
def sum_parse_action(x): # [ 1 + 2 - 3 ] -> 0
total = 0.0
@@ -217,4 +218,7 @@ if __name__=='__main__':
print evaluator({},{}, "-(7+5)")
print evaluator({},{}, "-0.33")
print evaluator({},{}, "-.33")
print evaluator({},{}, "5+1*j")
print evaluator({},{}, "j||1")
print evaluator({},{}, "e^(j*pi)")
print evaluator({},{}, "5+7 QWSEKO")

View File

@@ -8,7 +8,8 @@ class textline(object):
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}
size = element.get('size')
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size}
html=render_to_string("textinput.html", context)
return etree.XML(html)

View File

@@ -1,5 +1,6 @@
import json
import math
import numbers
import numpy
import random
import scipy
@@ -37,7 +38,6 @@ 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)
@@ -48,8 +48,11 @@ class numericalresponse(object):
''' 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:
correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance)
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
except:
raise StudentInputError('Invalid input -- please use a number only')
if correct:
@@ -141,7 +144,7 @@ class formularesponse(object):
except:
#traceback.print_exc()
raise StudentInputError("Error in formula")
if math.isnan(student_result) or math.isinf(student_result):
if numpy.isnan(student_result) or numpy.isinf(student_result):
return {self.answer_id:"incorrect"}
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return {self.answer_id:"incorrect"}
@@ -153,9 +156,9 @@ class formularesponse(object):
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 \
d=dict([(k, numpy.complex(d[k])) for k in d if type(k)==str and \
k.isalnum() and \
(type(d[k]) == float or type(d[k]) == int) ])
isinstance(d[k], numbers.Number)])
return d
def get_answers(self):

View File

@@ -1,23 +1,26 @@
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
import logging
import random
import urllib
from collections import namedtuple
from django.conf import settings
from lxml import etree
from models import StudentModule
from student.models import UserProfile
log = logging.getLogger("mitx.courseware")
Score = namedtuple("Score", "earned possible graded section")
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:
if id not in cache:
module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__?
module_id = id,
student = user,
@@ -44,6 +47,17 @@ def get_grade(user, problem, cache):
return (correct, total)
def grade_sheet(student):
"""
This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
- courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters,
each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded
problems, and is good for displaying a course summary with due dates, etc.
- grade_summary is a summary of how the final grade breaks down. It is an array of "sections". Each section can either be
a conglomerate of scores (like labs or homeworks) which has subscores and a totalscore, or a section can be all from one assignment
(such as a midterm or final) and only has a totalscore. Each section has a weight that shows how it contributes to the total grade.
"""
dom=content_parser.course_file(student)
course = dom.xpath('//course/@name')[0]
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
@@ -54,11 +68,14 @@ def grade_sheet(student):
response_by_id[response.module_id] = response
total_scores = {}
totaled_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',
@@ -84,22 +101,26 @@ def grade_sheet(student):
else:
correct = total
scores.append((int(correct),total, graded ))
scores.append( Score(int(correct),total, graded, s.get("name")) )
section_total = Score(sum([score.earned for score in scores]),
sum([score.possible for score in scores]),
False,
p.get("id"))
section_total = (sum([score[0] for score in scores]),
sum([score[1] for score in scores]))
graded_total = Score(sum([score.earned for score in scores if score.graded]),
sum([score.possible for score in scores if score.graded]),
True,
p.get("id"))
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
#Add the graded total to totaled_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
format_scores = totaled_scores.get(format, [])
format_scores.append( graded_total )
totaled_scores[ format ] = format_scores
score={'section':s.get("name"),
'scores':scores,
@@ -114,7 +135,20 @@ def grade_sheet(student):
chapters.append({'course':course,
'chapter' : c.get("name"),
'sections' : sections,})
grade_summary = grade_summary_6002x(totaled_scores)
return {'courseware_summary' : chapters,
'grade_summary' : grade_summary}
def grade_summary_6002x(totaled_scores):
"""
This function takes the a dictionary of (graded) section scores, and applies the course grading rules to create
the grade_summary. For 6.002x this means homeworks and labs all have equal weight, with the lowest 2 of each
being dropped. There is one midterm and one final.
"""
def totalWithDrops(scores, drop_count):
#Note that this key will sort the list descending
@@ -131,12 +165,12 @@ def grade_sheet(student):
return aggregate_score, dropped_indices
#Figure the homework scores
homework_scores = total_scores['Homework'] if 'Homework' in total_scores else []
homework_scores = totaled_scores['Homework'] if 'Homework' in totaled_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] )
percentage = homework_scores[1].earned / float(homework_scores[i].possible)
summary = "Homework {0} - {1} - {2:.0%} ({3:g}/{4:g})".format( i + 1, homework_scores[i].section , percentage, homework_scores[i].earned, homework_scores[i].possible )
else:
percentage = 0
summary = "Unreleased Homework {0} - 0% (?/?)".format(i + 1)
@@ -153,13 +187,12 @@ def grade_sheet(student):
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_scores = totaled_scores['Lab'] if 'Lab' in totaled_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] )
percentage = lab_scores[i].earned / float(lab_scores[i].possible)
summary = "Lab {0} - {1} - {2:.0%} ({3:g}/{4:g})".format( i + 1, lab_scores[i].section , percentage, lab_scores[i].earned, lab_scores[i].possible )
else:
percentage = 0
summary = "Unreleased Lab {0} - 0% (?/?)".format(i + 1)
@@ -177,18 +210,18 @@ def grade_sheet(student):
#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_score = Score('?', '?', True, "?")
midterm_percentage = 0
final_score = ('?', '?')
final_score = Score('?', '?', True, "?")
final_percentage = 0
if settings.GENERATE_PROFILE_SCORES:
midterm_score = (random.randrange(50, 150), 150)
midterm_percentage = midterm_score[0] / float(midterm_score[1])
midterm_score = Score(random.randrange(50, 150), 150, True, "?")
midterm_percentage = midterm_score.earned / float(midterm_score.possible)
final_score = (random.randrange(100, 300), 300)
final_percentage = final_score[0] / float(final_score[1])
final_score = Score(random.randrange(100, 300), 300, True, "?")
final_percentage = final_score.earned / float(final_score.possible)
grade_summary = [
@@ -196,7 +229,8 @@ def grade_sheet(student):
'category': 'Homework',
'subscores' : homework_percentages,
'dropped_indices' : homework_dropped_indices,
'totalscore' : {'score' : homework_total, 'summary' : "Homework Average - {0:.0%}".format(homework_total)},
'totalscore' : homework_total,
'totalscore_summary' : "Homework Average - {0:.0%}".format(homework_total),
'totallabel' : 'HW Avg',
'weight' : 0.15,
},
@@ -204,25 +238,25 @@ def grade_sheet(student):
'category': 'Labs',
'subscores' : lab_percentages,
'dropped_indices' : lab_dropped_indices,
'totalscore' : {'score' : lab_total, 'summary' : "Lab Average - {0:.0%}".format(lab_total)},
'totalscore' : lab_total,
'totalscore_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])},
'totalscore' : midterm_percentage,
'totalscore_summary' : "Midterm - {0:.0%} ({1}/{2})".format(midterm_percentage, midterm_score.earned, midterm_score.possible),
'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])},
'totalscore' : final_percentage,
'totalscore_summary' : "Final - {0:.0%} ({1}/{2})".format(final_percentage, final_score.earned, final_score.possible),
'totallabel' : 'Final',
'weight' : 0.40,
}
]
return {'grade_summary' : grade_summary,
'chapters':chapters}
return grade_summary

View File

@@ -26,6 +26,12 @@ import courseware.content_parser as content_parser
log = logging.getLogger("mitx.courseware")
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, complex):
return "{real:.7g}{imag:+.7g}*j".format(real = obj.real,imag = obj.imag)
return json.JSONEncoder.default(self, obj)
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
@@ -240,7 +246,8 @@ class Module(XModule):
if not self.answer_available():
raise Http404
else:
return json.dumps(self.lcp.get_question_answers())
return json.dumps(self.lcp.get_question_answers(),
cls=ComplexEncoder)
# Figure out if we should move these to capa_problem?

View File

@@ -20,7 +20,7 @@ class ModelsTest(unittest.TestCase):
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.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356"))<0.01)
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3':4.0}, {}, "13"), 13)
self.assertEqual(calc.evaluator(variables, functions, "13"), 13)
self.assertEqual(calc.evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5)
@@ -30,6 +30,8 @@ class ModelsTest(unittest.TestCase):
self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0)
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41"))<0.01)
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001)
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)")+1)<0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1")-0.5-0.5j)<0.00001)
exception_happened = False
try:
calc.evaluator({},{}, "5+7 QWSEKO")

View File

@@ -0,0 +1,146 @@
# 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 'PendingEmailChange'
db.create_table('student_pendingemailchange', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)),
('new_email', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, blank=True)),
('activation_key', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32, db_index=True)),
))
db.send_create_signal('student', ['PendingEmailChange'])
# Adding model 'PendingNameChange'
db.create_table('student_pendingnamechange', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)),
('new_name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
('rationale', self.gf('django.db.models.fields.CharField')(max_length=1024, blank=True)),
))
db.send_create_signal('student', ['PendingNameChange'])
# Changing field 'UserProfile.user'
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.OneToOneField')(unique=True, to=orm['auth.User']))
def backwards(self, orm):
# Deleting model 'PendingEmailChange'
db.delete_table('student_pendingemailchange')
# Deleting model 'PendingNameChange'
db.delete_table('student_pendingnamechange')
# Changing field 'UserProfile.user'
db.alter_column('auth_userprofile', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True))
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'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.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'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.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']

View File

@@ -12,6 +12,7 @@ import uuid
from django.db import models
from django.contrib.auth.models import User
import json
#from cache_toolbox import cache_model, cache_relation
@@ -29,6 +30,18 @@ class UserProfile(models.Model):
meta = models.CharField(blank=True, max_length=255) # JSON dictionary for future expansion
courseware = models.CharField(blank=True, max_length=255, default='course.xml')
def get_meta(self):
js_str = self.meta
if not js_str:
js_str = dict()
else:
js_str = json.loads(self.meta)
return js_str
def set_meta(self,js):
self.meta = json.dumps(js)
## TODO: Should be renamed to generic UserGroup, and possibly
# Given an optional field for type of group
class UserTestGroup(models.Model):
@@ -58,6 +71,16 @@ class Registration(models.Model):
self.user.save()
#self.delete()
class PendingNameChange(models.Model):
user = models.OneToOneField(User, unique=True, db_index=True)
new_name = models.CharField(blank=True, max_length=255)
rationale = models.CharField(blank=True, max_length=1024)
class PendingEmailChange(models.Model):
user = models.OneToOneField(User, unique=True, db_index=True)
new_email = models.CharField(blank=True, max_length=255, db_index=True)
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
#cache_relation(User.profile)
#### Helper methods for use from python manage.py shell.
@@ -88,6 +111,8 @@ def change_name(email, new_name):
up.save()
def user_count():
print "All users", User.objects.all().count()
print "Active users", User.objects.filter(is_active = True).count()
return User.objects.all().count()
def active_user_count():
@@ -99,12 +124,29 @@ def create_group(name, description):
utg.description = description
utg.save()
def add_user_to_group(group, user):
def add_user_to_group(user, group):
utg = UserTestGroup.objects.get(name = group)
utg.users.add(User.objects.get(username = user))
utg.save()
def remove_user_from_group(group, user):
def remove_user_from_group(user, group):
utg = UserTestGroup.objects.get(name = group)
utg.users.remove(User.objects.get(username = user))
utg.save()
default_groups = {'email_future_courses' : 'Receive e-mails about future MITx courses',
'email_helpers' : 'Receive e-mails about how to help with MITx',
'mitx_unenroll' : 'Fully unenrolled -- no further communications',
'6002x_unenroll' : 'Took and dropped 6002x'}
def add_user_to_default_group(user, group):
try:
utg = UserTestGroup.objects.get(name = group)
except UserTestGroup.DoesNotExist:
utg = UserTestGroup()
utg.name = group
utg.description = default_groups[group]
utg.save()
utg.users.add(User.objects.get(username = user))
utg.save()

View File

@@ -1,25 +1,33 @@
import datetime
import json
import logging
import random
import string
import sys
import uuid
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.validators import validate_email, validate_slug
from django.core.mail import send_mail
from django.core.validators import validate_email, validate_slug, ValidationError
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 mako import exceptions
from models import Registration, UserProfile
from django_future.csrf import ensure_csrf_cookie
from models import Registration, UserProfile, PendingNameChange, PendingEmailChange
log = logging.getLogger("mitx.student")
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 ''
@@ -27,6 +35,8 @@ def csrf_token(context):
@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:
@@ -37,6 +47,7 @@ def index(request):
# 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
@@ -78,20 +89,20 @@ def login_user(request, error=""):
@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):
''' JSON call to change a profile setting: Right now, location and language
'''
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()
@@ -101,6 +112,7 @@ def change_setting(request):
@ensure_csrf_cookie
def create_account(request, post_override=None):
''' JSON call to enroll in the course. '''
js={'success':False}
post_vars = post_override if post_override else request.POST
@@ -111,8 +123,6 @@ def create_account(request, post_override=None):
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))
@@ -140,13 +150,13 @@ def create_account(request, post_override=None):
try:
validate_email(post_vars['email'])
except:
except ValidationError:
js['value']="Valid e-mail is required.".format(field=a)
return HttpResponse(json.dumps(js))
try:
validate_slug(post_vars['username'])
except:
except ValidationError:
js['value']="Username should only consist of A-Z and 0-9.".format(field=a)
return HttpResponse(json.dumps(js))
@@ -178,25 +188,24 @@ def create_account(request, post_override=None):
up.save()
d={'name':post_vars['name'],
'key':r.activation_key,
'site':settings.SITE_NAME}
'key':r.activation_key}
subject = render_to_string('activation_email_subject.txt',d)
subject = render_to_string('emails/activation_email_subject.txt',d)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
message = render_to_string('activation_email.txt',d)
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']=str(sys.exc_info())
log.exception(sys.exc_info())
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):
@@ -214,8 +223,6 @@ def create_random_account(create_account_function):
'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
@@ -225,6 +232,8 @@ if settings.GENERATE_RANDOM_USER_CREDENTIALS:
@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:
@@ -252,3 +261,198 @@ def password_reset(request):
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. Untested. '''
email = request.POST['email']
try:
user = User.objects.get(email = 'email')
except User.DoesNotExist:
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}
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']
try:
validate_email(new_email)
except ValidationError:
return HttpResponse(json.dumps({'success':False,
'error':'Valid e-mail address required.'}))
if len(User.objects.filter(email = new_email)) != 0:
## CRITICAL TODO: Handle case sensitivity for e-mails
return HttpResponse(json.dumps({'success':False,
'error':'An account with this e-mail already exists.'}))
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 = {'key':pec.activation_key,
'old_email' : user.email,
'new_email' : pec.new_email}
subject = render_to_string('emails/email_change_subject.txt',d)
subject = ''.join(subject.splitlines())
message = render_to_string('emails/email_change.txt',d)
res=send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [pec.new_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 PendingEmailChange.DoesNotExist:
return render_to_response("invalid_email_key.html", {})
user = pec.user
d = {'old_email' : user.email,
'new_email' : pec.new_email}
if len(User.objects.filter(email = pec.new_email)) != 0:
return render_to_response("email_exists.html", d)
subject = render_to_string('emails/email_change_subject.txt',d)
subject = ''.join(subject.splitlines())
message = render_to_string('emails/confirm_email_change.txt',d)
up = UserProfile.objects.get( user = user )
meta = up.get_meta()
if 'old_emails' not in meta:
meta['old_emails'] = []
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
up.set_meta(meta)
up.save()
user.email = pec.new_email
user.save()
pec.delete()
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
return render_to_response("email_change_successful.html", d)
@ensure_csrf_cookie
def change_name_request(request):
''' Log a request for a new name. '''
if not request.user.is_authenticated:
raise Http404
try:
pnc = PendingNameChange.objects.get(user = request.user)
except PendingNameChange.DoesNotExist:
pnc = PendingNameChange()
pnc.user = request.user
pnc.new_name = request.POST['new_name']
pnc.rationale = request.POST['rationale']
if len(pnc.new_name)<2:
return HttpResponse(json.dumps({'success':False,'error':'Name required'}))
if len(pnc.rationale)<2:
return HttpResponse(json.dumps({'success':False,'error':'Rationale required'}))
pnc.save()
return HttpResponse(json.dumps({'success':True}))
@ensure_csrf_cookie
def pending_name_changes(request):
''' Web page which allows staff to approve or reject name changes. '''
if not request.user.is_staff:
raise Http404
changes = list(PendingNameChange.objects.all())
js = {'students' : [{'new_name': c.new_name,
'rationale':c.rationale,
'old_name':UserProfile.objects.get(user=c.user).name,
'email':c.user.email,
'uid':c.user.id,
'cid':c.id} for c in changes]}
return render_to_response('name_changes.html', js)
@ensure_csrf_cookie
def reject_name_change(request):
''' JSON: Name change process. Course staff clicks 'reject' on a given name change '''
if not request.user.is_staff:
raise Http404
try:
pnc = PendingNameChange.objects.get(id = int(request.POST['id']))
except PendingNameChange.DoesNotExist:
return HttpResponse(json.dumps({'success':False, 'error':'Invalid ID'}))
pnc.delete()
return HttpResponse(json.dumps({'success':True}))
@ensure_csrf_cookie
def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change '''
if not request.user.is_staff:
raise Http404
try:
pnc = PendingNameChange.objects.get(id = int(request.POST['id']))
except PendingNameChange.DoesNotExist:
return HttpResponse(json.dumps({'success':False, 'error':'Invalid ID'}))
u = pnc.user
up = UserProfile.objects.get(user=u)
# Save old name
meta = up.get_meta()
if 'old_names' not in meta:
meta['old_names'] = []
meta['old_names'].append([up.name, pnc.rationale, datetime.datetime.now().isoformat()])
up.set_meta(meta)
up.name = pnc.new_name
up.save()
pnc.delete()
return HttpResponse(json.dumps({'success':True}))