Commit 68ab1973 by Bridger Maxwell

Merge remote-tracking branch 'origin/master' into feature/bridger/course_grading

Conflicts:
	lms/djangoapps/courseware/views.py
parents 188eeed3 26a63850
...@@ -207,7 +207,7 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -207,7 +207,7 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor descriptor: An XModuleDescriptor
""" """
return ModuleSystem( return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']), ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems? # TODO (cpennington): Do we want to track how instructors are using the preview problems?
track_function=lambda type, event: None, track_function=lambda type, event: None,
filestore=descriptor.system.resources_fs, filestore=descriptor.system.resources_fs,
...@@ -247,7 +247,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ...@@ -247,7 +247,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module = descriptor.xmodule_constructor(system)(instance_state, shared_state) module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, "xmodule_display.html"), wrap_xmodule(module.get_html, module, "xmodule_display.html"),
module.metadata['data_dir'], module module.metadata['data_dir']
) )
save_preview_state(request, preview_id, descriptor.location.url(), save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state()) module.get_instance_state(), module.get_shared_state())
...@@ -276,9 +276,14 @@ def save_item(request): ...@@ -276,9 +276,14 @@ def save_item(request):
if not has_access(request.user, item_location): if not has_access(request.user, item_location):
raise Http404 # TODO (vshnayder): better error raise Http404 # TODO (vshnayder): better error
data = json.loads(request.POST['data']) if request.POST['data']:
data = request.POST['data']
modulestore().update_item(item_location, data) modulestore().update_item(item_location, data)
if request.POST['children']:
children = request.POST['children']
modulestore().update_children(item_location, children)
# Export the course back to github # Export the course back to github
# This uses wildcarding to find the course, which requires handling # This uses wildcarding to find the course, which requires handling
# multiple courses returned, but there should only ever be one # multiple courses returned, but there should only ever be one
......
...@@ -86,7 +86,12 @@ def export_to_github(course, commit_message, author_str=None): ...@@ -86,7 +86,12 @@ def export_to_github(course, commit_message, author_str=None):
If author_str is specified, uses it in the commit. If author_str is specified, uses it in the commit.
''' '''
course_dir = course.metadata.get('data_dir', course.location.course) course_dir = course.metadata.get('data_dir', course.location.course)
try:
repo_settings = load_repo_settings(course_dir) repo_settings = load_repo_settings(course_dir)
except InvalidRepo:
log.warning("Invalid repo {0}, not exporting data to xml".format(course_dir))
return
git_repo = setup_repo(repo_settings) git_repo = setup_repo(repo_settings)
fs = OSFS(git_repo.working_dir) fs = OSFS(git_repo.working_dir)
......
...@@ -2,6 +2,7 @@ class CMS.Models.Module extends Backbone.Model ...@@ -2,6 +2,7 @@ class CMS.Models.Module extends Backbone.Model
url: '/save_item' url: '/save_item'
defaults: defaults:
data: '' data: ''
children: ''
loadModule: (element) -> loadModule: (element) ->
elt = $(element).find('.xmodule_edit').first() elt = $(element).find('.xmodule_edit').first()
...@@ -11,5 +12,5 @@ class CMS.Models.Module extends Backbone.Model ...@@ -11,5 +12,5 @@ class CMS.Models.Module extends Backbone.Model
"/edit_item?#{$.param(id: @get('id'))}" "/edit_item?#{$.param(id: @get('id'))}"
save: (args...) -> save: (args...) ->
@set(data: JSON.stringify(@module.save())) if @module @set(data: @module.save()) if @module
super(args...) super(args...)
...@@ -13,18 +13,22 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -13,18 +13,22 @@ class CMS.Views.ModuleEdit extends Backbone.View
# Load preview modules # Load preview modules
XModule.loadModules('display') XModule.loadModules('display')
@$children = @$el.find('#sortable')
@enableDrag() @enableDrag()
enableDrag: -> enableDrag: =>
# Enable dragging things in the #sortable div (if there is one) # Enable dragging things in the #sortable div (if there is one)
if $("#sortable").length > 0 if @$children.length > 0
$("#sortable").sortable({ @$children.sortable(
placeholder: "ui-state-highlight" placeholder: "ui-state-highlight"
}) update: (event, ui) =>
$("#sortable").disableSelection(); @model.set(children: @$children.find('.module-edit').map(
(idx, el) -> $(el).data('id')
).toArray())
)
@$children.disableSelection()
save: (event) -> save: (event) =>
event.preventDefault() event.preventDefault()
@model.save().done((previews) => @model.save().done((previews) =>
alert("Your changes have been saved.") alert("Your changes have been saved.")
......
...@@ -141,11 +141,15 @@ textarea { ...@@ -141,11 +141,15 @@ textarea {
} }
} }
// .wip { .wip {
// outline: 1px solid #f00 !important; outline: 1px solid #f00 !important;
// position: relative; position: relative;
// } &:after {
content: "WIP";
.hidden { font-size: 8px;
display: none; padding: 2px;
background: #f00;
color: #fff;
@include position(absolute, 0px 0px 0 0);
}
} }
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
</section> </section>
% endfor % endfor
</section> </section>
<div class="actions wip"> <div class="actions">
<a href="" class="save-update">Save &amp; Update</a> <a href="" class="save-update">Save &amp; Update</a>
<a href="#" class="cancel">Cancel</a> <a href="#" class="cancel">Cancel</a>
</div> </div>
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
data-type="${module.js_module_name}" data-type="${module.js_module_name}"
data-preview-type="${module.module_class.js_module_name}"> data-preview-type="${module.module_class.js_module_name}">
<a href="#" class="module-edit">${module.url_name}</a> <a href="#" class="module-edit">${module.display_name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
% endfor % endfor
...@@ -68,31 +68,6 @@ ...@@ -68,31 +68,6 @@
</ul> </ul>
</li> </li>
%endfor %endfor
<li class="wip">
<header>
<h1>Course Scratch Pad</h1>
</header>
<ul>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13 </a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit"> Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
<%include file="module-dropdown.html"/>
</ul>
</li>
</ol> </ol>
<section class="new-section"> <section class="new-section">
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
<a href="#" class="module-edit" <a href="#" class="module-edit"
data-id="${child.location.url()}" data-id="${child.location.url()}"
data-type="${child.js_module_name}" data-type="${child.js_module_name}"
data-preview-type="${child.module_class.js_module_name}">${child.url_name}</a> data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor %endfor
...@@ -48,61 +48,6 @@ ...@@ -48,61 +48,6 @@
</ol> </ol>
</section> </section>
<section class="scratch-pad wip">
<ol>
<li class="new-module">
<%include file="new-module.html"/>
</li>
<li>
<header>
<h2>Section Scratch</h2>
</header>
<ul>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13 </a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit"> Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
</ul>
</li>
<li>
<header>
<h2>Course Scratch</h2>
</header>
<ul>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13 </a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit"> Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
</ul>
</li>
</ol>
</section>
</div> </div>
</section> </section>
<section class="week-new">
<header>
<div>
<h1 class="editable">Week 3</h1>
<p><a href="#">+ new goal</a></p>
</div>
<section class="goals">
<ul>
<li>
<p><strong>Please add a goal for this section</strong> </p>
</li>
</ul>
</section>
</header>
<section class="content">
<section class="filters">
<ul>
<li>
<label for="">Sort by</label>
<select>
<option value="">Recently Modified</option>
</select>
</li>
<li>
<label for="">Display</label>
<select>
<option value="">All content</option>
</select>
</li>
<li>
<select>
<option value="">Internal Only</option>
</select>
</li>
<li class="advanced">
<a href="#">Advanced filters</a>
</li>
<li>
<input type="search" name="" id="" value="" />
</li>
</ul>
</section>
<div>
<section class="modules empty">
<p>This are no groups or units in this section yet</p>
<a href="#">Add a Group</a>
<a href="#">Add a Unit</a>
</section>
<section class="scratch-pad">
<ol>
<li>
<header>
<h2>Section Scratch</h2>
</header>
<ul>
<li class="empty">
<p><a href="#">There are no units in this scratch yet. Add one</a></p>
</li>
</ul>
</li>
<li>
<header>
<h2>Course Scratch</h2>
</header>
<ul>
<li>
<a href="" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="sequence-edit">Problem Group</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
</ul>
</li>
<li class="new-module">
<%include file="new-module.html"/>
</li>
</ol>
</section>
</div>
</section>
</section>
...@@ -82,6 +82,8 @@ def index(request, extra_context={}, user=None): ...@@ -82,6 +82,8 @@ def index(request, extra_context={}, user=None):
domain=domain) domain=domain)
context = {'universities': universities, 'entries': entries} context = {'universities': universities, 'entries': entries}
context.update(extra_context) context.update(extra_context)
if request.REQUEST.get('next', False):
context['show_login_immediately'] = True
return render_to_response('index.html', context) return render_to_response('index.html', context)
def course_from_id(course_id): def course_from_id(course_id):
......
...@@ -35,7 +35,7 @@ def wrap_xmodule(get_html, module, template): ...@@ -35,7 +35,7 @@ def wrap_xmodule(get_html, module, template):
return _get_html return _get_html
def replace_course_urls(get_html, course_id, module): def replace_course_urls(get_html, course_id):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /course/... the old get_html function and substitutes urls of the form /course/...
...@@ -46,7 +46,7 @@ def replace_course_urls(get_html, course_id, module): ...@@ -46,7 +46,7 @@ def replace_course_urls(get_html, course_id, module):
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/') return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
return _get_html return _get_html
def replace_static_urls(get_html, prefix, module): def replace_static_urls(get_html, prefix):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/... the old get_html function and substitutes urls of the form /static/...
......
...@@ -333,6 +333,10 @@ def textline_dynamath(element, value, status, render_template, msg=''): ...@@ -333,6 +333,10 @@ def textline_dynamath(element, value, status, render_template, msg=''):
if '' in preprocessor.values(): if '' in preprocessor.values():
preprocessor = None preprocessor = None
# Escape characters in student input for safe XML parsing
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict)
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
'msg': msg, 'hidden': hidden, 'msg': msg, 'hidden': hidden,
'preprocessor': preprocessor, 'preprocessor': preprocessor,
......
...@@ -1627,6 +1627,10 @@ class ImageResponse(LoncapaResponse): ...@@ -1627,6 +1627,10 @@ class ImageResponse(LoncapaResponse):
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]' given = student_answers[aid] # this should be a string of the form '[x,y]'
if not given: # No answer to parse. Mark as incorrect and move on
correct_map.set(aid, 'incorrect')
continue
# parse expected answer # parse expected answer
# TODO: Compile regexp on file load # TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', '')) m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', ''))
......
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
<fieldset>
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}"> <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <label for="input_${id}_${choice_id}"> <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value: % if choice_id in value:
...@@ -8,14 +18,6 @@ ...@@ -8,14 +18,6 @@
/> ${choice_description} </label> /> ${choice_description} </label>
% endfor % endfor
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
</fieldset>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</form> </form>
import cgi
import datetime import datetime
import dateutil import dateutil
import dateutil.parser import dateutil.parser
...@@ -125,17 +126,17 @@ class CapaModule(XModule): ...@@ -125,17 +126,17 @@ class CapaModule(XModule):
self.name = only_one(dom2.xpath('/problem/@name')) self.name = only_one(dom2.xpath('/problem/@name'))
if self.rerandomize == 'never': if self.rerandomize == 'never':
seed = 1 self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'id'): elif self.rerandomize == "per_student" and hasattr(self.system, 'id'):
seed = system.id self.seed = system.id
else: else:
seed = None self.seed = None
try: try:
# TODO (vshnayder): move as much as possible of this work and error # TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time # checking to descriptor load time
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=seed, system=self.system) instance_state, seed=self.seed, system=self.system)
except Exception as err: except Exception as err:
msg = 'cannot create LoncapaProblem {loc}: {err}'.format( msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err) loc=self.location.url(), err=err)
...@@ -154,7 +155,7 @@ class CapaModule(XModule): ...@@ -154,7 +155,7 @@ class CapaModule(XModule):
(self.location.url(), msg)) (self.location.url(), msg))
self.lcp = LoncapaProblem( self.lcp = LoncapaProblem(
problem_text, self.location.html_id(), problem_text, self.location.html_id(),
instance_state, seed=seed, system=self.system) instance_state, seed=self.seed, system=self.system)
else: else:
# add extra info and raise # add extra info and raise
raise Exception(msg), None, sys.exc_info()[2] raise Exception(msg), None, sys.exc_info()[2]
...@@ -172,6 +173,8 @@ class CapaModule(XModule): ...@@ -172,6 +173,8 @@ class CapaModule(XModule):
return "per_student" return "per_student"
elif rerandomize == "never": elif rerandomize == "never":
return "never" return "never"
elif rerandomize == "onreset":
return "onreset"
else: else:
raise Exception("Invalid rerandomize attribute " + rerandomize) raise Exception("Invalid rerandomize attribute " + rerandomize)
...@@ -214,9 +217,10 @@ class CapaModule(XModule): ...@@ -214,9 +217,10 @@ class CapaModule(XModule):
try: try:
html = self.lcp.get_html() html = self.lcp.get_html()
except Exception, err: except Exception, err:
log.exception(err)
# TODO (vshnayder): another switch on DEBUG. # TODO (vshnayder): another switch on DEBUG.
if self.system.DEBUG: if self.system.DEBUG:
log.exception(err)
msg = ( msg = (
'[courseware.capa.capa_module] <font size="+1" color="red">' '[courseware.capa.capa_module] <font size="+1" color="red">'
'Failed to generate HTML for problem %s</font>' % 'Failed to generate HTML for problem %s</font>' %
...@@ -225,6 +229,46 @@ class CapaModule(XModule): ...@@ -225,6 +229,46 @@ class CapaModule(XModule):
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg html = msg
else: else:
# We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible
# Presumably, student submission has corrupted LoncapaProblem HTML.
# First, pull down all student answers
student_answers = self.lcp.student_answers
answer_ids = student_answers.keys()
# Some inputtypes, such as dynamath, have additional "hidden" state that
# is not exposed to the student. Keep those hidden
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
hidden_state_keywords = ['dynamath']
for answer_id in answer_ids:
for hidden_state_keyword in hidden_state_keywords:
if answer_id.find(hidden_state_keyword) >= 0:
student_answers.pop(answer_id)
# Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
state=None, # Tabula rasa
seed=self.seed, system=self.system)
# Prepend a scary warning to the student
warning = '<div class="capa_reset">'\
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
'The problem\'s state was corrupted by an invalid submission. ' \
'The submission consisted of:'\
'<ul>'
for student_answer in student_answers.values():
if student_answer != '':
warning += '<li>' + cgi.escape(student_answer) + '</li>'
warning += '</ul>'\
'If this error persists, please contact the course staff.'\
'</div>'
html = warning
try:
html += self.lcp.get_html()
except Exception, err: # Couldn't do it. Give up
log.exception(err)
raise raise
content = {'name': self.display_name, content = {'name': self.display_name,
...@@ -259,7 +303,7 @@ class CapaModule(XModule): ...@@ -259,7 +303,7 @@ class CapaModule(XModule):
save_button = False save_button = False
# Only show the reset button if pressing it will show different values # Only show the reset button if pressing it will show different values
if self.rerandomize != 'always': if self.rerandomize not in ["always", "onreset"]:
reset_button = False reset_button = False
# User hasn't submitted an answer yet -- we don't want resets # User hasn't submitted an answer yet -- we don't want resets
...@@ -569,7 +613,7 @@ class CapaModule(XModule): ...@@ -569,7 +613,7 @@ class CapaModule(XModule):
return "Refresh the page and make an attempt before resetting." return "Refresh the page and make an attempt before resetting."
self.lcp.do_reset() self.lcp.do_reset()
if self.rerandomize == "always": if self.rerandomize in ["always", "onreset"]:
# reset random number generator seed (note the self.lcp.get_state() # reset random number generator seed (note the self.lcp.get_state()
# in next line) # in next line)
self.lcp.seed = None self.lcp.seed = None
......
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
import time
import logging import logging
import requests
from lxml import etree from lxml import etree
from path import path # NOTE (THK): Only used for detecting presence of syllabus from path import path # NOTE (THK): Only used for detecting presence of syllabus
import requests
import time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import load_grading_policy from xmodule.graders import load_grading_policy
...@@ -21,10 +21,15 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -21,10 +21,15 @@ class CourseDescriptor(SequenceDescriptor):
self.title = title self.title = title
self.book_url = book_url self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3() self.table_of_contents = self._get_toc_from_s3()
self.start_page = int(self.table_of_contents[0].attrib['page'])
@classmethod # The last page should be the last element in the table of contents,
def from_xml_object(cls, xml_object): # but it may be nested. So recurse all the way down the last element
return cls(xml_object.get('title'), xml_object.get('book_url')) last_el = self.table_of_contents[-1]
while last_el.getchildren():
last_el = last_el[-1]
self.end_page = int(last_el.attrib['page'])
@property @property
def table_of_contents(self): def table_of_contents(self):
...@@ -57,10 +62,18 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -57,10 +62,18 @@ class CourseDescriptor(SequenceDescriptor):
return table_of_contents return table_of_contents
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
self.textbooks = self.definition['data']['textbooks']
self.textbooks = []
for title, book_url in self.definition['data']['textbooks']:
try:
self.textbooks.append(self.Textbook(title, book_url))
except:
# If we can't get to S3 (e.g. on a train with no internet), don't break
# the rest of the courseware.
log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url))
continue
self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course
...@@ -82,7 +95,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -82,7 +95,6 @@ class CourseDescriptor(SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus # disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus')) self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
def set_grading_policy(self, policy_str): def set_grading_policy(self, policy_str):
"""Parse the policy specified in policy_str, and save it""" """Parse the policy specified in policy_str, and save it"""
try: try:
...@@ -94,19 +106,11 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -94,19 +106,11 @@ class CourseDescriptor(SequenceDescriptor):
# the error log. # the error log.
self._grading_policy = {} self._grading_policy = {}
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
textbooks = [] textbooks = []
for textbook in xml_object.findall("textbook"): for textbook in xml_object.findall("textbook"):
try: textbooks.append((textbook.get('title'), textbook.get('book_url')))
txt = cls.Textbook.from_xml_object(textbook)
except:
# If we can't get to S3 (e.g. on a train with no internet), don't break
# the rest of the courseware.
log.exception("Couldn't load textbook")
continue
textbooks.append(txt)
xml_object.remove(textbook) xml_object.remove(textbook)
#Load the wiki tag if it exists #Load the wiki tag if it exists
...@@ -135,6 +139,13 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -135,6 +139,13 @@ class CourseDescriptor(SequenceDescriptor):
return self._grading_policy['GRADE_CUTOFFS'] return self._grading_policy['GRADE_CUTOFFS']
@property @property
def tabs(self):
"""
Return the tabs config, as a python object, or None if not specified.
"""
return self.metadata.get('tabs')
@property
def show_calculator(self): def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes" return self.metadata.get("show_calculator", None) == "Yes"
......
...@@ -36,9 +36,36 @@ section.problem { ...@@ -36,9 +36,36 @@ section.problem {
} }
.choicegroup { .choicegroup {
@include clearfix;
label.choicegroup_correct:after { label.choicegroup_correct:after {
content: url('../images/correct-icon.png'); content: url('../images/correct-icon.png');
} }
> span {
padding-right: 20px;
float: left;
background-position: 0 0 !important;
}
fieldset {
@include box-sizing(border-box);
float: left;
border-left: 1px solid #ddd;
padding-left: 20px;
margin: 20px 0;
}
input[type="radio"],
input[type="checkbox"] {
float: left;
margin: 4px 8px 0 0;
}
text {
display: block;
margin-left: 25px;
}
} }
ol.enumerate { ol.enumerate {
...@@ -52,6 +79,23 @@ section.problem { ...@@ -52,6 +79,23 @@ section.problem {
} }
} }
.solution-span {
> span {
margin: 20px 0;
display: block;
border: 1px solid #ddd;
padding: 9px 15px 20px;
background: #FFF;
position: relative;
@include box-shadow(inset 0 0 0 1px #eee);
@include border-radius(3px);
&:empty {
display: none;
}
}
}
div { div {
p { p {
&.answer { &.answer {
...@@ -432,18 +476,23 @@ section.problem { ...@@ -432,18 +476,23 @@ section.problem {
input.save { input.save {
@extend .blue-button; @extend .blue-button;
} }
.submission_feedback {
// background: #F3F3F3;
// border: 1px solid #ddd;
// @include border-radius(3px);
// padding: 8px 12px;
// margin-top: 10px;
@include inline-block;
font-style: italic;
margin: 8px 0 0 10px;
color: #777;
-webkit-font-smoothing: antialiased;
}
} }
.detailed-solution { .detailed-solution {
border: 1px solid #ddd; > p:first-child {
padding: 9px 15px 20px;
margin-bottom: 10px;
background: #FFF;
position: relative;
@include box-shadow(inset 0 0 0 1px #eee);
@include border-radius(3px);
p:first-child {
font-size: 0.9em; font-size: 0.9em;
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
...@@ -465,6 +514,22 @@ section.problem { ...@@ -465,6 +514,22 @@ section.problem {
margin-top: 10px; margin-top: 10px;
} }
div.capa_reset {
padding: 25px;
border: 1px solid $error-red;
background-color: lighten($error-red, 25%);
border-radius: 3px;
font-size: 1em;
margin-top: 10px;
margin-bottom: 10px;
}
.capa_reset>h2 {
color: #AA0000;
}
.capa_reset li {
font-size: 0.9em;
}
.hints { .hints {
border: 1px solid #ccc; border: 1px solid #ccc;
......
...@@ -11,6 +11,8 @@ class @Video ...@@ -11,6 +11,8 @@ class @Video
@parseSpeed() @parseSpeed()
$("#video_#{@id}").data('video', this).addClass('video-load-complete') $("#video_#{@id}").data('video', this).addClass('video-load-complete')
@hide_captions = $.cookie('hide_captions') == 'true'
if YT.Player if YT.Player
@embed() @embed()
else else
......
...@@ -49,7 +49,7 @@ class @VideoCaption extends Subview ...@@ -49,7 +49,7 @@ class @VideoCaption extends Subview
@$('.subtitles').html(container.html()) @$('.subtitles').html(container.html())
@$('.subtitles li[data-index]').click @seekPlayer @$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmatic reason # prepend and append an empty <li> for cosmetic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight())) @$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight())) .append($('<li class="spacing">').height(@bottomSpacingHeight()))
...@@ -130,13 +130,20 @@ class @VideoCaption extends Subview ...@@ -130,13 +130,20 @@ class @VideoCaption extends Subview
toggle: (event) => toggle: (event) =>
event.preventDefault() event.preventDefault()
if @el.hasClass('closed') if @el.hasClass('closed') # Captions are "closed" e.g. turned off
@hideCaptions(false)
else # Captions are on
@hideCaptions(true)
hideCaptions: (hide_captions) =>
if hide_captions
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
else
@$('.hide-subtitles').attr('title', 'Turn off captions') @$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed') @el.removeClass('closed')
@scrollCaption() @scrollCaption()
else $.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
captionHeight: -> captionHeight: ->
if @el.hasClass('fullscreen') if @el.hasClass('fullscreen')
......
...@@ -45,6 +45,7 @@ class @VideoPlayer extends Subview ...@@ -45,6 +45,7 @@ class @VideoPlayer extends Subview
events: events:
onReady: @onReady onReady: @onReady
onStateChange: @onStateChange onStateChange: @onStateChange
@caption.hideCaptions(@['video'].hide_captions)
addToolTip: -> addToolTip: ->
@$('.add-fullscreen, .hide-subtitles').qtip @$('.add-fullscreen, .hide-subtitles').qtip
......
...@@ -316,3 +316,9 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -316,3 +316,9 @@ class MongoModuleStore(ModuleStoreBase):
{'_id': True}) {'_id': True})
return [i['_id'] for i in items] return [i['_id'] for i in items]
def get_errored_courses(self):
"""
This function doesn't make sense for the mongo modulestore, as courses
are loaded on demand, rather than up front
"""
return {}
...@@ -414,7 +414,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -414,7 +414,6 @@ class XMLModuleStore(ModuleStoreBase):
policy_str = self.read_grading_policy(paths, tracker) policy_str = self.read_grading_policy(paths, tracker)
course_descriptor.set_grading_policy(policy_str) course_descriptor.set_grading_policy(policy_str)
log.debug('========> Done with course import from {0}'.format(course_dir)) log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor return course_descriptor
......
...@@ -21,7 +21,8 @@ class SequenceModule(XModule): ...@@ -21,7 +21,8 @@ class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence ''' Layout module which lays out content in a temporal sequence
''' '''
js = {'coffee': [resource_string(__name__, js = {'coffee': [resource_string(__name__,
'js/src/sequence/display.coffee')]} 'js/src/sequence/display.coffee')],
'js': [resource_string(__name__, 'js/src/sequence/display/jquery.sequence.js')]}
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]} css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence" js_module_name = "Sequence"
......
...@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2" ...@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1" SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt" BREW_FILE="$BASE/mitx/brew-formulas.txt"
LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log"
APT_PKGS="curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz libgraphviz-dev" APT_PKGS="pkg-config curl git python-virtualenv build-essential python-dev gfortran liblapack-dev libfreetype6-dev libpng12-dev libxml2-dev libxslt-dev yui-compressor coffeescript graphviz graphviz-dev"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
error "This script should not be run using sudo or as the root user" error "This script should not be run using sudo or as the root user"
...@@ -223,7 +223,7 @@ EO ...@@ -223,7 +223,7 @@ EO
command -v brew &>/dev/null || { command -v brew &>/dev/null || {
output "Installing brew" output "Installing brew"
/usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/master/Library/Contributions/install_homebrew.rb)" /usr/bin/ruby <(curl -fsSkL raw.github.com/mxcl/homebrew/go)
} }
command -v git &>/dev/null || { command -v git &>/dev/null || {
output "Installing git" output "Installing git"
......
...@@ -219,6 +219,13 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}. ...@@ -219,6 +219,13 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}.
* The order in which things appear does not matter, though it may be helpful to organize the file in the same order as things appear in the content. * The order in which things appear does not matter, though it may be helpful to organize the file in the same order as things appear in the content.
* NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This can be irritating at first. * NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This can be irritating at first.
Supported fields at the course level:
* "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00".
* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start".
* "tabs" -- have custom tabs in the courseware. See below for details on config.
* TODO: there are others
### Grading policy file contents ### Grading policy file contents
TODO: This needs to be improved, but for now here's a sketch of how grading works: TODO: This needs to be improved, but for now here's a sketch of how grading works:
...@@ -274,6 +281,7 @@ __Inherited:__ ...@@ -274,6 +281,7 @@ __Inherited:__
* `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. * `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional.
* `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false". * `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false".
* `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it) * `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it)
'onreset' (randomize question when reset button is pressed by the student)
'never' (all students see the same version of the problem) 'never' (all students see the same version of the problem)
'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see) 'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see)
Default: 'always'. Optional. Default: 'always'. Optional.
...@@ -340,7 +348,43 @@ If you look at some older xml, you may see some tags or metadata attributes that ...@@ -340,7 +348,43 @@ If you look at some older xml, you may see some tags or metadata attributes that
# Static links # Static links
if your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this in `YOUR_COURSE_DIR/blah/ponies.jpg`. Note that this is not looking in a `static/` subfolder in your course dir. This may (should?) change at some point. Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example. If your content links (e.g. in an html file) to `"static/blah/ponies.jpg"`, we will look for this...
* If your course dir has a `static/` subdirectory, we will look in `YOUR_COURSE_DIR/static/blah/ponies.jpg`. This is the prefered organization, as it does not expose anything except what's in `static/` to the world.
* If your course dir does not have a `static/` subdirectory, we will look in `YOUR_COURSE_DIR/blah/ponies.jpg`. This is the old organization, and requires that the web server allow access to everything in the couse dir. To switch to the new organization, move all your static content into a new `static/` dir (e.g. if you currently have things in `images/`, `css/`, and `special/`, create a dir called `static/`, and move `images/, css/, and special/` there).
Links that include `/course` will be rewritten to the root of your course in the courseware (e.g. `courses/{org}/{course}/{url_name}/` in the current url structure). This is useful for linking to the course wiki, for example.
# Tabs
If you want to customize the courseware tabs displayed for your course, specify a "tabs" list in the course-level policy. e.g.:
"tabs" : [
{"type": "courseware"}, # no name--always "Courseware" for consistency between courses
{"type": "course_info", "name": "Course Info"},
{"type": "external_link", "name": "My Discussion", "link": "http://www.mydiscussion.org/blah"},
{"type": "progress", "name": "Progress"},
{"type": "wiki", "name": "Wonderwiki"},
{"type": "static_tab", "url_slug": "news", "name": "Exciting news"},
{"type": "textbooks"} # generates one tab per textbook, taking names from the textbook titles
]
* If you specify any tabs, you must specify all tabs. They will appear in the order given.
* The first two tabs must have types `"courseware"` and `"course_info"`, in that order. Otherwise, we'll refuse to load the course.
* for static tabs, the url_slug will be the url that points to the tab. It can not be one of the existing courseware url types (even if those aren't used in your course). The static content will come from `tabs/{course_url_name}/{url_slug}.html`, or `tabs/{url_slug}.html` if that doesn't exist.
* An Instructor tab will be automatically added at the end for course staff users.
## Supported tab types:
* "courseware". No other parameters.
* "course_info". Parameter "name".
* "wiki". Parameter "name".
* "discussion". Parameter "name".
* "external_link". Parameters "name", "link".
* "textbooks". No parameters--generates tab names from book titles.
* "progress". Parameter "name".
# Tips for content developers # Tips for content developers
......
...@@ -254,12 +254,11 @@ def _get_module(user, request, location, student_module_cache, course_id, positi ...@@ -254,12 +254,11 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, 'xmodule_display.html'), wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
module.metadata['data_dir'], module module.metadata['data_dir'])
)
# Allow URLs of the form '/course/' refer to the root of multicourse directory # Allow URLs of the form '/course/' refer to the root of multicourse directory
# hierarchy of this course # hierarchy of this course
module.get_html = replace_course_urls(module.get_html, course_id, module) module.get_html = replace_course_urls(module.get_html, course_id)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'): if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
if has_access(user, module, 'staff'): if has_access(user, module, 'staff'):
......
...@@ -22,6 +22,7 @@ from django.views.decorators.cache import cache_control ...@@ -22,6 +22,7 @@ from django.views.decorators.cache import cache_control
from courseware import grades from courseware import grades
from courseware.access import has_access from courseware.access import has_access
from courseware.courses import (get_course_with_access, get_courses_by_university) from courseware.courses import (get_course_with_access, get_courses_by_university)
import courseware.tabs as tabs
from courseware.models import StudentModuleCache from courseware.models import StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module from module_render import toc_for_course, get_module, get_instance_module
from student.models import UserProfile from student.models import UserProfile
...@@ -343,6 +344,30 @@ def course_info(request, course_id): ...@@ -343,6 +344,30 @@ def course_info(request, course_id):
return render_to_response('courseware/info.html', {'course': course, return render_to_response('courseware/info.html', {'course': course,
'staff_access': staff_access,}) 'staff_access': staff_access,})
@ensure_csrf_cookie
def static_tab(request, course_id, tab_slug):
"""
Display the courses tab with the given name.
Assumes the course_id is in a valid format.
"""
course = get_course_with_access(request.user, course_id, 'load')
tab = tabs.get_static_tab_by_slug(course, tab_slug)
if tab is None:
raise Http404
contents = tabs.get_static_tab_contents(course, tab)
if contents is None:
raise Http404
staff_access = has_access(request.user, course, 'staff')
return render_to_response('courseware/static_tab.html',
{'course': course,
'tab': tab,
'tab_contents': contents,
'staff_access': staff_access,})
# TODO arjun: remove when custom tabs in place, see courseware/syllabus.py # TODO arjun: remove when custom tabs in place, see courseware/syllabus.py
@ensure_csrf_cookie @ensure_csrf_cookie
def syllabus(request, course_id): def syllabus(request, course_id):
...@@ -357,6 +382,7 @@ def syllabus(request, course_id): ...@@ -357,6 +382,7 @@ def syllabus(request, course_id):
return render_to_response('courseware/syllabus.html', {'course': course, return render_to_response('courseware/syllabus.html', {'course': course,
'staff_access': staff_access,}) 'staff_access': staff_access,})
def registered_for_course(course, user): def registered_for_course(course, user):
'''Return CourseEnrollment if user is registered for course, else False''' '''Return CourseEnrollment if user is registered for course, else False'''
if user is None: if user is None:
...@@ -404,6 +430,9 @@ def university_profile(request, org_id): ...@@ -404,6 +430,9 @@ def university_profile(request, org_id):
context = dict(courses=courses, org_id=org_id) context = dict(courses=courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower() template_file = "university_profile/{0}.html".format(org_id).lower()
if request.REQUEST.get('next', False):
context['show_login_immediately'] = True
return render_to_response(template_file, context) return render_to_response(template_file, context)
def render_notifications(request, course, notifications): def render_notifications(request, course, notifications):
......
...@@ -28,26 +28,6 @@ PAGES_NEARBY_DELTA = 2 ...@@ -28,26 +28,6 @@ PAGES_NEARBY_DELTA = 2
escapedict = {'"': '&quot;'} escapedict = {'"': '&quot;'}
log = logging.getLogger("edx.discussions") log = logging.getLogger("edx.discussions")
def _general_discussion_id(course_id):
return course_id.replace('/', '_').replace('.', '_')
def _should_perform_search(request):
return bool(request.GET.get('text', False) or \
request.GET.get('tags', False))
def render_accordion(request, course, discussion_id):
# TODO: Delete if obsolete
discussion_info = utils.get_categorized_discussion_info(request, course)
context = {
'course': course,
'discussion_info': discussion_info,
'active': discussion_id,
'csrf': csrf(request)['csrf_token'],
}
return render_to_string('discussion/_accordion.html', context)
def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE): def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAGE):
""" """
This may raise cc.utils.CommentClientError or This may raise cc.utils.CommentClientError or
...@@ -63,6 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG ...@@ -63,6 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
'tags': '', 'tags': '',
'commentable_id': discussion_id, 'commentable_id': discussion_id,
'course_id': course_id, 'course_id': course_id,
'user_id': request.user.id,
} }
if not request.GET.get('sort_key'): if not request.GET.get('sort_key'):
...@@ -101,6 +82,7 @@ def inline_discussion(request, course_id, discussion_id): ...@@ -101,6 +82,7 @@ def inline_discussion(request, course_id, discussion_id):
# TODO (vshnayder): since none of this code seems to be aware of the fact that # TODO (vshnayder): since none of this code seems to be aware of the fact that
# sometimes things go wrong, I suspect that the js client is also not # sometimes things go wrong, I suspect that the js client is also not
# checking for errors on request. Check and fix as needed. # checking for errors on request. Check and fix as needed.
log.error("Error loading inline discussion threads.")
raise Http404 raise Http404
def infogetter(thread): def infogetter(thread):
...@@ -134,6 +116,7 @@ def forum_form_discussion(request, course_id): ...@@ -134,6 +116,7 @@ def forum_form_discussion(request, course_id):
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
threads = [utils.safe_content(thread) for thread in unsafethreads] threads = [utils.safe_content(thread) for thread in unsafethreads]
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading forum discussion threads: %s" % str(err))
raise Http404 raise Http404
user_info = cc.User.from_django_user(request.user).to_dict() user_info = cc.User.from_django_user(request.user).to_dict()
...@@ -184,14 +167,18 @@ def forum_form_discussion(request, course_id): ...@@ -184,14 +167,18 @@ def forum_form_discussion(request, course_id):
@login_required @login_required
def single_thread(request, course_id, discussion_id, thread_id): def single_thread(request, course_id, discussion_id, thread_id):
if request.is_ajax():
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
user_info = cc.User.from_django_user(request.user).to_dict() cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict()
try: try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True) thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.")
raise Http404 raise Http404
if request.is_ajax():
courseware_context = get_courseware_context(thread, course) courseware_context = get_courseware_context(thread, course)
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info) annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
...@@ -208,13 +195,13 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -208,13 +195,13 @@ def single_thread(request, course_id, discussion_id, thread_id):
}) })
else: else:
course = get_course_with_access(request.user, course_id, 'load')
category_map = utils.get_discussion_category_map(course) category_map = utils.get_discussion_category_map(course)
try: try:
threads, query_params = get_threads(request, course_id) threads, query_params = get_threads(request, course_id)
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
threads.append(thread.to_dict()) threads.append(thread.to_dict())
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err: except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.")
raise Http404 raise Http404
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
...@@ -236,7 +223,6 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -236,7 +223,6 @@ def single_thread(request, course_id, discussion_id, thread_id):
# course_id, # course_id,
#) #)
user_info = cc.User.from_django_user(request.user).to_dict()
def infogetter(thread): def infogetter(thread):
return utils.get_annotated_content_infos(course_id, thread, request.user, user_info) return utils.get_annotated_content_infos(course_id, thread, request.user, user_info)
......
...@@ -189,7 +189,7 @@ def initialize_discussion_info(course): ...@@ -189,7 +189,7 @@ def initialize_discussion_info(course):
"sort_key": entry["sort_key"], "sort_key": entry["sort_key"],
"start_date": entry["start_date"]} "start_date": entry["start_date"]}
default_topics = {'General': course.location.html_id()} default_topics = {'General': {'id' :course.location.html_id()}}
discussion_topics = course.metadata.get('discussion_topics', default_topics) discussion_topics = course.metadata.get('discussion_topics', default_topics)
for topic, entry in discussion_topics.items(): for topic, entry in discussion_topics.items():
category_map['entries'][topic] = {"id": entry["id"], category_map['entries'][topic] = {"id": entry["id"],
...@@ -336,7 +336,8 @@ def safe_content(content): ...@@ -336,7 +336,8 @@ def safe_content(content):
'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at', 'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at',
'updated_at', 'depth', 'type', 'commentable_id', 'comments_count', 'updated_at', 'depth', 'type', 'commentable_id', 'comments_count',
'at_position_list', 'children', 'highlighted_title', 'highlighted_body', 'at_position_list', 'children', 'highlighted_title', 'highlighted_body',
'courseware_title', 'courseware_url', 'tags' 'courseware_title', 'courseware_url', 'tags', 'unread_comments_count',
'read',
] ]
if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False): if (content.get('anonymous') is False) and (content.get('anonymous_to_peers') is False):
......
...@@ -85,7 +85,8 @@ def instructor_dashboard(request, course_id): ...@@ -85,7 +85,8 @@ def instructor_dashboard(request, course_id):
writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
writer.writerow(datatable['header']) writer.writerow(datatable['header'])
for datarow in datatable['data']: for datarow in datatable['data']:
writer.writerow(datarow) encoded_row = [unicode(s).encode('utf-8') for s in datarow]
writer.writerow(encoded_row)
return response return response
def get_staff_group(course): def get_staff_group(course):
...@@ -250,7 +251,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True, ...@@ -250,7 +251,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
If get_raw_scores=True, then instead of grade summaries, the raw grades for all graded modules are returned. If get_raw_scores=True, then instead of grade summaries, the raw grades for all graded modules are returned.
''' '''
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username') enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).prefetch_related("groups").order_by('username')
header = ['ID', 'Username', 'Full Name', 'edX email', 'External email'] header = ['ID', 'Username', 'Full Name', 'edX email', 'External email']
if get_grades: if get_grades:
......
...@@ -7,16 +7,23 @@ from courseware.courses import get_course_with_access ...@@ -7,16 +7,23 @@ from courseware.courses import get_course_with_access
from lxml import etree from lxml import etree
@login_required @login_required
def index(request, course_id, book_index, page=0): def index(request, course_id, book_index, page=None):
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
textbook = course.textbooks[int(book_index)] book_index = int(book_index)
textbook = course.textbooks[book_index]
table_of_contents = textbook.table_of_contents table_of_contents = textbook.table_of_contents
if page is None:
page = textbook.start_page
return render_to_response('staticbook.html', return render_to_response('staticbook.html',
{'page': int(page), 'course': course, 'book_url': textbook.book_url, {'book_index': book_index, 'page': int(page),
'course': course, 'book_url': textbook.book_url,
'table_of_contents': table_of_contents, 'table_of_contents': table_of_contents,
'start_page' : textbook.start_page,
'end_page' : textbook.end_page,
'staff_access': staff_access}) 'staff_access': staff_access})
def index_shifted(request, course_id, page): def index_shifted(request, course_id, page):
......
"""
Settings for the LMS that runs alongside the CMS on AWS
"""
from ..dev import *
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': DATA_DIR,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
...@@ -406,7 +406,6 @@ MIDDLEWARE_CLASSES = ( ...@@ -406,7 +406,6 @@ MIDDLEWARE_CLASSES = (
# 'debug_toolbar.middleware.DebugToolbarMiddleware', # 'debug_toolbar.middleware.DebugToolbarMiddleware',
'django_comment_client.utils.ViewNameMiddleware', 'django_comment_client.utils.ViewNameMiddleware',
'django_comment_client.utils.QueryCountDebugMiddleware',
) )
############################### Pipeline ####################################### ############################### Pipeline #######################################
...@@ -551,6 +550,8 @@ PIPELINE_JS = { ...@@ -551,6 +550,8 @@ PIPELINE_JS = {
} }
} }
PIPELINE_DISABLE_WRAPPER = True
# Compile all coffee files in course data directories if they are out of date. # Compile all coffee files in course data directories if they are out of date.
# TODO: Remove this once we move data into Mongo. This is only temporary while # TODO: Remove this once we move data into Mongo. This is only temporary while
# course data directories are still in use. # course data directories are still in use.
......
...@@ -135,7 +135,8 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True ...@@ -135,7 +135,8 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
################################ DEBUG TOOLBAR ################################# ################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar',) INSTALLED_APPS += ('debug_toolbar',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',) MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
'debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',) INTERNAL_IPS = ('127.0.0.1',)
DEBUG_TOOLBAR_PANELS = ( DEBUG_TOOLBAR_PANELS = (
......
...@@ -8,8 +8,9 @@ class Thread(models.Model): ...@@ -8,8 +8,9 @@ class Thread(models.Model):
accessible_fields = [ accessible_fields = [
'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id', 'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'at_position_list', 'created_at', 'updated_at', 'comments_count', 'unread_comments_count',
'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed' 'at_position_list', 'children', 'type', 'highlighted_title',
'highlighted_body', 'endorsed', 'read'
] ]
updatable_fields = [ updatable_fields = [
...@@ -59,7 +60,21 @@ class Thread(models.Model): ...@@ -59,7 +60,21 @@ class Thread(models.Model):
else: else:
return super(Thread, cls).url(action, params) return super(Thread, cls).url(action, params)
# TODO: This is currently overriding Model._retrieve only to add parameters
# for the request. Model._retrieve should be modified to handle this such
# that subclasses don't need to override for this.
def _retrieve(self, *args, **kwargs): def _retrieve(self, *args, **kwargs):
url = self.url(action='get', params=self.attributes) url = self.url(action='get', params=self.attributes)
response = perform_request('get', url, {'recursive': kwargs.get('recursive')})
request_params = {
'recursive': kwargs.get('recursive'),
'user_id': kwargs.get('user_id'),
'mark_as_read': kwargs.get('mark_as_read', True),
}
# user_id may be none, in which case it shouldn't be part of the
# request.
request_params = strip_none(request_params)
response = perform_request('get', url, request_params)
self.update_attributes(**response) self.update_attributes(**response)
class @DiscussionFilter
@filterDrop: (e) ->
$drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper')
query = $(e.target).val()
$items = $drop.find('a')
if(query.length == 0)
$items.removeClass('hidden')
return;
$items.addClass('hidden')
$items.each (i) ->
thisText = $(this).not('.unread').text()
$(this).parents('ul').siblings('a').not('.unread').each (i) ->
thisText = thisText + ' ' + $(this).text();
test = true
terms = thisText.split(' ')
if(thisText.toLowerCase().search(query.toLowerCase()) == -1)
test = false
if(test)
$(this).removeClass('hidden')
# show children
$(this).parent().find('a').removeClass('hidden');
# show parents
$(this).parents('ul').siblings('a').removeClass('hidden');
...@@ -14,6 +14,9 @@ if Backbone? ...@@ -14,6 +14,9 @@ if Backbone?
@newPostView = new NewPostView(el: $(".new-post-article"), collection: @discussion) @newPostView = new NewPostView(el: $(".new-post-article"), collection: @discussion)
@nav.on "thread:created", @navigateToThread @nav.on "thread:created", @navigateToThread
@newPost = $('.new-post-article')
$('.new-post-btn').bind "click", @showNewPost
$('.new-post-cancel').bind "click", @hideNewPost
allThreads: -> allThreads: ->
@nav.updateSidebar() @nav.updateSidebar()
...@@ -24,6 +27,8 @@ if Backbone? ...@@ -24,6 +27,8 @@ if Backbone?
showThread: (forum_name, thread_id) -> showThread: (forum_name, thread_id) ->
@thread = @discussion.get(thread_id) @thread = @discussion.get(thread_id)
@thread.set("unread_comments_count", 0)
@thread.set("read", true)
@setActiveThread() @setActiveThread()
if(@main) if(@main)
@main.cleanup() @main.cleanup()
...@@ -43,3 +48,10 @@ if Backbone? ...@@ -43,3 +48,10 @@ if Backbone?
navigateToAllThreads: => navigateToAllThreads: =>
@navigate("", trigger: true) @navigate("", trigger: true)
showNewPost: (event) =>
@newPost.slideDown(300)
$('.new-post-title').focus()
hideNewPost: (event) =>
@newPost.slideUp(300)
$ ->
new TooltipManager
class @TooltipManager
constructor: () ->
@$body = $('body')
@$tooltip = $('<div class="tooltip"></div>')
@$body.delegate '[data-tooltip]',
'mouseover': @showTooltip,
'mousemove': @moveTooltip,
'mouseout': @hideTooltip,
'click': @hideTooltip
showTooltip: (e) =>
tooltipText = $(e.target).attr('data-tooltip')
@$tooltip.html(tooltipText)
@$body.append(@$tooltip)
$(e.target).children().css('pointer-events', 'none')
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
y: e.pageY - (@$tooltip.outerHeight() + 15)
@$tooltip.css
'left': tooltipCoords.x,
'top': tooltipCoords.y
@tooltipTimer = setTimeout ()=>
@$tooltip.show().css('opacity', 1)
@tooltipTimer = setTimeout ()=>
@hideTooltip()
, 3000
, 500
moveTooltip: (e) =>
tooltipCoords =
x: e.pageX - (@$tooltip.outerWidth() / 2)
y: e.pageY - (@$tooltip.outerHeight() + 15)
@$tooltip.css
'left': tooltipCoords.x
'top': tooltipCoords.y
hideTooltip: (e) =>
@$tooltip.hide().css('opacity', 0)
clearTimeout(@tooltipTimer)
...@@ -9,6 +9,7 @@ if Backbone? ...@@ -9,6 +9,7 @@ if Backbone?
"click .browse-topic-drop-search-input": "ignoreClick" "click .browse-topic-drop-search-input": "ignoreClick"
"click .post-list .list-item a": "threadSelected" "click .post-list .list-item a": "threadSelected"
"click .post-list .more-pages a": "loadMorePages" "click .post-list .more-pages a": "loadMorePages"
'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop
initialize: -> initialize: ->
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages) @displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
...@@ -129,6 +130,8 @@ if Backbone? ...@@ -129,6 +130,8 @@ if Backbone?
content.addClass("followed") content.addClass("followed")
if thread.get('endorsed') if thread.get('endorsed')
content.addClass("resolved") content.addClass("resolved")
if thread.get('read')
content.addClass("read")
@highlight(content) @highlight(content)
...@@ -244,7 +247,8 @@ if Backbone? ...@@ -244,7 +247,8 @@ if Backbone?
item = $(event.target).closest('li') item = $(event.target).closest('li')
if item.find("span.board-name").data("discussion_id") == "#all" if item.find("span.board-name").data("discussion_id") == "#all"
@discussionIds = "" @discussionIds = ""
@clearSearch() @$(".post-search-field").val("")
@retrieveAllThreads()
else else
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
@retrieveDiscussions(discussionIds) @retrieveDiscussions(discussionIds)
...@@ -277,6 +281,18 @@ if Backbone? ...@@ -277,6 +281,18 @@ if Backbone?
Content.loadContentInfos(response.content_info) Content.loadContentInfos(response.content_info)
@displayedCollection.reset(@collection.models) @displayedCollection.reset(@collection.models)
retrieveAllThreads: () ->
url = DiscussionUtil.urlFor("threads")
DiscussionUtil.safeAjax
url: url
type: "GET"
success: (response, textStatus) =>
@collection.current_page = response.page
@collection.pages = response.num_pages
@collection.reset(response.discussion_data)
Content.loadContentInfos(response.content_info)
@displayedCollection.reset(@collection.models)
sortThreads: (event) -> sortThreads: (event) ->
@$(".sort-bar a").removeClass("active") @$(".sort-bar a").removeClass("active")
$(event.target).addClass("active") $(event.target).addClass("active")
......
...@@ -21,6 +21,7 @@ if Backbone? ...@@ -21,6 +21,7 @@ if Backbone?
"click .topic_dropdown_button": "toggleTopicDropdown" "click .topic_dropdown_button": "toggleTopicDropdown"
"click .topic_menu_wrapper": "setTopic" "click .topic_menu_wrapper": "setTopic"
"click .topic_menu_search": "ignoreClick" "click .topic_menu_search": "ignoreClick"
"keyup .form-topic-drop-search-input": DiscussionFilter.filterDrop
# Because we want the behavior that when the body is clicked the menu is # Because we want the behavior that when the body is clicked the menu is
# closed, we need to ignore clicks in the search field and stop propagation. # closed, we need to ignore clicks in the search field and stop propagation.
...@@ -120,9 +121,6 @@ if Backbone? ...@@ -120,9 +121,6 @@ if Backbone?
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked") anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
follow = false || @$("input.discussion-follow").is(":checked") follow = false || @$("input.discussion-follow").is(":checked")
$formTopicDropBtn.bind('click', showFormTopicDrop)
$formTopicDropMenu.bind('click', setFormTopic)
url = DiscussionUtil.urlFor('create_thread', @topicId) url = DiscussionUtil.urlFor('create_thread', @topicId)
DiscussionUtil.safeAjax DiscussionUtil.safeAjax
......
var $body;
var $browse;
var $search;
var $searchField;
var $topicDrop;
var $currentBoard;
var $tooltip;
var $newPost;
var $thread;
var $sidebar;
var $sidebarWidthStyles;
var $formTopicDropBtn;
var $formTopicDropMenu;
var $postListWrapper;
var $dropFilter;
var $topicFilter;
var $discussionBody;
var sidebarWidth;
var sidebarHeight;
var sidebarHeaderHeight;
var sidebarXOffset;
var scrollTop;
var discussionsBodyTop;
var discussionsBodyBottom;
var tooltipTimer;
var tooltipCoords;
var SIDEBAR_PADDING = 10;
var SIDEBAR_HEADER_HEIGHT = 87;
$(document).ready(function() {
$body = $('body');
//$browse = $('.browse-search .browse');
//$search = $('.browse-search .search');
$searchField = $('.post-search-field');
//$topicDrop = $('.browse-topic-drop-menu-wrapper');
$currentBoard = $('.current-board');
$tooltip = $('<div class="tooltip"></div>');
$newPost = $('.new-post-article');
$sidebar = $('.sidebar');
$discussionBody = $('.discussion-body');
$postListWrapper = $('.post-list-wrapper');
$formTopicDropBtn = $('.new-post-article .form-topic-drop-btn');
$formTopicDropMenu = $('.new-post-article .form-topic-drop-menu-wrapper');
// $dropFilter = $('.browse-topic-drop-search-input');
// $topicFilter = $('.topic-drop-search-input');
$sidebarWidthStyles = $('<style></style>');
$body.append($sidebarWidthStyles);
sidebarWidth = $('.sidebar').width();
sidebarXOffset = $sidebar.offset().top;
//$browse.bind('click', showTopicDrop);
//$search.bind('click', showSearch);
// $topicDrop.bind('click', setTopic);
$formTopicDropBtn.bind('click', showFormTopicDrop);
$formTopicDropMenu.bind('click', setFormTopic);
$('.new-post-btn').bind('click', newPost);
$('.new-post-cancel').bind('click', closeNewPost);
$body.delegate('[data-tooltip]', {
'mouseover': showTooltip,
'mousemove': moveTooltip,
'mouseout': hideTooltip,
'click': hideTooltip
});
$body.delegate('.browse-topic-drop-search-input, .form-topic-drop-search-input', 'keyup', filterDrop);
});
function filterDrop(e) {
/*
* multiple queries
*/
// var $drop = $(e.target).parents('.form-topic-drop-menu-wrapper, .browse-topic-drop-menu-wrapper');
// var queries = $(this).val().split(' ');
// var $items = $drop.find('a');
// if(queries.length == 0) {
// $items.show();
// return;
// }
// $items.hide();
// $items.each(function(i) {
// var thisText = $(this).children().not('.unread').text();
// $(this).parents('ul').siblings('a').not('.unread').each(function(i) {
// thisText = thisText + ' ' + $(this).text();
// });
// var test = true;
// var terms = thisText.split(' ');
// for(var i = 0; i < queries.length; i++) {
// if(thisText.toLowerCase().search(queries[i].toLowerCase()) == -1) {
// test = false;
// }
// }
// if(test) {
// $(this).show();
// // show children
// $(this).parent().find('a').show();
// // show parents
// $(this).parents('ul').siblings('a').show();
// }
// });
/*
* single query
*/
var $drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper');
var query = $(this).val();
var $items = $drop.find('a');
if(query.length == 0) {
$items.removeClass('hidden');
return;
}
$items.addClass('hidden');
$items.each(function(i) {
var thisText = $(this).not('.unread').text();
$(this).parents('ul').siblings('a').not('.unread').each(function(i) {
thisText = thisText + ' ' + $(this).text();
});
var test = true;
var terms = thisText.split(' ');
if(thisText.toLowerCase().search(query.toLowerCase()) == -1) {
test = false;
}
if(test) {
$(this).removeClass('hidden');
// show children
$(this).parent().find('a').removeClass('hidden');
// show parents
$(this).parents('ul').siblings('a').removeClass('hidden');
}
});
}
function showTooltip(e) {
var tooltipText = $(this).attr('data-tooltip');
$tooltip.html(tooltipText);
$body.append($tooltip);
$(this).children().css('pointer-events', 'none');
tooltipCoords = {
x: e.pageX - ($tooltip.outerWidth() / 2),
y: e.pageY - ($tooltip.outerHeight() + 15)
};
$tooltip.css({
'left': tooltipCoords.x,
'top': tooltipCoords.y
});
tooltipTimer = setTimeout(function() {
$tooltip.show().css('opacity', 1);
tooltipTimer = setTimeout(function() {
hideTooltip();
}, 3000);
}, 500);
}
function moveTooltip(e) {
tooltipCoords = {
x: e.pageX - ($tooltip.outerWidth() / 2),
y: e.pageY - ($tooltip.outerHeight() + 15)
};
$tooltip.css({
'left': tooltipCoords.x,
'top': tooltipCoords.y
});
}
function hideTooltip(e) {
$tooltip.hide().css('opacity', 0);
clearTimeout(tooltipTimer);
}
function showBrowse(e) {
$browse.addClass('is-open');
$search.removeClass('is-open');
$searchField.val('');
}
function showSearch(e) {
$search.addClass('is-open');
$browse.removeClass('is-open');
setTimeout(function() {
$searchField.focus();
}, 200);
}
function showTopicDrop(e) {
e.preventDefault();
$browse.addClass('is-dropped');
if(!$topicDrop[0]) {
$topicDrop = $('.browse-topic-drop-menu-wrapper');
}
$topicDrop.show();
$browse.unbind('click', showTopicDrop);
$body.bind('keyup', setActiveDropItem);
$browse.bind('click', hideTopicDrop);
setTimeout(function() {
$body.bind('click', hideTopicDrop);
}, 0);
}
function hideTopicDrop(e) {
if(e.target == $('.browse-topic-drop-search-input')[0]) {
return;
}
$browse.removeClass('is-dropped');
$topicDrop.hide();
$body.unbind('click', hideTopicDrop);
$browse.bind('click', showTopicDrop);
}
function setTopic(e) {
if(e.target == $('.browse-topic-drop-search-input')[0]) {
return;
}
var $item = $(e.target).closest('a');
var boardName = $item.find('.board-name').html();
$item.parents('ul').not('.browse-topic-drop-menu').each(function(i) {
boardName = $(this).siblings('a').find('.board-name').html() + ' / ' + boardName;
});
if(!$currentBoard[0]) {
$currentBoard = $('.current-board');
}
$currentBoard.html(boardName);
var fontSize = 16;
$currentBoard.css('font-size', '16px');
while($currentBoard.width() > (sidebarWidth * .8) - 40) {
fontSize--;
if(fontSize < 11) {
break;
}
$currentBoard.css('font-size', fontSize + 'px');
}
showBrowse();
}
function newPost(e) {
$newPost.slideDown(300);
$('.new-post-title').focus();
}
function closeNewPost(e) {
$newPost.slideUp(300);
}
function showFormTopicDrop(e) {
$formTopicDropBtn.addClass('is-dropped');
$formTopicDropMenu.show();
$formTopicDropBtn.unbind('click', showFormTopicDrop);
$formTopicDropBtn.bind('click', hideFormTopicDrop);
setTimeout(function() {
$body.bind('click', hideFormTopicDrop);
}, 0);
}
function hideFormTopicDrop(e) {
if(e.target == $('.topic-drop-search-input')[0]) {
return;
}
$formTopicDropBtn.removeClass('is-dropped');
$formTopicDropMenu.hide();
$body.unbind('click', hideFormTopicDrop);
$formTopicDropBtn.unbind('click', hideFormTopicDrop);
$formTopicDropBtn.bind('click', showFormTopicDrop);
}
function setFormTopic(e) {
if(e.target == $('.topic-drop-search-input')[0]) {
return;
}
$formTopicDropBtn.removeClass('is-dropped');
hideFormTopicDrop(e);
var $item = $(e.target);
var boardName = $item.html();
$item.parents('ul').not('.form-topic-drop-menu').each(function(i) {
boardName = $(this).siblings('a').html() + ' / ' + boardName;
});
$formTopicDropBtn.html(boardName + ' <span class="drop-arrow">▾</span>');
}
function updateSidebar(e) {
// determine page scroll attributes
scrollTop = $(window).scrollTop();
discussionsBodyTop = $discussionBody.offset().top;
discussionsBodyBottom = discussionsBodyTop + $discussionBody.height();
var windowHeight = $(window).height();
// toggle fixed positioning
if(scrollTop > discussionsBodyTop - SIDEBAR_PADDING) {
$sidebar.addClass('fixed');
$sidebar.css('top', SIDEBAR_PADDING + 'px');
} else {
$sidebar.removeClass('fixed');
$sidebar.css('top', '0');
}
// set sidebar width
var sidebarWidth = .32 * $discussionBody.width() - 10;
$sidebar.css('width', sidebarWidth + 'px');
// show the entire sidebar at all times
var sidebarHeight = windowHeight - (scrollTop < discussionsBodyTop - SIDEBAR_PADDING ? discussionsBodyTop - scrollTop : SIDEBAR_PADDING) - SIDEBAR_PADDING - (scrollTop + windowHeight > discussionsBodyBottom + SIDEBAR_PADDING ? scrollTop + windowHeight - discussionsBodyBottom - SIDEBAR_PADDING : 0);
$sidebar.css('height', sidebarHeight > 400 ? sidebarHeight : 400 + 'px');
// update the list height
if(!$postListWrapper[0]) {
$postListWrapper = $('.post-list-wrapper');
}
$postListWrapper.css('height', (sidebarHeight - SIDEBAR_HEADER_HEIGHT - 4) + 'px');
// update title wrappers
var titleWidth = sidebarWidth - 115;
$sidebarWidthStyles.html('.discussion-body .post-list a .title { width: ' + titleWidth + 'px !important; }');
}
...@@ -79,7 +79,7 @@ jQuery.extend({ ...@@ -79,7 +79,7 @@ jQuery.extend({
try { try {
if(io.contentWindow){ if(io.contentWindow){
xml.responseText = io.contentWindow.document.body ? xml.responseText = io.contentWindow.document.body ?
io.contentWindow.document.body.innerText : null; io.contentWindow.document.body.textContent || io.contentWindow.document.body.innerText : null;
xml.responseXML = io.contentWindow.document.XMLDocument ? xml.responseXML = io.contentWindow.document.XMLDocument ?
io.contentWindow.document.XMLDocument : io.contentWindow.document; io.contentWindow.document.XMLDocument : io.contentWindow.document;
......
...@@ -940,7 +940,7 @@ body.discussion { ...@@ -940,7 +940,7 @@ body.discussion {
display: block; display: block;
width: 100%; width: 100%;
height: 30px; height: 30px;
padding: 0; padding: 0 0 0 30px;
margin: 14px auto; margin: 14px auto;
@include box-sizing(border-box); @include box-sizing(border-box);
border: 1px solid #acacac; border: 1px solid #acacac;
...@@ -951,7 +951,6 @@ body.discussion { ...@@ -951,7 +951,6 @@ body.discussion {
font-weight: 400; font-weight: 400;
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
text-indent: 30px;
color: #333; color: #333;
outline: 0; outline: 0;
cursor: pointer; cursor: pointer;
...@@ -1019,6 +1018,7 @@ body.discussion { ...@@ -1019,6 +1018,7 @@ body.discussion {
text-shadow: 0 -1px 0 rgba(0, 0, 0, .3); text-shadow: 0 -1px 0 rgba(0, 0, 0, .3);
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 1px rgba(0, 0, 0, .2) inset; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 1px rgba(0, 0, 0, .2) inset;
} }
} }
} }
...@@ -1053,14 +1053,17 @@ body.discussion { ...@@ -1053,14 +1053,17 @@ body.discussion {
} }
a { a {
position: relative;
display: block; display: block;
height: 36px; position: relative;
float: left;
clear: both;
width: 100%;
padding: 0 10px 0 18px; padding: 0 10px 0 18px;
margin-bottom: 1px; margin-bottom: 1px;
margin-right: -1px; margin-right: -1px;
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
background-color: #fff; background-color: #fff;
@include clearfix;
&:hover { &:hover {
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
...@@ -1097,16 +1100,24 @@ body.discussion { ...@@ -1097,16 +1100,24 @@ body.discussion {
} }
.title { .title {
display: block;
float: left;
width: 70%;
margin: 8px 0 10px;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 700;
line-height: 34px; line-height: 1.4;
color: #333; color: #333;
} }
&.read .title { &.read {
background: #f2f2f2;
.title {
font-weight: 400; font-weight: 400;
color: #737373; color: #737373;
} }
}
&.resolved:before { &.resolved:before {
content: ''; content: '';
...@@ -1122,7 +1133,7 @@ body.discussion { ...@@ -1122,7 +1133,7 @@ body.discussion {
content: ''; content: '';
position: absolute; position: absolute;
top: 0; top: 0;
right: 1px; right: 0;
width: 10px; width: 10px;
height: 12px; height: 12px;
background: url(../images/following-flag.png) no-repeat; background: url(../images/following-flag.png) no-repeat;
...@@ -1165,22 +1176,13 @@ body.discussion { ...@@ -1165,22 +1176,13 @@ body.discussion {
} }
} }
.title {
display: block;
float: left;
width: 70%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.votes-count, .votes-count,
.comments-count { .comments-count {
display: block; display: block;
float: right; float: right;
width: 32px; width: 32px;
height: 16px; height: 16px;
margin-top: 9px; margin-top: 8px;
border-radius: 2px; border-radius: 2px;
@include linear-gradient(top, #d4d4d4, #dfdfdf); @include linear-gradient(top, #d4d4d4, #dfdfdf);
font-size: 11px; font-size: 11px;
...@@ -1207,12 +1209,13 @@ body.discussion { ...@@ -1207,12 +1209,13 @@ body.discussion {
background-position: 0 -5px; background-position: 0 -5px;
} }
&.new { &.unread {
@include linear-gradient(top, #84d7fe, #99e0fe); @include linear-gradient(top, #84d7fe, #60a8d6);
color: #333; color: #333;
&:after { &:after {
color: #99e0fe; color: #99e0fe;
background-position: 0 0px;
} }
} }
} }
...@@ -1260,6 +1263,10 @@ body.discussion { ...@@ -1260,6 +1263,10 @@ body.discussion {
padding: 40px; padding: 40px;
min-height: 468px; min-height: 468px;
a {
word-wrap: break-word;
}
h1 { h1 {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 28px; font-size: 28px;
...@@ -1317,6 +1324,7 @@ body.discussion { ...@@ -1317,6 +1324,7 @@ body.discussion {
position: relative; position: relative;
z-index: 100; z-index: 100;
margin-top: 5px; margin-top: 5px;
margin-left: 40px;
} }
.post-tools { .post-tools {
...@@ -1472,17 +1480,31 @@ body.discussion { ...@@ -1472,17 +1480,31 @@ body.discussion {
} }
} }
blockquote {
background: #f6f6f6;
border-radius: 3px;
padding: 5px 10px;
font-size: 14px;
}
.comments { .comments {
list-style: none; list-style: none;
margin-top: 20px; margin-top: 20px;
padding: 0; padding: 0;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
li { > li {
background: #f6f6f6; background: #f6f6f6;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
blockquote {
background: #e6e6e6;
border-radius: 3px;
padding: 5px 10px;
font-size: 14px;
}
.comment-form { .comment-form {
background: #eee; background: #eee;
@include clearfix; @include clearfix;
...@@ -1506,7 +1528,6 @@ body.discussion { ...@@ -1506,7 +1528,6 @@ body.discussion {
.discussion-errors { .discussion-errors {
margin: 0px; margin: 0px;
} }
} }
.response-body { .response-body {
......
...@@ -6,54 +6,21 @@ if active_page == None and active_page_context is not UNDEFINED: ...@@ -6,54 +6,21 @@ if active_page == None and active_page_context is not UNDEFINED:
# If active_page is not passed in as an argument, it may be in the context as active_page_context # If active_page is not passed in as an argument, it may be in the context as active_page_context
active_page = active_page_context active_page = active_page_context
def url_class(url): def url_class(is_active):
if url == active_page: if is_active:
return "active" return "active"
return "" return ""
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from courseware.tabs import get_course_tabs %>
<%! from courseware.access import has_access %>
<nav class="${active_page} course-material"> <nav class="${active_page} course-material">
<div class="inner-wrapper"> <div class="inner-wrapper">
<ol class="course-tabs"> <ol class="course-tabs">
<li class="courseware"><a href="${reverse('courseware', args=[course.id])}" class="${url_class('courseware')}">Courseware</a></li> % for tab in get_course_tabs(user, course, active_page):
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li> <li>
% if hasattr(course,'syllabus_present') and course.syllabus_present: <a href="${tab.link | h}" class="${url_class(tab.is_active)}">${tab.name | h}</a>
<li class="syllabus"><a href="${reverse('syllabus', args=[course.id])}" class="${url_class('syllabus')}">Syllabus</a></li> </li>
% endif
% if user.is_authenticated():
% if settings.MITX_FEATURES.get('ENABLE_TEXTBOOK'):
% for index, textbook in enumerate(course.textbooks):
<li class="book"><a href="${reverse('book', args=[course.id, index])}" class="${url_class('book')}">${textbook.title}</a></li>
% endfor % endfor
% endif
## If they have a discussion link specified, use that even if we feature
## flag discussions off. Disabling that is mostly a server safety feature
## at this point, and we don't need to worry about external sites.
% if course.discussion_link:
<li class="discussion"><a href="${course.discussion_link}">Discussion</a></li>
% elif settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
<li class="discussion"><a href="${reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id])}" class="${url_class('discussion')}">Discussion</a></li>
## <li class="news"><a href="${reverse('news', args=[course.id])}" class="${url_class('news')}">News</a></li>
% endif
## This is Askbot, which we should be retiring soon...
% 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('course_wiki', args=[course.id])}" class="${url_class('wiki')}">Wiki</a></li>
% endif
% if user.is_authenticated() and not course.hide_progress_tab:
<li class="profile"><a href="${reverse('progress', args=[course.id])}" class="${url_class('progress')}">Progress</a></li>
% endif
% if staff_access:
<li class="instructor"><a href="${reverse('instructor_dashboard', args=[course.id])}" class="${url_class('instructor')}">Instructor</a></li>
% endif
<%block name="extratabs" /> <%block name="extratabs" />
</ol> </ol>
</div> </div>
......
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="headextra">
<%static:css group='course'/>
</%block>
<%block name="title"><title>${course.number} ${tab['name']}</title></%block>
<%include file="/courseware/course_navigation.html" args="active_page='static_tab_{0}'.format(tab['url_slug'])" />
<section class="container">
<div class="static_tab_wrapper">
${tab_contents}
</div>
</section>
...@@ -128,5 +128,13 @@ ...@@ -128,5 +128,13 @@
</script> </script>
<script type="text/template" id="thread-list-item-template"> <script type="text/template" id="thread-list-item-template">
<a href="${'<%- id %>'}" data-id="${'<%- id %>'}"><span class="title">${"<%- title %>"}</span> <span class="comments-count">${"<%- comments_count %>"}</span><span class="votes-count">+${"<%- votes['up_count'] %>"}</span></a> <a href="${'<%- id %>'}" data-id="${'<%- id %>'}">
<span class="title">${"<%- title %>"}</span>
${"<% if (unread_comments_count > 0) { %>"}
<span class="comments-count unread" data-tooltip="${"<%- unread_comments_count %>"} new comment${"<%- unread_comments_count > 1 ? 's' : '' %>"}">${"<%- comments_count %>"}</span>
${"<% } else { %>"}
<span class="comments-count">${"<%- comments_count %>"}</span>
${"<% } %>"}
<span class="votes-count">+${"<%- votes['up_count'] %>"}</span>
</a>
</script> </script>
...@@ -21,8 +21,6 @@ ...@@ -21,8 +21,6 @@
<%include file="_new_post.html" /> <%include file="_new_post.html" />
<script type="text/javascript" src="${static.url('js/discussions-temp.js')}"></script>
<section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-thread-pages="${thread_pages}"> <section class="discussion container" id="discussion-container" data-roles="${roles}" data-course-id="${course_id}" data-user-info="${user_info}" data-threads="${threads}" data-thread-pages="${thread_pages}">
<div class="discussion-body"> <div class="discussion-body">
<div class="sidebar"></div> <div class="sidebar"></div>
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
<%block name="js_extra"> <%block name="js_extra">
<%include file="_js_body_dependencies.html" /> <%include file="_js_body_dependencies.html" />
<%static:js group='discussion'/> <%static:js group='discussion'/>
<script type="text/javascript" src="${static.url('js/discussions-temp.js')}"></script>
</%block> </%block>
......
...@@ -147,28 +147,10 @@ ...@@ -147,28 +147,10 @@
% if show_signup_immediately is not UNDEFINED: % if show_signup_immediately is not UNDEFINED:
<script type="text/javascript"> <script type="text/javascript">
function dosignup(){ $(window).load(function() {$('#signup_action').trigger("click");});
comp = document.getElementById('signup_action'); </script>
try { //in firefox % elif show_login_immediately is not UNDEFINED:
comp.click(); <script type="text/javascript">
return; $(window).load(function() {$('#login').trigger("click");});
} 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> </script>
% endif % endif
\ No newline at end of file
...@@ -46,7 +46,11 @@ ...@@ -46,7 +46,11 @@
(function() { (function() {
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) { $(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) {
if(json.success) { if(json.success) {
% if request.REQUEST.get('next', False):
location.href="${request.REQUEST.get('next')}";
% else:
location.href="${reverse('dashboard')}"; location.href="${reverse('dashboard')}";
% endif
} else { } else {
if($('#login_error').length == 0) { if($('#login_error').length == 0) {
$('#login_form').prepend('<div id="login_error" class="modal-form-error"></div>'); $('#login_form').prepend('<div id="login_error" class="modal-form-error"></div>');
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<input type="hidden" name="problem_id" value="${ problem['name'] }"> <input type="hidden" name="problem_id" value="${ problem['name'] }">
% if check_button: % if check_button:
<input class="check" type="button" value="${ check_button }"> <input class="check ${ check_button }" type="button" value="${ check_button }">
% endif % endif
% if reset_button: % if reset_button:
<input class="reset" type="button" value="Reset"> <input class="reset" type="button" value="Reset">
......
<%inherit file="main.html" /> <%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%block name="title"><title>Textbook – MITx 6.002x</title></%block> <%block name="title"><title>${course.number} Textbook</title></%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='course'/> <%static:css group='course'/>
...@@ -38,14 +38,14 @@ function goto_page(n) { ...@@ -38,14 +38,14 @@ function goto_page(n) {
function prev_page() { function prev_page() {
var newpage=page-1; var newpage=page-1;
if(newpage<0) newpage=0; if(newpage< ${start_page}) newpage=${start_page};
goto_page(newpage); goto_page(newpage);
log_event("book", {"type":"prevpage","new":page}); log_event("book", {"type":"prevpage","new":page});
} }
function next_page() { function next_page() {
var newpage=page+1; var newpage=page+1;
if(newpage>1008) newpage=1008; if(newpage> ${end_page}) newpage=${end_page};
goto_page(newpage); goto_page(newpage);
log_event("book", {"type":"nextpage","new":page}); log_event("book", {"type":"nextpage","new":page});
} }
...@@ -61,7 +61,7 @@ $("#open_close_accordion a").click(function(){ ...@@ -61,7 +61,7 @@ $("#open_close_accordion a").click(function(){
</script> </script>
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='book'" /> <%include file="/courseware/course_navigation.html" args="active_page='textbook/{0}'.format(book_index)" />
<section class="container"> <section class="container">
<div class="book-wrapper"> <div class="book-wrapper">
...@@ -97,6 +97,10 @@ $("#open_close_accordion a").click(function(){ ...@@ -97,6 +97,10 @@ $("#open_close_accordion a").click(function(){
% for entry in table_of_contents: % for entry in table_of_contents:
${print_entry(entry)} ${print_entry(entry)}
% endfor % endfor
## Don't delete this empty list item. Without it, Jquery.TreeView won't
## render the last list item as expandable.
<li></li>
</ul> </ul>
</section> </section>
......
...@@ -97,6 +97,33 @@ urlpatterns = ('', ...@@ -97,6 +97,33 @@ urlpatterns = ('',
if settings.PERFSTATS: if settings.PERFSTATS:
urlpatterns += (url(r'^reprofile$','perfstats.views.end_profile'),) urlpatterns += (url(r'^reprofile$','perfstats.views.end_profile'),)
# Multicourse wiki (Note: wiki urls must be above the courseware ones because of
# the custom tab catch-all)
if settings.WIKI_ENABLED:
from wiki.urls import get_pattern as wiki_pattern
from django_notify.urls import get_pattern as notify_pattern
# Note that some of these urls are repeated in course_wiki.course_nav. Make sure to update
# them together.
urlpatterns += (
# First we include views from course_wiki that we use to override the default views.
# They come first in the urlpatterns so they get resolved first
url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'),
url(r'^wiki/', include(wiki_pattern())),
url(r'^notify/', include(notify_pattern())),
# These urls are for viewing the wiki in the context of a course. They should
# never be returned by a reverse() so they come after the other url patterns
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/course_wiki/?$',
'course_wiki.views.course_wiki_redirect', name="course_wiki"),
url(r'^courses/(?:[^/]+/[^/]+/[^/]+)/wiki/', include(wiki_pattern())),
)
if settings.COURSEWARE_ENABLED: if settings.COURSEWARE_ENABLED:
urlpatterns += ( urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users # Hook django-masquerade, allowing staff to view site as other users
...@@ -164,6 +191,10 @@ if settings.COURSEWARE_ENABLED: ...@@ -164,6 +191,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.grade_summary', name='grade_summary'), 'instructor.views.grade_summary', name='grade_summary'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/enroll_students$',
'instructor.views.enroll_students', name='enroll_students'), 'instructor.views.enroll_students', name='enroll_students'),
# This MUST be the last view in the courseware--it's a catch-all for custom tabs.
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/(?P<tab_slug>.*)$',
'courseware.views.static_tab', name="static_tab"),
) )
# discussion forums live within courseware, so courseware must be enabled first # discussion forums live within courseware, so courseware must be enabled first
...@@ -176,29 +207,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -176,29 +207,6 @@ if settings.COURSEWARE_ENABLED:
include('django_comment_client.urls')) include('django_comment_client.urls'))
) )
# Multicourse wiki
if settings.WIKI_ENABLED:
from wiki.urls import get_pattern as wiki_pattern
from django_notify.urls import get_pattern as notify_pattern
# Note that some of these urls are repeated in course_wiki.course_nav. Make sure to update
# them together.
urlpatterns += (
# First we include views from course_wiki that we use to override the default views.
# They come first in the urlpatterns so they get resolved first
url('^wiki/create-root/$', 'course_wiki.views.root_create', name='root_create'),
url(r'^wiki/', include(wiki_pattern())),
url(r'^notify/', include(notify_pattern())),
# These urls are for viewing the wiki in the context of a course. They should
# never be returned by a reverse() so they come after the other url patterns
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/course_wiki/?$',
'course_wiki.views.course_wiki_redirect', name="course_wiki"),
url(r'^courses/(?:[^/]+/[^/]+/[^/]+)/wiki/', include(wiki_pattern())),
)
if settings.QUICKEDIT: if settings.QUICKEDIT:
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),) urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),) urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),)
......
...@@ -125,8 +125,8 @@ TEST_TASKS = [] ...@@ -125,8 +125,8 @@ TEST_TASKS = []
end end
# Per environment tasks # Per environment tasks
Dir["#{system}/envs/*.py"].each do |env_file| Dir["#{system}/envs/**/*.py"].each do |env_file|
env = File.basename(env_file).gsub(/\.py/, '') env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors" desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors"
task "#{system}:check_settings:#{env}" => :predjango do task "#{system}:check_settings:#{env}" => :predjango do
sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}") sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment