Commit 660c0b08 by Rocky Duan

Merge branch 'mergedtom' into refactor

Conflicts:
	lms/djangoapps/django_comment_client/forum/views.py
parents a205b23e 6d19ec9c
...@@ -121,6 +121,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -121,6 +121,7 @@ MIDDLEWARE_CLASSES = (
) )
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa import monitoring.exceptions # noqa
############################ DJANGO_BUILTINS ################################ ############################ DJANGO_BUILTINS ################################
......
import logging
from django.conf import settings
from django.http import HttpResponseServerError
log = logging.getLogger("mitx")
class ExceptionLoggingMiddleware(object):
"""Just here to log unchecked exceptions that go all the way up the Django
stack"""
if not settings.TEMPLATE_DEBUG:
def process_exception(self, request, exception):
log.exception(exception)
return HttpResponseServerError("Server Error - Please try again later.")
...@@ -142,7 +142,7 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -142,7 +142,7 @@ def get_module(user, request, location, student_module_cache, position=None):
shared_module = None shared_module = None
instance_state = instance_module.state if instance_module is not None else {} instance_state = instance_module.state if instance_module is not None else '{}'
shared_state = shared_module.state if shared_module is not None else None shared_state = shared_module.state if shared_module is not None else None
# TODO (vshnayder): fix hardcoded urls (use reverse) # TODO (vshnayder): fix hardcoded urls (use reverse)
......
...@@ -34,7 +34,7 @@ def permitted(fn): ...@@ -34,7 +34,7 @@ def permitted(fn):
content = None content = None
return content return content
if check_permissions_by_view(request.user, fetch_content(), request.view_name): if check_permissions_by_view(request.user, kwargs['course_id'], fetch_content(), request.view_name):
return fn(request, *args, **kwargs) return fn(request, *args, **kwargs)
else: else:
return JsonError("unauthorized") return JsonError("unauthorized")
......
...@@ -54,7 +54,7 @@ def render_discussion(request, course_id, threads, discussion_id=None, \ ...@@ -54,7 +54,7 @@ def render_discussion(request, course_id, threads, discussion_id=None, \
'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, discussion_id])),
}[discussion_type]() }[discussion_type]()
annotated_content_info = {thread['id']: get_annotated_content_info(thread, request.user, is_thread=True) for thread in threads} annotated_content_info = {thread['id']: get_annotated_content_info(course_id, thread, request.user, is_thread=True) for thread in threads}
context = { context = {
'threads': threads, 'threads': threads,
...@@ -153,18 +153,18 @@ def forum_form_discussion(request, course_id, discussion_id): ...@@ -153,18 +153,18 @@ def forum_form_discussion(request, course_id, discussion_id):
return render_to_response('discussion/index.html', context) return render_to_response('discussion/index.html', context)
def get_annotated_content_info(content, user, is_thread): def get_annotated_content_info(course_id, content, user, is_thread):
return { return {
'editable': check_permissions_by_view(user, content, "update_thread" if is_thread else "update_comment"), 'editable': check_permissions_by_view(user, course_id, content, "update_thread" if is_thread else "update_comment"),
'can_reply': check_permissions_by_view(user, content, "create_comment" if is_thread else "create_sub_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, content, "endorse_comment") if not is_thread else False, '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, content, "delete_thread" if is_thread else "delete_comment"), 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if is_thread else "delete_comment"),
} }
def get_annotated_content_infos(thread, user, is_thread=True): def get_annotated_content_infos(course_id, thread, user, is_thread=True):
infos = {} infos = {}
def _annotate(content, is_thread=is_thread): def _annotate(content, is_thread=is_thread):
infos[str(content['id'])] = get_annotated_content_info(content, user, is_thread) infos[str(content['id'])] = get_annotated_content_info(course_id, content, user, is_thread)
for child in content.get('children', []): for child in content.get('children', []):
_annotate(child, is_thread=False) _annotate(child, is_thread=False)
_annotate(thread) _annotate(thread)
...@@ -175,7 +175,7 @@ def render_single_thread(request, discussion_id, course_id, thread_id): ...@@ -175,7 +175,7 @@ def render_single_thread(request, discussion_id, course_id, thread_id):
thread = cc.Thread.find(thread_id).retrieve_with_comments() thread = cc.Thread.find(thread_id).retrieve_with_comments()
#comment_client.get_thread(thread_id, recursive=True) #comment_client.get_thread(thread_id, recursive=True)
annotated_content_info = get_annotated_content_infos(thread=dict(thread), \ annotated_content_info = get_annotated_content_infos(course_id, thread=dict(thread), \
user=request.user, is_thread=True) user=request.user, is_thread=True)
context = { context = {
...@@ -193,7 +193,7 @@ def single_thread(request, course_id, discussion_id, thread_id): ...@@ -193,7 +193,7 @@ def single_thread(request, course_id, discussion_id, thread_id):
if request.is_ajax(): if request.is_ajax():
thread = cc.Thread.find(thread_id).retrieve_with_comments()#comment_client.get_thread(thread_id, recursive=True) thread = cc.Thread.find(thread_id).retrieve_with_comments()#comment_client.get_thread(thread_id, recursive=True)
annotated_content_info = get_annotated_content_infos(thread, request.user) annotated_content_info = get_annotated_content_infos(course_id, thread, request.user)
context = {'thread': dict(thread)} context = {'thread': dict(thread)}
html = render_to_string('discussion/_ajax_single_thread.html', context) html = render_to_string('discussion/_ajax_single_thread.html', context)
......
from django.core.management.base import BaseCommand, CommandError
from django_comment_client.models import Permission, Role
from django.contrib.auth.models import User
class Command(BaseCommand):
args = 'user role course_id'
help = 'Assign a role to a user'
def handle(self, *args, **options):
role = Role.objects.get(name=args[1], course_id=args[2])
if '@' in args[0]:
user = User.objects.get(email=args[0])
else:
user = User.objects.get(username=args[0])
user.roles.add(role)
\ No newline at end of file
from django.core.management.base import BaseCommand, CommandError
from django_comment_client.models import Permission, Role
class Command(BaseCommand):
args = ''
help = 'Seed default permisssions and roles'
def handle(self, *args, **options):
moderator_role = Role.objects.get_or_create(name="Moderator", course_id="MITx/6.002x/2012_Fall")[0]
student_role = Role.objects.get_or_create(name="Student", course_id="MITx/6.002x/2012_Fall")[0]
for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
"update_comment", "create_sub_comment", "unvote" , "create_thread",
"follow_commentable", "unfollow_commentable", "create_comment", ]:
student_role.add_permission(per)
for per in ["edit_content", "delete_thread", "openclose_thread",
"endorse_comment", "delete_comment"]:
moderator_role.add_permission(per)
moderator_role.inherit_permissions(student_role)
from django.core.management.base import BaseCommand, CommandError
from django_comment_client.models import Permission, Role
from django.contrib.auth.models import User
class Command(BaseCommand):
args = 'user'
help = "Show a user's roles and permissions"
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("The number of arguments does not match. ")
try:
if '@' in args[0]:
user = User.objects.get(email=args[0])
else:
user = User.objects.get(username=args[0])
except User.DoesNotExist:
print "User %s does not exist. " % args[0]
print "Available users: "
print User.objects.all()
return
roles = user.roles.all()
print "%s has %d roles:" % (user, len(roles))
for role in roles:
print "\t%s" % role
for role in roles:
print "%s has permissions: " % role
print role.permissions.all()
...@@ -10,7 +10,9 @@ class Migration(SchemaMigration): ...@@ -10,7 +10,9 @@ class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding model 'Role' # Adding model 'Role'
db.create_table('django_comment_client_role', ( db.create_table('django_comment_client_role', (
('name', self.gf('django.db.models.fields.CharField')(max_length=30, primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=30)),
('course_id', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=255, blank=True)),
)) ))
db.send_create_signal('django_comment_client', ['Role']) db.send_create_signal('django_comment_client', ['Role'])
...@@ -28,14 +30,6 @@ class Migration(SchemaMigration): ...@@ -28,14 +30,6 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('django_comment_client', ['Permission']) db.send_create_signal('django_comment_client', ['Permission'])
# Adding M2M table for field users on 'Permission'
db.create_table('django_comment_client_permission_users', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('permission', models.ForeignKey(orm['django_comment_client.permission'], null=False)),
('user', models.ForeignKey(orm['auth.user'], null=False))
))
db.create_unique('django_comment_client_permission_users', ['permission_id', 'user_id'])
# Adding M2M table for field roles on 'Permission' # Adding M2M table for field roles on 'Permission'
db.create_table('django_comment_client_permission_roles', ( db.create_table('django_comment_client_permission_roles', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
...@@ -55,9 +49,6 @@ class Migration(SchemaMigration): ...@@ -55,9 +49,6 @@ class Migration(SchemaMigration):
# Deleting model 'Permission' # Deleting model 'Permission'
db.delete_table('django_comment_client_permission') db.delete_table('django_comment_client_permission')
# Removing M2M table for field users on 'Permission'
db.delete_table('django_comment_client_permission_users')
# Removing M2M table for field roles on 'Permission' # Removing M2M table for field roles on 'Permission'
db.delete_table('django_comment_client_permission_roles') db.delete_table('django_comment_client_permission_roles')
...@@ -127,12 +118,13 @@ class Migration(SchemaMigration): ...@@ -127,12 +118,13 @@ class Migration(SchemaMigration):
'django_comment_client.permission': { 'django_comment_client.permission': {
'Meta': {'object_name': 'Permission'}, 'Meta': {'object_name': 'Permission'},
'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}),
'roles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['django_comment_client.Role']"}), 'roles': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['django_comment_client.Role']"})
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'permissions'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
}, },
'django_comment_client.role': { 'django_comment_client.role': {
'Meta': {'object_name': 'Role'}, 'Meta': {'object_name': 'Role'},
'name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}), 'course_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.User']"}) 'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'roles'", 'symmetrical': 'False', 'to': "orm['auth.User']"})
} }
} }
......
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
import logging
class Role(models.Model): class Role(models.Model):
name = models.CharField(max_length=30, null=False, blank=False, primary_key=True) name = models.CharField(max_length=30, null=False, blank=False)
users = models.ManyToManyField(User, related_name="roles") users = models.ManyToManyField(User, related_name="roles")
course_id = models.CharField(max_length=255, blank=True, db_index=True)
def __unicode__(self): def __unicode__(self):
return self.name return self.name + " for " + (self.course_id if self.course_id else "all courses")
@staticmethod def inherit_permissions(self, role):
def register(name): if role.course_id and role.course_id != self.course_id:
return Role.objects.get_or_create(name=name)[0] logging.warning("%s cannot inheret permissions from %s due to course_id inconsistency" %
(self, role))
for per in role.permissions.all():
self.add_permission(per)
def register_permissions(self, permissions): def add_permission(self, permission):
for p in permissions: self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
if not self.permissions.filter(name=p):
self.permissions.add(Permission.register(p))
def inherit_permissions(self, role): def has_permission(self, permission):
self.register_permissions(map(lambda p: p.name, role.permissions.all())) return self.permissions.filter(name=permission).exists()
class Permission(models.Model): class Permission(models.Model):
name = models.CharField(max_length=30, null=False, blank=False, primary_key=True) name = models.CharField(max_length=30, null=False, blank=False, primary_key=True)
users = models.ManyToManyField(User, related_name="permissions")
roles = models.ManyToManyField(Role, related_name="permissions") roles = models.ManyToManyField(Role, related_name="permissions")
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@staticmethod
def register(name):
return Permission.objects.get_or_create(name=name)[0]
...@@ -2,110 +2,98 @@ from .models import Role, Permission ...@@ -2,110 +2,98 @@ from .models import Role, Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from student.models import CourseEnrollment
import logging import logging
def has_permission(user, p):
if not Permission.objects.filter(name=p).exists(): @receiver(post_save, sender=CourseEnrollment)
logging.warning("Permission %s was not registered. " % p) def assign_default_role(sender, instance, **kwargs):
if Permission.objects.filter(users=user, name=p).exists(): if instance.user.is_staff:
return True role = Role.objects.get(course_id=instance.course_id, name="Moderator")
if Permission.objects.filter(roles__in=user.roles.all(), name=p).exists(): else:
return True role = Role.objects.get(course_id=instance.course_id, name="Student")
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
instance.user.roles.add(role)
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
return False return False
def has_permissions(user, *args):
for p in args:
if not has_permission(user, p):
return False
return True
def add_permission(instance, p): CONDITIONS = ['is_open', 'is_author']
permission = Permission.register(name=p) def check_condition(user, condition, course_id, data):
if isinstance(instance, User) or isinstance(isinstance, Role): def check_open(user, condition, course_id, data):
instance.permissions.add(permission) return not data['content']['closed']
else:
raise TypeError("Permission can only be added to a role or user")
def check_author(user, condition, course_id, data):
return data['content']['user_id'] == str(user.id)
@receiver(post_save, sender=User) handlers = {
def assign_default_role(sender, instance, **kwargs): 'is_open' : check_open,
# if kwargs.get("created", True): 'is_author' : check_author,
role = moderator_role if instance.is_staff else student_role }
logging.info("assign_default_role: adding %s as %s" % (instance, role))
instance.roles.add(role)
return handlers[condition](user, condition, course_id, data)
def check_permissions(user, content, per):
def check_conditions_permissions(user, permissions, course_id, **kwargs):
""" """
Accepts a list of permissions and proceed if any of the permission is valid. Accepts a list of permissions and proceed if any of the permission is valid.
Note that check_permissions("can_view", "can_edit") will proceed if the user has either Note that ["can_view", "can_edit"] will proceed if the user has either
"can_view" or "can_edit" permission. To use AND operator in between, wrap them in "can_view" or "can_edit" permission. To use AND operator in between, wrap them in
a list: a list.
check_permissions(["can_view", "can_edit"])
Special conditions can be used like permissions, e.g.
(["can_vote", "open"]) # where open is True if not content['closed']
""" """
permissions = filter(lambda x: len(x), list(per))
def test(user, per, operator="or"):
def test_permission(user, permission, operator="or"): if isinstance(per, basestring):
if isinstance(permission, basestring): if per in CONDITIONS:
# import pdb; pdb.set_trace() return check_condition(user, per, course_id, kwargs)
if permission == "": return has_permission(user, per, course_id=course_id)
return True elif isinstance(per, list) and operator in ["and", "or"]:
elif permission == "author": results = [test(user, x, operator="and") for x in per]
return content["user_id"] == str(user.id)
elif permission == "open":
return not content["closed"]
return has_permission(user, permission)
elif isinstance(permission, list) and operator in ["and", "or"]:
results = [test_permission(user, x, operator="and") for x in permission]
if operator == "or": if operator == "or":
return True in results return True in results
elif operator == "and": elif operator == "and":
return not False in results return not False in results
return test_permission(user, permissions, operator="or") return test(user, permissions, operator="or")
VIEW_PERMISSIONS = { VIEW_PERMISSIONS = {
'update_thread' : ('edit_content', ['update_thread', 'open', 'author']), 'update_thread' : ['edit_content', ['update_thread', 'is_open', 'author']],
'create_comment' : (["create_comment", "open"]), # 'create_comment' : [["create_comment", "is_open"]],
'delete_thread' : ('delete_thread'), 'create_comment' : ["create_comment"],
'update_comment' : ('edit_content', ['update_comment', 'open', 'author']), 'delete_thread' : ['delete_thread'],
'endorse_comment' : ('endorse_comment'), 'update_comment' : ['edit_content', ['update_comment', 'is_open', 'author']],
'openclose_thread' : ('openclose_thread'), 'endorse_comment' : ['endorse_comment'],
'create_sub_comment': (['create_sub_comment', 'open']), 'openclose_thread' : ['openclose_thread'],
'delete_comment' : ('delete_comment'), 'create_sub_comment': [['create_sub_comment', 'is_open']],
'vote_for_comment' : (['vote', 'open']), 'delete_comment' : ['delete_comment'],
'undo_vote_for_comment': (['unvote', 'open']), 'vote_for_comment' : [['vote', 'is_open']],
'vote_for_thread' : (['vote', 'open']), 'undo_vote_for_comment': [['unvote', 'is_open']],
'undo_vote_for_thread': (['unvote', 'open']), 'vote_for_thread' : [['vote', 'is_open']],
'follow_thread' : ('follow_thread'), 'undo_vote_for_thread': [['unvote', 'is_open']],
'follow_commentable': ('follow_commentable'), 'follow_thread' : ['follow_thread'],
'follow_user' : ('follow_user'), 'follow_commentable': ['follow_commentable'],
'unfollow_thread' : ('unfollow_thread'), 'follow_user' : ['follow_user'],
'unfollow_commentable': ('unfollow_commentable'), 'unfollow_thread' : ['unfollow_thread'],
'unfollow_user' : ('unfollow_user'), 'unfollow_commentable': ['unfollow_commentable'],
'create_thread' : ('create_thread'), 'unfollow_user' : ['unfollow_user'],
'create_thread' : ['create_thread'],
} }
def check_permissions_by_view(user, content, name):
def check_permissions_by_view(user, course_id, content, name):
# import pdb; pdb.set_trace()
try: try:
p = VIEW_PERMISSIONS[name] p = VIEW_PERMISSIONS[name]
except KeyError: except KeyError:
logging.warning("Permission for view named %s does not exist in permissions.py" % name) logging.warning("Permission for view named %s does not exist in permissions.py" % name)
permissions = list((p, ) if isinstance(p, basestring) else p) return check_conditions_permissions(user, p, course_id, content=content)
return check_permissions(user, content, permissions)
moderator_role = Role.register("Moderator")
student_role = Role.register("Student")
moderator_role.register_permissions(["edit_content", "delete_thread", "openclose_thread",
"endorse_comment", "delete_comment"])
student_role.register_permissions(["vote", "update_thread", "follow_thread", "unfollow_thread",
"update_comment", "create_sub_comment", "unvote" , "create_thread",
"follow_commentable", "unfollow_commentable", "create_comment", ])
moderator_role.inherit_permissions(student_role)
\ No newline at end of file
...@@ -174,6 +174,9 @@ MODULESTORE = { ...@@ -174,6 +174,9 @@ MODULESTORE = {
} }
} }
############################ SIGNAL HANDLERS ################################
# This is imported to register the exception signal handling that logs exceptions
import monitoring.exceptions # noqa
############################### DJANGO BUILT-INS ############################### ############################### DJANGO BUILT-INS ###############################
# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here
...@@ -286,7 +289,6 @@ TEMPLATE_LOADERS = ( ...@@ -286,7 +289,6 @@ TEMPLATE_LOADERS = (
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'util.middleware.ExceptionLoggingMiddleware',
'django_comment_client.middleware.AjaxExceptionMiddleware', 'django_comment_client.middleware.AjaxExceptionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
......
/Users/dementrock/coding/cs_comments_client_python/comment_client
\ No newline at end of file
import requests
import json
SERVICE_HOST = 'http://localhost:4567'
PREFIX = SERVICE_HOST + '/api/v1'
class CommentClientError(Exception):
def __init__(self, msg):
self.message = msg
class CommentClientUnknownError(CommentClientError):
pass
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, query_params={}, *args, **kwargs):
default_params = {'page': 1, 'per_page': 20, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
response = _perform_request('get', _url_for_threads(commentable_id), \
attributes, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def search_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'page': 1, 'per_page': 20, 'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
response = _perform_request('get', _url_for_search_threads(), \
attributes, *args, **kwargs)
return response.get('collection', []), response.get('page', 1), response.get('num_pages', 1)
def search_similar_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
return _perform_request('get', _url_for_search_similar_threads(), attributes, *args, **kwargs)
def search_recent_active_threads(course_id, recursive=False, query_params={}, *args, **kwargs):
default_params = {'course_id': course_id, 'recursive': recursive}
attributes = dict(default_params.items() + query_params.items())
return _perform_request('get', _url_for_search_recent_active_threads(), attributes, *args, **kwargs)
def search_trending_tags(course_id, query_params={}, *args, **kwargs):
default_params = {'course_id': course_id}
attributes = dict(default_params.items() + query_params.items())
return _perform_request('get', _url_for_search_trending_tags(), attributes, *args, **kwargs)
def create_user(attributes, *args, **kwargs):
return _perform_request('post', _url_for_users(), attributes, *args, **kwargs)
def update_user(user_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_user(user_id), attributes, *args, **kwargs)
def get_threads_tags(*args, **kwargs):
return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs)
def tags_autocomplete(value, *args, **kwargs):
return _perform_request('get', _url_for_threads_tags_autocomplete(), {'value': value}, *args, **kwargs)
def create_thread(commentable_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_threads(commentable_id), attributes, *args, **kwargs)
def get_thread(thread_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_thread(thread_id), {'recursive': recursive}, *args, **kwargs)
def update_thread(thread_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_thread(thread_id), attributes, *args, **kwargs)
def create_comment(thread_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_thread_comments(thread_id), attributes, *args, **kwargs)
def delete_thread(thread_id, *args, **kwargs):
return _perform_request('delete', _url_for_thread(thread_id), *args, **kwargs)
def get_comment(comment_id, recursive=False, *args, **kwargs):
return _perform_request('get', _url_for_comment(comment_id), {'recursive': recursive}, *args, **kwargs)
def update_comment(comment_id, attributes, *args, **kwargs):
return _perform_request('put', _url_for_comment(comment_id), attributes, *args, **kwargs)
def create_sub_comment(comment_id, attributes, *args, **kwargs):
return _perform_request('post', _url_for_comment(comment_id), attributes, *args, **kwargs)
def delete_comment(comment_id, *args, **kwargs):
return _perform_request('delete', _url_for_comment(comment_id), *args, **kwargs)
def vote_for_comment(comment_id, user_id, value, *args, **kwargs):
return _perform_request('put', _url_for_vote_comment(comment_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
def undo_vote_for_comment(comment_id, user_id, *args, **kwargs):
return _perform_request('delete', _url_for_vote_comment(comment_id), {'user_id': user_id}, *args, **kwargs)
def vote_for_thread(thread_id, user_id, value, *args, **kwargs):
return _perform_request('put', _url_for_vote_thread(thread_id), {'user_id': user_id, 'value': value}, *args, **kwargs)
def undo_vote_for_thread(thread_id, user_id, *args, **kwargs):
return _perform_request('delete', _url_for_vote_thread(thread_id), {'user_id': user_id}, *args, **kwargs)
def get_notifications(user_id, *args, **kwargs):
return _perform_request('get', _url_for_notifications(user_id), *args, **kwargs)
def get_user_info(user_id, complete=True, *args, **kwargs):
return _perform_request('get', _url_for_user(user_id), {'complete': complete}, *args, **kwargs)
def subscribe(user_id, subscription_detail, *args, **kwargs):
return _perform_request('post', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
def subscribe_user(user_id, followed_user_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
follow = subscribe_user
def subscribe_thread(user_id, thread_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
def subscribe_commentable(user_id, commentable_id, *args, **kwargs):
return subscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def unsubscribe(user_id, subscription_detail, *args, **kwargs):
return _perform_request('delete', _url_for_subscription(user_id), subscription_detail, *args, **kwargs)
def unsubscribe_user(user_id, followed_user_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'user', 'source_id': followed_user_id})
unfollow = unsubscribe_user
def unsubscribe_thread(user_id, thread_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'thread', 'source_id': thread_id})
def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
def _perform_request(method, url, data_or_params=None, *args, **kwargs):
if method in ['post', 'put', 'patch']:
response = requests.request(method, url, data=data_or_params)
else:
response = requests.request(method, url, params=data_or_params)
if 200 < response.status_code < 500:
raise CommentClientError(response.text)
elif response.status_code == 500:
raise CommentClientUnknownError(response.text)
else:
if kwargs.get("raw", False):
return response.text
else:
return json.loads(response.text)
def _url_for_threads(commentable_id):
return "{prefix}/{commentable_id}/threads".format(prefix=PREFIX, commentable_id=commentable_id)
def _url_for_thread(thread_id):
return "{prefix}/threads/{thread_id}".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_thread_comments(thread_id):
return "{prefix}/threads/{thread_id}/comments".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_comment(comment_id):
return "{prefix}/comments/{comment_id}".format(prefix=PREFIX, comment_id=comment_id)
def _url_for_vote_comment(comment_id):
return "{prefix}/comments/{comment_id}/votes".format(prefix=PREFIX, comment_id=comment_id)
def _url_for_vote_thread(thread_id):
return "{prefix}/threads/{thread_id}/votes".format(prefix=PREFIX, thread_id=thread_id)
def _url_for_notifications(user_id):
return "{prefix}/users/{user_id}/notifications".format(prefix=PREFIX, user_id=user_id)
def _url_for_subscription(user_id):
return "{prefix}/users/{user_id}/subscriptions".format(prefix=PREFIX, user_id=user_id)
def _url_for_user(user_id):
return "{prefix}/users/{user_id}".format(prefix=PREFIX, user_id=user_id)
def _url_for_search_threads():
return "{prefix}/search/threads".format(prefix=PREFIX)
def _url_for_search_similar_threads():
return "{prefix}/search/threads/more_like_this".format(prefix=PREFIX)
def _url_for_search_recent_active_threads():
return "{prefix}/search/threads/recent_active".format(prefix=PREFIX)
def _url_for_search_trending_tags():
return "{prefix}/search/tags/trending".format(prefix=PREFIX)
def _url_for_threads_tags():
return "{prefix}/threads/tags".format(prefix=PREFIX)
def _url_for_threads_tags_autocomplete():
return "{prefix}/threads/tags/autocomplete".format(prefix=PREFIX)
def _url_for_users():
return "{prefix}/users".format(prefix=PREFIX)
...@@ -183,6 +183,8 @@ initializeFollowThread = (thread) -> ...@@ -183,6 +183,8 @@ initializeFollowThread = (thread) ->
handleEndorse = (elem, endorsed) -> handleEndorse = (elem, endorsed) ->
url = Discussion.urlFor('endorse_comment', id) url = Discussion.urlFor('endorse_comment', id)
console.log endorsed
console.log url
Discussion.safeAjax Discussion.safeAjax
$elem: $(elem) $elem: $(elem)
url: url url: url
...@@ -196,6 +198,9 @@ initializeFollowThread = (thread) -> ...@@ -196,6 +198,9 @@ initializeFollowThread = (thread) ->
else else
$(content).removeClass("endorsed") $(content).removeClass("endorsed")
$(elem).unbind('click').click ->
handleEndorse(elem, !endorsed)
handleOpenClose = (elem, text) -> handleOpenClose = (elem, text) ->
url = Discussion.urlFor('openclose_thread', id) url = Discussion.urlFor('openclose_thread', id)
closed = undefined closed = undefined
...@@ -314,19 +319,19 @@ initializeFollowThread = (thread) -> ...@@ -314,19 +319,19 @@ initializeFollowThread = (thread) ->
else else
handleVote($elem, "down") handleVote($elem, "down")
"click .discussion-endorse": -> "click .admin-endorse": ->
handleEndorse(this, $(this).is(":checked")) handleEndorse(this, true)#, $(this).is(":checked"))
"click .discussion-openclose": -> "click .discussion-openclose": ->
handleOpenClose(this, $(this).text()) handleOpenClose(this, $(this).text())
"click .discussion-edit": -> "click .admin-edit": ->
if $content.hasClass("thread") if $content.hasClass("thread")
handleEditThread(this) handleEditThread(this)
else else
handleEditComment(this) handleEditComment(this)
"click .discussion-delete": -> "click .admin-delete": ->
handleDelete(this) handleDelete(this)
initializeContent: (content) -> initializeContent: (content) ->
......
...@@ -36,7 +36,7 @@ initializeFollowDiscussion = (discussion) -> ...@@ -36,7 +36,7 @@ initializeFollowDiscussion = (discussion) ->
title = $local(".new-post-title").val() title = $local(".new-post-title").val()
body = Discussion.getWmdContent $discussion, $local, "new-post-body" body = Discussion.getWmdContent $discussion, $local, "new-post-body"
tags = $local(".new-post-tags").val() tags = $local(".new-post-tags").val()
url = Discussion.urlFor('create_thread', $local(".new-post-form").attr("_id")) url = Discussion.urlFor('create_thread', id)
Discussion.safeAjax Discussion.safeAjax
$elem: $(elem) $elem: $(elem)
url: url url: url
...@@ -95,33 +95,33 @@ initializeFollowDiscussion = (discussion) -> ...@@ -95,33 +95,33 @@ initializeFollowDiscussion = (discussion) ->
$wrapper.hide() $wrapper.hide()
$title.attr("prev-text", text) $title.attr("prev-text", text)
handleNewPost = (elem) -> initializeNewPost = (elem) ->
newPostForm = $local(".new-post-form") #newPostForm = $local(".new-post-form")
if newPostForm.length #view = { discussion_id: id }
newPostForm.show() #$newPostButton = $local(".discussion-new-post")
$(elem).hide() #$newPostButton.after Mustache.render Discussion.newPostTemplate, view
else newPostBody = $discussion.find(".new-post-body")
view = { discussion_id: id } if newPostBody.length
$newPostButton = $local(".discussion-new-post") Discussion.makeWmdEditor $discussion, $local, "new-post-body"
$newPostButton.after Mustache.render Discussion.newPostTemplate, view
newPostBody = $discussion.find(".new-post-body") $input = Discussion.getWmdInput($discussion, $local, "new-post-body")
if newPostBody.length $input.attr("placeholder", "post a new topic...").bind 'focus', (e) ->
Discussion.makeWmdEditor $discussion, $local, "new-post-body" $local(".new-post-form").removeClass('collapsed')
$local(".new-post-tags").tagsInput Discussion.tagsInputOptions() $local(".new-post-tags").tagsInput Discussion.tagsInputOptions()
$local(".new-post-title").blur -> $local(".new-post-title").blur ->
handleSimilarPost(this) handleSimilarPost(this)
$local(".hide-similar-posts").click -> $local(".hide-similar-posts").click ->
$local(".new-post-similar-posts-wrapper").hide() $local(".new-post-similar-posts-wrapper").hide()
$local(".discussion-submit-post").click -> $local(".discussion-submit-post").click ->
handleSubmitNewPost(this) handleSubmitNewPost(this)
$local(".discussion-cancel-post").click -> $local(".discussion-cancel-post").click ->
handleCancelNewPost(this) handleCancelNewPost(this)
$(elem).hide() #$(elem).hide()
handleAjaxReloadDiscussion = (elem, url) -> handleAjaxReloadDiscussion = (elem, url) ->
$elem = $(elem) $elem = $(elem)
...@@ -151,6 +151,8 @@ initializeFollowDiscussion = (discussion) -> ...@@ -151,6 +151,8 @@ initializeFollowDiscussion = (discussion) ->
$elem = $(elem) $elem = $(elem)
url = $elem.attr("page-url") url = $elem.attr("page-url")
handleAjaxReloadDiscussion($elem, url) handleAjaxReloadDiscussion($elem, url)
initializeNewPost()
Discussion.bindLocalEvents $local, Discussion.bindLocalEvents $local,
...@@ -158,8 +160,8 @@ initializeFollowDiscussion = (discussion) -> ...@@ -158,8 +160,8 @@ initializeFollowDiscussion = (discussion) ->
event.preventDefault() event.preventDefault()
handleAjaxSearch(this) handleAjaxSearch(this)
"click .discussion-new-post": -> #"click .discussion-new-post": ->
handleNewPost(this) # handleNewPost(this)
"click .discussion-search-link": -> "click .discussion-search-link": ->
handleAjaxSearch($local(".search-wrapper>.discussion-search-form")) handleAjaxSearch($local(".search-wrapper>.discussion-search-form"))
......
...@@ -116,20 +116,26 @@ wmdEditors = {} ...@@ -116,20 +116,26 @@ wmdEditors = {}
appended_id = "-#{cls_identifier}-#{id}" appended_id = "-#{cls_identifier}-#{id}"
imageUploadUrl = Discussion.urlFor('upload') imageUploadUrl = Discussion.urlFor('upload')
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, Discussion.postMathJaxProcessor editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, Discussion.postMathJaxProcessor
console.log editor
wmdEditors["#{cls_identifier}-#{id}"] = editor wmdEditors["#{cls_identifier}-#{id}"] = editor
$input = $("#wmd-input-#{cls_identifier}-#{id}")
$input.attr("placeholder", "post a new topic...").bind 'focus', (e) ->
$('.new-post-form').removeClass('collapsed')
editor editor
getWmdEditor: ($content, $local, cls_identifier) -> getWmdEditor: ($content, $local, cls_identifier) ->
id = $content.attr("_id") id = $content.attr("_id")
wmdEditors["#{cls_identifier}-#{id}"] wmdEditors["#{cls_identifier}-#{id}"]
getWmdContent: ($content, $local, cls_identifier) -> getWmdInput: ($content, $local, cls_identifier) ->
id = $content.attr("_id") id = $content.attr("_id")
$local("#wmd-input-#{cls_identifier}-#{id}").val() $local("#wmd-input-#{cls_identifier}-#{id}")
getWmdContent: ($content, $local, cls_identifier) ->
Discussion.getWmdInput($content, $local, cls_identifier).val()
setWmdContent: ($content, $local, cls_identifier, text) -> setWmdContent: ($content, $local, cls_identifier, text) ->
id = $content.attr("_id") Discussion.getWmdInput($content, $local, cls_identifier).val(text)
$local("#wmd-input-#{cls_identifier}-#{id}").val(text)
wmdEditors["#{cls_identifier}-#{id}"].refreshPreview() wmdEditors["#{cls_identifier}-#{id}"].refreshPreview()
getContentInfo: (id, attr) -> getContentInfo: (id, attr) ->
......
$comment_margin_left: 30px; /*** Variables ***/
$discussion_title_size: 1.6em;
$comment_title_size: 1.0em; $comment-margin-left: 30px;
$comment_body_size: 0.9em; $discussion-title-size: 1.6em;
$comment_info_size: 0.75em; $comment-title-size: 1.0em;
$discussion_input_width: 100%; $post-font-size: 0.9em;
$comment-info-size: 0.75em;
$comment-font-size: 0.8em;
$discussion-input-width: 100%;
$tag-background-color: #e7ecdd;
$tag-border-color: #babdb3;
$tag-text-color: #5b614f;
/*** Mixins ***/
@mixin discussion-font { @mixin discussion-font {
font-family: inherit; font-family: inherit;
...@@ -16,132 +27,413 @@ $discussion_input_width: 100%; ...@@ -16,132 +27,413 @@ $discussion_input_width: 100%;
} }
} }
form label { @mixin standard-discussion-link {
@include discussion-font; text-decoration: none;
font-size: 14px; &:hover {
font-style: normal; color: #1C71DD;
font-weight: normal; text-decoration: none;
text-shadow: none; }
} }
div.recent-activity {
margin: 20% 4%;
div.recent-activity-title {
font-weight: bold;
}
a.recent-active-post {
display: block;
font-size: $comment_body_size;
line-height: 120%;
margin-top: 8px;
&:hover {
color: #1C71DD; /*** Discussions ***/
text-decoration: none;
font-weight: normal; .discussion {
}
#open_close_accordion {
display: none;
} }
}
div.trending-tags { p + p, ul + p, ol + p {
font-weight: bold; margin-top: 0;
margin: 15% 0 0 0;
.trending-tag {
margin-top: 5px;
} }
.trending-tag-link {
@include discussion-font;
background: #CDE69C; /*** Sidebar ***/
border: 1px solid #A5D24A;
-moz-border-radius: 2px; .sidebar-module {
-webkit-border-radius: 2px; @include clearfix;
color: #638421; padding: 0 24px 24px 0;
display: block; margin-bottom: 24px;
float: left; border-bottom: 1px solid #d3d3d3;
font-size: 13px; font-size: 0.8em;
margin-right: 4px;
padding: 5px 7px; header {
text-decoration: none; margin-bottom: 14px;
@include clearfix;
&:hover { }
border-color: #1E4612;
color: #1E4612; h4 {
text-decoration: none; float: left;
font-size: 1.1em;
font-weight: bold;
}
.sidebar-new-post-button {
@include button;
display: block;
box-sizing: border-box;
width: 100%;
margin: 20px 0;
padding: 11px;
font-size: 1.1em;
text-align: center;
&:hover {
text-decoration: none;
}
}
.sidebar-view-all {
float: right;
font-size: 0.9em;
line-height: 1.6em;
@include standard-discussion-link;
}
.discussion-sidebar-following-list {
li {
@include clearfix;
margin-bottom: 8px;
}
a {
@include standard-discussion-link;
}
}
.discussion-sidebar-tags-list li {
@include clearfix;
}
.sidebar-tag-count {
color: #9a9a9a;
font-size: .85em;
line-height: 3em;
}
.sidebar-following-name {
float: left;
width: 80%;
}
.sidebar-vote-count {
float: right;
width: 20%;
text-align: right;
color: #9a9a9a;
} }
} }
}
.discussion {
.discussion-non-content { .discussion-non-content {
margin-left: flex-gutter(); margin-left: flex-gutter();
} }
//TITLE /*** Post ***/
.discussion-title { .discussion-title {
@include discussion-font; @include discussion-font;
@include discussion-clickable; @include discussion-clickable;
display: inline-block; display: inline-block;
font-size: $discussion_title_size; font-size: $discussion-title-size;
font-weight: bold; font-weight: bold;
margin-bottom: flex-gutter(6); margin-bottom: flex-gutter(6);
} }
.discussion-title-wrapper { .discussion-title-wrapper {
.discussion-watch-discussion, .discussion-unwatch-discussion { .discussion-watch-discussion, .discussion-unwatch-discussion {
@include discussion-font; @include discussion-font;
display: none; display: none;
font-size: $comment_info_size; font-size: $comment-info-size;
margin-left: 5px; margin-left: 5px;
} }
} }
.blank { .discussion-right-wrapper {
margin: 2%; min-height: 40px;
margin: 24px 0 24px 68px;
} }
.admin-actions {
float: right;
margin: 0.4em 1em 0 2em;
padding: 0;
li {
margin-bottom: 6px !important;
}
a {
display: block;
height: 25px;
padding-left: 25px;
border-radius: 50%;
background: url(../images/admin-actions-sprite.png) no-repeat;
font-size: .8em;
line-height: 25px;
color: #b8b8b8;
@include transition(color, .1s);
&:hover {
text-decoration: none;
}
&.admin-endorse {
background-position: 0 0;
&:hover {
color: #63b141;
background-position: 0 -75px;
}
}
&.admin-edit {
background-position: 0 -25px;
&:hover {
color: #009fe2;
background-position: 0 -100px;
}
}
&.admin-delete {
background-position: 0 -50px;
&:hover {
color: #d45050;
background-position: 0 -125px;
}
}
}
}
.comments {
.admin-actions {
margin-top: 0;
li {
margin-bottom: 2px !important;
}
a {
width: 20px;
height: 20px;
padding-left: 0;
overflow: hidden;
text-indent: -9999px;
&.admin-endorse {
background-position: 0 -150px;
&:hover {
background-position: 0 -225px;
}
}
&.admin-edit {
background-position: 0 -175px;
&:hover {
background-position: 0 -250px;
}
}
&.admin-delete {
background-position: 0 -200px;
&:hover {
background-position: 0 -275px;
}
}
}
}
}
/*** thread ***/
//SORTING .thread {
//display: none;
.thread-title {
@include discussion-font;
@include discussion-clickable;
display: block;
margin-bottom: 1em;
font-size: $comment-title-size;
font-weight: bold;
line-height: 1.4em;
}
.thread-body, .content-body {
@include discussion-font;
font-size: $post-font-size;
margin-bottom: 4px;
margin-top: 3px;
p {
@include discussion-font;
}
}
.thread-tags {
display: inline-block;
}
.info {
@include discussion-font;
color: gray;
font-size: $comment-info-size;
font-style: italic;
margin-top: 1em;
a:hover {
text-decoration: none;
color: #1C71DD;
}
.comment-time {
display: inline;
float: right;
margin-right: 1em;
}
.comment-count {
display: inline;
margin-right: 20px;
}
.discussion-actions {
display: inline;
margin: 0;
padding: 0;
li {
display: inline;
margin-right: 20px;
}
}
.discussion-link {
@include discussion-font;
color: #1d9dd9;
display: inline;
&.discussion-unfollow-thread {
color: #dea03e;
}
}
}
.discussion-content {
border-top: lightgray 1px solid;
overflow: hidden;
// padding: 1.5% 0;
.discussion-reply-new {
@include discussion-font;
margin-left: 68px;
.reply-body {
@include discussion-font;
display: block;
font-size: $post-font-size;
margin-top: 10px;
width: 95%;
}
}
}
//COMMENT STYLES
.comments {
overflow: hidden;
.discussion-votes {
margin-top: 8px;
}
.discussion-right-wrapper {
margin: 10px 0 10px 68px;
}
.comment {
margin-left: 68px;
.comment-body, .content-body {
@include discussion-font;
color: black;
display: block;
font-size: $comment-font-size;
margin-top: 3px;
}
&.endorsed {
> .discussion-content {
background-color: #fcfcea;
}
}
}
}
}
/*** Sorting ***/
.discussion-sort { .discussion-sort {
float: right; float: right;
font-size: $comment_body_size; font-size: 0.8em;
margin-top: -2.5%; margin-top: -36px;
.discussion-label {
display: block;
float: left;
padding: 0 14px;
line-height: 34px;
}
.discussion-sort-link { .discussion-sort-link {
display: block;
float: left;
padding: 0 14px;
line-height: 34px;
&:hover { &:hover {
color: #1C71DD; color: #1C71DD;
text-decoration: none; text-decoration: none;
} }
} }
.discussion-sort-link.sorted { .discussion-sort-link.sorted {
color: #1C71DD; color: #000;
font-weight: bold; border-bottom: 2px solid #000;
} }
} }
//SEARCH /*** Search ***/
.search-wrapper-inline { .search-wrapper-inline {
display: inline-block; display: inline-block;
margin-bottom: 6%; margin-bottom: 6%;
margin-top: 3%; margin-top: 3%;
width: 80%; width: 80%;
} }
.search-wrapper {
height: 110px;
}
.discussion-search-form { .discussion-search-form {
display: inline-block; display: inline-block;
margin-bottom: 1%; margin-bottom: 1%;
width: flex-grid(12); width: flex-grid(12);
.discussion-link { .discussion-link {
@include button; @include button(simple, #999);
color: white; color: white;
display: inline-block; display: inline-block;
font-size: inherit; font-size: inherit;
...@@ -150,46 +442,41 @@ div.trending-tags { ...@@ -150,46 +442,41 @@ div.trending-tags {
padding-top: 9px; padding-top: 9px;
text-decoration: none; text-decoration: none;
} }
.discussion-search-text { .discussion-search-text {
@include discussion-font; @include discussion-font;
} }
.search-input { .search-input {
float: left; float: left;
font: inherit; font: inherit;
font-style: normal; font-style: normal;
width: 72%; // width: 72%;
width: flex-grid(8);
margin-left: flex-grid(1);
} }
} }
.search-within { .search-within {
display: block; display: block;
margin-bottom: 3%; margin-bottom: 3%;
} }
.discussion-search-within-board { .discussion-search-within-board {
font: inherit; font: inherit;
font-size: $comment_body_size; font-size: $post-font-size;
font-style: normal; font-style: normal;
} }
//BASIC BUTTON STYLES /*** buttons ***/
.control-button { .control-button {
@include button; @include button;
@include discussion-font; @include discussion-font;
background-color: #959595; background-color: #959595;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #959595),color-stop(100%, #7B7B7B)); @include background-image(linear-gradient(top, #959595, #7B7B7B));
background-image: -webkit-linear-gradient(top, #959595,#7B7B7B);
background-image: -moz-linear-gradient(top, #959595,#7B7B7B);
background-image: -ms-linear-gradient(top, #959595,#7B7B7B);
background-image: -o-linear-gradient(top, #959595,#7B7B7B);
background-image: linear-gradient(top, #959595,#7B7B7B);
border: 1px solid #6F6F6F; border: 1px solid #6F6F6F;
-webkit-box-shadow: inset 0 1px 0 #A2A2A2,0 0 3px #CCC; @include box-shadow(inset 0 1px 0 #A2A2A2, 0 0 3px #CCC);
-moz-box-shadow: inset 0 1px 0 #a2a2a2,0 0 3px #ccc;
box-shadow: inset 0 1px 0 #A2A2A2,0 0 3px #CCC;
color: white; color: white;
display: inline-block; display: inline-block;
font-size: inherit; font-size: inherit;
...@@ -199,132 +486,106 @@ div.trending-tags { ...@@ -199,132 +486,106 @@ div.trending-tags {
width: inherit; width: inherit;
text-decoration: none; text-decoration: none;
text-shadow: none; text-shadow: none;
&:hover { &:hover {
background-color: #A2A2A2; background-color: #A2A2A2;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #A2A2A2),color-stop(100%, #7B7B7B)); @include background-image(linear-gradient(top, #A2A2A2, #7B7B7B));
background-image: -webkit-linear-gradient(top, #A2A2A2,#7B7B7B);
background-image: -moz-linear-gradient(top, #A2A2A2,#7B7B7B);
background-image: -ms-linear-gradient(top, #A2A2A2,#7B7B7B);
background-image: -o-linear-gradient(top, #A2A2A2,#7B7B7B);
background-image: linear-gradient(top, #A2A2A2,#7B7B7B);
border: 1px solid #555; border: 1px solid #555;
-webkit-box-shadow: inset 0 1px 0 #BBB,0 0 3px #CCC; @include box-shadow(inset 0 1px 0 #BBB, 0 0 3px #CCC);
-moz-box-shadow: inset 0 1px 0 #bbb,0 0 3px #ccc;
box-shadow: inset 0 1px 0 #BBB,0 0 3px #CCC;
} }
} }
//FOLLOW BUTTON
.follow-wrapper { .follow-wrapper {
float: right; display: inline;
.discussion-link {
@include button;
background-color: #BEBEBE;
background-image: none;
border: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
display: inline-block;
font-size: $comment_info_size;
padding: 5px 8px;
text-decoration: none;
text-shadow: none;
&:hover {
background-color: #AAA;
background-image: none;
border: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
}
} }
//VOTES /*** votes ***/
.discussion-votes { .discussion-votes {
float: left; float: left;
height: flex-grid(3); width: 60px;
margin: 1% 2%; margin-top: 18px;
text-align: center; text-align: center;
.discussion-vote-count { .discussion-vote {
@include discussion-font; display: block;
font-size: $comment_body_size; width: 50px;
} height: 17px;
margin: auto;
a.discussion-vote { background: url(../images/vote-arrows.png) no-repeat;
color: black;
display: block;
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
color: black;
@include hide-text;
@include transition(all, 0, easeOut);
}
.discussion-vote-up {
margin-bottom: 5px;
background-position: -50px -3px;
&:hover { &:hover {
color: #1C71DD; background-position: -50px -5px;
text-decoration: none; @include transition-duration(0.05s);
} }
&.discussion-vote-up { &.voted {
margin-bottom: 3px; background-position: 0 -3px;
color: #1C71DD;
@include transition-duration(0);
} }
}
&.discussion-vote-down {
margin-top: 5px; .discussion-vote-down {
margin-top: 7px;
background-position: -50px -30px;
&:hover {
background-position: -50px -28px;
@include transition-duration(0.05s);
} }
&.voted { &.voted {
background-position: 0 -30px;
color: #1C71DD; color: #1C71DD;
@include transition-duration(0);
} }
}
.discussion-vote-count {
@include discussion-font;
font-size: $post-font-size;
} }
}
//CREATE NEW AND EDIT POSTS .discussion-votes-point {
font-size: 1.1em;
.discussion-right-wrapper { font-weight: bold;
float: left; color: #9a9a9a;
min-height: 40px; }
width: 90%;
} }
/*** new post ***/
.new-post-form, .discussion-thread-edit { .new-post-form, .discussion-thread-edit {
.title-input, .body-input { .title-input, .body-input {
display: block !important; display: block !important;
font: inherit; font: inherit;
font-style: normal; font-style: normal;
width: $discussion_input_width !important; width: $discussion-input-width !important;
}
.new-post-similar-posts-wrapper {
@include border-radius(3px);
border: 1px solid #EEE;
font-size: $comment_body_size;
line-height: 150%;
margin-top: 1%;
padding: 1% 1.5%;
}
.hide-similar-posts {
float: right;
} }
.new-post-similar-posts { .discussion-errors {
font: inherit; color: #8F0E0E;
.similar-post { display: block;
display: block; margin-left: -5%;
line-height: 150%;
}
} }
.new-post-body { .new-post-body {
margin-top: flex-gutter(); margin-top: flex-gutter();
} }
.tagsinput { .tagsinput {
background: #FAFAFA; background: #FAFAFA;
border: 1px solid #C8C8C8; border: 1px solid #C8C8C8;
...@@ -352,7 +613,7 @@ div.trending-tags { ...@@ -352,7 +613,7 @@ div.trending-tags {
color: #8F0E0E; color: #8F0E0E;
display: block; display: block;
font: inherit; font: inherit;
font-size: $comment_body_size; font-size: $post_font_size;
list-style: none; list-style: none;
margin-left: -3%; margin-left: -3%;
padding-left: 2em; padding-left: 2em;
...@@ -362,30 +623,51 @@ div.trending-tags { ...@@ -362,30 +623,51 @@ div.trending-tags {
color: #1C71DD; color: #1C71DD;
text-decoration: none; text-decoration: none;
}; };
&.collapsed {
.wmd-button-row {
height: 0;
}
.wmd-input {
height: 100px;
@include border-radius(3px);
}
.wmd-preview {
height: 0;
padding: 0;
border-width: 0;
}
.post-options {
height: 0;
}
.reply-post-control {
height: 0;
}
}
.new-post-control { .new-post-control {
margin-left: 80%; margin-left: 80%;
margin-top: 1%; margin-top: 1%;
} }
.reply-post-control {
margin-left: 73%;
}
.edit-post-control { .edit-post-control {
margin-left: 79%; margin-left: 79%;
margin-top: 1%; margin-top: 1%;
} }
.control-button { .control-button {
@include button; @include button;
@include discussion-font; @include discussion-font;
margin-right: 16px;
padding-top: 9px;
color: white; color: white;
display: inline-block; display: inline-block;
font-size: inherit; font-size: inherit;
font-weight: bold; font-weight: bold;
margin-left: 5%;
padding-top: 9px;
text-decoration: none; text-decoration: none;
width: inherit; width: inherit;
...@@ -393,12 +675,19 @@ div.trending-tags { ...@@ -393,12 +675,19 @@ div.trending-tags {
color: white; color: white;
} }
} }
label {
font-family: $sans-serif;
font-size: .8em;
font-style: normal;
font-weight: 400;
}
} }
.new-post-form { .new-post-form {
margin: 10px 0 40px 0; margin: 10px 0 40px 0;
} }
.discussion-reply-new { .discussion-reply-new {
.discussion-auto-watch { .discussion-auto-watch {
...@@ -406,162 +695,38 @@ div.trending-tags { ...@@ -406,162 +695,38 @@ div.trending-tags {
} }
} }
//THREAD STYLES .thread-tag {
background: $tag-background-color;
.thread { border: 1px solid $tag-border-color;
//display: none; -moz-border-radius: 2px;
-webkit-border-radius: 2px;
.search-highlight { color: $tag-text-color;
display: inline; float: left;
font-weight: bold; font-size: 13px;
background-color: lightyellow; margin: 5px 7px 5px 0;
} padding: 5px 7px;
text-decoration: none;
.thread-title {
@include discussion-font;
@include discussion-clickable;
display: block;
font-size: $comment_title_size;
font-weight: bold;
}
.thread-body, .content-body {
@include discussion-font;
font-size: $comment_body_size;
margin-bottom: 4px;
margin-top: 3px;
min-height: 35px;
p {
@include discussion-font;
margin: 0;
}
}
.thread-tags {
display: inline-block;
.thread-tag { &:hover {
@include discussion-font; border-color: #7b8761;
background: #CDE69C; color: #2f381c;
border: 1px solid #A5D24A; text-decoration: none;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
color: #638421;
float: left;
font-size: 13px;
margin: 5px 7px 5px 0;
padding: 5px 7px;
text-decoration: none;
&:hover {
border-color: #1E4612;
color: #1E4612;
}
}
} }
}
.info { /*** pagination ***/
@include discussion-font;
color: gray;
font-size: $comment_info_size;
font-style: italic;
margin-top: 2%;
a {
&:hover {
text-decoration: none;
color: #1C71DD;
}
}
.comment-time {
display: inline;
float: right;
margin-right: -4%;
}
.comment-count {
display: inline;
}
.discussion-reply {
margin-left: 4px;
}
.discussion-link {
@include discussion-font;
color: #1d9dd9;
display: inline;
margin-left: 2px;
&:hover {
text-decoration: none;
color: #1C71DD;
}
}
}
.discussion-content {
border-top: lightgray 1px solid;
overflow: hidden;
padding: 1.5% 0;
.discussion-reply-new {
@include discussion-font;
margin-left: 5%;
.reply-body {
@include discussion-font;
display: block;
font-size: $comment_body_size;
margin-top: 10px;
width: 95%;
}
}
}
//COMMENT STYLES
.comments {
//display: none;
margin-left: $comment_margin_left;
overflow: hidden;
.comment {
.comment-body, .content-body {
@include discussion-font;
color: black;
display: block;
font-size: $comment_body_size;
margin-top: 3px;
}
&.endorsed {
> .discussion-content {
background-color: lightyellow;
}
}
}
}
}
//PAGES
.discussion-paginator { .discussion-paginator {
font-size: $comment_body_size; font-size: $post-font-size;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 20px; margin-top: 20px;
text-align: center; text-align: center;
div { div {
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
margin: 0 5px; margin: 0 5px;
a { a {
background: #EEE; background: #EEE;
-webkit-border-radius: 3px; -webkit-border-radius: 3px;
...@@ -573,45 +738,92 @@ div.trending-tags { ...@@ -573,45 +738,92 @@ div.trending-tags {
font-weight: normal; font-weight: normal;
padding: 4px 10px; padding: 4px 10px;
text-decoration: none; text-decoration: none;
&:hover { &:hover {
background: #DDD; background: #DDD;
} }
} }
} }
} }
&.inline-discussion {
.new-post-form {
margin: 24px 60px;
.post-options {
margin: 8px 0 16px 0;
overflow: hidden;
label {
margin-right: 15px;
}
}
.reply-post-control {
overflow: hidden;
}
}
}
} }
//EDITOR STYLES
.wmd-panel
{ /*** base editor styles ***/
.wmd-panel {
width: 100%; width: 100%;
min-width: 500px; min-width: 500px;
} }
.wmd-button-bar .wmd-button-bar {
{
width: 100%; width: 100%;
background-color: Silver; background-color: Silver;
} }
.wmd-input .wmd-input {
{
height: 150px; height: 150px;
width: 100%; width: 100%;
background-color: Gainsboro; background-color: #e9e9e9;
border: 1px solid DarkGray; border: 1px solid #c8c8c8;
font: inherit; font-family: Monaco, 'Lucida Console', monospace;
font-style: normal;
font-size: 0.8em;
line-height: 1.6em;
@include border-radius(3px 3px 0 0);
&::-webkit-input-placeholder {
color: #888;
}
} }
.wmd-preview .wmd-preview {
{ position: relative;
background-color: #c0e0ff; font-family: $sans-serif;
padding: 25px 20px 10px 20px;
margin-bottom: 5px;
box-sizing: border-box;
border: 1px solid #c8c8c8;
border-top-width: 0;
@include border-radius(0 0 3px 3px);
overflow: hidden;
@include transition(all, .2s, easeOut);
&:before {
content: 'PREVIEW';
position: absolute;
top: 3px;
left: 5px;
font-size: 11px;
color: #bbb;
}
p {
font-family: $sans-serif;
}
background-color: #fafafa;
} }
.wmd-button-row .wmd-button-row {
{
position: relative; position: relative;
margin-left: 5px; margin-left: 5px;
margin-right: 5px; margin-right: 5px;
...@@ -619,10 +831,11 @@ div.trending-tags { ...@@ -619,10 +831,11 @@ div.trending-tags {
margin-top: 10px; margin-top: 10px;
padding: 0px; padding: 0px;
height: 20px; height: 20px;
overflow: hidden;
@include transition(all, .2s, easeOut);
} }
.wmd-spacer .wmd-spacer {
{
width: 1px; width: 1px;
height: 20px; height: 20px;
margin-left: 14px; margin-left: 14px;
...@@ -653,26 +866,22 @@ div.trending-tags { ...@@ -653,26 +866,22 @@ div.trending-tags {
display: inline-block; display: inline-block;
} }
.wmd-spacer1 .wmd-spacer1 {
{
left: 50px; left: 50px;
} }
.wmd-spacer2 .wmd-spacer2 {
{
left: 175px; left: 175px;
} }
.wmd-spacer3
{ .wmd-spacer3 {
left: 300px; left: 300px;
} }
.wmd-prompt-background .wmd-prompt-background {
{
background-color: Black; background-color: Black;
} }
.wmd-prompt-dialog .wmd-prompt-dialog {
{
border: 1px solid #999999; border: 1px solid #999999;
background-color: #F5F5F5; background-color: #F5F5F5;
} }
...@@ -688,7 +897,7 @@ div.trending-tags { ...@@ -688,7 +897,7 @@ div.trending-tags {
color: black; color: black;
} }
.wmd-prompt-dialog > form > input[type="button"]{ .wmd-prompt-dialog > form > input[type="button"] {
border: 1px solid #888888; border: 1px solid #888888;
font-family: trebuchet MS, helvetica, sans-serif; font-family: trebuchet MS, helvetica, sans-serif;
font-size: 0.8em; font-size: 0.8em;
......
...@@ -92,6 +92,10 @@ div.course-wrapper { ...@@ -92,6 +92,10 @@ div.course-wrapper {
list-style: none; list-style: none;
padding-left: 2em; padding-left: 2em;
} }
&.admin-actions {
list-style: none;
}
} }
nav.sequence-bottom { nav.sequence-bottom {
......
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<% <article class="sidebar-module">
def url_for(commentable): <a href="#" class="sidebar-new-post-button">New Post</a>
return reverse('django_comment_client.forum.views.forum_form_discussion', args=[course.id, commentable['discussion_id']]) </article>
%>
<%def name="make_category(category, commentables)"> <article class="discussion-sidebar-following sidebar-module">
<h3><a href="#">${category}</a></h3> <header>
<h4>Following</h4>
<a href="#" class="sidebar-view-all">view all &rsaquo;</a>
</header>
<ol class="discussion-sidebar-following-list">
<li><a href="#"><span class="sidebar-following-name">Hints on HW4</span> <span class="sidebar-vote-count">1348</span></a></li>
<li><a href="#"><span class="sidebar-following-name">Answers to Mi-term</span> <span class="sidebar-vote-count">-37</span></a></li>
<li><a href="#"><span class="sidebar-following-name">Summaries for Lectures 1 to 12 (more added, links fixed)</span> <span class="sidebar-vote-count">128</span></a></li>
<ol>
</article>
<ul> <article class="discussion-sidebar-tags sidebar-module">
% for commentable in commentables: <header>
<li${' class="active"' if active == commentable['discussion_id'] else ''}> <h4>Recent Tags</h4>
<a href="${url_for(commentable)}"> </header>
<p>${commentable['title']}</p> <ol class="discussion-sidebar-tags-list">
</a> <li><a href="#" class="thread-tag">week-1</a><span class="sidebar-tag-count">&times;1348</span></li>
% endfor <li><a href="#" class="thread-tag">S2V8: Method 3</a><span class="sidebar-tag-count">&times;37</span></li>
</ul> <li><a href="#" class="thread-tag">artificial-intelligence</a><span class="sidebar-tag-count">&times;128</span></li>
</%def> <ol>
</article>
% for category, commentables in discussion_info.items(): \ No newline at end of file
${make_category(category, commentables)}
% endfor
<%namespace name="renderer" file="_thread.html"/> <%namespace name="renderer" file="_thread.html"/>
<section class="discussion inline-discussion" _id="${discussion_id}"> <section class="discussion inline-discussion" _id="${discussion_id}">
<div class="discussion-non-content discussion-local"> <div class="discussion-non-content discussion-local">
<div class="discussion-title-wrapper">
<a class="discussion-title" href="javascript:void(0)">Discussion</a>
</div>
<div class="search-wrapper"> <div class="search-wrapper">
<%include file="_search_bar.html" /> <%include file="_search_bar.html" />
</div> </div>
<div class="search-within">
<input type="checkbox" id="discussion-search-within-board-${discussion_id}" class="discussion-search-within-board" checked/>
<label for="discussion-search-within-board-${discussion_id}" class="discussion-search-within-board">Search within board</label>
</div>
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
</div> </div>
% if len(threads) == 0: % if len(threads) == 0:
<div class="blank"> <div class="blank">
......
<%namespace name="renderer" file="_thread.html"/> <%namespace name="renderer" file="_thread.html"/>
<section class="discussion inline-discussion" _id="${discussion_id}"> <section class="discussion inline-discussion" _id="${discussion_id}">
<div class="discussion-non-content discussion-local"> <div class="discussion-non-content discussion-local">
<div class="search-wrapper-inline search-wrapper"> <form class="new-post-form collapsed" id="new-post-form" style="display: block; ">
<%include file="_search_bar.html" /> <ul class="discussion-errors"></ul>
</div> <div class="new-post-body reply-body"></div>
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div> <div class="post-options">
<input type="checkbox" class="discussion-post-anonymously" id="discussion-post-anonymously-${discussion_id}">
<label for="discussion-post-anonymously-${discussion_id}">post anonymously</label>
<input type="checkbox" class="discussion-auto-watch" id="discussion-autowatch-${discussion_id}" checked="">
<label for="discussion-auto-watch-${discussion_id}">follow this thread</label>
</div>
<div class="reply-post-control">
<a class="discussion-submit-post control-button" href="javascript:void(0)">Submit</a>
</div>
</form>
</div>
<div class="threads">
% for thread in threads:
${renderer.render_thread(course_id, thread, show_comments=False)}
% endfor
</div> </div>
% if len(threads) == 0:
<div class="blank"> <%include file="_paginator.html" />
<%include file="_blank_slate.html" />
</div>
<div class="threads"></div>
% else:
<%include file="_sort.html" />
<div class="threads">
% for thread in threads:
${renderer.render_thread(course_id, thread, show_comments=False)}
% endfor
</div>
<%include file="_paginator.html" />
% endif
</section> </section>
<%! <%!
...@@ -36,3 +40,5 @@ ...@@ -36,3 +40,5 @@
} }
$$annotated_content_info = $.extend($$annotated_content_info, JSON.parse("${annotated_content_info | escape_quotes}")); $$annotated_content_info = $.extend($$annotated_content_info, JSON.parse("${annotated_content_info | escape_quotes}"));
</script> </script>
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
<%def name="link_to_sort(key, title)"> <%def name="link_to_sort(key, title)">
% if key == sort_key: % if key == sort_key:
% if sort_order.lower() == 'desc': % if sort_order.lower() == 'desc':
${_link_to_sort(key, 'asc', title + ' &#x25BC;', 'sorted')} ${_link_to_sort(key, 'asc', title + '', 'sorted')}
% else: % else:
${_link_to_sort(key, 'desc', title + ' &#x25B2;', 'sorted')} ${_link_to_sort(key, 'desc', title + '', 'sorted')}
% endif % endif
% else: % else:
${_link_to_sort(key, 'desc', title)} ${_link_to_sort(key, 'desc', title)}
...@@ -22,14 +22,14 @@ ...@@ -22,14 +22,14 @@
%> %>
<a class="discussion-sort-link ${cls}" href="javascript:void(0)" sort-url="${url_for_sort(key, order)}">${title}</a> <a class="discussion-sort-link ${cls}" href="javascript:void(0)" sort-url="${url_for_sort(key, order)}">${title}</a>
</%def> </%def>
<div class="discussion-sort discussion-local"> <div class="discussion-sort discussion-local">
Sort by: <span class="discussion-label">Sort by:</span>
${link_to_sort('top', 'top')}
${link_to_sort('date', 'date')} ${link_to_sort('date', 'date')}
|
${link_to_sort('activity', 'activity')}
|
${link_to_sort('votes', 'votes')} ${link_to_sort('votes', 'votes')}
|
${link_to_sort('comments', 'comments')} ${link_to_sort('comments', 'comments')}
</div> </div>
...@@ -36,10 +36,17 @@ ...@@ -36,10 +36,17 @@
<%def name="render_content(content, type, **kwargs)"> <%def name="render_content(content, type, **kwargs)">
<div class="discussion-content"> <div class="discussion-content">
<div class="discussion-content-wrapper clearfix"> <div class="discussion-content-wrapper clearfix">
<div class="follow-wrapper">
</div>
${render_vote(content)} ${render_vote(content)}
<div class="discussion-right-wrapper clearfix"> <div class="discussion-right-wrapper clearfix">
<ul class="admin-actions">
% if type == 'comment':
<li><a href="javascript:void(0)" class="admin-endorse">Endorse</a></li>
% endif
<li><a href="javascript:void(0)" class="admin-edit">Edit</a></li>
<li><a href="javascript:void(0)" class="admin-delete">Delete</a></li>
</ul>
${render_title(content, type, **kwargs)} ${render_title(content, type, **kwargs)}
<div class="discussion-content-view"> <div class="discussion-content-view">
<a name="${content['id']}"></a> <a name="${content['id']}"></a>
...@@ -84,33 +91,23 @@ ...@@ -84,33 +91,23 @@
</%def> </%def>
<%def name="render_bottom_bar(content, type, **kwargs)"> <%def name="render_bottom_bar(content, type, **kwargs)">
<div class="info"> <div class="info">
${render_info(content)} ${render_info(content)}
${render_link("discussion-link discussion-reply discussion-reply-" + type, "Reply")} <ul class="discussion-actions">
${render_link("discussion-link discussion-edit", "Edit")} <li>${render_link("discussion-link discussion-reply discussion-reply-" + type, "Reply")}</li>
<li><div class="follow-wrapper"></div></li>
% if type == 'thread': <li>${render_link("discussion-link discussion-edit", "Edit")}</li>
<a class="discussion-link discussion-permanent-link" href="${reverse('django_comment_client.forum.views.single_thread', kwargs={'discussion_id':discussion_id, 'thread_id':content['id'], 'course_id':course_id})}">Permanent Link</a> <li>
% endif % if type == "comment" and request.user.is_staff:
<span class="discussion-endorse-control"> % if content['endorsed']:
% if content.get('endorsed', False): <input type="checkbox" checked="checked" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}">
<input type="checkbox" checked="checked" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}" /> % else:
% else: <input type="checkbox" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}">
<input type="checkbox" class="discussion-link discussion-endorse" id="discussion-endorse-${content['id']}" /> % endif
% endif <label class="discussion-link" for="discussion-endorse-${content['id']}">Endorsed</label>
<label class="discussion-link" for="discussion-endorse-${content['id']}">Endorsed</label>
</span>
% if type == "thread" and request.user.is_staff:
% if content['closed']:
<a class="discussion-openclose" id="discussion-openclose-${content['id']}" href="javascript:void(0);">Re-open thread</a>
% else:
<a class="discussion-openclose" id="discussion-openclose-${content['id']}" href="javascript:void(0);">Close thread</a>
% endif % endif
% endif </li>
</ul>
${render_link("discussion-link discussion-delete", "Delete")}
</div> </div>
</%def> </%def>
...@@ -125,7 +122,13 @@ ...@@ -125,7 +122,13 @@
</div> </div>
<div class="comment-count"> <div class="comment-count">
% if content.get('comments_count', -1) >= 0: % if content.get('comments_count', -1) >= 0:
<a href="javascript:void(0)" class="discussion-show-comments"> Show ${content['comments_count']} comment(s)</a> <a href="javascript:void(0)" class="discussion-show-comments">
% if content.get('comments_count', -1) >= 2:
Show ${content['comments_count']} comments
% else:
Show ${content['comments_count']} comment
% endif
</a>
% endif % endif
</div> </div>
</%def> </%def>
...@@ -136,8 +139,8 @@ ...@@ -136,8 +139,8 @@
<%def name="render_vote(content)"> <%def name="render_vote(content)">
<div class="discussion-votes"> <div class="discussion-votes">
${render_link("discussion-vote discussion-vote-up", "&#x2C4;")} ${render_link("discussion-vote discussion-vote-up", "&#9650;")}
<div class="discussion-votes-point">${content['votes']['point']}</div> <div class="discussion-votes-point">${content['votes']['point']}</div>
${render_link("discussion-vote discussion-vote-down", "&#x2C5;")} ${render_link("discussion-vote discussion-vote-down", "&#9660;")}
</div> </div>
</%def> </%def>
...@@ -2,14 +2,14 @@ ${module_content} ...@@ -2,14 +2,14 @@ ${module_content}
%if edit_link: %if edit_link:
<div><a href="${edit_link}">Edit</a></div> <div><a href="${edit_link}">Edit</a></div>
% endif % endif
<div><a href="javascript:void(0)" onclick="javascript:$('#${element_id}_debug').slideToggle()">Staff Debug Info</a></div>
<div class="staff_info">
<a href="javascript:void(0)" onclick="javascript:$('#${element_id}_debug').toggle()">Staff Debug Info</a>
<span style="display:none" id="${element_id}_debug"> <span style="display:none" id="${element_id}_debug">
<div class="staff_info">
definition = <pre>${definition | h}</pre> definition = <pre>${definition | h}</pre>
metadata = ${metadata | h} metadata = ${metadata | h}
</span>
</div> </div>
%if render_histogram: %if render_histogram:
<div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div> <div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
%endif %endif
</span>
...@@ -142,7 +142,6 @@ if settings.COURSEWARE_ENABLED: ...@@ -142,7 +142,6 @@ if settings.COURSEWARE_ENABLED:
# discussion # discussion
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')), include('django_comment_client.urls')),
# For the instructor # For the instructor
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
'courseware.views.gradebook'), 'courseware.views.gradebook'),
......
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