Merge master
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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">▾</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">▾</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>
|
||||
|
||||
@@ -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 © 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;">© 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 -->
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ======="
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
3
lms/djangoapps/dashboard/models.py
Normal file
3
lms/djangoapps/dashboard/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
16
lms/djangoapps/dashboard/tests.py
Normal file
16
lms/djangoapps/dashboard/tests.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
31
lms/djangoapps/dashboard/views.py
Normal file
31
lms/djangoapps/dashboard/views.py
Normal 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))
|
||||
0
lms/djangoapps/lms_migration/__init__.py
Normal file
0
lms/djangoapps/lms_migration/__init__.py
Normal file
110
lms/djangoapps/lms_migration/migrate.py
Normal file
110
lms/djangoapps/lms_migration/migrate.py
Normal 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('<','<').replace('>','>')
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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': {
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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 & 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 + '/'
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@function em($pxval, $base: 16) {
|
||||
@return #{$pxval / $base}em;
|
||||
}
|
||||
|
||||
// Line-height
|
||||
@function lh($amount: 1) {
|
||||
@return $body-line-height * $amount;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ div.book-wrapper {
|
||||
@extend .clearfix;
|
||||
|
||||
li {
|
||||
background-color: darken($cream, 4%);
|
||||
|
||||
&.last {
|
||||
display: block;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Generic layout styles for the discussion forums
|
||||
|
||||
body.askbot {
|
||||
|
||||
section.main-content {
|
||||
section.container {
|
||||
div.discussion-wrapper {
|
||||
@extend .table-wrapper;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
lms/static/sass/course/old/.gitignore
vendored
1
lms/static/sass/course/old/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.css
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
11
lms/templates/extauth_failure.html
Normal file
11
lms/templates/extauth_failure.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
14
lms/templates/tracking_log.html
Normal file
14
lms/templates/tracking_log.html
Normal 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>
|
||||
@@ -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}">
|
||||
|
||||
21
lms/urls.py
21
lms/urls.py
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user