Commit d2e0f27a by Greg Price

Merge pull request #1510 from edx/gprice/lms-forum-perf

Improve forums performance
parents c4434cb1 e6ecb6ec
......@@ -9,6 +9,9 @@ LMS: Improve forum error handling so that errors in the logs are
clearer and HTTP status codes from the comments service indicating
client error are correctly passed through to the client.
LMS: Improve performance of page load and thread list load for
discussion tab
LMS: The wiki markup cheatsheet dialog is now accessible to people with
disabilites. (LMS-1303)
......
......@@ -27,8 +27,6 @@ from .module_render import toc_for_course, get_module_for_descriptor, get_module
from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode
from django_comment_client.utils import get_discussion_title
from student.models import UserTestGroup, CourseEnrollment
from util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment
......@@ -39,8 +37,6 @@ from xmodule.modulestore.search import path_to_location
from xmodule.course_module import CourseDescriptor
import shoppingcart
import comment_client
log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib}
......@@ -674,29 +670,6 @@ def mktg_course_about(request, course_id):
})
def render_notifications(request, course, notifications):
context = {
'notifications': notifications,
'get_discussion_title': partial(get_discussion_title, request=request, course=course),
'course': course,
}
return render_to_string('courseware/notifications.html', context)
@login_required
def news(request, course_id):
course = get_course_with_access(request.user, course_id, 'load')
notifications = comment_client.get_notifications(request.user.id)
context = {
'course': course,
'content': render_notifications(request, course, notifications),
}
return render_to_response('courseware/news.html', context)
@login_required
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def progress(request, course_id, student_id=None):
......
......@@ -23,7 +23,7 @@ from mitxmako.shortcuts import render_to_string
from courseware.courses import get_course_with_access, get_course_by_id
from course_groups.cohorts import get_cohort_id, is_commentable_cohorted
from django_comment_client.utils import JsonResponse, JsonError, extract, get_courseware_context
from django_comment_client.utils import JsonResponse, JsonError, extract, add_courseware_context
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
from django_comment_common.models import Role
......@@ -128,10 +128,8 @@ def create_thread(request, course_id, commentable_id):
if post.get('auto_subscribe', 'false').lower() == 'true':
user = cc.User.from_django_user(request.user)
user.follow(thread)
courseware_context = get_courseware_context(thread, course)
data = thread.to_dict()
if courseware_context:
data.update(courseware_context)
add_courseware_context([data], course)
if request.is_ajax():
return ajax_content_response(request, course_id, data, 'discussion/ajax_create_thread.html')
else:
......
......@@ -15,7 +15,7 @@ from course_groups.cohorts import (is_course_cohorted, get_cohort_id, is_comment
from courseware.access import has_access
from django_comment_client.permissions import cached_has_permission
from django_comment_client.utils import (merge_dict, extract, strip_none, get_courseware_context)
from django_comment_client.utils import (merge_dict, extract, strip_none, add_courseware_context)
import django_comment_client.utils as utils
import comment_client as cc
......@@ -184,11 +184,8 @@ def forum_form_discussion(request, course_id):
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
annotated_content_info = utils.get_metadata_for_threads(course_id, threads, request.user, user_info)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context_to_threads"):
for thread in threads:
courseware_context = get_courseware_context(thread, course)
if courseware_context:
thread.update(courseware_context)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course)
if request.is_ajax():
return utils.JsonResponse({
......@@ -248,16 +245,14 @@ def single_thread(request, course_id, discussion_id, thread_id):
thread = cc.Thread.find(thread_id).retrieve(recursive=True, user_id=request.user.id)
if request.is_ajax():
with newrelic.agent.FunctionTrace(nr_transaction, "get_courseware_context"):
courseware_context = get_courseware_context(thread, course)
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
annotated_content_info = utils.get_annotated_content_infos(course_id, thread, request.user, user_info=user_info)
context = {'thread': thread.to_dict(), 'course_id': course_id}
# TODO: Remove completely or switch back to server side rendering
# html = render_to_string('discussion/_ajax_single_thread.html', context)
content = utils.safe_content(thread.to_dict())
if courseware_context:
content.update(courseware_context)
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context([content], course)
return utils.JsonResponse({
#'html': html,
'content': content,
......@@ -273,17 +268,16 @@ def single_thread(request, course_id, discussion_id, thread_id):
course = get_course_with_access(request.user, course_id, 'load_forum')
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context_to_threads"):
for thread in threads:
courseware_context = get_courseware_context(thread, course)
if courseware_context:
thread.update(courseware_context)
if thread.get('group_id') and not thread.get('group_name'):
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
#patch for backward compatibility with comments service
if not "pinned" in thread:
thread["pinned"] = False
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
add_courseware_context(threads, course)
for thread in threads:
if thread.get('group_id') and not thread.get('group_name'):
thread['group_name'] = get_cohort_by_id(course_id, thread.get('group_id')).name
#patch for backward compatibility with comments service
if not "pinned" in thread:
thread["pinned"] = False
threads = [utils.safe_content(thread) for thread in threads]
......
......@@ -20,10 +20,6 @@ from django.utils.timezone import UTC
log = logging.getLogger(__name__)
# TODO these should be cached via django's caching rather than in-memory globals
_FULLMODULES = None
_DISCUSSIONINFO = defaultdict(dict)
def extract(dic, keys):
return {k: dic.get(k) for k in keys}
......@@ -62,33 +58,33 @@ def has_forum_access(uname, course_id, rolename):
return role.users.filter(username=uname).exists()
def get_full_modules():
global _FULLMODULES
if not _FULLMODULES:
_FULLMODULES = modulestore().modules
return _FULLMODULES
def _get_discussion_modules(course):
all_modules = modulestore().get_items(
['i4x', course.location.org, course.location.course, 'discussion', None],
course_id=course.id
)
def get_discussion_id_map(course):
"""
return a dict of the form {category: modules}
"""
initialize_discussion_info(course)
return _DISCUSSIONINFO[course.id]['id_map']
def has_required_keys(module):
for key in ('discussion_id', 'discussion_category', 'discussion_target'):
if getattr(module, key) is None:
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
return False
return True
return filter(has_required_keys, all_modules)
def get_discussion_title(course, discussion_id):
initialize_discussion_info(course)
title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)')
return title
def _get_discussion_id_map(course):
def get_entry(module):
discussion_id = module.discussion_id
title = module.discussion_target
last_category = module.discussion_category.split("/")[-1].strip()
return (discussion_id, {"location": module.location, "title": last_category + " / " + title})
def get_discussion_category_map(course):
initialize_discussion_info(course)
return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map'])
return dict(map(get_entry, _get_discussion_modules(course)))
def filter_unstarted_categories(category_map):
def _filter_unstarted_categories(category_map):
now = datetime.now(UTC())
......@@ -126,7 +122,7 @@ def filter_unstarted_categories(category_map):
return result_map
def sort_map_entries(category_map, sort_alpha):
def _sort_map_entries(category_map, sort_alpha):
things = []
for title, entry in category_map["entries"].items():
if entry["sort_key"] == None and sort_alpha:
......@@ -134,37 +130,22 @@ def sort_map_entries(category_map, sort_alpha):
things.append((title, entry))
for title, category in category_map["subcategories"].items():
things.append((title, category))
sort_map_entries(category_map["subcategories"][title], sort_alpha)
_sort_map_entries(category_map["subcategories"][title], sort_alpha)
category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])]
def initialize_discussion_info(course):
def get_discussion_category_map(course):
course_id = course.id
discussion_id_map = {}
unexpanded_category_map = defaultdict(list)
# get all discussion models within this course_id
all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course,
'discussion', None], course_id=course_id)
for module in all_modules:
skip_module = False
for key in ('discussion_id', 'discussion_category', 'discussion_target'):
if getattr(module, key) is None:
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
skip_module = True
if skip_module:
continue
modules = _get_discussion_modules(course)
for module in modules:
id = module.discussion_id
category = module.discussion_category
title = module.discussion_target
sort_key = module.sort_key
category = " / ".join([x.strip() for x in category.split("/")])
last_category = category.split("/")[-1]
discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
category = " / ".join([x.strip() for x in module.discussion_category.split("/")])
#Handle case where module.start is None
entry_start_date = module.start if module.start else datetime.max.replace(tzinfo=pytz.UTC)
unexpanded_category_map[category].append({"title": title, "id": id, "sort_key": sort_key, "start_date": entry_start_date})
......@@ -214,11 +195,9 @@ def initialize_discussion_info(course):
"sort_key": entry.get("sort_key", topic),
"start_date": datetime.now(UTC())}
sort_map_entries(category_map, course.discussion_sort_alpha)
_sort_map_entries(category_map, course.discussion_sort_alpha)
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now(UTC())
return _filter_unstarted_categories(category_map)
class JsonResponse(HttpResponse):
......@@ -368,19 +347,19 @@ def extend_content(content):
return merge_dict(content, content_info)
def get_courseware_context(content, course):
id_map = get_discussion_id_map(course)
id = content['commentable_id']
content_info = None
if id in id_map:
location = id_map[id]["location"].url()
title = id_map[id]["title"]
def add_courseware_context(content_list, course):
id_map = _get_discussion_id_map(course)
for content in content_list:
commentable_id = content['commentable_id']
if commentable_id in id_map:
location = id_map[commentable_id]["location"].url()
title = id_map[commentable_id]["title"]
url = reverse('jump_to', kwargs={"course_id": course.location.course_id,
"location": location})
url = reverse('jump_to', kwargs={"course_id": course.location.course_id,
"location": location})
content_info = {"courseware_url": url, "courseware_title": title}
return content_info
content.update({"courseware_url": url, "courseware_title": title})
def safe_content(content):
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%
def url_for_thread(discussion_id, thread_id):
return reverse('django_comment_client.forum.views.single_thread', args=[course.id, discussion_id, thread_id])
%>
<%
def url_for_comment(discussion_id, thread_id, comment_id):
return url_for_thread(discussion_id, thread_id) + "#" + comment_id
%>
<%
def url_for_discussion(discussion_id):
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id, discussion_id])
%>
<%
def discussion_title(discussion_id):
return get_discussion_title(discussion_id=discussion_id)
%>
<%
def url_for_user(user_id): #TODO
return "javascript:void(0)"
%>
<div class="notifications">
% for notification in notifications:
${render_notification(notification)}
% endfor
</div>
<%def name="render_user_link(notification)">
<% info = notification['info'] %>
% if notification.get('actor_id', None):
<a href="${url_for_user(notification['actor_id'])}">${info['actor_username']}</a>
% else:
${_("Anonymous")}
% endif
</%def>
<%def name="render_thread_link(notification)">
<% info = notification['info'] %>
<a href="${url_for_thread(info['commentable_id'], info['thread_id'])}">${info['thread_title']}</a>
</%def>
<%def name="render_comment_link(notification)">
<% info = notification['info'] %>
<a href="${url_for_comment(info['commentable_id'], info['thread_id'], info['comment_id'])}">${_("comment")}</a>
</%def>
<%def name="render_discussion_link(notification)">
<% info = notification['info'] %>
<a href="${url_for_discussion(info['commentable_id'])}">${discussion_title(info['commentable_id'])}</a>
</%def>
<%def name="render_notification(notification)">
<div class="notification">
% if notification['notification_type'] == 'post_reply':
${_("{user} posted a {comment} to the thread {thread} in discussion {discussion}").format(
user=render_user_link(notification),
comment=render_comment_link(notification),
thread=render_thread_link(notification),
discussion=render_discussion_link(notification),
)}
% elif notification['notification_type'] == 'post_topic':
${_("{user} posted a new thread {thread} in discussion {discussion}").format(
user=render_user_link(notification),
thread=render_thread_link(notification),
discussion=render_discussion_link(notification),
)}
% elif notification['notification_type'] == 'at_user':
% if notification['info']['content_type'] == 'thread':
${_("{user} mentioned you in the thread {thread} in disucssion {discussion}").format(
user=render_user(info),
thread=render_thread_link(notification),
discussion=render_discussion_link(notification),
)}
% else:
${_("{user} mentioned you in {comment} to the thread {thread} in discussion {discussion}").format(
user=render_user(info),
comment=render_comment_link(notification),
thread=render_thread_link(notification),
discussion=render_discussion_link(notification),
)}
% endif
% endif
</div>
</%def>
......@@ -329,8 +329,6 @@ if settings.COURSEWARE_ENABLED:
# discussion forums live within courseware, so courseware must be enabled first
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
'courseware.views.news', name="news"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')),
url(r'^notification_prefs/enable/', 'notification_prefs.views.ajax_enable'),
......
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