Merge master

This commit is contained in:
kimth
2012-08-06 16:26:25 -04:00
126 changed files with 2703 additions and 1798 deletions

View File

@@ -20,7 +20,7 @@
{% include "widgets/header.html" %} {# Logo, user tool navigation and meta navitation #}
{# include "widgets/secondary_header.html" #} {# Scope selector, search input and ask button #}
<section class="main-content">
<section class="container">
{% block body %}
{% endblock %}
</section>

View File

@@ -1,3 +1,4 @@
{% load extra_filters_jinja %}
<!--<link href="{{"/style/style.css"|media }}" rel="stylesheet" type="text/css" />-->
{{ 'application' | compressed_css }}
{{ 'course' | compressed_css }}

View File

@@ -1,46 +1,27 @@
<header class="app" aria-label="Global Navigation">
<header class="global" aria-label="Global Navigation">
<nav>
<a href="{{ MITX_ROOT_URL }}" class="logo">
<img src="/static/images/logo.png" />
</a>
<h1 class="logo"><a href="${reverse('root')}"></a></h1>
<ol class="left">
<li class="primary">
<a href="${reverse('courses')}">Find Courses</a>
</li>
</ol>
{%if request.user.is_authenticated(): %}
<h1>Circuits and Electronics</h1>
<ol class="user">
<li>
<a href="/dashboard" class="user-dashboard">
<span class="avatar"><img src="/static/images/profile.jpg" /></span>
{{ request.user.username }}
<li class="primary">
<a href="${reverse('dashboard')}" class="user-link">
<span class="avatar"></span>
${user.username}
</a>
</li>
<li>
<a href="#" class="options">&#9662</a>
<ol class="user-options">
<li><a href="#">Account Settings</a></li>
<li><a href="/logout">Log Out</a></li>
</ol>
<li class="primary">
<a href="#" class="dropdown">&#9662</a>
<ul class="dropdown-menu">
## <li><a href="#">Account Settings</a></li>
<li><a href="${reverse('help_edx')}">Help</a></li>
<li><a href="${reverse('logout')}">Log Out</a></li>
</ul>
</li>
</ol>
{%else:%}
<ol>
<li><a href="/courses">Courses</a></li>
<li><a href="#">How It Works</a></li>
</ol>
<ol class="user">
<li><a href="/dashboard">Log In</a></li>
<li><a href="#">Sign Up</a></li>
</ol>
<ol class="secondary">
<li>
<a href="#">About</a>
</li>
<li>
<a href="#">Jobs</a>
</li>
<li>
<a href="#">faq</a>
</li>
</ol>
{%endif %}
</nav>
</header>

View File

@@ -1,26 +1,38 @@
<!-- template footer.html -->
<footer>
<!-- Template based on a design from http://www.dotemplate.com/ - Donated $10 (pmitros) so we don't need to include credit. -->
<p> Copyright &copy; 2012. MIT. <a href="/t/copyright.html">Some rights reserved.</a>
</p>
<nav>
<ul class="social">
<li class="linkedin">
<a href="http://www.linkedin.com/groups/Friends-Alumni-MITx-4316538">Linked In</a>
</li>
<li class="twitter">
<a href="https://twitter.com/#!/MyMITx">Twitter</a>
</li>
<li class="facebook">
<a href="http://www.facebook.com/pages/MITx/378592442151504">Facebook</a>
</li>
</ul>
<ul>
<footer>
<nav>
<section class="top">
<section class="primary">
<a href="${reverse('root')}" class="logo"></a>
<a href="${reverse('courses')}">Find Courses</a>
<a href="${reverse('about_edx')}">About</a>
<a href="http://edxonline.tumblr.com/">Blog</a>
<a href="${reverse('jobs')}">Jobs</a>
<a href="${reverse('contact')}">Contact</a>
</section>
<li><a href="/s/help.html">Help</a></li>
<li><a href="/logout">Log out</a></li>
</ul>
</nav>
</footer>
<section class="social">
<a href="http://youtube.com/user/edxonline"><img src="${static.url('images/social/youtube-sharing.png')}" /></a>
<a href="https://plus.google.com/108235383044095082735"><img src="${static.url('images/social/google-plus-sharing.png')}" /></a>
<a href="http://www.facebook.com/EdxOnline"><img src="${static.url('images/social/facebook-sharing.png')}" /></a>
<a href="https://twitter.com/edXOnline"><img src="${static.url('images/social/twitter-sharing.png')}" /></a>
</section>
</section>
<section class="bottom">
<section class="copyright">
<p style="float:left;">&copy; 2012 edX, some rights reserved.</p>
</section>
<section class="secondary">
<a href="${reverse('tos')}">Terms of Service</a>
<a href="${reverse('privacy_edx')}">Privacy Policy</a>
<a href="${reverse('honor')}">Honor Code</a>
<a href="${reverse('help_edx')}">Help</a>
</section>
</section>
</nav>
</footer>
<!-- end template footer.html -->

View File

@@ -1,91 +0,0 @@
"""
Course settings module. All settings in the global_settings are
first applied, and then any settings in the settings.DATA_DIR/course_settings.json
are applied. A setting must be in ALL_CAPS.
Settings are used by calling
from courseware.course_settings import course_settings
Note that courseware.course_settings.course_settings is not a module -- it's an object. So
importing individual settings is not possible:
from courseware.course_settings.course_settings import GRADER # This won't work.
"""
import json
import logging
from django.conf import settings
from xmodule import graders
log = logging.getLogger("mitx.courseware")
global_settings_json = """
{
"GRADER" : [
{
"type" : "Homework",
"min_count" : 12,
"drop_count" : 2,
"short_label" : "HW",
"weight" : 0.15
},
{
"type" : "Lab",
"min_count" : 12,
"drop_count" : 2,
"category" : "Labs",
"weight" : 0.15
},
{
"type" : "Midterm",
"name" : "Midterm Exam",
"short_label" : "Midterm",
"weight" : 0.3
},
{
"type" : "Final",
"name" : "Final Exam",
"short_label" : "Final",
"weight" : 0.4
}
],
"GRADE_CUTOFFS" : {
"A" : 0.87,
"B" : 0.7,
"C" : 0.6
}
}
"""
class Settings(object):
def __init__(self):
# Load the global settings as a dictionary
global_settings = json.loads(global_settings_json)
# Load the course settings as a dictionary
course_settings = {}
try:
# TODO: this doesn't work with multicourse
with open(settings.DATA_DIR + "/course_settings.json") as course_settings_file:
course_settings_string = course_settings_file.read()
course_settings = json.loads(course_settings_string)
except IOError:
log.warning("Unable to load course settings file from " + str(settings.DATA_DIR) + "/course_settings.json")
# Override any global settings with the course settings
global_settings.update(course_settings)
# Now, set the properties from the course settings on ourselves
for setting in global_settings:
setting_value = global_settings[setting]
setattr(self, setting, setting_value)
# Here is where we should parse any configurations, so that we can fail early
self.GRADER = graders.grader_from_conf(self.GRADER)
course_settings = Settings()

View File

@@ -1,3 +1,4 @@
from collections import defaultdict
from fs.errors import ResourceNotFoundError
from functools import wraps
import logging
@@ -33,6 +34,7 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
try:
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc)
except (KeyError, ItemNotFoundError):
raise Http404("Course not found.")
@@ -82,7 +84,7 @@ def get_course_about_section(course, section_key):
log.warning("Missing about section {key} in course {url}".format(key=section_key, url=course.location.url()))
return None
elif section_key == "title":
return course.metadata.get('display_name', course.name)
return course.metadata.get('display_name', course.url_name)
elif section_key == "university":
return course.location.org
elif section_key == "number":
@@ -113,3 +115,57 @@ def get_course_info_section(course, section_key):
return "! Info section missing !"
raise KeyError("Invalid about key " + str(section_key))
def course_staff_group_name(course):
'''
course should be either a CourseDescriptor instance, or a string (the .course entry of a Location)
'''
if isinstance(course,str):
coursename = course
else:
coursename = course.metadata.get('data_dir','UnknownCourseName')
if not coursename: # Fall 2012: not all course.xml have metadata correct yet
coursename = course.metadata.get('course','')
return 'staff_%s' % coursename
def has_staff_access_to_course(user,course):
'''
Returns True if the given user has staff access to the course.
This means that user is in the staff_* group, or is an overall admin.
'''
if user is None or (not user.is_authenticated()) or course is None:
return False
if user.is_staff:
return True
user_groups = [x[1] for x in user.groups.values_list()] # note this is the Auth group, not UserTestGroup
staff_group = course_staff_group_name(course)
log.debug('course %s user %s groups %s' % (staff_group, user, user_groups))
if staff_group in user_groups:
return True
return False
def has_access_to_course(user,course):
if course.metadata.get('ispublic'):
return True
return has_staff_access_to_course(user,course)
def get_courses_by_university(user):
'''
Returns dict of lists of courses available, keyed by course.org (ie university).
Courses are sorted by course.number.
if ACCESS_REQUIRE_STAFF_FOR_COURSE then list only includes those accessible to user.
'''
# TODO: Clean up how 'error' is done.
# filter out any courses that errored.
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
universities = defaultdict(list)
for course in courses:
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
if not has_access_to_course(user,course):
continue
universities[course.org].append(course)
return universities

View File

@@ -3,7 +3,6 @@ import logging
from django.conf import settings
from courseware.course_settings import course_settings
from xmodule import graders
from xmodule.graders import Score
from models import StudentModule
@@ -11,20 +10,25 @@ from models import StudentModule
_log = logging.getLogger("mitx.courseware")
def grade_sheet(student, course, student_module_cache):
def grade_sheet(student, course, grader, student_module_cache):
"""
This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
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.
- 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 the output from the course grader. More information on the format is in the docstring for CourseGrader.
- grade_summary is the output from the course grader. More information on
the format is in the docstring for CourseGrader.
Arguments:
student: A User object for the student to grade
course: An XModule containing the course to grade
student_module_cache: A StudentModuleCache initialized with all instance_modules for the student
student_module_cache: A StudentModuleCache initialized with all
instance_modules for the student
"""
totaled_scores = {}
chapters = []
@@ -52,12 +56,16 @@ def grade_sheet(student, course, student_module_cache):
correct = total
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage
#We simply cannot grade a problem that is 12/0, because we
#might need it as a percentage
graded = False
scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
scores.append(Score(correct, total, graded,
module.metadata.get('display_name')))
section_total, graded_total = graders.aggregate_scores(
scores, s.metadata.get('display_name'))
section_total, graded_total = graders.aggregate_scores(scores, s.metadata.get('display_name'))
#Add the graded total to totaled_scores
format = s.metadata.get('format', "")
if format and graded_total.possible > 0:
@@ -66,7 +74,8 @@ def grade_sheet(student, course, student_module_cache):
totaled_scores[format] = format_scores
sections.append({
'section': s.metadata.get('display_name'),
'display_name': s.display_name,
'url_name': s.url_name,
'scores': scores,
'section_total': section_total,
'format': format,
@@ -74,11 +83,11 @@ def grade_sheet(student, course, student_module_cache):
'graded': graded,
})
chapters.append({'course': course.metadata.get('display_name'),
'chapter': c.metadata.get('display_name'),
chapters.append({'course': course.display_name,
'display_name': c.display_name,
'url_name': c.url_name,
'sections': sections})
grader = course_settings.GRADER
grade_summary = grader.grade(totaled_scores)
return {'courseware_summary': chapters,

View File

@@ -10,37 +10,17 @@ from lxml import etree
from django.core.management.base import BaseCommand
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.errortracker import make_error_tracker
def traverse_tree(course):
'''Load every descriptor in course. Return bool success value.'''
queue = [course]
while len(queue) > 0:
node = queue.pop()
# print '{0}:'.format(node.location)
# if 'data' in node.definition:
# print '{0}'.format(node.definition['data'])
queue.extend(node.get_children())
return True
def make_logging_error_handler():
'''Return a tuple (handler, error_list), where
the handler appends the message and any exc_info
to the error_list on every call.
'''
errors = []
def error_handler(msg, exc_info=None):
'''Log errors'''
if exc_info is None:
if sys.exc_info() != (None, None, None):
exc_info = sys.exc_info()
errors.append((msg, exc_info))
return (error_handler, errors)
def export(course, export_dir):
"""Export the specified course to course_dir. Creates dir if it doesn't exist.
@@ -73,32 +53,18 @@ def import_with_checks(course_dir, verbose=True):
data_dir = course_dir.dirname()
course_dirs = [course_dir.basename()]
(error_handler, errors) = make_logging_error_handler()
# No default class--want to complain if it doesn't find plugins for any
# module.
modulestore = XMLModuleStore(data_dir,
default_class=None,
eager=True,
course_dirs=course_dirs,
error_handler=error_handler)
course_dirs=course_dirs)
def str_of_err(tpl):
(msg, exc_info) = tpl
if exc_info is None:
return msg
exc_str = '\n'.join(traceback.format_exception(*exc_info))
(msg, exc_str) = tpl
return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
courses = modulestore.get_courses()
if len(errors) != 0:
all_ok = False
print '\n'
print "=" * 40
print 'ERRORs during import:'
print '\n'.join(map(str_of_err,errors))
print "=" * 40
print '\n'
n = len(courses)
if n != 1:
@@ -107,6 +73,16 @@ def import_with_checks(course_dir, verbose=True):
return (False, None)
course = courses[0]
errors = modulestore.get_item_errors(course.location)
if len(errors) != 0:
all_ok = False
print '\n'
print "=" * 40
print 'ERRORs during import:'
print '\n'.join(map(str_of_err, errors))
print "=" * 40
print '\n'
#print course
validators = (
@@ -143,6 +119,7 @@ def check_roundtrip(course_dir):
# dircmp doesn't do recursive diffs.
# diff = dircmp(course_dir, export_dir, ignore=[], hide=[])
print "======== Roundtrip diff: ========="
sys.stdout.flush() # needed to make diff appear in the right place
os.system("diff -r {0} {1}".format(course_dir, export_dir))
print "======== ideally there is no diff above this ======="

View File

@@ -16,6 +16,8 @@ from xmodule.exceptions import NotFoundError
from xmodule.x_module import ModuleSystem
from xmodule_modifiers import replace_static_urls, add_histogram, wrap_xmodule
from courseware.courses import has_staff_access_to_course
log = logging.getLogger("mitx.courseware")
@@ -36,10 +38,12 @@ def toc_for_course(user, request, course, active_chapter, active_section):
Create a table of contents from the module store
Return format:
[ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ]
[ {'display_name': name, 'url_name': url_name,
'sections': SECTIONS, 'active': bool}, ... ]
where SECTIONS is a list
[ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...]
[ {'display_name': name, 'url_name': url_name,
'format': format, 'due': due, 'active' : bool}, ...]
active is set for the section and chapter corresponding to the passed
parameters. Everything else comes from the xml, or defaults to "".
@@ -55,19 +59,21 @@ def toc_for_course(user, request, course, active_chapter, active_section):
sections = list()
for section in chapter.get_display_items():
active = (chapter.metadata.get('display_name') == active_chapter and
section.metadata.get('display_name') == active_section)
active = (chapter.display_name == active_chapter and
section.display_name == active_section)
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
if not hide_from_toc:
sections.append({'name': section.metadata.get('display_name'),
sections.append({'display_name': section.display_name,
'url_name': section.url_name,
'format': section.metadata.get('format', ''),
'due': section.metadata.get('due', ''),
'active': active})
chapters.append({'name': chapter.metadata.get('display_name'),
chapters.append({'display_name': chapter.display_name,
'url_name': chapter.url_name,
'sections': sections,
'active': chapter.metadata.get('display_name') == active_chapter})
'active': chapter.display_name == active_chapter})
return chapters
@@ -77,8 +83,8 @@ def get_section(course_module, chapter, section):
or None if this doesn't specify a valid section
course: Course url
chapter: Chapter name
section: Section name
chapter: Chapter url_name
section: Section url_name
"""
if course_module is None:
@@ -86,7 +92,7 @@ def get_section(course_module, chapter, section):
chapter_module = None
for _chapter in course_module.get_children():
if _chapter.metadata.get('display_name') == chapter:
if _chapter.url_name == chapter:
chapter_module = _chapter
break
@@ -95,7 +101,7 @@ def get_section(course_module, chapter, section):
section_module = None
for _section in chapter_module.get_children():
if _section.metadata.get('display_name') == section:
if _section.url_name == section:
section_module = _section
break
@@ -142,12 +148,12 @@ def get_module(user, request, location, student_module_cache, position=None):
# Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
# Fully qualified callback URL for external queueing system
xqueue_callback_url = (request.build_absolute_uri('/') + settings.MITX_ROOT_URL +
'xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/' +
# Fully qualified callback URL for external queueing system
xqueue_callback_url = (request.build_absolute_uri('/') + settings.MITX_ROOT_URL +
'xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/' +
'score_update')
# Default queuename is course-specific and is derived from the course that
# Default queuename is course-specific and is derived from the course that
# contains the current module.
# TODO: Queuename should be derived from 'course_settings.json' of each course
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
@@ -176,6 +182,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# a module is coming through get_html and is therefore covered
# by the replace_static_urls code below
replace_urls=replace_urls,
is_staff=user.is_staff,
)
# pass position specified in URL to module through ModuleSystem
system.set('position', position)
@@ -187,8 +194,9 @@ def get_module(user, request, location, student_module_cache, position=None):
module.metadata['data_dir']
)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
module.get_html = add_histogram(module.get_html)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
if has_staff_access_to_course(user, module.location.course):
module.get_html = add_histogram(module.get_html, module)
# If StudentModule for this instance wasn't already in the database,
# and this isn't a guest user, create it.

View File

@@ -1,19 +1,20 @@
import copy
import json
from path import path
import os
from pprint import pprint
from nose import SkipTest
from django.test import TestCase
from django.test.client import Client
from mock import patch, Mock
from override_settings import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
from mock import patch, Mock
from override_settings import override_settings
from student.models import Registration
from django.contrib.auth.models import User
from student.models import Registration
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
@@ -189,11 +190,12 @@ class RealCoursesLoadTestCase(PageLoader):
xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop()
# TODO: Disabled test for now.. Fix once things are cleaned up.
def Xtest_real_courses_loads(self):
def test_real_courses_loads(self):
'''See if any real courses are available at the REAL_DATA_DIR.
If they are, check them.'''
# TODO: Disabled test for now.. Fix once things are cleaned up.
raise SkipTest
# TODO: adjust staticfiles_dirs
if not os.path.isdir(REAL_DATA_DIR):
# No data present. Just pass.

View File

@@ -1,4 +1,3 @@
from collections import defaultdict
import json
import logging
import urllib
@@ -19,8 +18,8 @@ from django.views.decorators.cache import cache_control
from module_render import toc_for_course, get_module, get_section
from models import StudentModuleCache
from student.models import UserProfile
from multicourse import multicourse_settings
from xmodule.modulestore import Location
from xmodule.modulestore.search import path_to_location
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
@@ -28,7 +27,7 @@ from xmodule.course_module import CourseDescriptor
from util.cache import cache, cache_if_anonymous
from student.models import UserTestGroup, CourseEnrollment
from courseware import grades
from courseware.courses import check_course
from courseware.courses import check_course, get_courses_by_university
log = logging.getLogger("mitx.courseware")
@@ -54,22 +53,16 @@ def user_groups(user):
return group_names
def format_url_params(params):
return [urllib.quote(string.replace(' ', '_')) for string in params]
@ensure_csrf_cookie
@cache_if_anonymous
def courses(request):
# TODO: Clean up how 'error' is done.
courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
universities = defaultdict(list)
for course in courses:
universities[course.org].append(course)
'''
Render "find courses" page. The course selection work is done in courseware.courses.
'''
universities = get_courses_by_university(request.user)
return render_to_response("courses.html", {'universities': universities})
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def gradebook(request, course_id):
if 'course_admin' not in user_groups(request.user):
@@ -110,7 +103,7 @@ def profile(request, course_id, student_id=None):
user_info = UserProfile.objects.get(user=student)
student_module_cache = StudentModuleCache(request.user, course)
course, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
context = {'name': user_info.name,
'username': student.username,
@@ -118,10 +111,9 @@ def profile(request, course_id, student_id=None):
'language': user_info.language,
'email': student.email,
'course': course,
'format_url_params': format_url_params,
'csrf': csrf(request)['csrf_token']
}
context.update(grades.grade_sheet(student, course, student_module_cache))
context.update(grades.grade_sheet(student, course_module, course.grader, student_module_cache))
return render_to_response('profile.html', context)
@@ -132,9 +124,9 @@ def render_accordion(request, course, chapter, section):
If chapter and section are '' or None, renders a default accordion.
Returns (initialization_javascript, content)'''
Returns the html string'''
# TODO (cpennington): do the right thing with courses
# grab the table of contents
toc = toc_for_course(request.user, request, course, chapter, section)
active_chapter = 1
@@ -146,11 +138,11 @@ def render_accordion(request, course, chapter, section):
('toc', toc),
('course_name', course.title),
('course_id', course.id),
('format_url_params', format_url_params),
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
return render_to_string('accordion.html', context)
@login_required
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def index(request, course_id, chapter=None, section=None,
@@ -163,9 +155,9 @@ def index(request, course_id, chapter=None, section=None,
Arguments:
- request : HTTP request
- course : coursename (str)
- chapter : chapter name (str)
- section : section name (str)
- course_id : course id (str: ORG/course/URL_NAME)
- chapter : chapter url_name (str)
- section : section url_name (str)
- position : position in module, eg of <sequential> module (str)
Returns:
@@ -173,50 +165,63 @@ def index(request, course_id, chapter=None, section=None,
- HTTPresponse
'''
course = check_course(course_id)
registered = registered_for_course(course, request.user)
if not registered:
log.debug('User %s tried to view course %s but is not enrolled' % (request.user,course.location.url()))
return redirect(reverse('about_course', args=[course.id]))
def clean(s):
''' Fixes URLs -- we convert spaces to _ in URLs to prevent
funny encoding characters and keep the URLs readable. This undoes
that transformation.
'''
return s.replace('_', ' ') if s is not None else None
try:
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': ''
}
chapter = clean(chapter)
section = clean(section)
if settings.ENABLE_MULTICOURSE:
settings.MODULESTORE['default']['OPTIONS']['data_dir'] = settings.DATA_DIR + multicourse_settings.get_course_xmlpath(course)
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': ''
}
look_for_module = chapter is not None and section is not None
if look_for_module:
# TODO (cpennington): Pass the right course in here
section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user,
section_descriptor)
module, _, _, _ = get_module(request.user, request,
section_descriptor.location,
student_module_cache)
context['content'] = module.get_html()
look_for_module = chapter is not None and section is not None
if look_for_module:
section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user,
section_descriptor)
module, _, _, _ = get_module(request.user, request,
section_descriptor.location,
student_module_cache)
context['content'] = module.get_html()
else:
log.warning("Couldn't find a section descriptor for course_id '{0}',"
"chapter '{1}', section '{2}'".format(
course_id, chapter, section))
else:
log.warning("Couldn't find a section descriptor for course_id '{0}',"
"chapter '{1}', section '{2}'".format(
course_id, chapter, section))
if request.user.is_staff:
# Add a list of all the errors...
context['course_errors'] = modulestore().get_item_errors(course.location)
result = render_to_response('courseware.html', context)
except:
# In production, don't want to let a 500 out for any reason
if settings.DEBUG:
raise
else:
log.exception("Error in index view: user={user}, course={course},"
" chapter={chapter} section={section}"
"position={position}".format(
user=request.user,
course=course,
chapter=chapter,
section=section,
position=position
))
try:
result = render_to_response('courseware-error.html', {})
except:
result = HttpResponse("There was an unrecoverable error")
result = render_to_response('courseware.html', context)
return result
@ensure_csrf_cookie
def jump_to(request, location):
'''
@@ -237,13 +242,13 @@ def jump_to(request, location):
# Complain if there's not data for this location
try:
(course_id, chapter, section, position) = modulestore().path_to_location(location)
(course_id, chapter, section, position) = path_to_location(modulestore(), location)
except ItemNotFoundError:
raise Http404("No data at this location: {0}".format(location))
except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling
return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie
@@ -258,14 +263,18 @@ def course_info(request, course_id):
return render_to_response('info.html', {'course': course})
def registered_for_course(course, user):
'''Return CourseEnrollment if user is registered for course, else False'''
if user is None:
return False
if user.is_authenticated():
return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists()
else:
return False
@ensure_csrf_cookie
@cache_if_anonymous
def course_about(request, course_id):
def registered_for_course(course, user):
if user.is_authenticated():
return CourseEnrollment.objects.filter(user=user, course_id=course.id).exists()
else:
return False
course = check_course(course_id, course_must_be_open=False)
registered = registered_for_course(course, request.user)
return render_to_response('portal/course_about.html', {'course': course, 'registered': registered})
@@ -280,7 +289,7 @@ def university_profile(request, org_id):
raise Http404("University Profile not found for {0}".format(org_id))
# Only grab courses for this org...
courses = [c for c in all_courses if c.org == org_id]
courses = get_courses_by_university(request.user)[org_id]
context = dict(courses=courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower()

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
# Create your views here.
import json
from datetime import datetime
from django.http import HttpResponse, Http404
def dictfetchall(cursor):
'''Returns all rows from a cursor as a dict.
Borrowed from Django documentation'''
desc = cursor.description
return [
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]
def dashboard(request):
"""
Quick hack to show staff enrollment numbers. This should be
replaced with a real dashboard later. This version is a short-term
bandaid for the next couple weeks.
"""
if not request.user.is_staff:
raise Http404
query = "select count(user_id) as students, course_id from student_courseenrollment group by course_id order by students desc"
from django.db import connection
cursor = connection.cursor()
cursor.execute(query)
results = dictfetchall(cursor)
return HttpResponse(json.dumps(results, indent=4))

View File

View File

@@ -0,0 +1,110 @@
#
# migration tools for content team to go from stable-edx4edx to LMS+CMS
#
import logging
from pprint import pprint
import xmodule.modulestore.django as xmodule_django
from xmodule.modulestore.django import modulestore
from django.http import HttpResponse
from django.conf import settings
log = logging.getLogger("mitx.lms_migrate")
LOCAL_DEBUG = True
ALLOWED_IPS = settings.LMS_MIGRATION_ALLOWED_IPS
def escape(s):
"""escape HTML special characters in string"""
return str(s).replace('<','&lt;').replace('>','&gt;')
def manage_modulestores(request,reload_dir=None):
'''
Manage the static in-memory modulestores.
If reload_dir is not None, then instruct the xml loader to reload that course directory.
'''
html = "<html><body>"
def_ms = modulestore()
courses = def_ms.get_courses()
#----------------------------------------
# check on IP address of requester
ip = request.META.get('HTTP_X_REAL_IP','') # nginx reverse proxy
if not ip:
ip = request.META.get('REMOTE_ADDR','None')
if LOCAL_DEBUG:
html += '<h3>IP address: %s ' % ip
html += '<h3>User: %s ' % request.user
log.debug('request from ip=%s, user=%s' % (ip,request.user))
if not (ip in ALLOWED_IPS or 'any' in ALLOWED_IPS):
if request.user and request.user.is_staff:
log.debug('request allowed because user=%s is staff' % request.user)
else:
html += 'Permission denied'
html += "</body></html>"
log.debug('request denied, ALLOWED_IPS=%s' % ALLOWED_IPS)
return HttpResponse(html)
#----------------------------------------
# reload course if specified
if reload_dir is not None:
if reload_dir not in def_ms.courses:
html += "<h2><font color='red'>Error: '%s' is not a valid course directory</font></h2>" % reload_dir
else:
html += "<h2><font color='blue'>Reloaded course directory '%s'</font></h2>" % reload_dir
def_ms.try_load_course(reload_dir)
#----------------------------------------
html += '<h2>Courses loaded in the modulestore</h2>'
html += '<ol>'
for cdir, course in def_ms.courses.items():
html += '<li><a href="%s/migrate/reload/%s">%s</a> (%s)</li>' % (settings.MITX_ROOT_URL,
escape(cdir),
escape(cdir),
course.location.url())
html += '</ol>'
#----------------------------------------
dumpfields = ['definition','location','metadata']
for cdir, course in def_ms.courses.items():
html += '<hr width="100%"/>'
html += '<h2>Course: %s (%s)</h2>' % (course.metadata['display_name'],cdir)
for field in dumpfields:
data = getattr(course,field)
html += '<h3>%s</h3>' % field
if type(data)==dict:
html += '<ul>'
for k,v in data.items():
html += '<li>%s:%s</li>' % (escape(k),escape(v))
html += '</ul>'
else:
html += '<ul><li>%s</li></ul>' % escape(data)
#----------------------------------------
html += '<hr width="100%"/>'
html += "courses: <pre>%s</pre>" % escape(courses)
ms = xmodule_django._MODULESTORES
html += "modules: <pre>%s</pre>" % escape(ms)
html += "default modulestore: <pre>%s</pre>" % escape(unicode(def_ms))
#----------------------------------------
log.debug('_MODULESTORES=%s' % ms)
log.debug('courses=%s' % courses)
log.debug('def_ms=%s' % unicode(def_ms))
html += "</body></html>"
return HttpResponse(html)

View File

@@ -1,290 +0,0 @@
"""
User authentication backend for ssl (no pw required)
"""
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User, check_password
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.middleware import RemoteUserMiddleware
from django.core.exceptions import ImproperlyConfigured
import os
import string
import re
from random import choice
from student.models import UserProfile
#-----------------------------------------------------------------------------
def ssl_dn_extract_info(dn):
'''
Extract username, email address (may be anyuser@anydomain.com) and full name
from the SSL DN string. Return (user,email,fullname) if successful, and None
otherwise.
'''
ss = re.search('/emailAddress=(.*)@([^/]+)', dn)
if ss:
user = ss.group(1)
email = "%s@%s" % (user, ss.group(2))
else:
return None
ss = re.search('/CN=([^/]+)/', dn)
if ss:
fullname = ss.group(1)
else:
return None
return (user, email, fullname)
def check_nginx_proxy(request):
'''
Check for keys in the HTTP header (META) to se if we are behind an ngix reverse proxy.
If so, get user info from the SSL DN string and return that, as (user,email,fullname)
'''
m = request.META
if m.has_key('HTTP_X_REAL_IP'): # we're behind a nginx reverse proxy, which has already done ssl auth
if not m.has_key('HTTP_SSL_CLIENT_S_DN'):
return None
dn = m['HTTP_SSL_CLIENT_S_DN']
return ssl_dn_extract_info(dn)
return None
#-----------------------------------------------------------------------------
def get_ssl_username(request):
x = check_nginx_proxy(request)
if x:
return x[0]
env = request._req.subprocess_env
if env.has_key('SSL_CLIENT_S_DN_Email'):
email = env['SSL_CLIENT_S_DN_Email']
user = email[:email.index('@')]
return user
return None
#-----------------------------------------------------------------------------
class NginxProxyHeaderMiddleware(RemoteUserMiddleware):
'''
Django "middleware" function for extracting user information from HTTP request.
'''
# this field is generated by nginx's reverse proxy
header = 'HTTP_SSL_CLIENT_S_DN' # specify the request.META field to use
def process_request(self, request):
# AuthenticationMiddleware is required so that request.user exists.
if not hasattr(request, 'user'):
raise ImproperlyConfigured(
"The Django remote user auth middleware requires the"
" authentication middleware to be installed. Edit your"
" MIDDLEWARE_CLASSES setting to insert"
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
" before the RemoteUserMiddleware class.")
#raise ImproperlyConfigured('[ProxyHeaderMiddleware] request.META=%s' % repr(request.META))
try:
username = request.META[self.header] # try the nginx META key first
except KeyError:
try:
env = request._req.subprocess_env # else try the direct apache2 SSL key
if env.has_key('SSL_CLIENT_S_DN'):
username = env['SSL_CLIENT_S_DN']
else:
raise ImproperlyConfigured('no ssl key, env=%s' % repr(env))
username = ''
except:
# If specified header doesn't exist then return (leaving
# request.user set to AnonymousUser by the
# AuthenticationMiddleware).
return
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
#raise ImproperlyConfigured('[ProxyHeaderMiddleware] username=%s' % username)
if request.user.is_authenticated():
if request.user.username == self.clean_username(username, request):
#raise ImproperlyConfigured('%s already authenticated (%s)' % (username,request.user.username))
return
# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
#raise ImproperlyConfigured('calling auth.authenticate, remote_user=%s' % username)
user = auth.authenticate(remote_user=username)
if user:
# User is valid. Set request.user and persist user in the session
# by logging the user in.
request.user = user
if settings.DEBUG: print "[ssl_auth.ssl_auth.NginxProxyHeaderMiddleware] logging in user=%s" % user
auth.login(request, user)
def clean_username(self, username, request):
'''
username is the SSL DN string - extract the actual username from it and return
'''
info = ssl_dn_extract_info(username)
if not info:
return None
(username, email, fullname) = info
return username
#-----------------------------------------------------------------------------
class SSLLoginBackend(ModelBackend):
'''
Django authentication back-end which auto-logs-in a user based on having
already authenticated with an MIT certificate (SSL).
'''
def authenticate(self, username=None, password=None, remote_user=None):
# remote_user is from the SSL_DN string. It will be non-empty only when
# the user has already passed the server authentication, which means
# matching with the certificate authority.
if not remote_user:
# no remote_user, so check username (but don't auto-create user)
if not username:
return None
return None # pass on to another authenticator backend
#raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
try:
user = User.objects.get(username=username) # if user already exists don't create it
return user
except User.DoesNotExist:
return None
return None
#raise ImproperlyConfigured("in SSLLoginBackend, username=%s, remote_user=%s" % (username,remote_user))
#if not os.environ.has_key('HTTPS'):
# return None
#if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
# return None
def GenPasswd(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)])
# convert remote_user to user, email, fullname
info = ssl_dn_extract_info(remote_user)
#raise ImproperlyConfigured("[SSLLoginBackend] looking up %s" % repr(info))
if not info:
#raise ImproperlyConfigured("[SSLLoginBackend] remote_user=%s, info=%s" % (remote_user,info))
return None
(username, email, fullname) = info
try:
user = User.objects.get(username=username) # if user already exists don't create it
except User.DoesNotExist:
if not settings.DEBUG:
raise "User does not exist. Not creating user; potential schema consistency issues"
#raise ImproperlyConfigured("[SSLLoginBackend] creating %s" % repr(info))
user = User(username=username, password=GenPasswd()) # create new User
user.is_staff = False
user.is_superuser = False
# get first, last name from fullname
name = fullname
if not name.count(' '):
user.first_name = " "
user.last_name = name
mn = ''
else:
user.first_name = name[:name.find(' ')]
ml = name[name.find(' '):].strip()
if ml.count(' '):
user.last_name = ml[ml.rfind(' '):]
mn = ml[:ml.rfind(' ')]
else:
user.last_name = ml
mn = ''
# set email
user.email = email
# cleanup last name
user.last_name = user.last_name.strip()
# save
user.save()
# auto-create user profile
up = UserProfile(user=user)
up.name = fullname
up.save()
#tui = user.get_profile()
#tui.middle_name = mn
#tui.role = 'Misc'
#tui.section = None # no section assigned at first
#tui.save()
# return None
return user
def get_user(self, user_id):
#if not os.environ.has_key('HTTPS'):
# return None
#if not os.environ.get('HTTPS')=='on': # only use this back-end if HTTPS on
# return None
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
#-----------------------------------------------------------------------------
# OLD!
class AutoLoginBackend:
def authenticate(self, username=None, password=None):
raise ImproperlyConfigured("in AutoLoginBackend, username=%s" % username)
if not os.environ.has_key('HTTPS'):
return None
if not os.environ.get('HTTPS') == 'on':# only use this back-end if HTTPS on
return None
def GenPasswd(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)])
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = User(username=username, password=GenPasswd())
user.is_staff = False
user.is_superuser = False
# get first, last name
name = os.environ.get('SSL_CLIENT_S_DN_CN').strip()
if not name.count(' '):
user.first_name = " "
user.last_name = name
mn = ''
else:
user.first_name = name[:name.find(' ')]
ml = name[name.find(' '):].strip()
if ml.count(' '):
user.last_name = ml[ml.rfind(' '):]
mn = ml[:ml.rfind(' ')]
else:
user.last_name = ml
mn = ''
# get email
user.email = os.environ.get('SSL_CLIENT_S_DN_Email')
# save
user.save()
tui = user.get_profile()
tui.middle_name = mn
tui.role = 'Misc'
tui.section = None# no section assigned at first
tui.save()
# return None
return user
def get_user(self, user_id):
if not os.environ.has_key('HTTPS'):
return None
if not os.environ.get('HTTPS') == 'on':# only use this back-end if HTTPS on
return None
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None

View File

@@ -48,6 +48,17 @@ MITX_FEATURES = {
## DO NOT SET TO True IN THIS FILE
## Doing so will cause all courses to be released on production
'DISABLE_START_DATES': False, # When True, all courses will be active, regardless of start date
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True,
'ENABLE_SQL_TRACKING_LOGS': False,
'ENABLE_LMS_MIGRATION': False,
# extrernal access methods
'ACCESS_REQUIRE_STAFF_FOR_COURSE': False,
'AUTH_USE_OPENID': False,
'AUTH_USE_MIT_CERTIFICATES' : False,
}
# Used for A/B testing
@@ -304,7 +315,7 @@ PIPELINE_CSS = {
'output_filename': 'css/lms-application.css',
},
'course': {
'source_filenames': ['sass/course.scss', 'js/vendor/CodeMirror/codemirror.css', 'css/vendor/jquery.treeview.css', 'css/vendor/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css'],
'source_filenames': ['js/vendor/CodeMirror/codemirror.css', 'css/vendor/jquery.treeview.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css', 'sass/course.scss'],
'output_filename': 'css/lms-course.css',
},
'ie-fixes': {

View File

@@ -14,10 +14,11 @@ DEBUG = True
TEMPLATE_DEBUG = True
MITX_FEATURES['DISABLE_START_DATES'] = True
MITX_FEATURES['ENABLE_SQL_TRACKING_LOGS'] = True
WIKI_ENABLED = True
LOGGING = get_logger_config(ENV_ROOT / "log",
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
@@ -30,7 +31,7 @@ DATABASES = {
}
CACHES = {
# This is the cache used for most things. Askbot will not work without a
# This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here.
'default': {
@@ -52,11 +53,35 @@ CACHES = {
}
}
# Make the keyedcache startup warnings go away
CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ LMS Migration #################################
MITX_FEATURES['ENABLE_LMS_MIGRATION'] = True
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = False # require that user be in the staff_* group to be able to enroll
LMS_MIGRATION_ALLOWED_IPS = ['127.0.0.1']
################################ OpenID Auth #################################
MITX_FEATURES['AUTH_USE_OPENID'] = True
MITX_FEATURES['BYPASS_ACTIVATION_EMAIL_FOR_EXTAUTH'] = True
INSTALLED_APPS += ('external_auth',)
INSTALLED_APPS += ('django_openid_auth',)
OPENID_CREATE_USERS = False
OPENID_UPDATE_DETAILS_FROM_SREG = True
OPENID_SSO_SERVER_URL = 'https://www.google.com/accounts/o8/id' # TODO: accept more endpoints
OPENID_USE_AS_ADMIN_LOGIN = False
################################ MIT Certificates SSL Auth #################################
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar',)
INSTALLED_APPS += ('debug_toolbar',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
@@ -71,8 +96,8 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.logger.LoggingPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
)

View File

@@ -7,142 +7,12 @@ sessions. Assumes structure:
/mitx # The location of this repo
/log # Where we're going to write log files
"""
import socket
if 'eecs1' in socket.gethostname():
MITX_ROOT_URL = '/mitx2'
from .common import *
from .logsettings import get_logger_config
from .dev import *
if 'eecs1' in socket.gethostname():
# MITX_ROOT_URL = '/mitx2'
MITX_ROOT_URL = 'https://eecs1.mit.edu/mitx2'
WIKI_ENABLED = False
MITX_FEATURES['ENABLE_TEXTBOOK'] = False
MITX_FEATURES['ENABLE_DISCUSSION'] = False
MITX_FEATURES['ACCESS_REQUIRE_STAFF_FOR_COURSE'] = True # require that user be in the staff_* group to be able to enroll
#-----------------------------------------------------------------------------
# edx4edx content server
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
MITX_FEATURES['REROUTE_ACTIVATION_EMAIL'] = 'ichuang@mit.edu'
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
#EMAIL_BACKEND = 'django_ses.SESBackend'
#-----------------------------------------------------------------------------
# ichuang
DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = False
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT ]
#MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False
MITX_FEATURES['DISPLAY_EDIT_LINK'] = True
MITX_FEATURES['DEBUG_LEVEL'] = 10 # 0 = lowest level, least verbose, 255 = max level, most verbose
COURSE_SETTINGS = {'6.002x_Fall_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
'xmlpath': '/6002x-fall-2012/',
'active' : True,
'default_chapter' : 'Week_1',
'default_section' : 'Administrivia_and_Circuit_Elements',
'location': 'i4x://edx/6002xs12/course/6.002x_Fall_2012',
},
'8.02_Spring_2013': {'number' : '8.02x',
'title' : 'Electricity &amp; Magnetism',
'xmlpath': '/802x/',
'github_url': 'https://github.com/MITx/8.02x',
'active' : True,
'default_chapter' : 'Introduction',
'default_section' : 'Introduction_%28Lewin_2002%29',
},
'6.189_Spring_2013': {'number' : '6.189x',
'title' : 'IAP Python Programming',
'xmlpath': '/6.189x/',
'github_url': 'https://github.com/MITx/6.189x',
'active' : True,
'default_chapter' : 'Week_1',
'default_section' : 'Variables_and_Binding',
},
'8.01_Fall_2012': {'number' : '8.01x',
'title' : 'Mechanics',
'xmlpath': '/8.01x/',
'github_url': 'https://github.com/MITx/8.01x',
'active': True,
'default_chapter' : 'Mechanics_Online_Spring_2012',
'default_section' : 'Introduction_to_the_course',
'location': 'i4x://edx/6002xs12/course/8.01_Fall_2012',
},
'edx4edx': {'number' : 'edX.01',
'title' : 'edx4edx: edX Author Course',
'xmlpath': '/edx4edx/',
'github_url': 'https://github.com/MITx/edx4edx',
'active' : True,
'default_chapter' : 'Introduction',
'default_section' : 'edx4edx_Course',
'location': 'i4x://edx/6002xs12/course/edx4edx',
},
'7.03x_Fall_2012': {'number' : '7.03x',
'title' : 'Genetics',
'xmlpath': '/7.03x/',
'github_url': 'https://github.com/MITx/7.03x',
'active' : True,
'default_chapter' : 'Week_2',
'default_section' : 'ps1_question_1',
},
'3.091x_Fall_2012': {'number' : '3.091x',
'title' : 'Introduction to Solid State Chemistry',
'xmlpath': '/3.091x/',
'github_url': 'https://github.com/MITx/3.091x',
'active' : True,
'default_chapter' : 'Week_1',
'default_section' : 'Problem_Set_1',
},
'18.06x_Linear_Algebra': {'number' : '18.06x',
'title' : 'Linear Algebra',
'xmlpath': '/18.06x/',
'github_url': 'https://github.com/MITx/18.06x',
'default_chapter' : 'Unit_1',
'default_section' : 'Midterm_1',
'active' : True,
},
'6.00x_Fall_2012': {'number' : '6.00x',
'title' : 'Introduction to Computer Science and Programming',
'xmlpath': '/6.00x/',
'github_url': 'https://github.com/MITx/6.00x',
'active' : True,
'default_chapter' : 'Week_0',
'default_section' : 'Problem_Set_0',
'location': 'i4x://edx/6002xs12/course/6.00x_Fall_2012',
},
'7.00x_Fall_2012': {'number' : '7.00x',
'title' : 'Introduction to Biology',
'xmlpath': '/7.00x/',
'github_url': 'https://github.com/MITx/7.00x',
'active' : True,
'default_chapter' : 'Unit 1',
'default_section' : 'Introduction',
},
}
#-----------------------------------------------------------------------------
MIDDLEWARE_CLASSES = MIDDLEWARE_CLASSES + (
'ssl_auth.ssl_auth.NginxProxyHeaderMiddleware', # ssl authentication behind nginx proxy
)
AUTHENTICATION_BACKENDS = (
'ssl_auth.ssl_auth.SSLLoginBackend',
'django.contrib.auth.backends.ModelBackend',
)
INSTALLED_APPS = INSTALLED_APPS + (
'ssl_auth',
)
LOGIN_REDIRECT_URL = MITX_ROOT_URL + '/'
LOGIN_URL = MITX_ROOT_URL + '/'

View File

@@ -2,9 +2,9 @@
@import 'base/reset';
@import 'base/font_face';
@import 'base/mixins';
@import 'base/variables';
@import 'base/base';
@import 'base/mixins';
@import 'base/extends';
@import 'base/animations';

View File

@@ -1,3 +1,7 @@
@function em($pxval, $base: 16) {
@return #{$pxval / $base}em;
}
// Line-height
@function lh($amount: 1) {
@return $body-line-height * $amount;

View File

@@ -4,10 +4,15 @@ $gw-gutter: 20px;
$fg-column: $gw-column;
$fg-gutter: $gw-gutter;
$fg-max-columns: 12;
$fg-max-width: 1400px;
$fg-min-width: 810px;
$sans-serif: 'Open Sans', $verdana;
$body-font-family: $sans-serif;
$serif: $georgia;
$body-font-size: em(14);
$body-line-height: golden-ratio(.875em, 1);
$base-font-color: rgb(60,60,60);
$lighter-base-font-color: rgb(160,160,160);
@@ -15,18 +20,11 @@ $blue: rgb(29,157,217);
$pink: rgb(182,37,104);
$yellow: rgb(255, 252, 221);
$error-red: rgb(253, 87, 87);
$border-color: #C8C8C8;
// old variables
$body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
$body-font-size: 14px;
$body-line-height: golden-ratio($body-font-size, 1);
$fg-max-width: 1400px;
$fg-min-width: 810px;
$light-gray: #ddd;
$dark-gray: #333;
$mit-red: #993333;
$cream: #F6EFD4;
$text-color: $dark-gray;
$border-color: $light-gray;

View File

@@ -2,9 +2,9 @@
@import 'base/reset';
@import 'base/font_face';
@import 'base/mixins';
@import 'base/variables';
@import 'base/base';
@import 'base/mixins';
@import 'base/extends';
@import 'base/animations';

View File

@@ -1,54 +0,0 @@
section.help.main-content {
padding: lh();
h1 {
border-bottom: 1px solid #ddd;
margin-bottom: lh();
margin-top: 0;
padding-bottom: lh();
}
p {
max-width: 700px;
}
h2 {
margin-top: 0;
}
section.self-help {
float: left;
margin-bottom: lh();
margin-right: flex-gutter();
width: flex-grid(6);
ul {
margin-left: flex-gutter(6);
li {
margin-bottom: lh(.5);
}
}
}
section.help-email {
float: left;
width: flex-grid(6);
dl {
display: block;
margin-bottom: lh();
dd {
margin-bottom: lh();
}
dt {
clear: left;
float: left;
font-weight: bold;
width: flex-grid(2, 6);
}
}
}
}

View File

@@ -3,6 +3,7 @@ div.info-wrapper {
section.updates {
@extend .content;
line-height: lh();
> h1 {
@extend .top-header;
@@ -15,30 +16,35 @@ div.info-wrapper {
> ol {
list-style: none;
padding-left: 0;
margin-bottom: lh();
> li {
@extend .clearfix;
border-bottom: 1px solid #e3e3e3;
margin-bottom: lh(.5);
margin-bottom: lh();
padding-bottom: lh(.5);
list-style-type: disk;
&:first-child {
background: $cream;
border-bottom: 1px solid darken($cream, 10%);
margin: 0 (-(lh(.5))) lh();
padding: lh(.5);
}
ol, ul {
margin: lh() 0 0 lh();
list-style-type: circle;
margin: 0;
list-style-type: disk;
ol,ul {
list-style-type: circle;
}
}
h2 {
float: left;
margin: 0 flex-gutter() 0 0;
width: flex-grid(2, 9);
font-size: $body-font-size;
font-weight: bold;
}
section.update-description {
@@ -64,16 +70,20 @@ div.info-wrapper {
@extend .sidebar;
border-left: 1px solid #d3d3d3;
@include border-radius(0 4px 4px 0);
@include box-shadow(none);
border-right: 0;
header {
h1 {
@extend .bottom-border;
padding: lh(.5) lh(.75);
padding: lh(.5) lh(.5);
}
h1 {
font-size: 18px;
margin: 0 ;
}
header {
// h1 {
// font-weight: 100;
// font-style: italic;
// }
p {
color: #666;
@@ -94,7 +104,7 @@ div.info-wrapper {
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
@include box-sizing(border-box);
padding: 7px lh(.75);
padding: em(7) lh(.75);
position: relative;
&.expandable,
@@ -108,13 +118,13 @@ div.info-wrapper {
ul {
background: none;
margin: 7px (-(lh(.75))) 0;
margin: em(7) (-(lh(.75))) 0;
li {
border-bottom: 0;
border-top: 1px solid #d3d3d3;
@include box-shadow(inset 0 1px 0 #eee);
padding-left: 18px + lh(.75);
padding-left: lh(1.5);
}
}
@@ -150,7 +160,7 @@ div.info-wrapper {
border-bottom: 0;
@include box-shadow(none);
color: #999;
font-size: 12px;
font-size: $body-font-size;
font-weight: bold;
text-transform: uppercase;
}

View File

@@ -62,7 +62,6 @@ div.book-wrapper {
@extend .clearfix;
li {
background-color: darken($cream, 4%);
&.last {
display: block;

View File

@@ -5,3 +5,17 @@ body {
h1, h2, h3, h4, h5, h6 {
font-family: $sans-serif;
}
table {
table-layout: fixed;
}
.container {
padding: lh(2);
> div {
display: table;
width: 100%;
table-layout: fixed;
}
}

View File

@@ -1,43 +1,9 @@
.wrapper {
margin: 0 auto;
max-width: $fg-max-width;
min-width: $fg-min-width;
text-align: left;
width: flex-grid(12);
div.table-wrapper {
display: table;
width: flex-grid(12);
overflow: hidden;
}
}
h1.top-header {
background: #f3f3f3;
border-bottom: 1px solid #e3e3e3;
margin: (-(lh())) (-(lh())) lh();
padding: lh();
text-align: left;
}
.button {
border: 1px solid darken(#888, 10%);
@include border-radius(3px);
@include box-shadow(inset 0 1px 0 lighten(#888, 10%), 0 0 3px #ccc);
color: #fff;
cursor: pointer;
font: bold $body-font-size $body-font-family;
@include linear-gradient(lighten(#888, 5%), darken(#888, 5%));
padding: 4px 8px;
text-decoration: none;
text-shadow: none;
-webkit-font-smoothing: antialiased;
&:hover, &:focus {
border: 1px solid darken(#888, 20%);
@include box-shadow(inset 0 1px 0 lighten(#888, 20%), 0 0 3px #ccc);
@include linear-gradient(lighten(#888, 10%), darken(#888, 5%));
}
font-size: 24px;
font-weight: 100;
padding-bottom: lh();
}
.light-button, a.light-button {
@@ -84,7 +50,8 @@ h1.top-header {
}
.sidebar {
border-right: 1px solid #d3d3d3;
border-right: 1px solid #C8C8C8;
@include box-shadow(inset -1px 0 0 #e6e6e6);
@include box-sizing(border-box);
display: table-cell;
font-family: $sans-serif;
@@ -93,11 +60,13 @@ h1.top-header {
width: flex-grid(3);
h1, h2 {
font-size: 18px;
font-weight: bold;
font-size: em(18);
font-weight: 100;
letter-spacing: 0;
text-transform: none;
font-family: $sans-serif;
text-align: left;
font-style: italic;
}
a {
@@ -146,27 +115,20 @@ h1.top-header {
}
header#open_close_accordion {
border-bottom: 1px solid #d3d3d3;
@include box-shadow(0 1px 0 #eee);
padding: lh(.5) lh();
position: relative;
h2 {
margin: 0;
padding-right: 20px;
}
a {
background: #eee url('../images/slide-left-icon.png') center center no-repeat;
background: #f6f6f6 url('../images/slide-left-icon.png') center center no-repeat;
border: 1px solid #D3D3D3;
@include border-radius(3px 0 0 3px);
height: 16px;
padding: 8px;
padding: 6px;
position: absolute;
right: -1px;
text-indent: -9999px;
top: 6px;
width: 16px;
z-index: 99;
&:hover {
background-color: white;
@@ -181,33 +143,17 @@ h1.top-header {
.topbar {
@extend .clearfix;
background: $cream;
border-bottom: 1px solid darken($cream, 10%);
border-top: 1px solid #fff;
font-size: 12px;
line-height: 46px;
text-shadow: 0 1px 0 #fff;
border-bottom: 1px solid $border-color;
font-size: 14px;
@media print {
display: none;
}
a {
line-height: 46px;
border-bottom: 0;
color: darken($cream, 80%);
&:hover {
color: darken($cream, 60%);
text-decoration: none;
}
&.block-link {
// background: darken($cream, 5%);
border-left: 1px solid darken($cream, 20%);
@include box-shadow(inset 1px 0 0 lighten($cream, 5%));
border-left: 1px solid lighten($border-color, 10%);
display: block;
text-transform: uppercase;
&:hover {
background: none;
@@ -219,12 +165,3 @@ h1.top-header {
.tran {
@include transition( all, .2s, $ease-in-out-quad);
}
p.ie-warning {
background: yellow;
display: block !important;
line-height: 1.3em;
margin-bottom: 0;
padding: lh();
text-align: left;
}

View File

@@ -3,22 +3,6 @@ html {
max-height: 100%;
}
body.courseware {
height: 100%;
max-height: 100%;
.container {
padding-bottom: 40px;
margin-top: 20px;
}
footer {
&.fixed-bottom {
Position: static;
}
}
}
div.course-wrapper {
@extend .table-wrapper;
@@ -59,6 +43,9 @@ div.course-wrapper {
}
ol.vert-mod {
padding: 0;
margin: 0;
> li {
@extend .clearfix;
@extend .problem-set;
@@ -194,17 +181,9 @@ div.course-wrapper {
overflow: hidden;
header#open_close_accordion {
padding: 0;
min-height: 47px;
a {
background-image: url('../images/slide-right-icon.png');
}
h2 {
visibility: hidden;
width: 10px;
}
}
div#accordion {

View File

@@ -13,44 +13,51 @@ section.course-index {
div#accordion {
h3 {
@include box-shadow(inset 0 1px 0 0 #eee);
border-top: 1px solid #d3d3d3;
overflow: hidden;
@include border-radius(0);
border-top: 1px solid #e3e3e3;
margin: 0;
overflow: hidden;
&:first-child {
border: none;
}
&:hover {
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
background: #f6f6f6;
text-decoration: none;
}
&.ui-accordion-header {
color: #000;
a {
font-size: $body-font-size;
@include border-radius(0);
@include box-shadow(none);
color: lighten($text-color, 10%);
font-size: $body-font-size;
}
&.ui-state-active {
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(225,225,225)));
@extend .active;
border-bottom: 1px solid #d3d3d3;
border-bottom: none;
&:hover {
background: none;
}
}
}
}
ul.ui-accordion-content {
@include border-radius(0);
@include box-shadow(inset -1px 0 0 #e6e6e6);
background: transparent;
border: none;
font-size: 12px;
margin: 0;
padding: 1em 1.5em;
li {
@include border-radius(0);
margin-bottom: lh(.5);
a {
@@ -98,7 +105,7 @@ section.course-index {
&:after {
opacity: 1;
right: 15px;
@include transition(all, 0.2s, linear);
@include transition();
}
> a p {
@@ -120,8 +127,6 @@ section.course-index {
font-weight: bold;
> a {
background: rgb(240,240,240);
@include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(230,230,230)));
border-color: rgb(200,200,200);
&:after {

View File

@@ -1,8 +1,6 @@
// Generic layout styles for the discussion forums
body.askbot {
section.main-content {
section.container {
div.discussion-wrapper {
@extend .table-wrapper;

View File

@@ -72,7 +72,6 @@ body.user-profile-page {
margin-bottom: 30px;
li {
background-color: lighten($cream, 3%);
background-position: 10px center;
background-repeat: no-repeat;
@include border-radius(4px);

View File

@@ -32,8 +32,6 @@ div.question-header {
&.post-vote {
@include border-radius(4px);
background-color: lighten($cream, 5%);
border: 1px solid darken( $cream, 10% );
@include box-shadow(inset 0 1px 0px #fff);
}
@@ -149,7 +147,7 @@ div.question-header {
&.revision {
text-align: center;
background:lighten($cream, 7%);
// background:lighten($cream, 7%);
a {
color: black;
@@ -313,7 +311,6 @@ div.question-header {
}
a.edit {
@extend .button;
font-size: 12px;
padding: 2px 10px;
}

View File

@@ -1,9 +1,8 @@
nav.course-material {
background: rgb(210,210,210);
@include clearfix;
@include box-sizing(border-box);
@include box-shadow(inset 0 1px 5px 0 rgba(0,0,0, 0.05));
border-bottom: 1px solid rgb(190,190,190);
background: #f6f6f6;
border-bottom: 1px solid rgb(200,200,200);
margin: 0px auto 0px;
padding: 0px;
width: 100%;
@@ -24,12 +23,14 @@ nav.course-material {
list-style: none;
a {
color: $lighter-base-font-color;
color: darken($lighter-base-font-color, 20%);
display: block;
text-align: center;
padding: 5px 13px;
padding: 8px 13px 12px;
font-size: 14px;
font-weight: 400;
text-decoration: none;
text-shadow: 0 1px rgba(255,255,255, 0.4);
text-shadow: 0 1px rgb(255,255,255);
&:hover {
color: $base-font-color;
@@ -41,7 +42,7 @@ nav.course-material {
border-bottom: 0px;
@include border-top-radius(4px);
@include box-shadow(0 2px 0 0 rgba(255,255,255, 1));
color: $base-font-color;
color: $blue;
}
}
}

View File

@@ -1 +0,0 @@
*.css

View File

@@ -24,7 +24,6 @@ div.wiki-wrapper {
}
p {
color: darken($cream, 55%);
float: left;
line-height: 46px;
margin-bottom: 0;
@@ -40,14 +39,12 @@ div.wiki-wrapper {
input[type="button"] {
@extend .block-link;
background-color: darken($cream, 5%);
background-position: 12px center;
background-repeat: no-repeat;
border: 0;
border-left: 1px solid darken(#f6efd4, 20%);
@include border-radius(0);
@include box-shadow(inset 1px 0 0 lighten(#f6efd4, 5%));
color: darken($cream, 80%);
display: block;
font-size: 12px;
font-weight: normal;

View File

@@ -2,6 +2,27 @@
@include clearfix;
padding: 60px 0px 120px;
.dashboard-banner {
background: $yellow;
border: 1px solid rgb(200,200,200);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
padding: 10px;
margin-bottom: 30px;
&:empty {
display: none;
background-color: #FFF;
}
h2 {
margin-bottom: 0;
}
p {
margin-bottom: 0;
}
}
.profile-sidebar {
background: transparent;
float: left;

View File

@@ -1,13 +1,13 @@
<%! from django.core.urlresolvers import reverse %>
<%def name="make_chapter(chapter)">
<h3><a href="#">${chapter['name']}</a></h3>
<h3><a href="#">${chapter['display_name']}</a></h3>
<ul>
% for section in chapter['sections']:
<li${' class="active"' if 'active' in section and section['active'] else ''}>
<a href="${reverse('courseware_section', args=[course_id] + format_url_params([chapter['name'], section['name']]))}">
<p>${section['name']}
<a href="${reverse('courseware_section', args=[course_id, chapter['url_name'], section['url_name']])}">
<p>${section['display_name']}
<span class="subtitle">
${section['format']} ${"due " + section['due'] if 'due' in section and section['due'] != '' else ''}
</span>

View File

@@ -14,10 +14,16 @@ def url_class(url):
<li class="courseware"><a href="${reverse('courseware', args=[course.id])}" class="${url_class('courseware')}">Courseware</a></li>
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
% if user.is_authenticated():
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
<li class="book"><a href="${reverse('book', args=[course.id])}" class="${url_class('book')}">Textbook</a></li>
% endif
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li>
% endif
% endif
% if settings.WIKI_ENABLED:
<li class="wiki"><a href="${reverse('wiki_root', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
% endif
% if user.is_authenticated():
<li class="profile"><a href="${reverse('profile', args=[course.id])}" class="${url_class('profile')}">Profile</a></li>
% endif

View File

@@ -18,14 +18,6 @@
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/xml/xml.js')}"></script>
## <script type="text/javascript" src="${static.url('js/vendor/CodeMirror-2.25/mode/python/python.js')}"></script>
## image input: for clicking on images (see imageinput.html)
<script type="text/javascript" src="${static.url('js/vendor/imageinput.js')}"></script>
## TODO (cpennington): Remove this when we have a good way for modules to specify js to load on the page
## and in the wiki
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<%static:js group='courseware'/>
<%include file="mathjax_include.html" />
@@ -43,7 +35,6 @@
<div class="course-wrapper">
<section aria-label="Course Navigation" class="course-index">
<header id="open_close_accordion">
<h2>Courseware Index</h2>
<a href="#">close</a>
</header>
@@ -56,6 +47,19 @@
<section class="course-content">
${content}
% if course_errors is not UNDEFINED:
<h2>Course errors</h2>
<div id="course-errors">
<ul>
% for (msg, err) in course_errors:
<li>${msg}
<ul><li><pre>${err}</pre></li></ul>
</li>
% endfor
</ul>
</div>
% endif
</section>
</div>
</section>

View File

@@ -34,10 +34,11 @@
<section class="container dashboard">
<section class="dashboard-banner">
${message}
<br/>
</section>
%if message:
<section class="dashboard-banner">
${message}
</section>
%endif
<section class="profile-sidebar">
<header class="profile">

View File

@@ -0,0 +1,11 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>OpenID failed</title>
</head>
<body>
<h1>OpenID failed</h1>
<p>${message}</p>
</body>
</html>

View File

@@ -144,3 +144,31 @@
<iframe width="640" height="360" src="http://www.youtube.com/embed/C2OQ51tu7W4?showinfo=0" frameborder="0" allowfullscreen></iframe>
</div>
</section>
% if show_signup_immediately is not UNDEFINED:
<script type="text/javascript">
function dosignup(){
comp = document.getElementById('signup');
try { //in firefox
comp.click();
return;
} catch(ex) {}
try { // in old chrome
if(document.createEvent) {
var e = document.createEvent('MouseEvents');
e.initEvent( 'click', true, true );
comp.dispatchEvent(e);
return;
}
} catch(ex) {}
try { // in IE, safari
if(document.createEventObject) {
var evObj = document.createEventObject();
comp.fireEvent("onclick", evObj);
return;
}
} catch(ex) {}
}
$(window).load(dosignup);
</script>
% endif

View File

@@ -20,23 +20,25 @@ $(document).ready(function(){
</%block>
<section class="container">
<section class="courseware">
<div class="info-wrapper">
% if user.is_authenticated():
<section class="updates">
${get_course_info_section(course, 'updates')}
</section>
<section aria-label="Handout Navigation" class="handouts">
${get_course_info_section(course, 'handouts')}
</section>
% else:
<section class="updates">
${get_course_info_section(course, 'guest_updates')}
</section>
<section aria-label="Handout Navigation" class="handouts">
${get_course_info_section(course, 'guest_handouts')}
</section>
% endif
</div>
</section>
<div class="info-wrapper">
% if user.is_authenticated():
<section class="updates">
<h1>Course Updates &amp; News</h1>
${get_course_info_section(course, 'updates')}
</section>
<section aria-label="Handout Navigation" class="handouts">
<h1>Course Handouts</h1>
${get_course_info_section(course, 'handouts')}
</section>
% else:
<section class="updates">
<h1>Course Updates &amp; News</h1>
${get_course_info_section(course, 'guest_updates')}
</section>
<section aria-label="Handout Navigation" class="handouts">
<h1>Course Handouts</h1>
${get_course_info_section(course, 'guest_handouts')}
</section>
% endif
</div>
</section>

View File

@@ -27,6 +27,9 @@
<span>Not enrolled? <a href="#signup-modal" class="close-login" rel="leanModal">Sign up.</a></span>
<a href="#forgot-password-modal" rel="leanModal" class="pwd-reset">Forgot password?</a>
</p>
<p>
<a href="${MITX_ROOT_URL}/openid/login/">login via openid</a>
</p>
</section>
<div class="close-modal">

View File

@@ -3,7 +3,7 @@
<html>
<head>
<%block name="title"><title>edX</title></%block>
<link rel="icon" type="image/x-icon" href="${static.url('images/favicon.ico')}" />
<%static:css group='application'/>

View File

@@ -1,4 +1,13 @@
<section class="outside-app">
<h1>There has been an error on the <em>MITx</em> servers</h1>
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
% if is_staff:
<h1>Staff-only details below:</h1>
<p>Error: ${error | h}</p>
<p>Raw data: ${data | h}</p>
% endif
</section>

View File

@@ -19,6 +19,8 @@
$(document).delegate('#class_enroll_form', 'ajax:success', function(data, json, xhr) {
if(json.success) {
location.href="${reverse('dashboard')}";
}else{
$('#register_message).html("<p><font color='red'>" + json.error + "</font></p>")
}
});
})(this)
@@ -60,9 +62,24 @@
<div class="main-cta">
%if user.is_authenticated():
%if registered:
<%
## TODO: move this logic into a view
if course.has_started() or settings.MITX_FEATURES['DISABLE_START_DATES']:
course_target = reverse('info', args=[course.id])
else:
course_target = reverse('about_course', args=[course.id])
show_link = settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION')
%>
%if show_link:
<a href="${course_target}">
%endif
<span class="register disabled">You are registered for this course (${course.number}).</span>
%if show_link:
</a>
%endif
%else:
<a href="#" class="register">Register for ${course.number}</a>
<div id="register_message"></div>
%endif
%else:
<a href="#signup-modal" class="register" rel="leanModal" data-notice='You must Sign Up or <a href="#login-modal" rel="leanModal">Log In</a> to enroll.'>Register for ${course.number}</a>

View File

@@ -4,6 +4,7 @@
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%namespace name="profile_graphs" file="profile_graphs.js"/>
<%block name="title"><title>Profile - edX 6.002x</title></%block>
@@ -71,7 +72,7 @@ $(function() {
var new_email = $('#new_email_field').val();
var new_password = $('#new_email_password').val();
postJSON('/change_email',{"new_email":new_email,
postJSON('/change_email',{"new_email":new_email,
"password":new_password},
function(data){
if(data.success){
@@ -80,7 +81,7 @@ $(function() {
$("#change_email_error").html(data.error);
}
});
log_event("profile", {"type":"email_change_request",
log_event("profile", {"type":"email_change_request",
"old_email":"${email}",
"new_email":new_email});
return false;
@@ -90,7 +91,7 @@ $(function() {
var new_name = $('#new_name_field').val();
var rationale = $('#name_rationale_field').val();
postJSON('/change_name',{"new_name":new_name,
postJSON('/change_name',{"new_name":new_name,
"rationale":rationale},
function(data){
if(data.success){
@@ -99,7 +100,7 @@ $(function() {
$("#change_name_error").html(data.error);
}
});
log_event("profile", {"type":"name_change_request",
log_event("profile", {"type":"name_change_request",
"new_name":new_name,
"rationale":rationale});
return false;
@@ -110,9 +111,9 @@ $(function() {
</%block>
<%include file="navigation.html" args="active_page='profile'" />
<%include file="course_navigation.html" args="active_page='profile'" />
<section class="main-content">
<section class="container">
<div class="profile-wrapper">
<section class="course-info">
@@ -124,10 +125,9 @@ $(function() {
<ol class="chapters">
%for chapter in courseware_summary:
%if not chapter['chapter'] == "hidden":
%if not chapter['display_name'] == "hidden":
<li>
<h2><a href="${reverse('courseware_chapter', args=format_url_params([chapter['course'], chapter['chapter']])) }">
${ chapter['chapter'] }</a></h2>
<h2>${ chapter['display_name'] }</h2>
<ol class="sections">
%for section in chapter['sections']:
@@ -137,14 +137,14 @@ $(function() {
total = section['section_total'].possible
percentageString = "{0:.0%}".format( float(earned)/total) if earned > 0 and total > 0 else ""
%>
<h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }">
${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3>
<h3><a href="${reverse('courseware_section', kwargs={'course_id' : course.id, 'chapter' : chapter['url_name'], 'section' : section['url_name']})}">
${ section['display_name'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3>
${section['format']}
%if 'due' in section and section['due']!="":
due ${section['due']}
%endif
%if len(section['scores']) > 0:
<ol class="scores">
${ "Problem Scores: " if section['graded'] else "Practice Scores: "}
@@ -153,7 +153,7 @@ $(function() {
%endfor
</ol>
%endif
</li> <!--End section-->
%endfor
</ol> <!--End sections-->
@@ -181,7 +181,7 @@ $(function() {
</li>
<li>
Forum name: <strong>${username}</strong>
Forum name: <strong>${username}</strong>
</li>
<li>
@@ -215,7 +215,7 @@ $(function() {
<form id="change_name_form">
<div id="change_name_error"> </div>
<fieldset>
<p>To uphold the credibility of <span class="edx">edX</span> 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>
<p>To uphold the credibility of <span class="edx">edX</span> 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 <span class="edx">edX</span> Certificate: </label>
@@ -234,7 +234,7 @@ $(function() {
</div>
<div id="change_email" class="leanModal_box">
<h1>Change e-mail</h1>
<h1>Change e-mail</h1>
<div id="apply_name_change_error"></div>
<form id="change_email_form">
<div id="change_email_error"> </div>

View File

@@ -19,6 +19,7 @@
<div id="register_error" name="register_error"></div>
<div class="input-group">
% if has_extauth_info is UNDEFINED:
<label data-field="email">E-mail*</label>
<input name="email" type="email" placeholder="E-mail*">
<label data-field="password">Password*</label>
@@ -27,6 +28,12 @@
<input name="username" type="text" placeholder="Public Username*">
<label data-field="name">Full Name</label>
<input name="name" type="text" placeholder="Full Name*">
% else:
<p><i>Welcome</i> ${extauth_email}</p><br/>
<p><i>Enter a public username:</i></p>
<label data-field="username">Public Username*</label>
<input name="username" type="text" value="${extauth_username}" placeholder="Public Username*">
% endif
</div>
<div class="input-group">
@@ -93,11 +100,13 @@
</div>
</form>
% if has_extauth_info is UNDEFINED:
<section class="login-extra">
<p>
<span>Already have an account? <a href="#login-modal" class="close-signup" rel="leanModal">Login.</a></span>
</p>
</section>
% endif
</div>

View File

@@ -1,11 +1,11 @@
${module_content}
<div class="staff_info">
definition = ${definition | h}
metadata = ${metadata | h}
</div>
%if edit_link:
<div><a href="${edit_link}">Edit</a></div>
% endif
<div class="staff_info">
definition = <pre>${definition | h}</pre>
metadata = ${metadata | h}
</div>
%if render_histogram:
<div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
%endif

View File

@@ -67,7 +67,6 @@ $("#open_close_accordion a").click(function(){
<section aria-label="Textbook Navigation" class="book-sidebar">
<header id="open_close_accordion">
<h2>Table of Contents</h2>
<a href="#">close</a>
</header>

View File

@@ -0,0 +1,14 @@
<html>
<h1>Tracking Log</h1>
<table border="1"><tr><th>datetime</th><th>username</th><th>ipaddr</th><th>source</th><th>type</th></tr>
% for rec in records:
<tr>
<td>${rec.time}</td>
<td>${rec.username}</td>
<td>${rec.ip}</td>
<td>${rec.event_source}</td>
<td>${rec.event_type}</td>
</tr>
% endfor
</table>
</html>

View File

@@ -1,5 +1,5 @@
% if name is not UNDEFINED and name is not None:
<h1> ${name} </h1>
<h1> ${display_name} </h1>
% endif
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}">

View File

@@ -14,6 +14,8 @@ urlpatterns = ('',
url(r'^$', 'student.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^admin_dashboard$', 'dashboard.views.dashboard'),
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'),
@@ -160,12 +162,29 @@ if settings.DEBUG:
## Jasmine
urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
if settings.MITX_FEATURES.get('AUTH_USE_OPENID'):
urlpatterns += (
url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'),
url(r'^openid/complete/$', 'external_auth.views.edXauth_openid_login_complete', name='openid-complete'),
url(r'^openid/logo.gif$', 'django_openid_auth.views.logo', name='openid-logo'),
)
if settings.MITX_FEATURES.get('ENABLE_LMS_MIGRATION'):
urlpatterns += (
url(r'^migrate/modules$', 'lms_migration.migrate.manage_modulestores'),
url(r'^migrate/reload/(?P<reload_dir>[^/]+)$', 'lms_migration.migrate.manage_modulestores'),
)
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
urlpatterns += (
url(r'^event_logs$', 'track.views.view_tracking_log'),
)
urlpatterns = patterns(*urlpatterns)
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
#Custom error pages
handler404 = 'static_template_view.views.render_404'
handler500 = 'static_template_view.views.render_500'