Commit 939b927f by Ibrahim Awwal

Merge branch 'master' into feature/ibrahim/followed-threads

Conflicts:
	lms/djangoapps/django_comment_client/forum/views.py
parents e809fb3b 40d25acf
......@@ -207,7 +207,7 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
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?
track_function=lambda type, event: None,
filestore=descriptor.system.resources_fs,
......@@ -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.get_html = replace_static_urls(
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(),
module.get_instance_state(), module.get_shared_state())
......@@ -276,8 +276,13 @@ def save_item(request):
if not has_access(request.user, item_location):
raise Http404 # TODO (vshnayder): better error
data = json.loads(request.POST['data'])
modulestore().update_item(item_location, data)
if request.POST['data']:
data = request.POST['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
# This uses wildcarding to find the course, which requires handling
......
......@@ -86,7 +86,12 @@ def export_to_github(course, commit_message, author_str=None):
If author_str is specified, uses it in the commit.
'''
course_dir = course.metadata.get('data_dir', course.location.course)
repo_settings = load_repo_settings(course_dir)
try:
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)
fs = OSFS(git_repo.working_dir)
......
......@@ -2,6 +2,7 @@ class CMS.Models.Module extends Backbone.Model
url: '/save_item'
defaults:
data: ''
children: ''
loadModule: (element) ->
elt = $(element).find('.xmodule_edit').first()
......@@ -11,5 +12,5 @@ class CMS.Models.Module extends Backbone.Model
"/edit_item?#{$.param(id: @get('id'))}"
save: (args...) ->
@set(data: JSON.stringify(@module.save())) if @module
@set(data: @module.save()) if @module
super(args...)
......@@ -13,18 +13,22 @@ class CMS.Views.ModuleEdit extends Backbone.View
# Load preview modules
XModule.loadModules('display')
@$children = @$el.find('#sortable')
@enableDrag()
enableDrag: ->
enableDrag: =>
# Enable dragging things in the #sortable div (if there is one)
if $("#sortable").length > 0
$("#sortable").sortable({
if @$children.length > 0
@$children.sortable(
placeholder: "ui-state-highlight"
})
$("#sortable").disableSelection();
save: (event) ->
update: (event, ui) =>
@model.set(children: @$children.find('.module-edit').map(
(idx, el) -> $(el).data('id')
).toArray())
)
@$children.disableSelection()
save: (event) =>
event.preventDefault()
@model.save().done((previews) =>
alert("Your changes have been saved.")
......
......@@ -141,11 +141,15 @@ textarea {
}
}
// .wip {
// outline: 1px solid #f00 !important;
// position: relative;
// }
.hidden {
display: none;
.wip {
outline: 1px solid #f00 !important;
position: relative;
&:after {
content: "WIP";
font-size: 8px;
padding: 2px;
background: #f00;
color: #fff;
@include position(absolute, 0px 0px 0 0);
}
}
......@@ -49,7 +49,7 @@
</section>
% endfor
</section>
<div class="actions wip">
<div class="actions">
<a href="" class="save-update">Save &amp; Update</a>
<a href="#" class="cancel">Cancel</a>
</div>
......
......@@ -60,7 +60,7 @@
data-type="${module.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>
</li>
% endfor
......@@ -68,31 +68,6 @@
</ul>
</li>
%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>
<section class="new-section">
......
......@@ -39,7 +39,7 @@
<a href="#" class="module-edit"
data-id="${child.location.url()}"
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>
</li>
%endfor
......@@ -48,61 +48,6 @@
</ol>
</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>
</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):
domain=domain)
context = {'universities': universities, 'entries': entries}
context.update(extra_context)
if request.REQUEST.get('next', False):
context['show_login_immediately'] = True
return render_to_response('index.html', context)
def course_from_id(course_id):
......
......@@ -35,7 +35,7 @@ def wrap_xmodule(get_html, module, template):
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
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):
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
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
the old get_html function and substitutes urls of the form /static/...
......
......@@ -1627,6 +1627,10 @@ class ImageResponse(LoncapaResponse):
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]'
if not given: # No answer to parse. Mark as incorrect and move on
correct_map.set(aid, 'incorrect')
continue
# parse expected answer
# TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', ''))
......
......@@ -179,6 +179,8 @@ class CapaModule(XModule):
return "per_student"
elif rerandomize == "never":
return "never"
elif rerandomize == "onreset":
return "onreset"
else:
raise Exception("Invalid rerandomize attribute " + rerandomize)
......@@ -307,7 +309,7 @@ class CapaModule(XModule):
save_button = False
# 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
# User hasn't submitted an answer yet -- we don't want resets
......@@ -617,7 +619,7 @@ class CapaModule(XModule):
return "Refresh the page and make an attempt before resetting."
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()
# in next line)
self.lcp.seed = None
......
from fs.errors import ResourceNotFoundError
import time
import logging
import requests
from lxml import etree
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.graders import load_grading_policy
......@@ -21,10 +21,15 @@ class CourseDescriptor(SequenceDescriptor):
self.title = title
self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3()
@classmethod
def from_xml_object(cls, xml_object):
return cls(xml_object.get('title'), xml_object.get('book_url'))
self.start_page = int(self.table_of_contents[0].attrib['page'])
# The last page should be the last element in the table of contents,
# but it may be nested. So recurse all the way down the last element
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
def table_of_contents(self):
......@@ -57,10 +62,18 @@ class CourseDescriptor(SequenceDescriptor):
return table_of_contents
def __init__(self, system, definition=None, **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
......@@ -82,7 +95,6 @@ class CourseDescriptor(SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
def set_grading_policy(self, policy_str):
"""Parse the policy specified in policy_str, and save it"""
try:
......@@ -94,19 +106,11 @@ class CourseDescriptor(SequenceDescriptor):
# the error log.
self._grading_policy = {}
@classmethod
def definition_from_xml(cls, xml_object, system):
textbooks = []
for textbook in xml_object.findall("textbook"):
try:
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)
textbooks.append((textbook.get('title'), textbook.get('book_url')))
xml_object.remove(textbook)
#Load the wiki tag if it exists
......@@ -116,7 +120,7 @@ class CourseDescriptor(SequenceDescriptor):
wiki_slug = wiki_tag.attrib.get("slug", default=None)
xml_object.remove(wiki_tag)
definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
definition = super(CourseDescriptor, cls).definition_from_xml(xml_object, system)
definition.setdefault('data', {})['textbooks'] = textbooks
definition['data']['wiki_slug'] = wiki_slug
......@@ -135,6 +139,13 @@ class CourseDescriptor(SequenceDescriptor):
return self._grading_policy['GRADE_CUTOFFS']
@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):
return self.metadata.get("show_calculator", None) == "Yes"
......
......@@ -53,6 +53,18 @@ section.problem {
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;
}
}
......
......@@ -316,3 +316,9 @@ class MongoModuleStore(ModuleStoreBase):
{'_id': True})
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):
policy_str = self.read_grading_policy(paths, tracker)
course_descriptor.set_grading_policy(policy_str)
log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor
......
......@@ -21,7 +21,8 @@ class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence
'''
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')]}
js_module_name = "Sequence"
......
......@@ -105,7 +105,7 @@ NUMPY_VER="1.6.2"
SCIPY_VER="0.10.1"
BREW_FILE="$BASE/mitx/brew-formulas.txt"
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
error "This script should not be run using sudo or as the root user"
......
......@@ -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.
* 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
TODO: This needs to be improved, but for now here's a sketch of how grading works:
......@@ -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.
* `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)
'onreset' (randomize question when reset button is pressed by the student)
'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)
Default: 'always'. Optional.
......@@ -340,7 +348,43 @@ If you look at some older xml, you may see some tags or metadata attributes that
# 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
......
......@@ -254,12 +254,11 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
module.get_html = replace_static_urls(
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
# 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 has_access(user, module, 'staff'):
......
......@@ -22,6 +22,7 @@ from django.views.decorators.cache import cache_control
from courseware import grades
from courseware.access import has_access
from courseware.courses import (get_course_with_access, get_courses_by_university)
import courseware.tabs as tabs
from models import StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module
from student.models import UserProfile
......@@ -343,6 +344,30 @@ def course_info(request, course_id):
return render_to_response('courseware/info.html', {'course': course,
'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
@ensure_csrf_cookie
def syllabus(request, course_id):
......@@ -357,6 +382,7 @@ def syllabus(request, course_id):
return render_to_response('courseware/syllabus.html', {'course': course,
'staff_access': staff_access,})
def registered_for_course(course, user):
'''Return CourseEnrollment if user is registered for course, else False'''
if user is None:
......@@ -404,6 +430,9 @@ def university_profile(request, org_id):
context = dict(courses=courses, org_id=org_id)
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)
def render_notifications(request, course, notifications):
......
......@@ -43,6 +43,7 @@ def get_threads(request, course_id, discussion_id=None, per_page=THREADS_PER_PAG
'tags': '',
'commentable_id': discussion_id,
'course_id': course_id,
'user_id': request.user.id,
}
if not request.GET.get('sort_key'):
......@@ -81,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
# sometimes things go wrong, I suspect that the js client is also not
# checking for errors on request. Check and fix as needed.
log.error("Error loading inline discussion threads.")
raise Http404
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
......@@ -111,6 +113,7 @@ def forum_form_discussion(request, course_id):
unsafethreads, query_params = get_threads(request, course_id) # This might process a search query
threads = [utils.safe_content(thread) for thread in unsafethreads]
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading forum discussion threads: %s" % str(err))
raise Http404
user_info = cc.User.from_django_user(request.user).to_dict()
......@@ -159,14 +162,18 @@ def forum_form_discussion(request, course_id):
@login_required
def single_thread(request, course_id, discussion_id, thread_id):
course = get_course_with_access(request.user, course_id, 'load')
cc_user = cc.User.from_django_user(request.user)
user_info = cc_user.to_dict()
try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.")
raise Http404
if request.is_ajax():
course = get_course_with_access(request.user, course_id, 'load')
user_info = cc.User.from_django_user(request.user).to_dict()
try:
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
raise Http404
courseware_context = get_courseware_context(thread, course)
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
......@@ -183,13 +190,13 @@ def single_thread(request, course_id, discussion_id, thread_id):
})
else:
course = get_course_with_access(request.user, course_id, 'load')
category_map = utils.get_discussion_category_map(course)
try:
threads, query_params = get_threads(request, course_id)
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
threads.append(thread.to_dict())
except (cc.utils.CommentClientError, cc.utils.CommentClientUnknownError) as err:
log.error("Error loading single thread.")
raise Http404
course = get_course_with_access(request.user, course_id, 'load')
......@@ -211,8 +218,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
# course_id,
#)
user_info = cc.User.from_django_user(request.user).to_dict()
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
context = {
......
......@@ -189,7 +189,7 @@ def initialize_discussion_info(course):
"sort_key": entry["sort_key"],
"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)
for topic, entry in discussion_topics.items():
category_map['entries'][topic] = {"id": entry["id"],
......@@ -351,7 +351,8 @@ def safe_content(content):
'endorsed', 'parent_id', 'thread_id', 'votes', 'closed', 'created_at',
'updated_at', 'depth', 'type', 'commentable_id', 'comments_count',
'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):
......
......@@ -85,7 +85,8 @@ def instructor_dashboard(request, course_id):
writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
writer.writerow(datatable['header'])
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
def get_staff_group(course):
......@@ -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.
'''
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']
if get_grades:
......
......@@ -7,16 +7,23 @@ from courseware.courses import get_course_with_access
from lxml import etree
@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')
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
if page is None:
page = textbook.start_page
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,
'start_page' : textbook.start_page,
'end_page' : textbook.end_page,
'staff_access': staff_access})
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',
}
}
}
......@@ -550,6 +550,8 @@ PIPELINE_JS = {
}
}
PIPELINE_DISABLE_WRAPPER = True
# 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
# course data directories are still in use.
......
......@@ -8,8 +8,9 @@ class Thread(models.Model):
accessible_fields = [
'id', 'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id',
'closed', 'tags', 'votes', 'commentable_id', 'username', 'user_id',
'created_at', 'updated_at', 'comments_count', 'at_position_list',
'children', 'type', 'highlighted_title', 'highlighted_body', 'endorsed'
'created_at', 'updated_at', 'comments_count', 'unread_comments_count',
'at_position_list', 'children', 'type', 'highlighted_title',
'highlighted_body', 'endorsed', 'read'
]
updatable_fields = [
......@@ -59,7 +60,21 @@ class Thread(models.Model):
else:
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):
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)
......@@ -27,6 +27,8 @@ if Backbone?
showThread: (forum_name, thread_id) ->
@thread = @discussion.get(thread_id)
@thread.set("unread_comments_count", 0)
@thread.set("read", true)
@setActiveThread()
if(@main)
@main.cleanup()
......
......@@ -140,6 +140,8 @@ if Backbone?
content.addClass("followed")
if thread.get('endorsed')
content.addClass("resolved")
if thread.get('read')
content.addClass("read")
@highlight(content)
......
......@@ -1018,6 +1018,7 @@ body.discussion {
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;
}
}
}
......@@ -1052,14 +1053,17 @@ body.discussion {
}
a {
position: relative;
display: block;
height: 36px;
position: relative;
float: left;
clear: both;
width: 100%;
padding: 0 10px 0 18px;
margin-bottom: 1px;
margin-right: -1px;
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
background-color: #fff;
@include clearfix;
&:hover {
@include linear-gradient(top, rgba(255, 255, 255, .7), rgba(255, 255, 255, 0));
......@@ -1096,15 +1100,23 @@ body.discussion {
}
.title {
display: block;
float: left;
width: 70%;
margin: 8px 0 10px;
font-size: 13px;
font-weight: 700;
line-height: 34px;
line-height: 1.4;
color: #333;
}
&.read .title {
font-weight: 400;
color: #737373;
&.read {
background: #f2f2f2;
.title {
font-weight: 400;
color: #737373;
}
}
&.resolved:before {
......@@ -1121,7 +1133,7 @@ body.discussion {
content: '';
position: absolute;
top: 0;
right: 1px;
right: 0;
width: 10px;
height: 12px;
background: url(../images/following-flag.png) no-repeat;
......@@ -1164,22 +1176,13 @@ body.discussion {
}
}
.title {
display: block;
float: left;
width: 70%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.votes-count,
.comments-count {
display: block;
float: right;
width: 32px;
height: 16px;
margin-top: 9px;
margin-top: 8px;
border-radius: 2px;
@include linear-gradient(top, #d4d4d4, #dfdfdf);
font-size: 11px;
......@@ -1206,12 +1209,13 @@ body.discussion {
background-position: 0 -5px;
}
&.new {
@include linear-gradient(top, #84d7fe, #99e0fe);
&.unread {
@include linear-gradient(top, #84d7fe, #60a8d6);
color: #333;
&:after {
color: #99e0fe;
background-position: 0 0px;
}
}
}
......@@ -1320,6 +1324,7 @@ body.discussion {
position: relative;
z-index: 100;
margin-top: 5px;
margin-left: 40px;
}
.post-tools {
......@@ -1475,17 +1480,31 @@ body.discussion {
}
}
blockquote {
background: #f6f6f6;
border-radius: 3px;
padding: 5px 10px;
font-size: 14px;
}
.comments {
list-style: none;
margin-top: 20px;
padding: 0;
border-top: 1px solid #ddd;
li {
> li {
background: #f6f6f6;
border-bottom: 1px solid #ddd;
}
blockquote {
background: #e6e6e6;
border-radius: 3px;
padding: 5px 10px;
font-size: 14px;
}
.comment-form {
background: #eee;
@include clearfix;
......@@ -1509,7 +1528,6 @@ body.discussion {
.discussion-errors {
margin: 0px;
}
}
.response-body {
......
......@@ -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
active_page = active_page_context
def url_class(url):
if url == active_page:
def url_class(is_active):
if is_active:
return "active"
return ""
%>
<%! from django.core.urlresolvers import reverse %>
<%! from courseware.access import has_access %>
<%! from courseware.tabs import get_course_tabs %>
<nav class="${active_page} course-material">
<div class="inner-wrapper">
<ol class="course-tabs">
<li class="courseware"><a href="${reverse('courseware', args=[course.id])}" class="${url_class('courseware')}">Courseware</a></li>
<li class="info"><a href="${reverse('info', args=[course.id])}" class="${url_class('info')}">Course Info</a></li>
% if hasattr(course,'syllabus_present') and course.syllabus_present:
<li class="syllabus"><a href="${reverse('syllabus', args=[course.id])}" class="${url_class('syllabus')}">Syllabus</a></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
% 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
% for tab in get_course_tabs(user, course, active_page):
<li>
<a href="${tab.link | h}" class="${url_class(tab.is_active)}">${tab.name | h}</a>
</li>
% endfor
<%block name="extratabs" />
</ol>
</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>
......@@ -119,7 +119,7 @@
<script type="text/template" id="response-comment-show-template">
<div id="comment_${'<%- id %>'}">
<div class="response-body">${'<%- body %>'}</div>
<p class="posted-details">&ndash;posted <span class="timeago" title="${'<%- created_at %>'}">${'<%- created_at %>'}</span> by
<p class="posted-details">&ndash;posted <span class="timeago" title="${'<%- created_at %>'}">${'<%- created_at %>'}</span> by
${"<% if (obj.username) { %>"}
<a href="${'<%- user_url %>'}" class="profile-link">${'<%- username %>'}</a>
${"<% } else {print('anonymous');} %>"}
......@@ -128,5 +128,13 @@
</script>
<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>
......@@ -147,28 +147,10 @@
% if show_signup_immediately is not UNDEFINED:
<script type="text/javascript">
function dosignup(){
comp = document.getElementById('signup_action');
try { //in firefox
comp.click();
return;
} catch(ex) {}
try { // in old chrome
if(document.createEvent) {
var e = document.createEvent('MouseEvents');
e.initEvent( 'click', true, true );
comp.dispatchEvent(e);
return;
}
} catch(ex) {}
try { // in IE, safari
if(document.createEventObject) {
var evObj = document.createEventObject();
comp.fireEvent("onclick", evObj);
return;
}
} catch(ex) {}
}
$(window).load(dosignup);
$(window).load(function() {$('#signup_action').trigger("click");});
</script>
% endif
% elif show_login_immediately is not UNDEFINED:
<script type="text/javascript">
$(window).load(function() {$('#login').trigger("click");});
</script>
% endif
\ No newline at end of file
......@@ -46,7 +46,11 @@
(function() {
$(document).delegate('#login_form', 'ajax:success', function(data, json, xhr) {
if(json.success) {
location.href="${reverse('dashboard')}";
% if request.REQUEST.get('next', False):
location.href="${request.REQUEST.get('next')}";
% else:
location.href="${reverse('dashboard')}";
% endif
} else {
if($('#login_error').length == 0) {
$('#login_form').prepend('<div id="login_error" class="modal-form-error"></div>');
......
<%inherit file="main.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">
<%static:css group='course'/>
......@@ -38,14 +38,14 @@ function goto_page(n) {
function prev_page() {
var newpage=page-1;
if(newpage<0) newpage=0;
if(newpage< ${start_page}) newpage=${start_page};
goto_page(newpage);
log_event("book", {"type":"prevpage","new":page});
}
function next_page() {
var newpage=page+1;
if(newpage>1008) newpage=1008;
if(newpage> ${end_page}) newpage=${end_page};
goto_page(newpage);
log_event("book", {"type":"nextpage","new":page});
}
......@@ -61,7 +61,7 @@ $("#open_close_accordion a").click(function(){
</script>
</%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">
<div class="book-wrapper">
......@@ -97,6 +97,10 @@ $("#open_close_accordion a").click(function(){
% for entry in table_of_contents:
${print_entry(entry)}
% endfor
## Don't delete this empty list item. Without it, Jquery.TreeView won't
## render the last list item as expandable.
<li></li>
</ul>
</section>
......
......@@ -97,6 +97,33 @@ urlpatterns = ('',
if settings.PERFSTATS:
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:
urlpatterns += (
# Hook django-masquerade, allowing staff to view site as other users
......@@ -175,29 +202,11 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
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())),
)
# 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"),
)
if settings.QUICKEDIT:
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
......
......@@ -125,8 +125,8 @@ TEST_TASKS = []
end
# Per environment tasks
Dir["#{system}/envs/*.py"].each do |env_file|
env = File.basename(env_file).gsub(/\.py/, '')
Dir["#{system}/envs/**/*.py"].each do |env_file|
env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors"
task "#{system}:check_settings:#{env}" => :predjango do
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