Merge branch 'master' into discussion2

This commit is contained in:
Rocky Duan
2012-08-01 16:51:54 -04:00
45 changed files with 1071 additions and 494 deletions

View File

@@ -33,6 +33,7 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
try:
course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc)
except (KeyError, ItemNotFoundError):
raise Http404("Course not found.")

View File

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

View File

@@ -172,6 +172,7 @@ def get_module(user, request, location, student_module_cache, position=None):
# a module is coming through get_html and is therefore covered
# by the replace_static_urls code below
replace_urls=replace_urls,
is_staff=user.is_staff,
)
# pass position specified in URL to module through ModuleSystem
system.set('position', position)

View File

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

View File

@@ -31,6 +31,7 @@ from multicourse import multicourse_settings
from django_comment_client.utils import get_discussion_title
from xmodule.modulestore import Location
from xmodule.modulestore.search import path_to_location
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
@@ -70,14 +71,20 @@ def user_groups(user):
def format_url_params(params):
return [urllib.quote(string.replace(' ', '_')) for string in params]
return [urllib.quote(string.replace(' ', '_'))
if string is not None else None
for string in params]
@ensure_csrf_cookie
@cache_if_anonymous
def courses(request):
# TODO: Clean up how 'error' is done.
courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
# filter out any courses that errored.
courses = [c for c in modulestore().get_courses()
if isinstance(c, CourseDescriptor)]
courses = sorted(courses, key=lambda course: course.number)
universities = defaultdict(list)
for course in courses:
universities[course.org].append(course)
@@ -199,34 +206,57 @@ def index(request, course_id, chapter=None, section=None,
chapter = clean(chapter)
section = clean(section)
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': ''
}
try:
context = {
'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, course, chapter, section),
'COURSE_TITLE': course.title,
'course': course,
'init': '',
'content': ''
}
look_for_module = chapter is not None and section is not None
if look_for_module:
# TODO (cpennington): Pass the right course in here
look_for_module = chapter is not None and section is not None
if look_for_module:
# TODO (cpennington): Pass the right course in here
section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user,
section_descriptor)
module, _, _, _ = get_module(request.user, request,
section_descriptor.location,
student_module_cache)
context['content'] = module.get_html()
section_descriptor = get_section(course, chapter, section)
if section_descriptor is not None:
student_module_cache = StudentModuleCache(request.user,
section_descriptor)
module, _, _, _ = get_module(request.user, request,
section_descriptor.location,
student_module_cache)
context['content'] = module.get_html()
else:
log.warning("Couldn't find a section descriptor for course_id '{0}',"
"chapter '{1}', section '{2}'".format(
course_id, chapter, section))
else:
log.warning("Couldn't find a section descriptor for course_id '{0}',"
"chapter '{1}', section '{2}'".format(
course_id, chapter, section))
if request.user.is_staff:
# Add a list of all the errors...
context['course_errors'] = modulestore().get_item_errors(course.location)
result = render_to_response('courseware.html', context)
except:
# In production, don't want to let a 500 out for any reason
if settings.DEBUG:
raise
else:
log.exception("Error in index view: user={user}, course={course},"
" chapter={chapter} section={section}"
"position={position}".format(
user=request.user,
course=course,
chapter=chapter,
section=section,
position=position
))
try:
result = render_to_response('courseware-error.html', {})
except:
result = HttpResponse("There was an unrecoverable error")
result = render_to_response('courseware.html', context)
return result
@ensure_csrf_cookie
@@ -249,7 +279,7 @@ def jump_to(request, location):
# Complain if there's not data for this location
try:
(course_id, chapter, section, position) = modulestore().path_to_location(location)
(course_id, chapter, section, position) = path_to_location(modulestore(), location)
except ItemNotFoundError:
raise Http404("No data at this location: {0}".format(location))
except NoPathToItem:

View File

@@ -3,18 +3,28 @@ from django.views.decorators.http import require_POST
from django.http import HttpResponse
from django.utils import simplejson
from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response, render_to_string
from courseware.courses import check_course
import comment_client
import dateutil
from dateutil.tz import tzlocal
from datehelper import time_ago_in_words
from django_comment_client.utils import get_categorized_discussion_info
from urllib import urlencode
import json
import comment_client
import dateutil
THREADS_PER_PAGE = 20
PAGES_NEARBY_DELTA = 2
class HtmlResponse(HttpResponse):
def __init__(self, html=''):
super(HtmlResponse, self).__init__(html, content_type='text/plain')
def render_accordion(request, course, discussion_id):
@@ -29,29 +39,58 @@ def render_accordion(request, course, discussion_id):
return render_to_string('discussion/_accordion.html', context)
def render_discussion(request, course_id, threads, discussion_id=None, with_search_bar=True, search_text='', template='discussion/_inline.html'):
def render_discussion(request, course_id, threads, discussion_id=None, with_search_bar=True,
search_text='', discussion_type='inline', page=1, num_pages=None,
per_page=THREADS_PER_PAGE):
template = {
'inline': 'discussion/_inline.html',
'forum': 'discussion/_forum.html',
}[discussion_type]
def _url_for_inline_page(page, per_page):
raw_url = reverse('django_comment_client.forum.views.inline_discussion', args=[course_id, discussion_id])
return raw_url + '?' + urlencode({'page': page, 'per_page': per_page})
def _url_for_forum_page(page, per_page):
raw_url = reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, discussion_id])
return raw_url + '?' + urlencode({'page': page, 'per_page': per_page})
url_for_page = {
'inline': _url_for_inline_page,
'forum': _url_for_forum_page,
}[discussion_type]
context = {
'threads': threads,
'discussion_id': discussion_id,
'search_bar': '' if not with_search_bar \
else render_search_bar(request, course_id, discussion_id, text=search_text),
'user_info': comment_client.get_user_info(request.user.id, raw=True),
'tags': comment_client.get_threads_tags(raw=True),
'course_id': course_id,
'request': request,
'page': page,
'per_page': per_page,
'num_pages': num_pages,
'pages_nearby_delta': PAGES_NEARBY_DELTA,
'discussion_type': discussion_type,
'url_for_page': url_for_page,
}
return render_to_string(template, context)
def render_inline_discussion(*args, **kwargs):
return render_discussion(template='discussion/_inline.html', *args, **kwargs)
return render_discussion(discussion_type='inline', *args, **kwargs)
def render_forum_discussion(*args, **kwargs):
return render_discussion(template='discussion/_forum.html', *args, **kwargs)
return render_discussion(discussion_type='forum', *args, **kwargs)
# discussion per page is fixed for now
def inline_discussion(request, course_id, discussion_id):
threads = comment_client.get_threads(discussion_id, recursive=False)
html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id)
return HttpResponse(html, content_type="text/plain")
page = request.GET.get('page', 1)
threads, page, per_page, num_pages = comment_client.get_threads(discussion_id, recursive=False, page=page, per_page=THREADS_PER_PAGE)
html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, num_pages=num_pages, page=page, per_page=per_page)
return HtmlResponse(html)
def render_search_bar(request, course_id, discussion_id=None, text=''):
if not discussion_id:
@@ -66,18 +105,29 @@ def render_search_bar(request, course_id, discussion_id=None, text=''):
def forum_form_discussion(request, course_id, discussion_id):
course = check_course(course_id)
search_text = request.GET.get('text', '')
page = request.GET.get('page', 1)
if len(search_text) > 0:
threads = comment_client.search_threads({'text': search_text, 'commentable_id': discussion_id})
threads, page, per_page, num_pages = comment_client.search_threads({
'text': search_text,
'commentable_id': discussion_id,
'course_id': course_id,
'page': page,
'per_page': THREADS_PER_PAGE,
})
else:
threads = comment_client.get_threads(discussion_id, recursive=False)
threads, page, per_page, num_pages = comment_client.get_threads(discussion_id, recursive=False, page=page, per_page=THREADS_PER_PAGE)
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'content': render_forum_discussion(request, course_id, threads, discussion_id=discussion_id, search_text=search_text),
'content': render_forum_discussion(request, course_id, threads,
discussion_id=discussion_id,
search_text=search_text,
num_pages=num_pages,
per_page=per_page,
page=page),
'accordion': render_accordion(request, course, discussion_id),
}
@@ -102,7 +152,6 @@ def render_single_thread(request, course_id, thread_id):
'thread': thread,
'user_info': comment_client.get_user_info(request.user.id, raw=True),
'annotated_content_info': json.dumps(get_annotated_content_info(thread=thread, user_id=request.user.id)),
'tags': comment_client.get_threads_tags(raw=True),
'course_id': course_id,
'request': request,
}

View File

@@ -17,7 +17,7 @@ MITX_FEATURES['DISABLE_START_DATES'] = True
WIKI_ENABLED = True
LOGGING = get_logger_config(ENV_ROOT / "log",
LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
@@ -30,7 +30,7 @@ DATABASES = {
}
CACHES = {
# This is the cache used for most things. Askbot will not work without a
# This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here.
'default': {
@@ -52,11 +52,14 @@ CACHES = {
}
}
# Make the keyedcache startup warnings go away
CACHE_TIMEOUT = 0
# Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar',)
INSTALLED_APPS += ('debug_toolbar',)
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
INTERNAL_IPS = ('127.0.0.1',)
@@ -71,8 +74,8 @@ DEBUG_TOOLBAR_PANELS = (
'debug_toolbar.panels.logger.LoggingPanel',
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
# hit twice). So you can uncomment when you need to diagnose performance
# problems, but you shouldn't leave it on.
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
)

View File

@@ -15,8 +15,9 @@ class CommentClientUnknownError(CommentClientError):
def delete_threads(commentable_id, *args, **kwargs):
return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs)
def get_threads(commentable_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_threads(commentable_id), {'recursive': recursive}, *args, **kwargs)
def get_threads(commentable_id, recursive=False, page=1, per_page=20, *args, **kwargs):
response = _perform_request('get', _url_for_threads(commentable_id), {'recursive': recursive, 'page': page, 'per_page': per_page}, *args, **kwargs)
return response['collection'], response['page'], response['per_page'], response['num_pages']
def get_threads_tags(*args, **kwargs):
return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs)
@@ -98,6 +99,8 @@ def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def search_threads(attributes, *args, **kwargs):
default_attributes = {'page': 1, 'per_page': 20}
attributes = dict(default_attributes.items() + attributes.items())
return _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs)
def _perform_request(method, url, data_or_params=None, *args, **kwargs):

View File

@@ -276,7 +276,12 @@ $discussion_input_width: 90%;
}
}
}
.discussion-paginator {
margin-top: 40px;
div {
display: inline-block;
}
}
}

View File

@@ -52,6 +52,19 @@
<section class="course-content">
${content}
% if course_errors is not UNDEFINED:
<h2>Course errors</h2>
<div id="course-errors">
<ul>
% for (msg, err) in course_errors:
<li>${msg}
<ul><li><pre>${err}</pre></li></ul>
</li>
% endfor
</ul>
</div>
% endif
</section>
</div>
</section>

View File

@@ -8,9 +8,11 @@
${search_bar}
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
</div>
<%include file="_paginator.html" />
% for thread in threads:
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
% endfor
<%include file="_paginator.html" />
</section>
<%!
@@ -21,5 +23,4 @@
<script type="text/javascript">
var $$user_info = JSON.parse('${user_info | escape_quotes}');
var $$course_id = "${course_id}";
var $$tags = JSON.parse("${tags | escape_quotes}");
</script>

View File

@@ -8,9 +8,11 @@
${search_bar}
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
</div>
<%include file="_paginator.html" />
% for thread in threads:
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
% endfor
<%include file="_paginator.html" />
</section>
<%!
@@ -21,5 +23,4 @@
<script type="text/javascript">
var $$user_info = JSON.parse('${user_info | escape_quotes}');
var $$course_id = "${course_id}";
var $$tags = JSON.parse("${tags | escape_quotes}");
</script>

View File

@@ -0,0 +1,52 @@
<%def name="link_to_page(_page)">
% if _page != page:
<div class="page-link">
<a href="${url_for_page(_page, per_page)}">${_page}</a>
</div>
% else:
<div class="page-link">${_page}</div>
% endif
</%def>
<%def name="list_pages(*args)">
% for arg in args:
% if arg == 'dots':
<div class="page-dots">...</div>
% elif isinstance(arg, list):
% for _page in arg:
${link_to_page(_page)}
% endfor
% else:
${link_to_page(arg)}
% endif
% endfor
</%def>
<div class="discussion-${discussion_type}-paginator discussion-paginator">
<div class="prev-page">
% if page > 1:
<a href="${url_for_page(page - 1, per_page)}">&lt; Previous page</a>
% else:
&lt; Previous page
% endif
</div>
% if num_pages <= 2 * pages_nearby_delta + 2:
${list_pages(range(1, num_pages + 1))}
% else:
% if page <= 2 * pages_nearby_delta:
${list_pages(range(1, 2 * pages_nearby_delta + 2), 'dots', num_pages)}
% elif num_pages - page + 1 <= 2 * pages_nearby_delta:
${list_pages(1, 'dots', range(num_pages - 2 * pages_nearby_delta, num_pages + 1))}
% else:
${list_pages(1, 'dots', range(page - pages_nearby_delta, page + pages_nearby_delta + 1), 'dots', num_pages)}
% endif
% endif
<div class="next-page">
% if page < num_pages:
<a href="${url_for_page(page + 1, per_page)}">Next page &gt;</a>
% else:
Next page &gt;
% endif
</div>
</div>

View File

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