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

BIN
.DS_Store vendored

Binary file not shown.

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ courseware/static/js/mathjax/*
db.newaskbot
db.oldaskbot
flushdb.sh
build
\#*\#

View File

@@ -1,29 +0,0 @@
#! /bin/bash
set -x
set -e
#sass sass:static/css -r templates/sass/bourbon/lib/bourbon.rb --style :compressed
if [ -z "${GIT_COMMIT}" ]; then
GIT_COMMIT=$(git rev-parse HEAD)
fi
if [ -z "${GIT_BRANCH}" ]; then
GIT_BRANCH=$(git symbolic-ref -q HEAD)
GIT_BRANCH=${GIT_BRANCH##refs/heads/}
GIT_BRANCH=${GIT_BRANCH:-HEAD}
fi
GIT_BRANCH=${GIT_BRANCH##origin/}
GIT_BRANCH=${GIT_BRANCH//\//_}
if [ -z "${BUILD_NUMBER}" ]; then
BUILD_NUMBER=dev
fi
ID=mitx-${GIT_BRANCH}-${BUILD_NUMBER}-${GIT_COMMIT}
REPO_ROOT=$(dirname $0)/..
BUILD_DIR=${REPO_ROOT}/build
mkdir -p ${BUILD_DIR}
tar --exclude=.git --exclude=build --transform="s#^#mitx/#" -czf ${BUILD_DIR}/${ID}.tgz ${REPO_ROOT}

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

View File

@@ -7,7 +7,7 @@
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# distribuetd under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -15,6 +15,7 @@
from mako.lookup import TemplateLookup
import tempfile
from django.template import RequestContext
from django.conf import settings
requestcontext = None
lookup = {}
@@ -44,4 +45,5 @@ class MakoMiddleware(object):
def process_request (self, request):
global requestcontext
requestcontext = RequestContext(request)
requestcontext['is_secure'] = request.is_secure()
requestcontext['site'] = settings.SITE_NAME

View File

@@ -18,18 +18,21 @@ from django.http import HttpResponse
import mitxmako.middleware as middleware
from django.conf import settings
from mitxmako.middleware import requestcontext
import mitxmako.middleware
def render_to_string(template_name, dictionary, context_instance=None, namespace='main'):
context_instance = context_instance or Context(dictionary)
def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_instance = Context(dictionary)
# add dictionary to context_instance
context_instance.update(dictionary or {})
# collapse context_instance to a single dictionary for mako
context_dictionary = {}
context_instance['settings'] = settings
context_instance['request_context'] = requestcontext
for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
if context:
context_dictionary.update(context)
# fetch and render template
template = middleware.lookup[namespace].get_template(template_name)
return template.render(**context_dictionary)

1
pre-requirements.txt Normal file
View File

@@ -0,0 +1 @@
numpy

78
rakefile Normal file
View File

@@ -0,0 +1,78 @@
require 'rake/clean'
require 'tempfile'
# Build Constants
REPO_ROOT = File.dirname(__FILE__)
BUILD_DIR = File.join(REPO_ROOT, "build")
# Packaging constants
DEPLOY_DIR = "/opt/wwc"
PACKAGE_NAME = "mitx"
LINK_PATH = File.join(DEPLOY_DIR, PACKAGE_NAME)
VERSION = "0.1"
COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10]
BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '').gsub('/', '_')
BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp()
if BRANCH == "master"
DEPLOY_NAME = "#{PACKAGE_NAME}-#{BUILD_NUMBER}-#{COMMIT}"
else
DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}"
end
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, DEPLOY_NAME)
PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming"
# Set up the clean and clobber tasks
CLOBBER.include('build')
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
task :package do
FileUtils.mkdir_p(BUILD_DIR)
Dir.chdir(BUILD_DIR) do
postinstall = Tempfile.new('postinstall')
postinstall.write <<-POSTINSTALL.gsub(/^\s*/, '')
#! /bin/sh
set -e
set -x
service gunicorn stop
rm -f #{LINK_PATH}
ln -s #{INSTALL_DIR_PATH} #{LINK_PATH}
service gunicorn start
POSTINSTALL
postinstall.close()
FileUtils.chmod(0755, postinstall.path)
args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
"--after-install=#{postinstall.path}",
"--prefix=#{INSTALL_DIR_PATH}",
"-C", "#{REPO_ROOT}",
"--depends=python-mysqldb",
"--depends=python-django",
"--depends=python-pip",
"--depends=python-flup",
"--depends=python-numpy",
"--depends=python-scipy",
"--depends=python-matplotlib",
"--depends=python-libxml2",
"--depends=python2.7-dev",
"--depends=libxml2-dev",
"--depends=libxslt-dev",
"--depends=python-markdown",
"--depends=python-pygments",
"--depends=mysql-client",
"--name=#{DEPLOY_NAME}",
"--version=#{VERSION}",
"-a", "all",
"."]
system(*args) || raise("fpm failed to build the .deb")
end
end
task :publish => :package do
sh("scp #{BUILD_DIR}/#{DEPLOY_NAME}_#{VERSION}-1_all.deb #{PACKAGE_REPO}")
end

14
requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
django<1.4
pip
flup
scipy
matplotlib
markdown
pygments
django-mako
django-ses
lxml
boto
mako
python-memcached
django-celery

View File

@@ -166,6 +166,15 @@ MAKO_TEMPLATES = {}
LOGGING_ENV = "dev" # override this in different environments
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '../mitx.db',
}
}
SECRET_KEY = 'unsecure'
# Default dev cache (i.e. no caching)
CACHES = {
'default': {
@@ -174,7 +183,9 @@ CACHES = {
}
# Make sure we execute correctly regardless of where we're called from
execfile(os.path.join(BASE_DIR, "settings.py"))
override_settings = os.path.join(BASE_DIR, "settings.py")
if os.path.isfile(override_settings):
execfile(override_settings)
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
@@ -238,7 +249,7 @@ LOGGING = {
},
'loggers' : {
'django' : {
'handlers' : handlers + ['mail_admins'],
'handlers' : handlers, # + ['mail_admins'],
'propagate' : True,
'level' : 'INFO'
},

File diff suppressed because one or more lines are too long

View File

@@ -754,7 +754,8 @@ div.leanModal_box {
-moz-box-sizing: border-box;
box-sizing: border-box;
display: none;
padding: 51.776px; }
padding: 51.776px;
text-align: left; }
div.leanModal_box a.modal_close {
color: #aaa;
display: block;
@@ -914,6 +915,23 @@ div#pwd_reset p {
div#pwd_reset input[type="email"] {
margin-bottom: 25.888px; }
div#apply_name_change, div#change_email, div#unenroll, div#deactivate-account {
max-width: 700px; }
div#apply_name_change ul, div#change_email ul, div#unenroll ul, div#deactivate-account ul {
list-style: none; }
div#apply_name_change ul li, div#change_email ul li, div#unenroll ul li, div#deactivate-account ul li {
margin-bottom: 12.944px; }
div#apply_name_change ul li textarea, div#apply_name_change ul li input[type="email"], div#apply_name_change ul li input[type="number"], div#apply_name_change ul li input[type="password"], div#apply_name_change ul li input[type="search"], div#apply_name_change ul li input[type="tel"], div#apply_name_change ul li input[type="text"], div#apply_name_change ul li input[type="url"], div#apply_name_change ul li input[type="color"], div#apply_name_change ul li input[type="date"], div#apply_name_change ul li input[type="datetime"], div#apply_name_change ul li input[type="datetime-local"], div#apply_name_change ul li input[type="month"], div#apply_name_change ul li input[type="time"], div#apply_name_change ul li input[type="week"], div#change_email ul li textarea, div#change_email ul li input[type="email"], div#change_email ul li input[type="number"], div#change_email ul li input[type="password"], div#change_email ul li input[type="search"], div#change_email ul li input[type="tel"], div#change_email ul li input[type="text"], div#change_email ul li input[type="url"], div#change_email ul li input[type="color"], div#change_email ul li input[type="date"], div#change_email ul li input[type="datetime"], div#change_email ul li input[type="datetime-local"], div#change_email ul li input[type="month"], div#change_email ul li input[type="time"], div#change_email ul li input[type="week"], div#unenroll ul li textarea, div#unenroll ul li input[type="email"], div#unenroll ul li input[type="number"], div#unenroll ul li input[type="password"], div#unenroll ul li input[type="search"], div#unenroll ul li input[type="tel"], div#unenroll ul li input[type="text"], div#unenroll ul li input[type="url"], div#unenroll ul li input[type="color"], div#unenroll ul li input[type="date"], div#unenroll ul li input[type="datetime"], div#unenroll ul li input[type="datetime-local"], div#unenroll ul li input[type="month"], div#unenroll ul li input[type="time"], div#unenroll ul li input[type="week"], div#deactivate-account ul li textarea, div#deactivate-account ul li input[type="email"], div#deactivate-account ul li input[type="number"], div#deactivate-account ul li input[type="password"], div#deactivate-account ul li input[type="search"], div#deactivate-account ul li input[type="tel"], div#deactivate-account ul li input[type="text"], div#deactivate-account ul li input[type="url"], div#deactivate-account ul li input[type="color"], div#deactivate-account ul li input[type="date"], div#deactivate-account ul li input[type="datetime"], div#deactivate-account ul li input[type="datetime-local"], div#deactivate-account ul li input[type="month"], div#deactivate-account ul li input[type="time"], div#deactivate-account ul li input[type="week"] {
display: block;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
div#apply_name_change ul li textarea, div#change_email ul li textarea, div#unenroll ul li textarea, div#deactivate-account ul li textarea {
height: 60px; }
div#apply_name_change ul li input[type="submit"], div#change_email ul li input[type="submit"], div#unenroll ul li input[type="submit"], div#deactivate-account ul li input[type="submit"] {
white-space: normal; }
div#feedback_div form ol li {
float: none;
width: 100%; }

BIN
static/images/info-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

View File

@@ -6,8 +6,7 @@
<section class="activation">
<h1>Account already active!</h1>
<!-- <p>Now go <a href="/">log in</a> and try the course!</a></p> -->
<p> This account has already been activated. We will notify you as
soon as the course starts.</p>
<p>For now you can go to the <a href="http://mitx.mit.edu/">MITx homepage</a> or the <a href="/">6.002x course page</a>.</p>
<p> This account has already been activated. You can log in at
the <a href="/">6.002x course page</a>.</p>
</div>
</section>

View File

@@ -4,7 +4,6 @@
<div>
<h1>Activation Complete!</h1>
<!-- <p>Now go <a href="/">log in</a> and try the course!</a></p> -->
<p>Thanks for activating your email. We will notify you as soon as the course starts.</p>
<p>For now you can go to the <a href="http://mitx.mit.edu/">MITx homepage</a> or the <a href="/">6.002x course page</a>.</p>
<p>Thanks for activating your account. You can log in at the <a href="/">6.002x course page</a>.</p>
</div>
</section>

View File

@@ -0,0 +1,3 @@
<h1>E-mail change successful!</h1>
<p> You should see your new name in your profile.

View File

@@ -0,0 +1,3 @@
<h1> Could not change e-mail </h1>
An account with the new e-mail address already exists. Sorry.

View File

@@ -3,7 +3,11 @@ offering of 6.002 using this email address. If it was you, and you'd
like to activate and use your account, copy and paste this address
into your web browser's address bar:
http://${ site }/activate/${ key }
% if is_secure:
https://${ site }/activate/${ key }
% else:
http://${ site }/activate/${ key }
% endif
If you didn't request this, you don't need to do anything; you won't
receive any more email from us. Please do not reply to this e-mail; if

View File

@@ -0,0 +1,13 @@
This is to confirm that you changed the e-mail associated with MITx
from ${old_email} to ${new_email}. If you did not make this request,
please contact the course staff immediately. Contact information is
listed at:
% if is_secure:
https://${ site }/t/mitx_help.html
% else:
http://${ site }/t/mitx_help.html
% endif
We keep a log of old e-mails, so if this request was unintentional, we
can investigate.

View File

@@ -0,0 +1,13 @@
We received a request to change the e-mail associated with your MITx
account from ${old_email} to ${new_email}. If this is correct, please
confirm your new e-mail address by visiting:
% if is_secure:
https://${ site }/email_confirm/${ key }
% else:
http://${ site }/email_confirm/${ key }
% endif
If you didn't request this, you don't need to do anything; you won't
receive any more email from us. Please do not reply to this e-mail; if
you require assistance, check the help section of the MITx web site.

View File

@@ -0,0 +1 @@
Request to change MITx account e-mail

View File

@@ -0,0 +1,5 @@
(Not currently used)
We are sorry. Our course staff did not approve your request to change
your name from ${old_name} to ${new_name}. If you need further
assistance, please e-mail the course staff at ta@mitx.mit.edu.

View File

@@ -1,6 +1,10 @@
MITx's prototype offering, 6.002x, is now open. To log in, visit
% if is_secure:
https://6002x.mitx.mit.edu
% else:
http://6002x.mitx.mit.edu
% endif
where you will find a login button at the top right-hand corner of the
window.

View File

@@ -0,0 +1,3 @@
<h1>E-mail change successful!</h1>
<p> You should see your new name in your profile.

View File

@@ -1,22 +1,76 @@
<%inherit file="main.html" />
<%block name="headextra">
<script type="text/javascript" src="/static/js/flot/jquery.flot.js"></script>
<script type="text/javascript" src="/static/js/flot/jquery.flot.stack.js"></script>
<script type="text/javascript" src="/static/js/flot/jquery.flot.symbol.js"></script>
<style type="text/css">
.grade_a {color:green;}
.grade_b {color:Chocolate;}
.grade_c {color:DimGray;}
.grade_none {color:LightGray;}
</style>
</%block>
<%include file="navigation.html" args="active_page=''" />
<section class="main-content">
<div class="gradebook-wrapper">
<section class="gradebook-content">
<h1>Gradebook</h1>
% for s in students:
<h2><a href=/profile/${s['id']}>${s['username']}</a></h2>
% for c in s['grade_info']['grade_summary']:
<h3>${c['category']} </h3>
<p>
% if 'subscores' in c:
% for ss in c['subscores']:
<br>${ss['summary']}
% endfor
% endif
</p>
% endfor
% endfor
<h1>Gradebook</h1>
%if len(students) > 0:
<table>
<%
templateSummary = students[0]['grade_info']['grade_summary']
%>
<tr> <!-- Header Row -->
<th>Student</th>
%for section in templateSummary:
%if 'subscores' in section:
%for subsection in section['subscores']:
<th>${subsection['label']}</th>
%endfor
<th>${section['totallabel']}</th>
%else:
<th>${section['category']}</th>
%endif
%endfor
</tr>
<%def name="percent_data(percentage)">
<%
data_class = "grade_none"
if percentage > .87:
data_class = "grade_a"
elif percentage > .70:
data_class = "grade_b"
elif percentage > .6:
data_class = "grade_c"
%>
<td class="${data_class}">${ "{0:.0%}".format( percentage ) }</td>
</%def>
%for student in students:
<tr>
<td><a href="/discussion/users/${student['id']}/${student['username']}/">${student['username']}</a></td>
%for section in student['grade_info']['grade_summary']:
%if 'subscores' in section:
%for subsection in section['subscores']:
${percent_data( subsection['percentage'] )}
%endfor
${percent_data( section['totalscore'] )}
%else:
${percent_data( section['totalscore'] )}
%endif
%endfor
</tr>
%endfor
</table>
%endif
</section>
</div>
</section>
</section>

View File

@@ -0,0 +1,30 @@
<%inherit file="main.html" />
<%namespace name="profile_graphs" file="profile_graphs.js"/>
<%block name="headextra">
<script type="text/javascript" src="/static/js/flot/jquery.flot.js"></script>
<script type="text/javascript" src="/static/js/flot/jquery.flot.stack.js"></script>
<script type="text/javascript" src="/static/js/flot/jquery.flot.symbol.js"></script>
% for s in students:
<script>
${profile_graphs.body(s['grade_info']['grade_summary'], "grade-detail-graph-" + str(s['id']))}
</script>
%endfor
</%block>
<%include file="navigation.html" args="active_page=''" />
<section class="main-content">
<div class="gradebook-wrapper">
<section class="gradebook-content">
<h1>Gradebook</h1>
<ol>
% for s in students:
<li>
<h2><a href=/profile/${s['id']}>${s['username']}</a></h2>
<div id="grade-detail-graph-${s['id']}" style="width:1000px;height:300px;"></div>
</li>
% endfor
</ol>
</section>
</div>
</section>

View File

@@ -0,0 +1,8 @@
<h1>Invalid key</h1>
<p> This e-mail key is not valid. Please check:
<ul>
<li> Was this key already used? Check whether the e-mail change has already happened.
<li> Did your e-mail client break the URL into two lines?
<li> The keys are valid for a limited amount of time. Has the key expired?
</ul>

View File

@@ -57,21 +57,27 @@
<div id="calculator_wrapper">
<form id="calculator">
<input type="text" id="calculator_input" />
<dl class="help">
<dt>Suffixes:</dt>
<dd> %kMGTcmunp</dd>
<dt>Operations:</dt>
<dd>^ * / + - ()</dd>
<dt>Functions:</dt>
<dd>sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs </dd>
<dt>Constants</dt>
<dd>e, pi</dd>
<div class="input-wrapper">
<input type="text" id="calculator_input" />
<!-- Students won't know what parallel means at this time. Complex numbers aren't well tested in the courseware, so we would prefer to not expose them. If you read the comments in the source, feel free to use them. If you run into a bug, please let us know. But we can't officially support them right now.
<div class="help-wrapper">
<a href="#">Hints</a>
<dl class="help">
<dt>Suffixes:</dt>
<dd> %kMGTcmunp</dd>
<dt>Operations:</dt>
<dd>^ * / + - ()</dd>
<dt>Functions:</dt>
<dd>sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs </dd>
<dt>Constants</dt>
<dd>e, pi</dd>
<dt>Unsupported:</dt> <dd>||, j </dd> -->
</dl>
<!-- Students won't know what parallel means at this time. Complex numbers aren't well tested in the courseware, so we would prefer to not expose them. If you read the comments in the source, feel free to use them. If you run into a bug, please let us know. But we can't officially support them right now.
<dt>Unsupported:</dt> <dd>||, j </dd> -->
</dl>
</div>
</div>
<input id="calculator_button" type="submit" value="="/>
<input type="text" id="calculator_output" readonly />
</form>
@@ -128,11 +134,20 @@ $(function() {
$("#calculator_wrapper").hide();
$(".calc").click(function(){
$("#calculator_wrapper").slideToggle();
$("#calculator_wrapper").slideToggle("fast");
$("#calculator_wrapper #calculator_input").focus();
$(this).toggleClass("closed");
return false;
});
$("div.help-wrapper a").hover(function(){
$(".help").toggleClass("shown");
});
$("div.help-wrapper a").click(function(){
return false;
});
$("form#calculator").submit(function(e){
e.preventDefault();
$.getJSON("/calculate", {"equation":$("#calculator_input").attr("value")},

View File

@@ -0,0 +1,44 @@
<%inherit file="main.html" />
<%include file="navigation.html" args="active_page=''" />
<section class="main-content">
<script>
function name_confirm(id) {
postJSON('/accept_name_change',{"id":id},
function(data){
if(data.success){
$("#div"+id).html("Accepted");
} else {
alert('Error');
}
});
}
function name_deny(id) {
postJSON('/reject_name_change',{"id":id},
function(data){
if(data.success){
$("#div"+id).html("Rejected");
} else {
alert('Error');
}
});
}
</script>
<div class="gradebook-wrapper">
<section class="gradebook-content">
<h1>Pending name changes</h1>
<table>
% for s in students:
<tr>
<td><a href=/profile/${s['uid']}/>${s['old_name']}</td>
<td>${s['new_name']|h}</td>
<td>${s['email']|h}</td>
<td>${s['rationale']|h}</td>
<td><span id="div${s['cid']}"><span onclick="name_confirm(${s['cid']});">[Confirm]</span>
<span onclick="name_deny(${s['cid']});">[Reject]</span></span></td></tr>
% endfor
</table>
</section>
</div>
</section>

View File

@@ -1,4 +1,6 @@
<%inherit file="main.html" />
<%namespace name="profile_graphs" file="profile_graphs.js"/>
<%block name="title"><title>Profile - MITx 6.002x</title></%block>
<%!
@@ -10,7 +12,7 @@
<script type="text/javascript" src="/static/js/flot/jquery.flot.stack.js"></script>
<script type="text/javascript" src="/static/js/flot/jquery.flot.symbol.js"></script>
<script>
<%include file="profile_graphs.js"/>
${profile_graphs.body(grade_summary, "grade-detail-graph")}
</script>
<script>
@@ -78,6 +80,46 @@ $(function() {
log_event("profile", {"type":"password_send"});
});
});
$("#change_email_form").submit(function(){
var new_email = $('#new_email_field').val();
var new_password = $('#new_email_password').val();
postJSON('/change_email',{"new_email":new_email,
"password":new_password},
function(data){
if(data.success){
$("#change_email").html("<h1>Please verify your new email</h1><p>You'll receive a confirmation in your in-box. Please click the link in the email to confirm the email change.</p>");
} else {
$("#change_email_error").html(data.error);
}
});
log_event("profile", {"type":"email_change_request",
"old_email":"${email}",
"new_email":new_email});
return false;
});
$("#change_name_form").submit(function(){
var new_name = $('#new_name_field').val();
var rationale = $('#name_rationale_field').val();
postJSON('/change_name',{"new_name":new_name,
"rationale":rationale},
function(data){
if(data.success){
$("#apply_name_change").html("<h1>Your request has been submitted.</h1><p>We'll send you an e-mail when approve the change or need further information. Please allow for up to a week for us to process your request.</p>");
} else {
$("#change_name_error").html(data.error);
}
});
log_event("profile", {"type":"name_change_request",
"new_name":new_name,
"rationale":rationale});
return false;
});
});
</script>
</%block>
@@ -89,23 +131,25 @@ $(function() {
<div class="profile-wrapper">
<section class="course-info">
<h1>Course Progress</h1>
<header>
<h1>Course Progress</h1>
</header>
<div id="grade-detail-graph"></div>
<ol class="chapters">
%for chapter in chapters:
%for chapter in courseware_summary:
%if not chapter['chapter'] == "hidden":
<li>
<h2><a href="${reverse('courseware_chapter', args=format_url_params([chapter['course'], chapter['chapter']])) }">
${ chapter['chapter'] }</a></h2>
<ol class="sections">
%for section in chapter['sections']:
<li>
<%
earned = section['section_total'][0]
total = section['section_total'][1]
earned = section['section_total'].earned
total = section['section_total'].possible
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 else ""
%>
@@ -120,7 +164,7 @@ $(function() {
<ol class="scores">
${ "Problem Scores: " if section['graded'] else "Practice Scores: "}
%for score in section['scores']:
<li class="score">${"{0:g}/{1:g}".format(score[0],score[1])}</li>
<li class="score">${"{0:g}/{1:g}".format(score.earned,score.possible)}</li>
%endfor
</ol>
%endif
@@ -132,37 +176,135 @@ $(function() {
%endif
%endfor
</ol> <!--End chapters-->
</section>
<section class="user-info">
<h1>${name}</h1>
<header>
<h1>Student Profile</h1>
</header>
<ul>
<li>Forum name: <strong>${username}</strong></li>
<li>E-mail: <strong>${email}</strong></li>
<li>
Name: <strong>${name}</strong>
%if True:
<a href="#apply_name_change" rel="leanModal" class="name-edit">Edit</a>
%else:
(Name change pending)
%endif
</li>
<li>
Forum name: <strong>${username}</strong>
</li>
<li>
E-mail: <strong>${email}</strong> <a href="#change_email" rel="leanModal" class="edit-email">Edit</a>
</li>
<li>
Location: <div id="location_sub">${location}</div><div id="description"></div> <a href="#" id="change_location">Edit</a>
</li>
<li>
Language: <div id="language_sub">${language}</div> <a href="#" id="change_language">Edit</a>
</li>
<li>
Password reset
<input id="id_email" type="hidden" name="email" maxlength="75" value="${email}" />
<input type="submit" id="pwd_reset_button" value="Reset" />
<p>We'll e-mail a password reset link to ${email}.</p>
</li>
</ul>
<div id="change_password_pop">
<h2>Password change</h2>
<p>We'll e-mail a password reset link to ${email}.</p>
<input id="id_email" type="hidden" name="email" maxlength="75" value="${email}" />
<input type="submit" id="pwd_reset_button" value="Reset Password" />
</div>
</section>
</div>
</section>
<div id="password_reset_complete" class="leanModal_box">
<a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a>
<h1>Password Reset Email Sent</h1>
An email has been sent to ${email}. Follow the link in the email to change your password.
</div>
<div id="password_reset_complete" class="leanModal_box">
<a href="#password_reset_complete" rel="leanModal" id="password_reset_complete_link"></a>
<h1>Password Reset Email Sent</h1>
<p>
An email has been sent to ${email}. Follow the link in the email to change your password.
</p>
</div>
<div id="apply_name_change" class="leanModal_box">
<h1>Apply to change your name</h1>
<form id="change_name_form">
<div id="change_name_error"> </div>
<fieldset>
<p>To uphold the credibility of MITx certificates, name changes must go through an approval process. A member of the course staff will review your request, and if approved, update your information. Please allow up to a week for your request to be processed. Thank you.</p>
<ul>
<li>
<label>Enter your desired full name, as it will appear on the MITx Certificate: </label>
<input id="new_name_field" value="" type="text" />
</li>
<li>
<label>Reason for name change:</label>
<textarea id="name_rationale_field" value=""></textarea>
</li>
<li>
<input type="submit" id="submit">
</li>
</ul>
</fieldset>
</form>
</div>
<div id="change_email" class="leanModal_box">
<h1>Change e-mail</h1>
<div id="apply_name_change_error"></div>
<form id="change_email_form">
<div id="change_email_error"> </div>
<fieldset>
<ul>
<li>
<label> Please enter your new email address: </label>
<input id="new_email_field" type="email" value="" />
</li>
<li>
<label> Please confirm your password: </label>
<input id="new_email_password" value="" type="password" />
</li>
<li>
<p>We will send a confirmation to both ${email} and your new e-mail as part of the process.</p>
<input type="submit" id="submit_email_change" />
</li>
</ul>
</fieldset>
</form>
</div>
<div id="deactivate-account" class="leanModal_box">
<h1>Deactivate MITx Account</h1>
<p>Once you deactivate you&rsquo;re MIT<em>x</em> account you will no longer recieve updates and new class announcements from MIT<em>x</em>.</p>
<p>If you&rsquo;d like to still get updates and new class announcements you can just <a href="#unenroll" rel="leanModal">unenroll</a> and keep your account active.</p>
<form id="unenroll_form">
<div id="unenroll_error"> </div>
<fieldset>
<ul>
<li>
<input type="submit" id="" value="Yes, I don't want an MITx account or hear about any new classes or updates to MITx" />
</li>
</ul>
</fieldset>
</form>
</div>
<div id="unenroll" class="leanModal_box">
<h1>Unenroll from 6.002x</h1>
<p>Please note: you will still receive updates and new class announcements from MIT<em>x</em>. If you don&rsquo;t wish to receive any more updates or announcements <a href="#deactivate-account" rel="leanModal">deactivate your account</a>.</p>
<form id="unenroll_form">
<div id="unenroll_error"> </div>
<fieldset>
<ul>
<li>
<input type="submit" id="" value="Yes, I want to unenroll from 6.002x but still hear about any new classes or updates to MITx" />
</li>
</ul>
</fieldset>
</form>
</div>

View File

@@ -1,3 +1,4 @@
<%page args="grade_summary, graph_div_id, **kwargs"/>
<%!
import json
%>
@@ -57,21 +58,21 @@ $(function () {
category_total_label = section['category'] + " Total"
series.append({
'label' : category_total_label,
'data' : [ [tickIndex, section['totalscore']['score']] ],
'data' : [ [tickIndex, section['totalscore']] ],
'color' : colors[sectionIndex]
})
ticks.append( [tickIndex, section['totallabel']] )
detail_tooltips[category_total_label] = [section['totalscore']['summary']]
detail_tooltips[category_total_label] = [section['totalscore_summary']]
else:
series.append({
'label' : section['category'],
'data' : [ [tickIndex, section['totalscore']['score']] ],
'data' : [ [tickIndex, section['totalscore']] ],
'color' : colors[sectionIndex]
})
ticks.append( [tickIndex, section['totallabel']] )
detail_tooltips[section['category']] = [section['totalscore']['summary']]
detail_tooltips[section['category']] = [section['totalscore_summary']]
tickIndex += 1 + sectionSpacer
sectionIndex += 1
@@ -86,12 +87,12 @@ $(function () {
overviewBarX = tickIndex
for section in grade_summary:
weighted_score = section['totalscore']['score'] * section['weight']
weighted_score = section['totalscore'] * section['weight']
summary_text = "{0} - {1:.1%} of a possible {2:.0%}".format(section['category'], weighted_score, section['weight'])
weighted_category_label = section['category'] + " - Weighted"
if section['totalscore']['score'] > 0:
if section['totalscore'] > 0:
series.append({
'label' : weighted_category_label,
'data' : [ [overviewBarX, weighted_score] ],
@@ -101,7 +102,7 @@ $(function () {
detail_tooltips[weighted_category_label] = [ summary_text ]
sectionIndex += 1
totalWeight += section['weight']
totalScore += section['totalscore']['score'] * section['weight']
totalScore += section['totalscore'] * section['weight']
ticks += [ [overviewBarX, "Total"] ]
tickIndex += 1 + sectionSpacer
@@ -128,7 +129,7 @@ $(function () {
legend: {show: false},
};
var $grade_detail_graph = $("#grade-detail-graph");
var $grade_detail_graph = $("#${graph_div_id}");
if ($grade_detail_graph.length > 0) {
var plot = $.plot($grade_detail_graph, series, options);
@@ -137,7 +138,7 @@ $(function () {
}
var previousPoint = null;
$("#grade-detail-graph").bind("plothover", function (event, pos, item) {
$grade_detail_graph.bind("plothover", function (event, pos, item) {
$("#x").text(pos.x.toFixed(2));
$("#y").text(pos.y.toFixed(2));
if (item) {

View File

@@ -8,4 +8,4 @@ div.gradebook-wrapper {
@extend .top-header;
}
}
}
}

View File

@@ -21,12 +21,12 @@ div.info-wrapper {
@extend .clearfix;
border-bottom: 1px solid #e3e3e3;
// &:first-child {
// padding: lh(.5);
// margin-left: (-(lh(.5)));
// background: $cream;
// border-bottom: 1px solid darken($cream, 10%);
// }
&:first-child {
padding: lh(.5);
margin: 0 (-(lh(.5))) lh();
background: $cream;
border-bottom: 1px solid darken($cream, 10%);
}
h2 {
float: left;

View File

@@ -8,11 +8,29 @@ div.profile-wrapper {
border-left: 1px solid #d3d3d3;
border-right: 0;
h1 {
header {
padding: lh(.5) lh();
font-size: 18px;
margin: 0 ;
@extend .bottom-border;
h1 {
font-size: 18px;
margin: 0;
padding-right: 30px;
}
a {
position: absolute;
top: 13px;
right: lh(.5);
text-transform: uppercase;
font-size: 12px;
color: #999;
&:hover {
color: #555;
}
}
}
ul {
@@ -57,7 +75,11 @@ div.profile-wrapper {
font-size: 12px;
}
a#change_language, a#change_location {
a#change_language,
a#change_location,
a.edit-email,
a.name-edit,
a.email-edit {
position: absolute;
top: 9px;
right: lh(.5);
@@ -69,20 +91,66 @@ div.profile-wrapper {
color: #555;
}
}
p {
font-size: 12px;
margin-bottom: 0;
margin-top: 4px;
color: #999;
}
a.deactivate {
color: #aaa;
font-style: italic;
}
input {
background: none;
border: none;
@include box-shadow(none);
color: #999;
font-size: 12px;
font-weight: normal;
margin: 0;
padding: 0;
position: absolute;
right: lh(.5);
text-transform: uppercase;
top: 9px;
&:hover {
color: #555;
}
}
}
}
div#change_password_pop {
padding: 7px lh();
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
color: #4D4D4D;
padding: 7px lh();
h2 {
margin-top: 0;
font-weight: bold;
text-transform: uppercase;
font-size: $body-font-size;
}
}
}
section.course-info {
@extend .content;
> h1 {
@extend .top-header;
header {
@extend h1.top-header;
@extend .clearfix;
h1 {
margin: 0;
float: left;
}
}
div#grade-detail-graph {
@@ -122,29 +190,29 @@ div.profile-wrapper {
padding-left: flex-gutter(9);
width: flex-grid(7, 9);
> li {
padding:0 0 lh() 0;
> li {
padding:0 0 lh() 0;
&:first-child {
padding-top: 0;
}
&:last-child {
border-bottom: 0;
}
h3 {
color: #666;
}
ol {
list-style: none;
li {
display: inline-block;
padding-right: 1em;
&:first-child {
padding-top: 0;
}
&:last-child {
border-bottom: 0;
}
h3 {
color: #666;
}
ol {
list-style: none;
li {
display: inline-block;
padding-right: 1em;
}
}
}
}
}
}

View File

@@ -1,56 +1,7 @@
@mixin border-image($images) {
-webkit-border-image: border-add-prefix($images, webkit);
-moz-border-image: border-add-prefix($images, moz);
-o-border-image: border-add-prefix($images, o);
border-image: border-add-prefix($images);
@mixin border-image ($image) {
-webkit-border-image: $image;
-moz-border-image: $image;
-ms-border-image: $image;
-o-border-image: $image;
border-image: $image;
}
@function border-add-prefix($images, $vendor: false) {
$border-image: ();
$images-type: type-of(nth($images, 1));
$first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial)
// If input is a gradient
@if $images-type == string {
@if ($first-var == "linear") or ($first-var == "radial") {
@for $i from 2 through length($images) {
$gradient-type: nth($images, 1); // Get type of gradient (linear || radial)
$gradient-args: nth($images, $i); // Get actual gradient (red, blue)
$border-image: render-gradients($gradient-args, $gradient-type, $vendor);
}
}
// If input is a URL
@else {
$border-image: $images;
}
}
// If input is gradient or url + additional args
@else if $images-type == list {
@for $i from 1 through length($images) {
$type: type-of(nth($images, $i)); // Get type of variable - List or String
// If variable is a list - Gradient
@if $type == list {
$gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial)
$gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue)
$border-image: render-gradients($gradient-args, $gradient-type, $vendor);
}
// If variable is a string - Image or number
@else if ($type == string) or ($type == number) {
$border-image: append($border-image, nth($images, $i));
}
}
}
@return $border-image;
}
//Examples:
// @include border-image(url("image.png"));
// @include border-image(url("image.png") 20 stretch);
// @include border-image(linear-gradient(45deg, orange, yellow));
// @include border-image(linear-gradient(45deg, orange, yellow) stretch);
// @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round);
// @include border-image(radial-gradient(top, cover, orange, yellow, orange));

View File

@@ -1,19 +1,28 @@
nav.sequence-nav {
@extend .topbar;
@include box-sizing(border-box);
margin-bottom: $body-line-height;
position: relative;
ol {
display: table-row;
float: left;
width: flex-grid(8,9) + flex-gutter();
position: relative;
border-bottom: 1px solid darken($cream, 20%);
@include box-sizing(border-box);
display: table;
padding-right: flex-grid(1, 9);
width: 100%;
a {
@extend .block-link;
}
li {
border-left: 1px solid darken($cream, 20%);
display: table-cell;
min-width: 20px;
&:first-child {
border-left: none;
}
.inactive {
background-repeat: no-repeat;
@@ -35,9 +44,9 @@ nav.sequence-nav {
}
.active {
@include box-shadow(0 1px 0 #fff);
background-color: #fff;
background-repeat: no-repeat;
@include box-shadow(0 1px 0 #fff);
&:hover {
background-color: #fff;
@@ -46,15 +55,14 @@ nav.sequence-nav {
}
a {
@include box-shadow(1px 0 0 #fff);
background-position: center center;
border: none;
border-right: 1px solid darken($cream, 10%);
cursor: pointer;
padding: 15px 4px 14px;
width: 28px;
display: block;
height: 17px;
padding: 15px 0 14px;
@include transition(all, .4s, $ease-in-out-quad);
width: 100%;
// @media screen and (max-width: 800px) {
// padding: 12px 8px;
@@ -134,8 +142,8 @@ nav.sequence-nav {
z-index: 99;
&.shown {
opacity: 1;
margin-top: 4px;
opacity: 1;
}
&:empty {
@@ -151,9 +159,9 @@ nav.sequence-nav {
content: " ";
display: block;
height: 10px;
left: 18px;
position: absolute;
top: -5px;
left: 18px;
@include transform(rotate(45deg));
width: 10px;
}
@@ -162,33 +170,34 @@ nav.sequence-nav {
}
ul {
float: right;
margin-right: 1px;
position: absolute;
right: 0;
top: 0;
width: flex-grid(1, 9);
display: table-row;
li {
display: table-cell;
float: left;
width: 50%;
&.prev, &.next {
a {
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
background-color: darken($cream, 5%);
background-position: center center;
background-repeat: no-repeat;
border-left: 1px solid darken(#f6efd4, 20%);
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
@include box-sizing(border-box);
cursor: pointer;
padding: 0 4px;
text-indent: -9999px;
width: 38px;
display: block;
text-indent: -9999px;
&:hover {
text-decoration: none;
background-color: none;
color: darken($cream, 60%);
text-decoration: none;
background-color: none;
text-decoration: none;
}
&.disabled {
@@ -223,28 +232,26 @@ nav.sequence-nav {
section.course-content {
position: relative;
div#seq_content {
margin-bottom: 60px;
}
nav.sequence-bottom {
position: absolute;
bottom: 0;
right: 50%;
margin-right: -53px;
bottom: (-(lh()));
position: relative;
ul {
@extend .clearfix;
background-color: darken(#F6EFD4, 5%);
background-color: darken($cream, 5%);
border: 1px solid darken(#f6efd4, 20%);
border-bottom: 0;
@include border-radius(3px 3px 0 0);
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
margin: 0 auto;
overflow: hidden;
width: 106px;
background-color: darken($cream, 5%);
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
li {
float: left;
@@ -257,11 +264,11 @@ section.course-content {
background-repeat: no-repeat;
border-bottom: none;
display: block;
display: block;
padding: lh(.75) 4px;
text-indent: -9999px;
width: 45px;
display: block;
@include transition(all, .4s, $ease-in-out-quad);
width: 45px;
&:hover {
background-color: darken($cream, 10%);

View File

@@ -1,5 +1,5 @@
li.calc-main {
bottom: 0;
bottom: -36px;
left: 0;
position: fixed;
width: 100%;
@@ -16,6 +16,8 @@ li.calc-main {
padding: 8px 12px;
width: 16px;
height: 20px;
position: relative;
top: -36px;
&:hover {
opacity: .8;
@@ -28,27 +30,14 @@ li.calc-main {
div#calculator_wrapper {
background: rgba(#111, .9);
position: relative;
top: -36px;
clear: both;
form {
padding: lh();
@extend .clearfix;
input#calculator_input {
border: none;
@include box-shadow(none);
@include box-sizing(border-box);
font-size: 16px;
padding: 10px;
width: flex-grid(7.5);
margin: 0;
float: left;
&:focus {
outline: none;
border: none;
}
}
input#calculator_button {
background: #111;
@@ -83,20 +72,70 @@ li.calc-main {
padding: 10px;
width: flex-grid(4);
}
}
dl {
display: none;
dt {
clear: both;
div.input-wrapper {
position: relative;
@extend .clearfix;
width: flex-grid(7.5);
margin: 0;
float: left;
font-weight: bold;
padding-right: lh(.5);
input#calculator_input {
border: none;
@include box-shadow(none);
@include box-sizing(border-box);
font-size: 16px;
padding: 10px;
width: 100%;
&:focus {
outline: none;
border: none;
}
}
dd {
float: left;
div.help-wrapper {
position: absolute;
right: 8px;
top: 15px;
a {
@include hide-text;
width: 17px;
height: 17px;
background: url("/static/images/info-icon.png") center center no-repeat;
}
dl {
background: #fff;
@include border-radius(3px);
@include box-shadow(0 0 3px #999);
color: #333;
opacity: 0;
padding: 10px;
position: absolute;
right: -40px;
top: -110px;
width: 500px;
@include transition();
&.shown {
opacity: 1;
top: -115px;
}
dt {
clear: both;
float: left;
font-weight: bold;
padding-right: lh(.5);
}
dd {
float: left;
}
}
}
}
}
}

View File

@@ -17,7 +17,7 @@ html {
@include box-shadow(0 0 4px #dfdfdf);
@include box-sizing(border-box);
margin-top: 3px;
overflow: hidden;
// overflow: hidden;
@media print {
border-bottom: 0;

View File

@@ -17,6 +17,7 @@ div.leanModal_box {
@include box-sizing(border-box);
display: none;
padding: lh(2);
text-align: left;
a.modal_close {
color: #aaa;
@@ -204,6 +205,35 @@ div#pwd_reset {
}
}
div#apply_name_change,
div#change_email,
div#unenroll,
div#deactivate-account {
max-width: 700px;
ul {
list-style: none;
li {
margin-bottom: lh(.5);
textarea, #{$all-text-inputs} {
display: block;
width: 100%;
@include box-sizing(border-box);
}
textarea {
height: 60px;
}
input[type="submit"] {
white-space: normal;
}
}
}
}
div#feedback_div{
form{
ol {

View File

@@ -1,5 +1,9 @@
<section class="text-input">
<input type="text" name="input_${id}" id="input_${id}" value="${value}" />
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
/>
<span id="answer_${id}"></span>

18
urls.py
View File

@@ -10,19 +10,25 @@ import django.contrib.auth.views
# admin.autodiscover()
urlpatterns = ('',
url(r'^$', 'student.views.index'), # Main marketing page, or redirect to courseware
url(r'^change_email$', 'student.views.change_email_request'),
url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
url(r'^change_name$', 'student.views.change_name_request'),
url(r'^accept_name_change$', 'student.views.accept_name_change'),
url(r'^reject_name_change$', 'student.views.reject_name_change'),
url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
url(r'^gradebook$', 'courseware.views.gradebook'),
url(r'^event$', 'track.views.user_track'),
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'),
url(r'^logout$', 'student.views.logout_user'),
url(r'^info$', 'util.views.info'),
url(r'^login$', 'student.views.login_user'),
url(r'^login/(?P<error>[^/]*)$', 'student.views.login_user'),
url(r'^logout$', 'student.views.logout_user'),
url(r'^create_account$', 'student.views.create_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account'),
url(r'^$', 'student.views.index'),
# url(r'^password_reset/$', 'django.contrib.auth.views.password_reset',
# dict(from_email='registration@mitx.mit.edu'),name='auth_password_reset'),
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
url(r'^password_reset/$', 'student.views.password_reset'),
## Obsolete Django views for password resets
## TODO: Replace with Mako-ized views
url(r'^password_change/$',django.contrib.auth.views.password_change,name='auth_password_change'),
url(r'^password_change_done/$',django.contrib.auth.views.password_change_done,name='auth_password_change_done'),
url(r'^password_reset_confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',django.contrib.auth.views.password_reset_confirm,
@@ -31,6 +37,7 @@ urlpatterns = ('',
name='auth_password_reset_complete'),
url(r'^password_reset_done/$',django.contrib.auth.views.password_reset_done,
name='auth_password_reset_done'),
## Feedback
url(r'^send_feedback$', 'util.views.send_feedback'),
)
@@ -40,6 +47,7 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED:
urlpatterns += (
url(r'^courseware/$', 'courseware.views.index', name="courseware"),
url(r'^info$', 'util.views.info'),
url(r'^wiki/', include('simplewiki.urls')),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', 'courseware.views.index', name="courseware_section"),
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/$', 'courseware.views.index', name="courseware_chapter"),