Commit 03370e95 by Rocky Duan

Merge branch 'master' into profile

Conflicts:
	lms/djangoapps/django_comment_client/forum/views.py
	lms/templates/course_navigation.html
parents b0abc62b 02d4901c
......@@ -58,6 +58,32 @@ In the discussion service, notifications are handled asynchronously using a thir
bundle exec rake jobs:work
## Initialize roles and permissions
To fully test the discussion forum, you might want to act as a moderator or an administrator. Currently, moderators can manage everything in the forum, and administrator can manage everything plus assigning and revoking moderator status of other users.
First make sure that the database is up-to-date:
rake django-admin[syncdb]
rake django-admin[migrate]
For convenience, add the following environment variables to the terminal (assuming that you're using configuration set lms.envs.dev):
export DJANGO_SETTINGS_MODULE=lms.envs.dev
export PYTHONPATH=.
Now initialzie roles and permissions:
django-admin.py seed_permissions_roles
To assign yourself as a moderator, use the following command (assuming your username is "test", and the course id is "MITx/6.002x/2012_Fall"):
django-admin.py assign_role test Moderator "MITx/6.002x/2012_Fall"
To assign yourself as an administrator, use the following command
django-admin.py assign_role test Administrator "MITx/6.002x/2012_Fall"
## Some other useful commands
### generate seeds for a specific forum
......@@ -104,18 +130,30 @@ We also have a command for generating comments within a forum with the specified
bundle exec rake db:generate_comments[type_the_discussion_id_here]
For instance, if you want to generate comments for the general discussion, for which the discussion id is the course id with slashes and dots replaced by underscores (you **should** do this before testing forum view) and you are in 6.002x, use the following command
For instance, if you want to generate comments for a new discussion tab named "lab_3", then use the following command
bundle exec rake db:generate_comments[MITx_6_002x_2012_Fall]
bundle exec rake db:generate_comments[lab_3]
### Running tests for the service
bundle exec rspec
Warning: due to an unresolved bug in the test code, testing the service will "flush" the development database. So you need to generate seed again after testing.
Warning: the development and test environments share the same elasticsearch index. After running tests, search may not work in the development environment. You simply need to reindex:
bundle exec rake db:reindex_search
### debugging the service
You can use the following command to launch a console within the service environment:
bundle exec rake console
### show user roles and permissions
Use the following command to see the roles and permissions of a user in a given course (assuming, again, that the username is "test"):
django-admin.py show_permissions moderator
You need to make sure that the environment variables are exported. Otherwise you would need to do
django-admin.py show_permissions moderator --settings=lms.envs.dev --pythonpath=.
......@@ -5,5 +5,5 @@ urlpatterns = patterns('django_comment_client.forum.views',
url(r'users/(?P<user_id>\w+)$', 'user_profile', name='user_profile'),
url(r'(?P<discussion_id>\w+)/threads/(?P<thread_id>\w+)$', 'single_thread', name='single_thread'),
url(r'(?P<discussion_id>\w+)/inline$', 'inline_discussion', name='inline_discussion'),
url(r'(?P<discussion_id>\w+)$', 'forum_form_discussion', name='forum_form_discussion'),
url(r'', 'forum_form_discussion', name='forum_form_discussion'),
)
......@@ -14,6 +14,7 @@ from datehelper import time_ago_in_words
import django_comment_client.utils as utils
from urllib import urlencode
from django_comment_client.permissions import check_permissions_by_view
import json
import comment_client as cc
......@@ -23,6 +24,10 @@ import dateutil
THREADS_PER_PAGE = 5
PAGES_NEARBY_DELTA = 2
def _general_discussion_id(course_id):
return course_id.replace('/', '_').replace('.', '_')
def _should_perform_search(request):
return bool(request.GET.get('text', False) or \
request.GET.get('tags', False))
......@@ -56,7 +61,7 @@ def render_discussion(request, course_id, threads, *args, **kwargs):
base_url = {
'inline': (lambda: reverse('django_comment_client.forum.views.inline_discussion', args=[course_id, discussion_id])),
'forum': (lambda: reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, discussion_id])),
'forum': (lambda: reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id])),
'user': (lambda: reverse('django_comment_client.forum.views.user_profile', args=[course_id, user_id])),
}[discussion_type]()
......@@ -92,11 +97,11 @@ def render_forum_discussion(*args, **kwargs):
def render_user_discussion(*args, **kwargs):
return render_discussion(discussion_type='user', *args, **kwargs)
def get_threads(request, course_id, discussion_id):
def get_threads(request, course_id, discussion_id=None):
query_params = {
'page': request.GET.get('page', 1),
'per_page': THREADS_PER_PAGE, #TODO maybe change this later
'sort_key': request.GET.get('sort_key', 'date'),
'sort_key': request.GET.get('sort_key', 'activity'),
'sort_order': request.GET.get('sort_order', 'desc'),
'text': request.GET.get('text', ''),
'tags': request.GET.get('tags', ''),
......@@ -128,22 +133,20 @@ def render_search_bar(request, course_id, discussion_id=None, text=''):
}
return render_to_string('discussion/_search_bar.html', context)
def forum_form_discussion(request, course_id, discussion_id):
def forum_form_discussion(request, course_id):
course = get_course_with_access(request.user, course_id, 'load')
threads, query_params = get_threads(request, course_id, discussion_id)
content = render_forum_discussion(request, course_id, threads, discussion_id=discussion_id, \
query_params=query_params)
threads, query_params = get_threads(request, course_id)
content = render_forum_discussion(request, course_id, threads, discussion_id=_general_discussion_id(course_id), query_params=query_params)
recent_active_threads = cc.search_recent_active_threads(
course_id,
recursive=False,
query_params={'follower_id': request.user.id,
'commentable_id': discussion_id},
query_params={'follower_id': request.user.id},
)
trending_tags = cc.search_trending_tags(
course_id,
query_params={'commentable_id': discussion_id},
)
if request.is_ajax():
......@@ -153,12 +156,31 @@ def forum_form_discussion(request, course_id, discussion_id):
'csrf': csrf(request)['csrf_token'],
'course': course,
'content': content,
'accordion': render_accordion(request, course, discussion_id),
'recent_active_threads': recent_active_threads,
'trending_tags': trending_tags,
}
return render_to_response('discussion/index.html', context)
def get_annotated_content_info(course_id, content, user, is_thread):
permissions = {
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if is_thread else "update_comment"),
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if is_thread else "create_sub_comment"),
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if not is_thread else False,
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if is_thread else "delete_comment"),
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if is_thread else False,
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if is_thread else "vote_for_comment"),
}
return permissions
def get_annotated_content_infos(course_id, thread, user, is_thread=True):
infos = {}
def _annotate(content, is_thread=is_thread):
infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, is_thread)
for child in content.get('children', []):
_annotate(child, is_thread=False)
_annotate(thread)
return infos
def render_single_thread(request, discussion_id, course_id, thread_id):
thread = cc.Thread.find(thread_id).retrieve(recursive=True)
......
......@@ -5,6 +5,7 @@ from django.dispatch import receiver
from student.models import CourseEnrollment
import logging
from util.cache import cache
@receiver(post_save, sender=CourseEnrollment)
......@@ -17,9 +18,20 @@ def assign_default_role(sender, instance, **kwargs):
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
instance.user.roles.add(role)
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.
"""
CACHE_LIFESPAN = 60
key = "permission_%d_%s_%s" % (user.id, str(course_id), 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
def has_permission(user, permission, course_id=None):
# if user.permissions.filter(name=permission).exists():
# return True
for role in user.roles.filter(course_id=course_id):
if role.has_permission(permission):
return True
......@@ -60,7 +72,7 @@ def check_conditions_permissions(user, permissions, course_id, **kwargs):
if isinstance(per, basestring):
if per in CONDITIONS:
return check_condition(user, per, course_id, kwargs)
return has_permission(user, per, course_id=course_id)
return cached_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":
......
......@@ -5,7 +5,8 @@ from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from django.http import HttpResponse
from django.utils import simplejson
from django.db import connection
import logging
from django.conf import settings
import operator
import itertools
......@@ -128,6 +129,31 @@ class ViewNameMiddleware(object):
def process_view(self, request, view_func, view_args, view_kwargs):
request.view_name = view_func.__name__
class QueryCountDebugMiddleware(object):
"""
This middleware will log the number of queries run
and the total time taken for each request (with a
status code of 200). It does not currently support
multi-db setups.
"""
def process_response(self, request, response):
if response.status_code == 200:
total_time = 0
for query in connection.queries:
query_time = query.get('time')
if query_time is None:
# django-debug-toolbar monkeypatches the connection
# cursor wrapper and adds extra information in each
# item in connection.queries. The query time is stored
# under the key "duration" rather than "time" and is
# in milliseconds, not seconds.
query_time = query.get('duration', 0) / 1000
total_time += float(query_time)
logging.info('%s queries run, total %s seconds' % (len(connection.queries), total_time))
return response
def get_annotated_content_info(course_id, content, user, type):
return {
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if type == 'thread' else "update_comment"),
......@@ -135,6 +161,7 @@ def get_annotated_content_info(course_id, content, user, type):
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if type == 'comment' else False,
'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if type == 'thread' else "delete_comment"),
'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if type == 'thread' else False,
'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if type == 'thread' else "vote_for_comment"),
}
def get_annotated_content_infos(course_id, thread, user, type='thread'):
......
......@@ -34,10 +34,9 @@ from .discussionsettings import *
################################### FEATURES ###################################
COURSEWARE_ENABLED = True
ASKBOT_ENABLED = True
ASKBOT_ENABLED = False
GENERATE_RANDOM_USER_CREDENTIALS = False
PERFSTATS = False
DISCUSSION_SERVICE_ENABLED = True
# Features
MITX_FEATURES = {
......@@ -57,7 +56,7 @@ MITX_FEATURES = {
'SUBDOMAIN_COURSE_LISTINGS' : False,
'ENABLE_TEXTBOOK' : True,
'ENABLE_DISCUSSION' : True,
'ENABLE_DISCUSSION' : False,
'ENABLE_DISCUSSION_SERVICE': True,
'ENABLE_SQL_TRACKING_LOGS': False,
......@@ -350,6 +349,7 @@ MIDDLEWARE_CLASSES = (
# 'debug_toolbar.middleware.DebugToolbarMiddleware',
'django_comment_client.utils.ViewNameMiddleware',
'django_comment_client.utils.QueryCountDebugMiddleware',
)
############################### Pipeline #######################################
......
......@@ -78,7 +78,16 @@ class Model(object):
def initializable_attributes(self):
return extract(self.attributes, self.initializable_fields)
@classmethod
def before_save(cls, instance):
pass
@classmethod
def after_save(cls, instance):
pass
def save(self):
self.__class__.before_save(self)
if self.id: # if we have id already, treat this as an update
url = self.url(action='put', params=self.attributes)
response = perform_request('put', url, self.updatable_attributes())
......@@ -87,6 +96,7 @@ class Model(object):
response = perform_request('post', url, self.initializable_attributes())
self.retrieved = True
self.update_attributes(**response)
self.__class__.after_save(self)
def delete(self):
url = self.url(action='delete', params=self.attributes)
......
......@@ -35,13 +35,17 @@ class Thread(models.Model):
url = cls.url(action='search')
else:
url = cls.url(action='get_all', params=extract(params, 'commentable_id'))
del params['commentable_id']
if params.get('commentable_id'):
del params['commentable_id']
response = perform_request('get', url, params, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
@classmethod
def url_for_threads(cls, params={}):
return "{prefix}/{commentable_id}/threads".format(prefix=settings.PREFIX, commentable_id=params['commentable_id'])
if params.get('commentable_id'):
return "{prefix}/{commentable_id}/threads".format(prefix=settings.PREFIX, commentable_id=params['commentable_id'])
else:
return "{prefix}/threads".format(prefix=settings.PREFIX)
@classmethod
def url_for_search_threads(cls, params={}):
......
......@@ -402,3 +402,5 @@ initializeFollowThread = (thread) ->
$local(".admin-delete").remove()
if not Discussion.getContentInfo id, 'can_openclose'
$local(".discussion-openclose").remove()
if not Discussion.getContentInfo id, 'can_vote'
$local(".discussion-vote").css "visibility", "hidden"
......@@ -102,10 +102,12 @@ $tag-text-color: #5b614f;
li {
@include clearfix;
margin-bottom: 8px;
border: none;
}
a {
@include standard-discussion-link;
background: none;
}
}
......
......@@ -20,19 +20,19 @@ def url_class(url):
<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 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 settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
<li class="discussion"><a href="${reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id, course.id.replace('/', '_').replace('.', '_')])}" 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
% if settings.MITX_FEATURES.get('ENABLE_DISCUSSION'):
<li class="discussion"><a href="${reverse('questions')}">Discussion</a></li>
% endif
% 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 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
% 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>
......
......@@ -44,10 +44,10 @@
<%def name="render_content(content, type, **kwargs)">
<div class="discussion-content">
<div class="discussion-content-wrapper clearfix">
<div class="discussion-content-wrapper">
${render_vote(content)}
<div class="discussion-right-wrapper clearfix">
<div class="discussion-right-wrapper">
<ul class="admin-actions">
% if type == 'comment':
<li><a href="javascript:void(0)" class="admin-endorse">Endorse</a></li>
......@@ -96,7 +96,7 @@
<%def name="render_tags(content, type, **kwargs)">
<%
def url_for_tags(tags):
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, content['commentable_id']]) + '?' + urllib.urlencode({'tags': ",".join(tags)})
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id]) + '?' + urllib.urlencode({'tags': ",".join(tags)})
%>
% if type == "thread":
<div class="thread-tags">
......
......@@ -16,10 +16,6 @@
<section class="container">
<div class="course-wrapper">
<section aria-label="Course Navigation" class="course-index">
<header id="open_close_accordion">
<h2>Discussion Boards</h2>
<a href="#">close</a>
</header>
<nav>
<article class="sidebar-module discussion-sidebar">
......
......@@ -156,7 +156,7 @@ if settings.COURSEWARE_ENABLED:
)
# discussion forums live within courseware, so courseware must be enabled first
if settings.DISCUSSION_SERVICE_ENABLED:
if settings.MITX_FEATURES.get('ENABLE_DISCUSSION_SERVICE'):
urlpatterns += (
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
......
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