Merge pull request #8528 from edx/benmcmorran/discussion-caching
TNL-2291 Discussion forums permission caching
This commit is contained in:
@@ -29,7 +29,7 @@ from django_comment_client.utils import (
|
||||
get_discussion_categories_ids,
|
||||
get_discussion_id_map,
|
||||
)
|
||||
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
||||
from django_comment_client.permissions import check_permissions_by_view, has_permission
|
||||
from eventtracking import tracker
|
||||
import lms.lib.comment_client as cc
|
||||
|
||||
@@ -490,7 +490,10 @@ def un_flag_abuse_for_thread(request, course_id, thread_id):
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
thread = cc.Thread.find(thread_id)
|
||||
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
|
||||
remove_all = (
|
||||
has_permission(request.user, 'openclose_thread', course_key) or
|
||||
has_access(request.user, 'staff', course)
|
||||
)
|
||||
thread.unFlagAbuse(user, thread, remove_all)
|
||||
|
||||
return JsonResponse(prepare_content(thread.to_dict(), course_key))
|
||||
@@ -522,7 +525,10 @@ def un_flag_abuse_for_comment(request, course_id, comment_id):
|
||||
user = cc.User.from_django_user(request.user)
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
remove_all = cached_has_permission(request.user, 'openclose_thread', course_key) or has_access(request.user, 'staff', course)
|
||||
remove_all = (
|
||||
has_permission(request.user, 'openclose_thread', course_key) or
|
||||
has_access(request.user, 'staff', course)
|
||||
)
|
||||
comment = cc.Comment.find(comment_id)
|
||||
comment.unFlagAbuse(user, comment, remove_all)
|
||||
return JsonResponse(prepare_content(comment.to_dict(), course_key))
|
||||
|
||||
@@ -316,12 +316,12 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
|
||||
@ddt.data(
|
||||
# old mongo with cache: 15
|
||||
(ModuleStoreEnum.Type.mongo, 1, 21, 15, 40, 27),
|
||||
(ModuleStoreEnum.Type.mongo, 50, 315, 15, 628, 27),
|
||||
# old mongo with cache
|
||||
(ModuleStoreEnum.Type.mongo, 1, 7, 5, 12, 7),
|
||||
(ModuleStoreEnum.Type.mongo, 50, 7, 5, 12, 7),
|
||||
# split mongo: 3 queries, regardless of thread response size.
|
||||
(ModuleStoreEnum.Type.split, 1, 3, 3, 40, 27),
|
||||
(ModuleStoreEnum.Type.split, 50, 3, 3, 628, 27),
|
||||
(ModuleStoreEnum.Type.split, 1, 3, 3, 12, 7),
|
||||
(ModuleStoreEnum.Type.split, 50, 3, 3, 12, 7),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_number_of_mongo_queries(
|
||||
@@ -363,27 +363,15 @@ class SingleThreadQueryCountTestCase(ModuleStoreTestCase):
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(len(json.loads(response.content)["content"]["children"]), num_thread_responses)
|
||||
|
||||
# TODO: update this once django cache is disabled in tests
|
||||
# Test with and without cache, clearing before and after use.
|
||||
single_thread_local_cache = cache.get_cache(
|
||||
backend='default',
|
||||
LOCATION='single_thread_local_cache'
|
||||
)
|
||||
single_thread_dummy_cache = cache.get_cache(
|
||||
backend='django.core.cache.backends.dummy.DummyCache',
|
||||
LOCATION='single_thread_local_cache'
|
||||
)
|
||||
# Test uncached first, then cached now that the cache is warm.
|
||||
cached_calls = [
|
||||
[single_thread_dummy_cache, num_uncached_mongo_calls, num_uncached_sql_queries],
|
||||
[single_thread_local_cache, num_cached_mongo_calls, num_cached_sql_queries]
|
||||
[num_uncached_mongo_calls, num_uncached_sql_queries],
|
||||
[num_cached_mongo_calls, num_cached_sql_queries],
|
||||
]
|
||||
for single_thread_cache, expected_mongo_calls, expected_sql_queries in cached_calls:
|
||||
single_thread_cache.clear()
|
||||
with patch("django_comment_client.permissions.CACHE", single_thread_cache):
|
||||
with self.assertNumQueries(expected_sql_queries):
|
||||
with check_mongo_calls(expected_mongo_calls):
|
||||
call_single_thread()
|
||||
single_thread_cache.clear()
|
||||
for expected_mongo_calls, expected_sql_queries in cached_calls:
|
||||
with self.assertNumQueries(expected_sql_queries):
|
||||
with check_mongo_calls(expected_mongo_calls):
|
||||
call_single_thread()
|
||||
|
||||
|
||||
@patch('requests.request')
|
||||
|
||||
@@ -30,7 +30,7 @@ from courseware.access import has_access
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from ccx.overrides import get_current_ccx
|
||||
|
||||
from django_comment_client.permissions import cached_has_permission
|
||||
from django_comment_client.permissions import has_permission
|
||||
from django_comment_client.utils import (
|
||||
merge_dict,
|
||||
extract,
|
||||
@@ -209,7 +209,7 @@ def inline_discussion(request, course_key, discussion_id):
|
||||
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
|
||||
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "add_courseware_context"):
|
||||
add_courseware_context(threads, course, request.user)
|
||||
@@ -241,7 +241,7 @@ def forum_form_discussion(request, course_key):
|
||||
|
||||
try:
|
||||
unsafethreads, query_params = get_threads(request, course) # This might process a search query
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in unsafethreads]
|
||||
except cc.utils.CommentClientMaintenanceError:
|
||||
log.warning("Forum is in maintenance mode")
|
||||
@@ -275,11 +275,14 @@ def forum_form_discussion(request, course_key):
|
||||
'threads': _attr_safe_json(threads),
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'user_info': _attr_safe_json(user_info),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course),
|
||||
'flag_moderator': (
|
||||
has_permission(request.user, 'openclose_thread', course.id) or
|
||||
has_access(request.user, 'staff', course)
|
||||
),
|
||||
'annotated_content_info': _attr_safe_json(annotated_content_info),
|
||||
'course_id': course.id.to_deprecated_string(),
|
||||
'roles': _attr_safe_json(utils.get_role_ids(course_key)),
|
||||
'is_moderator': cached_has_permission(request.user, "see_all_cohorts", course_key),
|
||||
'is_moderator': has_permission(request.user, "see_all_cohorts", course_key),
|
||||
'cohorts': course_settings["cohorts"], # still needed to render _thread_list_template
|
||||
'user_cohort': user_cohort_id, # read from container in NewPostView
|
||||
'is_course_cohorted': is_course_cohorted(course_key), # still needed to render _thread_list_template
|
||||
@@ -304,7 +307,7 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
course_settings = make_course_settings(course, request.user)
|
||||
cc_user = cc.User.from_django_user(request.user)
|
||||
user_info = cc_user.to_dict()
|
||||
is_moderator = cached_has_permission(request.user, "see_all_cohorts", course_key)
|
||||
is_moderator = has_permission(request.user, "see_all_cohorts", course_key)
|
||||
|
||||
# Verify that the student has access to this thread if belongs to a discussion module
|
||||
if discussion_id not in utils.get_discussion_categories_ids(course, request.user):
|
||||
@@ -331,7 +334,7 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
if getattr(thread, "group_id", None) is not None and user_group_id != thread.group_id:
|
||||
raise Http404
|
||||
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
if request.is_ajax():
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_annotated_content_infos"):
|
||||
annotated_content_info = utils.get_annotated_content_infos(course_key, thread, request.user, user_info=user_info)
|
||||
@@ -381,7 +384,10 @@ def single_thread(request, course_key, discussion_id, thread_id):
|
||||
'is_moderator': is_moderator,
|
||||
'thread_pages': query_params['num_pages'],
|
||||
'is_course_cohorted': is_course_cohorted(course_key),
|
||||
'flag_moderator': cached_has_permission(request.user, 'openclose_thread', course.id) or has_access(request.user, 'staff', course),
|
||||
'flag_moderator': (
|
||||
has_permission(request.user, 'openclose_thread', course.id) or
|
||||
has_access(request.user, 'staff', course)
|
||||
),
|
||||
'cohorts': course_settings["cohorts"],
|
||||
'user_cohort': user_cohort,
|
||||
'sort_preference': cc_user.default_sort_key,
|
||||
@@ -428,7 +434,7 @@ def user_profile(request, course_key, user_id):
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
|
||||
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
|
||||
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
threads = [utils.prepare_content(thread, course_key, is_staff) for thread in threads]
|
||||
if request.is_ajax():
|
||||
return utils.JsonResponse({
|
||||
@@ -509,7 +515,7 @@ def followed_threads(request, course_key, user_id):
|
||||
with newrelic.agent.FunctionTrace(nr_transaction, "get_metadata_for_threads"):
|
||||
annotated_content_info = utils.get_metadata_for_threads(course_key, threads, request.user, user_info)
|
||||
if request.is_ajax():
|
||||
is_staff = cached_has_permission(request.user, 'openclose_thread', course.id)
|
||||
is_staff = has_permission(request.user, 'openclose_thread', course.id)
|
||||
return utils.JsonResponse({
|
||||
'annotated_content_info': annotated_content_info,
|
||||
'discussion_data': [utils.prepare_content(thread, course_key, is_staff) for thread in threads],
|
||||
|
||||
@@ -5,34 +5,27 @@ Module for checking permissions with the comment_client backend
|
||||
import logging
|
||||
from types import NoneType
|
||||
from django.core import cache
|
||||
|
||||
from request_cache.middleware import RequestCache
|
||||
from lms.lib.comment_client import Thread
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
CACHE = cache.get_cache('default')
|
||||
CACHE_LIFESPAN = 60
|
||||
|
||||
|
||||
def cached_has_permission(user, permission, course_id=None):
|
||||
"""
|
||||
Call has_permission if it's not cached. A change in a user's role or
|
||||
a role's permissions will only become effective after CACHE_LIFESPAN seconds.
|
||||
"""
|
||||
assert isinstance(course_id, (NoneType, CourseKey))
|
||||
key = u"permission_{user_id:d}_{course_id}_{permission}".format(
|
||||
user_id=user.id, course_id=course_id, permission=permission)
|
||||
val = CACHE.get(key, None)
|
||||
if val not in [True, False]:
|
||||
val = has_permission(user, permission, course_id=course_id)
|
||||
CACHE.set(key, val, CACHE_LIFESPAN)
|
||||
return val
|
||||
from django_comment_common.models import all_permissions_for_user_in_course
|
||||
|
||||
|
||||
def has_permission(user, permission, course_id=None):
|
||||
assert isinstance(course_id, (NoneType, CourseKey))
|
||||
for role in user.roles.filter(course_id=course_id):
|
||||
if role.has_permission(permission):
|
||||
return True
|
||||
return False
|
||||
request_cache_dict = RequestCache.get_request_cache().data
|
||||
cache_key = "django_comment_client.permissions.has_permission.all_permissions.{}.{}".format(
|
||||
user.id, course_id
|
||||
)
|
||||
if cache_key in request_cache_dict:
|
||||
all_permissions = request_cache_dict[cache_key]
|
||||
else:
|
||||
all_permissions = all_permissions_for_user_in_course(user, course_id)
|
||||
request_cache_dict[cache_key] = all_permissions
|
||||
|
||||
return permission in all_permissions
|
||||
|
||||
|
||||
CONDITIONS = ['is_open', 'is_author', 'is_question_author']
|
||||
@@ -84,7 +77,7 @@ def _check_conditions_permissions(user, permissions, course_id, content):
|
||||
if isinstance(per, basestring):
|
||||
if per in CONDITIONS:
|
||||
return _check_condition(user, per, content)
|
||||
return cached_has_permission(user, per, course_id=course_id)
|
||||
return has_permission(user, per, course_id=course_id)
|
||||
elif isinstance(per, list) and operator in ["and", "or"]:
|
||||
results = [test(user, x, operator="and") for x in per]
|
||||
if operator == "or":
|
||||
|
||||
@@ -15,7 +15,7 @@ from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from django_comment_common.models import Role, FORUM_ROLE_STUDENT
|
||||
from django_comment_client.permissions import check_permissions_by_view, cached_has_permission
|
||||
from django_comment_client.permissions import check_permissions_by_view, has_permission
|
||||
from edxmako import lookup_template
|
||||
|
||||
from courseware.access import has_access
|
||||
@@ -506,8 +506,8 @@ def prepare_content(content, course_key, is_staff=False, course_is_cohorted=None
|
||||
|
||||
# Only reveal endorser if requester can see author or if endorser is staff
|
||||
if (
|
||||
endorser and
|
||||
("username" in fields or cached_has_permission(endorser, "endorse_comment", course_key))
|
||||
endorser and
|
||||
("username" in fields or has_permission(endorser, "endorse_comment", course_key))
|
||||
):
|
||||
endorsement["username"] = endorser.username
|
||||
else:
|
||||
@@ -552,7 +552,7 @@ def get_group_id_for_comments_service(request, course_key, commentable_id=None):
|
||||
requested_group_id = request.GET.get('group_id')
|
||||
elif request.method == "POST":
|
||||
requested_group_id = request.POST.get('group_id')
|
||||
if cached_has_permission(request.user, "see_all_cohorts", course_key):
|
||||
if has_permission(request.user, "see_all_cohorts", course_key):
|
||||
if not requested_group_id:
|
||||
return None
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user