Merge remote-tracking branch 'origin/master' into feature/kevin/groups
This commit is contained in:
18
lms/CHANGELOG.md
Normal file
18
lms/CHANGELOG.md
Normal file
@@ -0,0 +1,18 @@
|
||||
Instructions
|
||||
============
|
||||
For each pull request, add one or more lines to the bottom of the change list. When
|
||||
code is released to production, change the `Upcoming` entry to todays date, and add
|
||||
a new block at the bottom of the file.
|
||||
|
||||
Upcoming
|
||||
--------
|
||||
|
||||
Change log entries should be targeted at end users. A good place to start is the
|
||||
user story that instigated the pull request.
|
||||
|
||||
Changes
|
||||
=======
|
||||
|
||||
Upcoming
|
||||
--------
|
||||
* Created changelog
|
||||
@@ -4,8 +4,11 @@ from xmodule.course_module import CourseDescriptor
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_subdomain(domain):
|
||||
return domain.split(".")[0]
|
||||
def pick_subdomain(domain, options, default='default'):
|
||||
for option in options:
|
||||
if domain.startswith(option):
|
||||
return option
|
||||
return default
|
||||
|
||||
|
||||
def get_visible_courses(domain=None):
|
||||
@@ -17,9 +20,7 @@ def get_visible_courses(domain=None):
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
|
||||
if domain and settings.MITX_FEATURES.get('SUBDOMAIN_COURSE_LISTINGS'):
|
||||
subdomain = get_subdomain(domain)
|
||||
if subdomain not in settings.COURSE_LISTINGS:
|
||||
subdomain = 'default'
|
||||
subdomain = pick_subdomain(domain, settings.COURSE_LISTINGS.keys())
|
||||
visible_ids = frozenset(settings.COURSE_LISTINGS[subdomain])
|
||||
return [course for course in courses if course.id in visible_ids]
|
||||
else:
|
||||
@@ -34,7 +35,7 @@ def get_university(domain=None):
|
||||
if not settings.MITX_FEATURES['SUBDOMAIN_BRANDING'] or domain is None:
|
||||
return None
|
||||
|
||||
subdomain = get_subdomain(domain)
|
||||
subdomain = pick_subdomain(domain, settings.SUBDOMAIN_BRANDING.keys())
|
||||
return settings.SUBDOMAIN_BRANDING.get(subdomain)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ class Command(BaseCommand):
|
||||
This command does not do anything other than report the current
|
||||
certificate status.
|
||||
|
||||
unavailable - A student is not eligible for a certificate.
|
||||
generating - A request has been made to generate a certificate,
|
||||
but it has not been generated yet.
|
||||
regenerating - A request has been made to regenerate a certificate,
|
||||
@@ -64,11 +63,7 @@ class Command(BaseCommand):
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id).prefetch_related(
|
||||
"groups").order_by('username')
|
||||
unavailable_count = enrolled_students.count() - \
|
||||
GeneratedCertificate.objects.filter(
|
||||
course_id__exact=course_id).count()
|
||||
cert_data[course_id] = {'enrolled': enrolled_students.count()}
|
||||
cert_data[course_id].update({'unavailable': unavailable_count})
|
||||
|
||||
tallies = GeneratedCertificate.objects.filter(
|
||||
course_id__exact=course_id).values('status').annotate(
|
||||
|
||||
@@ -23,26 +23,37 @@ class Command(BaseCommand):
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('-n', '--noop',
|
||||
action='store_true',
|
||||
dest='noop',
|
||||
default=False,
|
||||
help="Don't add certificate requests to the queue"),
|
||||
action='store_true',
|
||||
dest='noop',
|
||||
default=False,
|
||||
help="Don't add certificate requests to the queue"),
|
||||
make_option('-c', '--course',
|
||||
metavar='COURSE_ID',
|
||||
dest='course',
|
||||
default=False,
|
||||
help='Grade and generate certificates for a specific course'),
|
||||
metavar='COURSE_ID',
|
||||
dest='course',
|
||||
default=False,
|
||||
help='Grade and generate certificates '
|
||||
'for a specific course'),
|
||||
make_option('-f', '--force-gen',
|
||||
metavar='STATUS',
|
||||
dest='force',
|
||||
default=False,
|
||||
help='Will generate new certificates for only those users '
|
||||
'whose entry in the certificate table matches STATUS. '
|
||||
'STATUS can be generating, unavailable, deleted, error '
|
||||
'or notpassing.'),
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
# Will only generate a certificate if the current
|
||||
# status is in this state
|
||||
# status is in the unavailable state, can be set
|
||||
# to something else with the force flag
|
||||
|
||||
VALID_STATUSES = [
|
||||
CertificateStatuses.unavailable
|
||||
]
|
||||
if options['force']:
|
||||
valid_statuses = getattr(CertificateStatuses, options['force'])
|
||||
else:
|
||||
valid_statuses = [CertificateStatuses.unavailable]
|
||||
|
||||
# Print update after this many students
|
||||
|
||||
@@ -54,8 +65,8 @@ class Command(BaseCommand):
|
||||
# Find all courses that have ended
|
||||
ended_courses = []
|
||||
for course_id in [course # all courses in COURSE_LISTINGS
|
||||
for sub in settings.COURSE_LISTINGS
|
||||
for course in settings.COURSE_LISTINGS[sub]]:
|
||||
for sub in settings.COURSE_LISTINGS
|
||||
for course in settings.COURSE_LISTINGS[sub]]:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
course = modulestore().get_instance(course_id, course_loc)
|
||||
if course.has_ended():
|
||||
@@ -64,8 +75,8 @@ class Command(BaseCommand):
|
||||
for course_id in ended_courses:
|
||||
print "Fetching enrolled students for {0}".format(course_id)
|
||||
enrolled_students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id).prefetch_related(
|
||||
"groups").order_by('username')
|
||||
courseenrollment__course_id=course_id).prefetch_related(
|
||||
"groups").order_by('username')
|
||||
xq = XQueueCertInterface()
|
||||
total = enrolled_students.count()
|
||||
count = 0
|
||||
@@ -81,11 +92,11 @@ class Command(BaseCommand):
|
||||
hours, remainder = divmod(timeleft.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
print "{0}/{1} completed ~{2:02}:{3:02}m remaining".format(
|
||||
count, total, hours, minutes)
|
||||
count, total, hours, minutes)
|
||||
start = datetime.datetime.now()
|
||||
|
||||
if certificate_status_for_student(
|
||||
student, course_id)['status'] in VALID_STATUSES:
|
||||
student, course_id)['status'] in valid_statuses:
|
||||
if not options['noop']:
|
||||
# Add the certificate request to the queue
|
||||
ret = xq.add_cert(student, course_id)
|
||||
|
||||
@@ -192,7 +192,7 @@ class XQueueCertInterface(object):
|
||||
Will change the certificate status to 'deleting'.
|
||||
|
||||
Certificate must be in the 'unavailable', 'error',
|
||||
or 'deleted' state.
|
||||
'deleted' or 'generating' state.
|
||||
|
||||
If a student has a passing grade a request will made
|
||||
for a new cert
|
||||
@@ -204,7 +204,8 @@ class XQueueCertInterface(object):
|
||||
|
||||
"""
|
||||
|
||||
VALID_STATUSES = [status.unavailable, status.deleted, status.error,
|
||||
VALID_STATUSES = [ status.generating,
|
||||
status.unavailable, status.deleted, status.error,
|
||||
status.notpassing]
|
||||
|
||||
cert_status = certificate_status_for_student(
|
||||
|
||||
@@ -30,6 +30,19 @@ def course_wiki_redirect(request, course_id):
|
||||
course = get_course_by_id(course_id)
|
||||
|
||||
course_slug = course.wiki_slug
|
||||
|
||||
|
||||
# cdodge: fix for cases where self.location.course can be interpreted as an number rather than
|
||||
# a string. We're seeing in Studio created courses that people often will enter in a stright number
|
||||
# for 'course' (e.g. 201). This Wiki library expects a string to "do the right thing". We haven't noticed this before
|
||||
# because - to now - 'course' has always had non-numeric characters in them
|
||||
try:
|
||||
float(course_slug)
|
||||
# if the float() doesn't throw an exception, that means it's a number
|
||||
course_slug = course_slug + "_"
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
valid_slug = True
|
||||
if not course_slug:
|
||||
|
||||
@@ -7,6 +7,7 @@ import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
@@ -19,12 +20,21 @@ DEBUG_ACCESS = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CourseContextRequired(Exception):
|
||||
"""
|
||||
Raised when a course_context is required to determine permissions
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def debug(*args, **kwargs):
|
||||
# to avoid overly verbose output, this is off by default
|
||||
if DEBUG_ACCESS:
|
||||
log.debug(*args, **kwargs)
|
||||
|
||||
def has_access(user, obj, action):
|
||||
|
||||
def has_access(user, obj, action, course_context=None):
|
||||
"""
|
||||
Check whether a user has the access to do action on obj. Handles any magic
|
||||
switching based on various settings.
|
||||
@@ -44,6 +54,10 @@ def has_access(user, obj, action):
|
||||
actions depend on the obj type, but include e.g. 'enroll' for courses. See the
|
||||
type-specific functions below for the known actions for that type.
|
||||
|
||||
course_context: A course_id specifying which course run this access is for.
|
||||
Required when accessing anything other than a CourseDescriptor, 'global',
|
||||
or a location with category 'course'
|
||||
|
||||
Returns a bool. It is up to the caller to actually deny access in a way
|
||||
that makes sense in context.
|
||||
"""
|
||||
@@ -53,26 +67,27 @@ def has_access(user, obj, action):
|
||||
return _has_access_course_desc(user, obj, action)
|
||||
|
||||
if isinstance(obj, ErrorDescriptor):
|
||||
return _has_access_error_desc(user, obj, action)
|
||||
return _has_access_error_desc(user, obj, action, course_context)
|
||||
|
||||
# NOTE: any descriptor access checkers need to go above this
|
||||
if isinstance(obj, XModuleDescriptor):
|
||||
return _has_access_descriptor(user, obj, action)
|
||||
return _has_access_descriptor(user, obj, action, course_context)
|
||||
|
||||
if isinstance(obj, XModule):
|
||||
return _has_access_xmodule(user, obj, action)
|
||||
return _has_access_xmodule(user, obj, action, course_context)
|
||||
|
||||
if isinstance(obj, Location):
|
||||
return _has_access_location(user, obj, action)
|
||||
return _has_access_location(user, obj, action, course_context)
|
||||
|
||||
if isinstance(obj, basestring):
|
||||
return _has_access_string(user, obj, action)
|
||||
return _has_access_string(user, obj, action, course_context)
|
||||
|
||||
# Passing an unknown object here is a coding error, so rather than
|
||||
# returning a default, complain.
|
||||
raise TypeError("Unknown object type in has_access(): '{0}'"
|
||||
.format(type(obj)))
|
||||
|
||||
|
||||
def get_access_group_name(obj, action):
|
||||
'''
|
||||
Returns group name for user group which has "action" access to the given object.
|
||||
@@ -88,8 +103,8 @@ def get_access_group_name(obj, action):
|
||||
raise TypeError("Unknown object type in get_access_group_name(): '{0}'"
|
||||
.format(type(obj)))
|
||||
|
||||
# ================ Implementation helpers ================================
|
||||
|
||||
# ================ Implementation helpers ================================
|
||||
def _has_access_course_desc(user, course, action):
|
||||
"""
|
||||
Check if user has access to a course descriptor.
|
||||
@@ -127,7 +142,7 @@ def _has_access_course_desc(user, course, action):
|
||||
return True
|
||||
|
||||
# if user is in CourseEnrollmentAllowed with right course_id then can also enroll
|
||||
if user is not None and CourseEnrollmentAllowed:
|
||||
if user is not None and user.is_authenticated() and CourseEnrollmentAllowed:
|
||||
if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id):
|
||||
return True
|
||||
|
||||
@@ -180,7 +195,8 @@ def _get_access_group_name_course_desc(course, action):
|
||||
|
||||
|
||||
|
||||
def _has_access_error_desc(user, descriptor, action):
|
||||
|
||||
def _has_access_error_desc(user, descriptor, action, course_context):
|
||||
"""
|
||||
Only staff should see error descriptors.
|
||||
|
||||
@@ -189,7 +205,7 @@ def _has_access_error_desc(user, descriptor, action):
|
||||
'staff' -- staff access to descriptor.
|
||||
"""
|
||||
def check_for_staff():
|
||||
return _has_staff_access_to_descriptor(user, descriptor)
|
||||
return _has_staff_access_to_descriptor(user, descriptor, course_context)
|
||||
|
||||
checkers = {
|
||||
'load': check_for_staff,
|
||||
@@ -199,7 +215,7 @@ def _has_access_error_desc(user, descriptor, action):
|
||||
return _dispatch(checkers, action, user, descriptor)
|
||||
|
||||
|
||||
def _has_access_descriptor(user, descriptor, action):
|
||||
def _has_access_descriptor(user, descriptor, action, course_context=None):
|
||||
"""
|
||||
Check if user has access to this descriptor.
|
||||
|
||||
@@ -232,7 +248,7 @@ def _has_access_descriptor(user, descriptor, action):
|
||||
debug("Allow: now > effective start date")
|
||||
return True
|
||||
# otherwise, need staff access
|
||||
return _has_staff_access_to_descriptor(user, descriptor)
|
||||
return _has_staff_access_to_descriptor(user, descriptor, course_context)
|
||||
|
||||
# No start date, so can always load.
|
||||
debug("Allow: no start date")
|
||||
@@ -240,13 +256,13 @@ def _has_access_descriptor(user, descriptor, action):
|
||||
|
||||
checkers = {
|
||||
'load': can_load,
|
||||
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor)
|
||||
'staff': lambda: _has_staff_access_to_descriptor(user, descriptor, course_context)
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, descriptor)
|
||||
|
||||
|
||||
def _has_access_xmodule(user, xmodule, action):
|
||||
def _has_access_xmodule(user, xmodule, action, course_context):
|
||||
"""
|
||||
Check if user has access to this xmodule.
|
||||
|
||||
@@ -254,10 +270,10 @@ def _has_access_xmodule(user, xmodule, action):
|
||||
- same as the valid actions for xmodule.descriptor
|
||||
"""
|
||||
# Delegate to the descriptor
|
||||
return has_access(user, xmodule.descriptor, action)
|
||||
return has_access(user, xmodule.descriptor, action, course_context)
|
||||
|
||||
|
||||
def _has_access_location(user, location, action):
|
||||
def _has_access_location(user, location, action, course_context):
|
||||
"""
|
||||
Check if user has access to this location.
|
||||
|
||||
@@ -271,13 +287,13 @@ def _has_access_location(user, location, action):
|
||||
And in general, prefer checking access on loaded items, rather than locations.
|
||||
"""
|
||||
checkers = {
|
||||
'staff': lambda: _has_staff_access_to_location(user, location)
|
||||
'staff': lambda: _has_staff_access_to_location(user, location, course_context)
|
||||
}
|
||||
|
||||
return _dispatch(checkers, action, user, location)
|
||||
|
||||
|
||||
def _has_access_string(user, perm, action):
|
||||
def _has_access_string(user, perm, action, course_context):
|
||||
"""
|
||||
Check if user has certain special access, specified as string. Valid strings:
|
||||
|
||||
@@ -321,13 +337,35 @@ def _dispatch(table, action, user, obj):
|
||||
raise ValueError("Unknown action for object type '{0}': '{1}'".format(
|
||||
type(obj), action))
|
||||
|
||||
def _course_staff_group_name(location):
|
||||
"""
|
||||
Get the name of the staff group for a location. Right now, that's staff_COURSE.
|
||||
|
||||
location: something that can passed to Location.
|
||||
def _does_course_group_name_exist(name):
|
||||
return len(Group.objects.filter(name=name)) > 0
|
||||
|
||||
|
||||
def _course_staff_group_name(location, course_context=None):
|
||||
"""
|
||||
return 'staff_%s' % Location(location).course
|
||||
Get the name of the staff group for a location in the context of a course run.
|
||||
|
||||
location: something that can passed to Location
|
||||
course_context: A course_id that specifies the course run in which the location occurs.
|
||||
Required if location doesn't have category 'course'
|
||||
|
||||
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
|
||||
using course_id rather than just the course number. So first check to see if the group name exists
|
||||
"""
|
||||
loc = Location(location)
|
||||
legacy_name = 'staff_%s' % loc.course
|
||||
if _does_course_group_name_exist(legacy_name):
|
||||
return legacy_name
|
||||
|
||||
if loc.category == 'course':
|
||||
course_id = loc.course_id
|
||||
else:
|
||||
if course_context is None:
|
||||
raise CourseContextRequired()
|
||||
course_id = course_context
|
||||
|
||||
return 'staff_%s' % course_id
|
||||
|
||||
def course_beta_test_group_name(location):
|
||||
"""
|
||||
@@ -338,15 +376,36 @@ def course_beta_test_group_name(location):
|
||||
"""
|
||||
return 'beta_testers_{0}'.format(Location(location).course)
|
||||
|
||||
# nosetests thinks that anything with _test_ in the name is a test.
|
||||
# Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html)
|
||||
course_beta_test_group_name.__test__ = False
|
||||
|
||||
def _course_instructor_group_name(location):
|
||||
|
||||
def _course_instructor_group_name(location, course_context=None):
|
||||
"""
|
||||
Get the name of the instructor group for a location. Right now, that's instructor_COURSE.
|
||||
Get the name of the instructor group for a location, in the context of a course run.
|
||||
A course instructor has all staff privileges, but also can manage list of course staff (add, remove, list).
|
||||
|
||||
location: something that can passed to Location.
|
||||
course_context: A course_id that specifies the course run in which the location occurs.
|
||||
Required if location doesn't have category 'course'
|
||||
|
||||
cdodge: We're changing the name convention of the group to better epxress different runs of courses by
|
||||
using course_id rather than just the course number. So first check to see if the group name exists
|
||||
"""
|
||||
return 'instructor_%s' % Location(location).course
|
||||
loc = Location(location)
|
||||
legacy_name = 'instructor_%s' % loc.course
|
||||
if _does_course_group_name_exist(legacy_name):
|
||||
return legacy_name
|
||||
|
||||
if loc.category == 'course':
|
||||
course_id = loc.course_id
|
||||
else:
|
||||
if course_context is None:
|
||||
raise CourseContextRequired()
|
||||
course_id = course_context
|
||||
|
||||
return 'instructor_%s' % course_id
|
||||
|
||||
|
||||
def _has_global_staff_access(user):
|
||||
@@ -403,15 +462,15 @@ def _adjust_start_date_for_beta_testers(user, descriptor):
|
||||
|
||||
return descriptor.start
|
||||
|
||||
def _has_instructor_access_to_location(user, location):
|
||||
return _has_access_to_location(user, location, 'instructor')
|
||||
def _has_instructor_access_to_location(user, location, course_context=None):
|
||||
return _has_access_to_location(user, location, 'instructor', course_context)
|
||||
|
||||
|
||||
def _has_staff_access_to_location(user, location):
|
||||
return _has_access_to_location(user, location, 'staff')
|
||||
def _has_staff_access_to_location(user, location, course_context=None):
|
||||
return _has_access_to_location(user, location, 'staff', course_context)
|
||||
|
||||
|
||||
def _has_access_to_location(user, location, access_level):
|
||||
def _has_access_to_location(user, location, access_level, course_context):
|
||||
'''
|
||||
Returns True if the given user has access_level (= staff or
|
||||
instructor) access to a location. For now this is equivalent to
|
||||
@@ -437,14 +496,14 @@ def _has_access_to_location(user, location, access_level):
|
||||
user_groups = [g.name for g in user.groups.all()]
|
||||
|
||||
if access_level == 'staff':
|
||||
staff_group = _course_staff_group_name(location)
|
||||
staff_group = _course_staff_group_name(location, course_context)
|
||||
if staff_group in user_groups:
|
||||
debug("Allow: user in group %s", staff_group)
|
||||
return True
|
||||
debug("Deny: user not in group %s", staff_group)
|
||||
|
||||
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
|
||||
instructor_group = _course_instructor_group_name(location)
|
||||
instructor_group = _course_instructor_group_name(location, course_context)
|
||||
if instructor_group in user_groups:
|
||||
debug("Allow: user in group %s", instructor_group)
|
||||
return True
|
||||
@@ -459,22 +518,22 @@ def _has_access_to_location(user, location, access_level):
|
||||
def _has_staff_access_to_course_id(user, course_id):
|
||||
"""Helper method that takes a course_id instead of a course name"""
|
||||
loc = CourseDescriptor.id_to_location(course_id)
|
||||
return _has_staff_access_to_location(user, loc)
|
||||
return _has_staff_access_to_location(user, loc, course_id)
|
||||
|
||||
|
||||
def _has_instructor_access_to_descriptor(user, descriptor):
|
||||
def _has_instructor_access_to_descriptor(user, descriptor, course_context=None):
|
||||
"""Helper method that checks whether the user has staff access to
|
||||
the course of the location.
|
||||
|
||||
descriptor: something that has a location attribute
|
||||
"""
|
||||
return _has_instructor_access_to_location(user, descriptor.location)
|
||||
return _has_instructor_access_to_location(user, descriptor.location, course_context)
|
||||
|
||||
def _has_staff_access_to_descriptor(user, descriptor):
|
||||
|
||||
def _has_staff_access_to_descriptor(user, descriptor, course_context=None):
|
||||
"""Helper method that checks whether the user has staff access to
|
||||
the course of the location.
|
||||
|
||||
descriptor: something that has a location attribute
|
||||
"""
|
||||
return _has_staff_access_to_location(user, descriptor.location)
|
||||
|
||||
return _has_staff_access_to_location(user, descriptor.location, course_context)
|
||||
|
||||
@@ -2,45 +2,71 @@ from collections import defaultdict
|
||||
from fs.errors import ResourceNotFoundError
|
||||
from functools import wraps
|
||||
import logging
|
||||
import inspect
|
||||
|
||||
from lxml.html import rewrite_links
|
||||
|
||||
from path import path
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
|
||||
from module_render import get_module
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.x_module import XModule
|
||||
from static_replace import replace_urls, try_staticfiles_lookup
|
||||
from courseware.access import has_access
|
||||
import branding
|
||||
from courseware.models import StudentModuleCache
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def get_request_for_thread():
|
||||
"""Walk up the stack, return the nearest first argument named "request"."""
|
||||
frame = None
|
||||
try:
|
||||
for f in inspect.stack()[1:]:
|
||||
frame = f[0]
|
||||
code = frame.f_code
|
||||
if code.co_varnames[:1] == ("request",):
|
||||
return frame.f_locals["request"]
|
||||
elif code.co_varnames[:2] == ("self", "request",):
|
||||
return frame.f_locals["request"]
|
||||
finally:
|
||||
del frame
|
||||
|
||||
def get_course_by_id(course_id):
|
||||
|
||||
def get_course_by_id(course_id, depth=0):
|
||||
"""
|
||||
Given a course id, return the corresponding course descriptor.
|
||||
|
||||
If course_id is not valid, raises a 404.
|
||||
depth: The number of levels of children for the modulestore to cache. None means infinite depth
|
||||
"""
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
return modulestore().get_instance(course_id, course_loc)
|
||||
return modulestore().get_instance(course_id, course_loc, depth=depth)
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
|
||||
def get_course_with_access(user, course_id, action):
|
||||
def get_course_with_access(user, course_id, action, depth=0):
|
||||
"""
|
||||
Given a course_id, look up the corresponding course descriptor,
|
||||
check that the user has the access to perform the specified action
|
||||
on the course, and return the descriptor.
|
||||
|
||||
Raises a 404 if the course_id is invalid, or the user doesn't have access.
|
||||
|
||||
depth: The number of levels of children for the modulestore to cache. None means infinite depth
|
||||
"""
|
||||
course = get_course_by_id(course_id)
|
||||
course = get_course_by_id(course_id, depth=depth)
|
||||
if not has_access(user, course, action):
|
||||
# Deliberately return a non-specific error message to avoid
|
||||
# leaking info about access control settings
|
||||
@@ -89,8 +115,14 @@ def get_cohort_ids(course_id):
|
||||
def course_image_url(course):
|
||||
"""Try to look up the image url for the course. If it's not found,
|
||||
log an error and return the dead link"""
|
||||
path = course.metadata['data_dir'] + "/images/course_image.jpg"
|
||||
return try_staticfiles_lookup(path)
|
||||
if isinstance(modulestore(), XMLModuleStore):
|
||||
path = course.metadata['data_dir'] + "/images/course_image.jpg"
|
||||
return try_staticfiles_lookup(path)
|
||||
else:
|
||||
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
|
||||
path = StaticContent.get_url_path_from_location(loc)
|
||||
return path
|
||||
|
||||
|
||||
def find_file(fs, dirs, filename):
|
||||
"""
|
||||
@@ -108,6 +140,7 @@ def find_file(fs, dirs, filename):
|
||||
return filepath
|
||||
raise ResourceNotFoundError("Could not find {0}".format(filename))
|
||||
|
||||
|
||||
def get_course_about_section(course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course about page,
|
||||
@@ -144,14 +177,20 @@ def get_course_about_section(course, section_key):
|
||||
'effort', 'end_date', 'prerequisites', 'ocw_links']:
|
||||
|
||||
try:
|
||||
fs = course.system.resources_fs
|
||||
# first look for a run-specific version
|
||||
dirs = [path("about") / course.url_name, path("about")]
|
||||
filepath = find_file(fs, dirs, section_key + ".html")
|
||||
with fs.open(filepath) as htmlFile:
|
||||
return replace_urls(htmlFile.read().decode('utf-8'),
|
||||
course.metadata['data_dir'])
|
||||
except ResourceNotFoundError:
|
||||
|
||||
request = get_request_for_thread()
|
||||
|
||||
loc = course.location._replace(category='about', name=section_key)
|
||||
course_module = get_module(request.user, request, loc, None, course.id, not_found_ok = True, wrap_xmodule_display = False)
|
||||
|
||||
html = ''
|
||||
|
||||
if course_module is not None:
|
||||
html = course_module.get_html()
|
||||
|
||||
return html
|
||||
|
||||
except ItemNotFoundError:
|
||||
log.warning("Missing about section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
return None
|
||||
@@ -165,7 +204,8 @@ def get_course_about_section(course, section_key):
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
|
||||
|
||||
def get_course_info_section(course, section_key):
|
||||
|
||||
def get_course_info_section(request, cache, course, section_key):
|
||||
"""
|
||||
This returns the snippet of html to be rendered on the course info page,
|
||||
given the key for the section.
|
||||
@@ -177,31 +217,15 @@ def get_course_info_section(course, section_key):
|
||||
- guest_updates
|
||||
"""
|
||||
|
||||
# Many of these are stored as html files instead of some semantic
|
||||
# markup. This can change without effecting this interface when we find a
|
||||
# good format for defining so many snippets of text/html.
|
||||
|
||||
if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']:
|
||||
try:
|
||||
fs = course.system.resources_fs
|
||||
# first look for a run-specific version
|
||||
dirs = [path("info") / course.url_name, path("info")]
|
||||
filepath = find_file(fs, dirs, section_key + ".html")
|
||||
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
|
||||
course_module = get_module(request.user, request, loc, cache, course.id, wrap_xmodule_display = False)
|
||||
html = ''
|
||||
|
||||
with fs.open(filepath) as htmlFile:
|
||||
# Replace '/static/' urls
|
||||
info_html = replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir'])
|
||||
if course_module is not None:
|
||||
html = course_module.get_html()
|
||||
|
||||
# Replace '/course/' urls
|
||||
course_root = reverse('course_root', args=[course.id])[:-1] # Remove trailing slash
|
||||
info_html = replace_urls(info_html, course_root, '/course/')
|
||||
return info_html
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing info section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
return "! Info section missing !"
|
||||
|
||||
raise KeyError("Invalid about key " + str(section_key))
|
||||
return html
|
||||
|
||||
|
||||
# TODO: Fix this such that these are pulled in as extra course-specific tabs.
|
||||
@@ -229,7 +253,7 @@ def get_course_syllabus_section(course, section_key):
|
||||
filepath = find_file(fs, dirs, section_key + ".html")
|
||||
with fs.open(filepath) as htmlFile:
|
||||
return replace_urls(htmlFile.read().decode('utf-8'),
|
||||
course.metadata['data_dir'])
|
||||
course.metadata['data_dir'], course_namespace=course.location)
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing syllabus section {key} in course {url}".format(
|
||||
key=section_key, url=course.location.url()))
|
||||
@@ -262,4 +286,18 @@ def get_courses(user, domain=None):
|
||||
courses = [c for c in courses if has_access(user, c, 'see_exists')]
|
||||
|
||||
courses = sorted(courses, key=lambda course:course.number)
|
||||
|
||||
return courses
|
||||
|
||||
|
||||
def sort_by_announcement(courses):
|
||||
"""
|
||||
Sorts a list of courses by their announcement date. If the date is
|
||||
not available, sort them by their start date.
|
||||
"""
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.sorting_score
|
||||
courses = sorted(courses, key=key)
|
||||
|
||||
return courses
|
||||
|
||||
@@ -36,8 +36,7 @@ def yield_dynamic_descriptor_descendents(descriptor, module_creator):
|
||||
def get_dynamic_descriptor_children(descriptor):
|
||||
if descriptor.has_dynamic_children():
|
||||
module = module_creator(descriptor)
|
||||
child_locations = module.get_children_locations()
|
||||
return [descriptor.system.load_item(child_location) for child_location in child_locations ]
|
||||
return module.get_child_descriptors()
|
||||
else:
|
||||
return descriptor.get_children()
|
||||
|
||||
@@ -291,7 +290,7 @@ def progress_summary(student, request, course, student_module_cache):
|
||||
graded = section_module.metadata.get('graded', False)
|
||||
scores = []
|
||||
|
||||
module_creator = lambda descriptor : section_module.system.get_module(descriptor.location)
|
||||
module_creator = section_module.system.get_module
|
||||
|
||||
for module_descriptor in yield_dynamic_descriptor_descendents(section_module.descriptor, module_creator):
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ from xmodule.x_module import ModuleSystem
|
||||
from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
|
||||
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from statsd import statsd
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
@@ -81,7 +82,8 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course.id, user, course, depth=2)
|
||||
course_module = get_module(user, request, course.location, student_module_cache, course.id)
|
||||
course_module = get_module_for_descriptor(user, request, course,
|
||||
student_module_cache, course.id)
|
||||
if course_module is None:
|
||||
return None
|
||||
|
||||
@@ -114,7 +116,9 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
return chapters
|
||||
|
||||
|
||||
def get_module(user, request, location, student_module_cache, course_id, position=None):
|
||||
def get_module(user, request, location, student_module_cache, course_id,
|
||||
position=None, not_found_ok=False, wrap_xmodule_display=True,
|
||||
depth=0):
|
||||
"""
|
||||
Get an instance of the xmodule class identified by location,
|
||||
setting the state based on an existing StudentModule, or creating one if none
|
||||
@@ -129,27 +133,45 @@ def get_module(user, request, location, student_module_cache, course_id, positio
|
||||
- course_id : the course_id in the context of which to load module
|
||||
- position : extra information from URL for user-specified
|
||||
position within module
|
||||
- depth : number of levels of descendents to cache when loading this module.
|
||||
None means cache all descendents
|
||||
|
||||
Returns: xmodule instance, or None if the user does not have access to the
|
||||
module. If there's an error, will try to return an instance of ErrorModule
|
||||
if possible. If not possible, return None.
|
||||
"""
|
||||
try:
|
||||
return _get_module(user, request, location, student_module_cache, course_id, position)
|
||||
location = Location(location)
|
||||
descriptor = modulestore().get_instance(course_id, location, depth=depth)
|
||||
return get_module_for_descriptor(user, request, descriptor, student_module_cache, course_id,
|
||||
position=position, not_found_ok=not_found_ok,
|
||||
wrap_xmodule_display=wrap_xmodule_display)
|
||||
except ItemNotFoundError:
|
||||
if not not_found_ok:
|
||||
log.exception("Error in get_module")
|
||||
return None
|
||||
except:
|
||||
# Something has gone terribly wrong, but still not letting it turn into a 500.
|
||||
log.exception("Error in get_module")
|
||||
return None
|
||||
|
||||
def _get_module(user, request, location, student_module_cache, course_id, position=None):
|
||||
|
||||
def get_module_for_descriptor(user, request, descriptor, student_module_cache, course_id,
|
||||
position=None, not_found_ok=False, wrap_xmodule_display=True):
|
||||
"""
|
||||
Actually implement get_module. See docstring there for details.
|
||||
"""
|
||||
return _get_module(user, request, descriptor, student_module_cache, course_id,
|
||||
position=position, wrap_xmodule_display=wrap_xmodule_display)
|
||||
|
||||
def _get_module(user, request, descriptor, student_module_cache, course_id,
|
||||
position=None, wrap_xmodule_display=True):
|
||||
"""
|
||||
Actually implement get_module. See docstring there for details.
|
||||
"""
|
||||
location = Location(location)
|
||||
descriptor = modulestore().get_instance(course_id, location)
|
||||
|
||||
# Short circuit--if the user shouldn't have access, bail without doing any work
|
||||
if not has_access(user, descriptor, 'load'):
|
||||
if not has_access(user, descriptor, 'load', course_id):
|
||||
return None
|
||||
|
||||
# Only check the cache if this module can possibly have state
|
||||
@@ -201,12 +223,12 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
|
||||
}
|
||||
|
||||
def inner_get_module(location):
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Delegate to get_module. It does an access check, so may return None
|
||||
"""
|
||||
return get_module(user, request, location,
|
||||
student_module_cache, course_id, position)
|
||||
return get_module_for_descriptor(user, request, descriptor,
|
||||
student_module_cache, course_id, position)
|
||||
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
# prefix is going to have to be specific to the module, not the directory
|
||||
@@ -241,7 +263,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
|
||||
# make an ErrorDescriptor -- assuming that the descriptor's system is ok
|
||||
import_system = descriptor.system
|
||||
if has_access(user, location, 'staff'):
|
||||
if has_access(user, descriptor.location, 'staff', course_id):
|
||||
err_descriptor = ErrorDescriptor.from_xml(str(descriptor), import_system,
|
||||
error_msg=exc_info_to_str(sys.exc_info()))
|
||||
else:
|
||||
@@ -251,16 +273,22 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
|
||||
# Make an error module
|
||||
return err_descriptor.xmodule_constructor(system)(None, None)
|
||||
|
||||
_get_html = module.get_html
|
||||
|
||||
if wrap_xmodule_display == True:
|
||||
_get_html = wrap_xmodule(module.get_html, module, 'xmodule_display.html')
|
||||
|
||||
module.get_html = replace_static_urls(
|
||||
wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
|
||||
module.metadata['data_dir'])
|
||||
_get_html,
|
||||
module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
|
||||
course_namespace = module.location._replace(category=None, name=None))
|
||||
|
||||
# Allow URLs of the form '/course/' refer to the root of multicourse directory
|
||||
# hierarchy of this course
|
||||
module.get_html = replace_course_urls(module.get_html, course_id)
|
||||
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF'):
|
||||
if has_access(user, module, 'staff'):
|
||||
if has_access(user, module, 'staff', course_id):
|
||||
module.get_html = add_histogram(module.get_html, module, user)
|
||||
|
||||
return module
|
||||
|
||||
@@ -18,8 +18,17 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
from fs.errors import ResourceNotFoundError
|
||||
|
||||
from lxml.html import rewrite_links
|
||||
|
||||
from module_render import get_module
|
||||
from courseware.access import has_access
|
||||
from static_replace import replace_urls
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
from xmodule.x_module import XModule
|
||||
|
||||
|
||||
|
||||
from open_ended_grading.peer_grading_service import PeerGradingService
|
||||
from open_ended_grading.staff_grading_service import StaffGradingService
|
||||
@@ -305,27 +314,16 @@ def get_static_tab_by_slug(course, tab_slug):
|
||||
|
||||
return None
|
||||
|
||||
def get_static_tab_contents(request, cache, course, tab):
|
||||
|
||||
def get_static_tab_contents(course, tab):
|
||||
"""
|
||||
Given a course and a static tab config dict, load the tab contents,
|
||||
returning None if not found.
|
||||
loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])
|
||||
tab_module = get_module(request.user, request, loc, cache, course.id)
|
||||
|
||||
Looks in tabs/{course_url_name}/{tab_slug}.html first, then tabs/{tab_slug}.html.
|
||||
"""
|
||||
slug = tab['url_slug']
|
||||
paths = ['tabs/{0}/{1}.html'.format(course.url_name, slug),
|
||||
'tabs/{0}.html'.format(slug)]
|
||||
fs = course.system.resources_fs
|
||||
for p in paths:
|
||||
if fs.exists(p):
|
||||
try:
|
||||
with fs.open(p) as tabfile:
|
||||
# TODO: redundant with module_render.py. Want to be helper methods in static_replace or something.
|
||||
text = tabfile.read().decode('utf-8')
|
||||
contents = replace_urls(text, course.metadata['data_dir'])
|
||||
return replace_urls(contents, staticfiles_prefix='/courses/'+course.id, replace_prefix='/course/')
|
||||
except (ResourceNotFoundError) as err:
|
||||
log.exception("Couldn't load tab contents from '{0}': {1}".format(p, err))
|
||||
return None
|
||||
return None
|
||||
logging.debug('course_module = {0}'.format(tab_module))
|
||||
|
||||
html = ''
|
||||
|
||||
if tab_module is not None:
|
||||
html = tab_module.get_html()
|
||||
|
||||
return html
|
||||
|
||||
@@ -14,6 +14,8 @@ from django.core.urlresolvers import reverse
|
||||
from override_settings import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from courseware import grades
|
||||
@@ -43,6 +45,25 @@ def registration(email):
|
||||
'''look up registration object by email'''
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
# A bit of a hack--want mongo modulestore for these tests, until
|
||||
# jump_to works with the xmlmodulestore or we have an even better solution
|
||||
# NOTE: this means this test requires mongo to be running.
|
||||
|
||||
def mongo_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
@@ -56,6 +77,7 @@ def xml_store_config(data_dir):
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
|
||||
class ActivateLoginTestCase(TestCase):
|
||||
'''Check that we can activate and log in'''
|
||||
@@ -217,17 +239,9 @@ class PageLoader(ActivateLoginTestCase):
|
||||
|
||||
|
||||
|
||||
def check_pages_load(self, course_name, data_dir, modstore):
|
||||
def check_pages_load(self, module_store):
|
||||
"""Make all locations in course load"""
|
||||
print "Checking course {0} in {1}".format(course_name, data_dir)
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor'
|
||||
load_error_modules=True
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
course_dirs=[course_name],
|
||||
load_error_modules=load_error_modules,
|
||||
)
|
||||
|
||||
|
||||
# enroll in the course before trying to access pages
|
||||
courses = module_store.get_courses()
|
||||
@@ -239,35 +253,68 @@ class PageLoader(ActivateLoginTestCase):
|
||||
n = 0
|
||||
num_bad = 0
|
||||
all_ok = True
|
||||
for descriptor in module_store.modules[course_id].itervalues():
|
||||
|
||||
for descriptor in module_store.get_items(
|
||||
Location(None, None, None, None, None)):
|
||||
|
||||
n += 1
|
||||
print "Checking ", descriptor.location.url()
|
||||
#print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('jump_to',
|
||||
kwargs={'course_id': course_id,
|
||||
'location': descriptor.location.url()}), follow=True)
|
||||
# check status codes first
|
||||
msg = str(resp.status_code)
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg + ": " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif resp.redirect_chain[0][1] != 302:
|
||||
msg = "ERROR on redirect from " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
|
||||
# check content to make sure there were no rendering failures
|
||||
content = resp.content
|
||||
if content.find("this module is temporarily unavailable")>=0:
|
||||
msg = "ERROR unavailable module "
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif isinstance(descriptor, ErrorDescriptor):
|
||||
msg = "ERROR error descriptor loaded: "
|
||||
msg = msg + descriptor.definition['data']['error_msg']
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
# We have ancillary course information now as modules and we can't simply use 'jump_to' to view them
|
||||
if descriptor.location.category == 'about':
|
||||
resp = self.client.get(reverse('about_course', kwargs={'course_id': course_id}))
|
||||
msg = str(resp.status_code)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'static_tab':
|
||||
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug' : descriptor.location.name}))
|
||||
msg = str(resp.status_code)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'course_info':
|
||||
resp = self.client.get(reverse('info', kwargs={'course_id': course_id}))
|
||||
msg = str(resp.status_code)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'custom_tag_template':
|
||||
pass
|
||||
else:
|
||||
#print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('jump_to',
|
||||
kwargs={'course_id': course_id,
|
||||
'location': descriptor.location.url()}), follow=True)
|
||||
msg = str(resp.status_code)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg + ": " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif resp.redirect_chain[0][1] != 302:
|
||||
msg = "ERROR on redirect from " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
|
||||
# check content to make sure there were no rendering failures
|
||||
content = resp.content
|
||||
if content.find("this module is temporarily unavailable")>=0:
|
||||
msg = "ERROR unavailable module "
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif isinstance(descriptor, ErrorDescriptor):
|
||||
msg = "ERROR error descriptor loaded: "
|
||||
msg = msg + descriptor.definition['data']['error_msg']
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
|
||||
print msg
|
||||
self.assertTrue(all_ok) # fail fast
|
||||
|
||||
@@ -278,21 +325,51 @@ class PageLoader(ActivateLoginTestCase):
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
class TestCoursesLoadTestCase(PageLoader):
|
||||
class TestCoursesLoadTestCase_XmlModulestore(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
|
||||
def setUp(self):
|
||||
ActivateLoginTestCase.setUp(self)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
# xmodule.modulestore.django.modulestore().collection.drop()
|
||||
# store = xmodule.modulestore.django.modulestore()
|
||||
# is there a way to empty the store?
|
||||
|
||||
|
||||
def test_toy_course_loads(self):
|
||||
self.check_pages_load('toy', TEST_DATA_DIR, modulestore())
|
||||
module_store = XMLModuleStore(
|
||||
TEST_DATA_DIR,
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor',
|
||||
course_dirs=['toy'],
|
||||
load_error_modules=True,
|
||||
)
|
||||
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
def test_full_course_loads(self):
|
||||
self.check_pages_load('full', TEST_DATA_DIR, modulestore())
|
||||
module_store = XMLModuleStore(
|
||||
TEST_DATA_DIR,
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor',
|
||||
course_dirs=['full'],
|
||||
load_error_modules=True,
|
||||
)
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestCoursesLoadTestCase_MongoModulestore(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
|
||||
def setUp(self):
|
||||
ActivateLoginTestCase.setUp(self)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
modulestore().collection.drop()
|
||||
|
||||
def test_toy_course_loads(self):
|
||||
module_store = modulestore()
|
||||
import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
def test_full_course_loads(self):
|
||||
module_store = modulestore()
|
||||
import_from_xml(module_store, TEST_DATA_DIR, ['full'])
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
|
||||
@@ -17,10 +17,11 @@ from django.views.decorators.cache import cache_control
|
||||
|
||||
from courseware import grades
|
||||
from courseware.access import has_access
|
||||
from courseware.courses import (get_courses, get_course_with_access, get_courses_by_university)
|
||||
from courseware.courses import (get_courses, get_course_with_access,
|
||||
get_courses_by_university, sort_by_announcement)
|
||||
import courseware.tabs as tabs
|
||||
from courseware.models import StudentModuleCache
|
||||
from module_render import toc_for_course, get_module, get_instance_module
|
||||
from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
|
||||
|
||||
from django_comment_client.utils import get_discussion_title
|
||||
|
||||
@@ -67,11 +68,8 @@ def courses(request):
|
||||
'''
|
||||
Render "find courses" page. The course selection work is done in courseware.courses.
|
||||
'''
|
||||
courses = get_courses(request.user, domain=request.META.get('HTTP_HOST'))
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.days_until_start
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
courses = get_courses(request.user, request.META.get('HTTP_HOST'))
|
||||
courses = sort_by_announcement(courses)
|
||||
|
||||
return render_to_response("courseware/courses.html", {'courses': courses})
|
||||
|
||||
@@ -180,7 +178,7 @@ def index(request, course_id, chapter=None, section=None,
|
||||
|
||||
- HTTPresponse
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
course = get_course_with_access(request.user, course_id, 'load', depth=2)
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
registered = registered_for_course(course, request.user)
|
||||
if not registered:
|
||||
@@ -195,7 +193,8 @@ def index(request, course_id, chapter=None, section=None,
|
||||
# Has this student been in this course before?
|
||||
first_time = student_module_cache.lookup(course_id, 'course', course.location.url()) is None
|
||||
|
||||
course_module = get_module(request.user, request, course.location, student_module_cache, course.id)
|
||||
# Load the module for the course
|
||||
course_module = get_module_for_descriptor(request.user, request, course, student_module_cache, course.id)
|
||||
if course_module is None:
|
||||
log.warning('If you see this, something went wrong: if we got this'
|
||||
' far, should have gotten a course module for this user')
|
||||
@@ -215,30 +214,28 @@ def index(request, course_id, chapter=None, section=None,
|
||||
'xqa_server': settings.MITX_FEATURES.get('USE_XQA_SERVER','http://xqa:server@content-qa.mitx.mit.edu/xqa')
|
||||
}
|
||||
|
||||
chapter_descriptor = course.get_child_by_url_name(chapter)
|
||||
chapter_descriptor = course.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_descriptor is not None:
|
||||
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
|
||||
save_child_position(course_module, chapter, instance_module)
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
chapter_module = get_module(request.user, request, chapter_descriptor.location,
|
||||
student_module_cache, course_id)
|
||||
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
|
||||
if chapter_module is None:
|
||||
# User may be trying to access a chapter that isn't live yet
|
||||
raise Http404
|
||||
|
||||
if section is not None:
|
||||
section_descriptor = chapter_descriptor.get_child_by_url_name(section)
|
||||
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.url_name == section)
|
||||
if section_descriptor is None:
|
||||
# Specifically asked-for section doesn't exist
|
||||
raise Http404
|
||||
|
||||
section_student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
course_id, request.user, section_descriptor)
|
||||
section_module = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
section_student_module_cache, course_id, position)
|
||||
# Load all descendents of the section, because we're going to display it's
|
||||
# html, which in general will need all of its children
|
||||
section_module = get_module(request.user, request, section_descriptor.location,
|
||||
student_module_cache, course.id, depth=None)
|
||||
if section_module is None:
|
||||
# User may be trying to be clever and access something
|
||||
# they don't have access to.
|
||||
@@ -342,8 +339,8 @@ def course_info(request, course_id):
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
|
||||
return render_to_response('courseware/info.html', {'course': course,
|
||||
'staff_access': staff_access,})
|
||||
return render_to_response('courseware/info.html', {'request' : request, 'course_id' : course_id, 'cache' : None,
|
||||
'course': course, 'staff_access': staff_access})
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def static_tab(request, course_id, tab_slug):
|
||||
@@ -358,7 +355,7 @@ def static_tab(request, course_id, tab_slug):
|
||||
if tab is None:
|
||||
raise Http404
|
||||
|
||||
contents = tabs.get_static_tab_contents(course, tab)
|
||||
contents = tabs.get_static_tab_contents(request, None, course, tab)
|
||||
if contents is None:
|
||||
raise Http404
|
||||
|
||||
@@ -431,17 +428,14 @@ def university_profile(request, org_id):
|
||||
Return the profile for the particular org_id. 404 if it's not valid.
|
||||
"""
|
||||
all_courses = modulestore().get_courses()
|
||||
valid_org_ids = set(c.org for c in all_courses)
|
||||
valid_org_ids = set(c.org for c in all_courses).union(settings.VIRTUAL_UNIVERSITIES)
|
||||
if org_id not in valid_org_ids:
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
|
||||
# Only grab courses for this org...
|
||||
courses = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
|
||||
# Sort courses by how far are they from they start day
|
||||
key = lambda course: course.days_until_start
|
||||
courses = sorted(courses, key=key, reverse=True)
|
||||
courses = sort_by_announcement(courses)
|
||||
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
@@ -14,6 +15,18 @@ FORUM_ROLE_MODERATOR = 'Moderator'
|
||||
FORUM_ROLE_COMMUNITY_TA = 'Community TA'
|
||||
FORUM_ROLE_STUDENT = 'Student'
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseEnrollment)
|
||||
def assign_default_role(sender, instance, **kwargs):
|
||||
if instance.user.is_staff:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Moderator")[0]
|
||||
else:
|
||||
role = Role.objects.get_or_create(course_id=instance.course_id, name="Student")[0]
|
||||
|
||||
logging.info("assign_default_role: adding %s as %s" % (instance.user, role))
|
||||
instance.user.roles.add(role)
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=30, null=False, blank=False)
|
||||
users = models.ManyToManyField(User, related_name="roles")
|
||||
|
||||
@@ -2,6 +2,7 @@ from collections import defaultdict
|
||||
import logging
|
||||
import time
|
||||
import urllib
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -63,22 +64,19 @@ def get_discussion_id_map(course):
|
||||
return a dict of the form {category: modules}
|
||||
"""
|
||||
global _DISCUSSIONINFO
|
||||
if not _DISCUSSIONINFO[course.id]:
|
||||
initialize_discussion_info(course)
|
||||
initialize_discussion_info(course)
|
||||
return _DISCUSSIONINFO[course.id]['id_map']
|
||||
|
||||
def get_discussion_title(course, discussion_id):
|
||||
global _DISCUSSIONINFO
|
||||
if not _DISCUSSIONINFO[course.id]:
|
||||
initialize_discussion_info(course)
|
||||
initialize_discussion_info(course)
|
||||
title = _DISCUSSIONINFO[course.id]['id_map'].get(discussion_id, {}).get('title', '(no title)')
|
||||
return title
|
||||
|
||||
def get_discussion_category_map(course):
|
||||
|
||||
global _DISCUSSIONINFO
|
||||
if not _DISCUSSIONINFO[course.id]:
|
||||
initialize_discussion_info(course)
|
||||
initialize_discussion_info(course)
|
||||
return filter_unstarted_categories(_DISCUSSIONINFO[course.id]['category_map'])
|
||||
|
||||
def filter_unstarted_categories(category_map):
|
||||
@@ -127,39 +125,50 @@ def sort_map_entries(category_map):
|
||||
sort_map_entries(category_map["subcategories"][title])
|
||||
category_map["children"] = [x[0] for x in sorted(things, key=lambda x: x[1]["sort_key"])]
|
||||
|
||||
|
||||
def initialize_discussion_info(course):
|
||||
|
||||
global _DISCUSSIONINFO
|
||||
|
||||
# only cache in-memory discussion information for 10 minutes
|
||||
# this is because we need a short-term hack fix for
|
||||
# mongo-backed courseware whereby new discussion modules can be added
|
||||
# without LMS service restart
|
||||
|
||||
if _DISCUSSIONINFO[course.id]:
|
||||
return
|
||||
timestamp = _DISCUSSIONINFO[course.id].get('timestamp', datetime.now())
|
||||
age = datetime.now() - timestamp
|
||||
# expire every 5 minutes
|
||||
if age.seconds < 300:
|
||||
return
|
||||
|
||||
course_id = course.id
|
||||
all_modules = get_full_modules()[course_id]
|
||||
|
||||
discussion_id_map = {}
|
||||
|
||||
unexpanded_category_map = defaultdict(list)
|
||||
|
||||
for location, module in all_modules.items():
|
||||
if location.category == 'discussion':
|
||||
skip_module = False
|
||||
for key in ('id', 'discussion_category', 'for'):
|
||||
if key not in module.metadata:
|
||||
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
|
||||
skip_module = True
|
||||
# get all discussion models within this course_id
|
||||
all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
|
||||
|
||||
if skip_module:
|
||||
continue
|
||||
for module in all_modules:
|
||||
skip_module = False
|
||||
for key in ('id', 'discussion_category', 'for'):
|
||||
if key not in module.metadata:
|
||||
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
|
||||
skip_module = True
|
||||
|
||||
id = module.metadata['id']
|
||||
category = module.metadata['discussion_category']
|
||||
title = module.metadata['for']
|
||||
sort_key = module.metadata.get('sort_key', title)
|
||||
category = " / ".join([x.strip() for x in category.split("/")])
|
||||
last_category = category.split("/")[-1]
|
||||
discussion_id_map[id] = {"location": location, "title": last_category + " / " + title}
|
||||
unexpanded_category_map[category].append({"title": title, "id": id,
|
||||
"sort_key": sort_key, "start_date": module.start})
|
||||
if skip_module:
|
||||
continue
|
||||
|
||||
id = module.metadata['id']
|
||||
category = module.metadata['discussion_category']
|
||||
title = module.metadata['for']
|
||||
sort_key = module.metadata.get('sort_key', title)
|
||||
category = " / ".join([x.strip() for x in category.split("/")])
|
||||
last_category = category.split("/")[-1]
|
||||
discussion_id_map[id] = {"location": module.location, "title": last_category + " / " + title}
|
||||
unexpanded_category_map[category].append({"title": title, "id": id,
|
||||
"sort_key": sort_key, "start_date": module.start})
|
||||
|
||||
category_map = {"entries": defaultdict(dict), "subcategories": defaultdict(dict)}
|
||||
for category_path, entries in unexpanded_category_map.items():
|
||||
@@ -208,6 +217,7 @@ def initialize_discussion_info(course):
|
||||
|
||||
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
|
||||
_DISCUSSIONINFO[course.id]['category_map'] = category_map
|
||||
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
|
||||
|
||||
class JsonResponse(HttpResponse):
|
||||
def __init__(self, data=None):
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
from django.conf.urls import *
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', 'heartbeat.views.heartbeat', name='heartbeat'),
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from django.http import HttpResponse
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
def heartbeat(request):
|
||||
"""
|
||||
Simple view that a loadbalancer can check to verify that the app is up
|
||||
"""
|
||||
output = {
|
||||
'date': datetime.now().isoformat(),
|
||||
'courses': [course.location.url() for course in modulestore().get_courses()],
|
||||
}
|
||||
return HttpResponse(json.dumps(output, indent=4))
|
||||
@@ -82,9 +82,10 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
|
||||
# All the not-actually-in-the-course hw and labs come from the
|
||||
# default grading policy string in graders.py
|
||||
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm","Final"
|
||||
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0.0","0.0"
|
||||
expected_body = '''"ID","Username","Full Name","edX email","External email","HW 01","HW 02","HW 03","HW 04","HW 05","HW 06","HW 07","HW 08","HW 09","HW 10","HW 11","HW 12","HW Avg","Lab 01","Lab 02","Lab 03","Lab 04","Lab 05","Lab 06","Lab 07","Lab 08","Lab 09","Lab 10","Lab 11","Lab 12","Lab Avg","Midterm 01","Midterm Avg","Final 01","Final Avg"
|
||||
"2","u2","Fred Weasley","view2@test.com","","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0","0"
|
||||
'''
|
||||
|
||||
self.assertEqual(body, expected_body, msg)
|
||||
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ def instructor_dashboard(request, course_id):
|
||||
except Group.DoesNotExist:
|
||||
group = Group(name=grpname) # create the group
|
||||
group.save()
|
||||
return group
|
||||
|
||||
def get_beta_group(course):
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.core.management import call_command
|
||||
@before.harvest
|
||||
def initial_setup(server):
|
||||
# Launch firefox
|
||||
world.browser = Browser('firefox')
|
||||
world.browser = Browser('chrome')
|
||||
|
||||
@before.each_scenario
|
||||
def reset_data(scenario):
|
||||
@@ -24,3 +24,4 @@ def reset_data(scenario):
|
||||
def teardown_browser(total):
|
||||
# Quit firefox
|
||||
world.browser.quit()
|
||||
pass
|
||||
@@ -1,9 +1,13 @@
|
||||
import factory
|
||||
from student.models import User, UserProfile, Registration
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from factory import Factory
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from time import gmtime
|
||||
from uuid import uuid4
|
||||
from xmodule.timeparse import stringify_time
|
||||
|
||||
class UserProfileFactory(factory.Factory):
|
||||
class UserProfileFactory(Factory):
|
||||
FACTORY_FOR = UserProfile
|
||||
|
||||
user = None
|
||||
@@ -13,13 +17,13 @@ class UserProfileFactory(factory.Factory):
|
||||
mailing_address = None
|
||||
goals = 'World domination'
|
||||
|
||||
class RegistrationFactory(factory.Factory):
|
||||
class RegistrationFactory(Factory):
|
||||
FACTORY_FOR = Registration
|
||||
|
||||
user = None
|
||||
activation_key = uuid.uuid4().hex
|
||||
activation_key = uuid4().hex
|
||||
|
||||
class UserFactory(factory.Factory):
|
||||
class UserFactory(Factory):
|
||||
FACTORY_FOR = User
|
||||
|
||||
username = 'robot'
|
||||
@@ -32,3 +36,113 @@ class UserFactory(factory.Factory):
|
||||
is_superuser = False
|
||||
last_login = datetime(2012, 1, 1)
|
||||
date_joined = datetime(2011, 1, 1)
|
||||
|
||||
def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
|
||||
return XModuleCourseFactory._create(class_to_create, **kwargs)
|
||||
|
||||
def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
|
||||
return XModuleItemFactory._create(class_to_create, **kwargs)
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_COURSE_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
|
||||
template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
|
||||
org = kwargs.get('org')
|
||||
number = kwargs.get('number')
|
||||
display_name = kwargs.get('display_name')
|
||||
location = Location('i4x', org, number,
|
||||
'course', Location.clean(display_name))
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.clone_item(template, location)
|
||||
|
||||
# This metadata code was copied from cms/djangoapps/contentstore/views.py
|
||||
if display_name is not None:
|
||||
new_course.metadata['display_name'] = display_name
|
||||
|
||||
new_course.metadata['data_dir'] = uuid4().hex
|
||||
new_course.metadata['start'] = stringify_time(gmtime())
|
||||
new_course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
# Update the data in the mongo datastore
|
||||
store.update_metadata(new_course.location.url(), new_course.own_metadata)
|
||||
|
||||
return new_course
|
||||
|
||||
class Course:
|
||||
pass
|
||||
|
||||
class CourseFactory(XModuleCourseFactory):
|
||||
FACTORY_FOR = Course
|
||||
|
||||
template = 'i4x://edx/templates/course/Empty'
|
||||
org = 'MITx'
|
||||
number = '999'
|
||||
display_name = 'Robot Super Course'
|
||||
|
||||
class XModuleItemFactory(Factory):
|
||||
"""
|
||||
Factory for XModule items.
|
||||
"""
|
||||
|
||||
ABSTRACT_FACTORY = True
|
||||
_creation_function = (XMODULE_ITEM_CREATION,)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, *args, **kwargs):
|
||||
"""
|
||||
kwargs must include parent_location, template. Can contain display_name
|
||||
target_class is ignored
|
||||
"""
|
||||
|
||||
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
|
||||
|
||||
parent_location = Location(kwargs.get('parent_location'))
|
||||
template = Location(kwargs.get('template'))
|
||||
display_name = kwargs.get('display_name')
|
||||
|
||||
store = modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
|
||||
|
||||
new_item = store.clone_item(template, dest_location)
|
||||
|
||||
# TODO: This needs to be deleted when we have proper storage for static content
|
||||
new_item.metadata['data_dir'] = parent.metadata['data_dir']
|
||||
|
||||
# replace the display name with an optional parameter passed in from the caller
|
||||
if display_name is not None:
|
||||
new_item.metadata['display_name'] = display_name
|
||||
|
||||
store.update_metadata(new_item.location.url(), new_item.own_metadata)
|
||||
|
||||
if new_item.location.category not in DETACHED_CATEGORIES:
|
||||
store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
|
||||
return new_item
|
||||
|
||||
class Item:
|
||||
pass
|
||||
|
||||
class ItemFactory(XModuleItemFactory):
|
||||
FACTORY_FOR = Item
|
||||
|
||||
parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
|
||||
template = 'i4x://edx/templates/chapter/Empty'
|
||||
display_name = 'Section One'
|
||||
@@ -44,6 +44,10 @@ def and_i_press_the_button(step, value):
|
||||
button_css = 'input[value="%s"]' % value
|
||||
world.browser.find_by_css(button_css).first.click()
|
||||
|
||||
@step(u'I click the link with the text "([^"]*)"$')
|
||||
def click_the_link_with_the_text_group1(step, linktext):
|
||||
world.browser.find_link_by_text(linktext).first.click()
|
||||
|
||||
@step('I should see that the path is "([^"]*)"$')
|
||||
def i_should_see_that_the_path_is(step, path):
|
||||
assert world.browser.url == django_url(path)
|
||||
|
||||
@@ -8,8 +8,8 @@ Common traits:
|
||||
"""
|
||||
import json
|
||||
|
||||
from .logsettings import get_logger_config
|
||||
from .common import *
|
||||
from logsettings import get_logger_config
|
||||
|
||||
############################### ALWAYS THE SAME ################################
|
||||
DEBUG = False
|
||||
@@ -36,6 +36,7 @@ with open(ENV_ROOT / "env.json") as env_file:
|
||||
ENV_TOKENS = json.load(env_file)
|
||||
|
||||
SITE_NAME = ENV_TOKENS['SITE_NAME']
|
||||
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
|
||||
|
||||
BOOK_URL = ENV_TOKENS['BOOK_URL']
|
||||
MEDIA_URL = ENV_TOKENS['MEDIA_URL']
|
||||
@@ -57,6 +58,7 @@ LOGGING = get_logger_config(LOG_DIR,
|
||||
|
||||
COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
|
||||
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
|
||||
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL",'')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY",'')
|
||||
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
|
||||
@@ -76,6 +78,11 @@ DATABASES = AUTH_TOKENS['DATABASES']
|
||||
|
||||
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
|
||||
|
||||
# Get the MODULESTORE from auth.json, but if it doesn't exist,
|
||||
# use the one from common.py
|
||||
MODULESTORE = AUTH_TOKENS.get('MODULESTORE', MODULESTORE)
|
||||
CONTENTSTORE = AUTH_TOKENS.get('CONTENTSTORE', CONTENTSTORE)
|
||||
|
||||
STAFF_GRADING_INTERFACE = AUTH_TOKENS.get('STAFF_GRADING_INTERFACE', STAFF_GRADING_INTERFACE)
|
||||
PEER_GRADING_INTERFACE = AUTH_TOKENS.get('PEER_GRADING_INTERFACE', PEER_GRADING_INTERFACE)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Settings for the LMS that runs alongside the CMS on AWS
|
||||
"""
|
||||
|
||||
from .aws import *
|
||||
from ..aws import *
|
||||
|
||||
with open(ENV_ROOT / "cms.auth.json") as auth_file:
|
||||
CMS_AUTH_TOKENS = json.load(auth_file)
|
||||
|
||||
@@ -4,16 +4,42 @@ Settings for the LMS that runs alongside the CMS on AWS
|
||||
|
||||
from ..dev import *
|
||||
|
||||
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
|
||||
|
||||
SUBDOMAIN_BRANDING['edge'] = 'edge'
|
||||
SUBDOMAIN_BRANDING['preview.edge'] = 'edge'
|
||||
VIRTUAL_UNIVERSITIES = ['edge']
|
||||
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': DATA_DIR,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': DATA_DIR,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
}
|
||||
|
||||
CONTENTSTORE = {
|
||||
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
|
||||
'OPTIONS': {
|
||||
'host': 'localhost',
|
||||
'db': 'xcontent',
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLED_APPS += (
|
||||
# Mongo perf stats
|
||||
'debug_toolbar_mongo',
|
||||
)
|
||||
|
||||
|
||||
DEBUG_TOOLBAR_PANELS += (
|
||||
'debug_toolbar_mongo.panel.MongoDebugPanel',
|
||||
)
|
||||
|
||||
12
lms/envs/cms/preview_dev.py
Normal file
12
lms/envs/cms/preview_dev.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Settings for the LMS that runs alongside the CMS on AWS
|
||||
"""
|
||||
|
||||
from .dev import *
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
}
|
||||
@@ -21,11 +21,7 @@ Longer TODO:
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import glob2
|
||||
import errno
|
||||
import hashlib
|
||||
from collections import defaultdict
|
||||
import socket
|
||||
from xmodule.static_content import write_module_styles, write_module_js
|
||||
|
||||
from path import path
|
||||
|
||||
@@ -33,6 +29,8 @@ from .discussionsettings import *
|
||||
|
||||
################################### FEATURES ###################################
|
||||
COURSEWARE_ENABLED = True
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
GENERATE_RANDOM_USER_CREDENTIALS = False
|
||||
PERFSTATS = False
|
||||
|
||||
@@ -232,6 +230,7 @@ MODULESTORE = {
|
||||
}
|
||||
}
|
||||
}
|
||||
CONTENTSTORE = None
|
||||
|
||||
############################ SIGNAL HANDLERS ################################
|
||||
# This is imported to register the exception signal handling that logs exceptions
|
||||
@@ -380,6 +379,7 @@ TEMPLATE_LOADERS = (
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'contentserver.middleware.StaticContentServer',
|
||||
'django_comment_client.middleware.AjaxExceptionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
@@ -405,15 +405,55 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
|
||||
|
||||
from xmodule.hidden_module import HiddenDescriptor
|
||||
from rooted_paths import rooted_glob, remove_root
|
||||
|
||||
write_module_styles(PROJECT_ROOT / 'static/sass/module', [HiddenDescriptor])
|
||||
module_js = remove_root(
|
||||
PROJECT_ROOT / 'static',
|
||||
write_module_js(PROJECT_ROOT / 'static/coffee/module', [HiddenDescriptor])
|
||||
)
|
||||
|
||||
courseware_js = (
|
||||
[
|
||||
'coffee/src/' + pth + '.coffee'
|
||||
for pth in ['courseware', 'histogram', 'navigation', 'time']
|
||||
] +
|
||||
sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/modules/**/*.coffee'))
|
||||
)
|
||||
|
||||
# 'js/vendor/RequireJS.js' - Require JS wrapper.
|
||||
# See https://edx-wiki.atlassian.net/wiki/display/LMS/Integration+of+Require+JS+into+the+system
|
||||
main_vendor_js = [
|
||||
'js/vendor/RequireJS.js',
|
||||
'js/vendor/json2.js',
|
||||
'js/vendor/jquery.min.js',
|
||||
'js/vendor/jquery-ui.min.js',
|
||||
'js/vendor/jquery.cookie.js',
|
||||
'js/vendor/jquery.qtip.min.js',
|
||||
'js/vendor/swfobject/swfobject.js',
|
||||
'js/vendor/jquery.ba-bbq.min.js',
|
||||
]
|
||||
|
||||
discussion_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/discussion/**/*.coffee'))
|
||||
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.coffee'))
|
||||
peer_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static','coffee/src/peer_grading/**/*.coffee'))
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'application': {
|
||||
'source_filenames': ['sass/application.scss'],
|
||||
'output_filename': 'css/lms-application.css',
|
||||
},
|
||||
'course': {
|
||||
'source_filenames': ['js/vendor/CodeMirror/codemirror.css', 'css/vendor/jquery.treeview.css', 'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css', 'css/vendor/jquery.qtip.min.css', 'sass/course.scss'],
|
||||
'output_filename': 'css/lms-course.css',
|
||||
},
|
||||
'source_filenames': [
|
||||
'js/vendor/CodeMirror/codemirror.css',
|
||||
'css/vendor/jquery.treeview.css',
|
||||
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
|
||||
'css/vendor/jquery.qtip.min.css',
|
||||
'sass/course.scss'
|
||||
],
|
||||
'output_filename': 'css/lms-course.css',
|
||||
},
|
||||
'ie-fixes': {
|
||||
'source_filenames': ['sass/ie.scss'],
|
||||
'output_filename': 'css/lms-ie.css',
|
||||
@@ -421,110 +461,15 @@ PIPELINE_CSS = {
|
||||
}
|
||||
|
||||
PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/ie.scss', 'sass/course.scss']
|
||||
|
||||
courseware_only_js = [
|
||||
PROJECT_ROOT / 'static/coffee/src/' + pth + '.coffee'
|
||||
for pth
|
||||
in ['courseware', 'histogram', 'navigation', 'time']
|
||||
]
|
||||
courseware_only_js += [
|
||||
pth for pth
|
||||
in glob2.glob(PROJECT_ROOT / 'static/coffee/src/modules/**/*.coffee')
|
||||
]
|
||||
|
||||
main_vendor_js = [
|
||||
'js/vendor/RequireJS.js',
|
||||
'js/vendor/json2.js',
|
||||
'js/vendor/RequireJS.js',
|
||||
'js/vendor/jquery.min.js',
|
||||
'js/vendor/jquery-ui.min.js',
|
||||
'js/vendor/jquery.cookie.js',
|
||||
'js/vendor/jquery.qtip.min.js',
|
||||
'js/vendor/swfobject/swfobject.js',
|
||||
]
|
||||
|
||||
discussion_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/discussion/**/*.coffee'))
|
||||
|
||||
staff_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/staff_grading/**/*.coffee'))
|
||||
peer_grading_js = sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/peer_grading/**/*.coffee'))
|
||||
|
||||
|
||||
# Load javascript from all of the available xmodules, and
|
||||
# prep it for use in pipeline js
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.hidden_module import HiddenDescriptor
|
||||
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
|
||||
css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
|
||||
module_styles_path = css_file_dir / "_module-styles.scss"
|
||||
|
||||
for dir_ in (js_file_dir, css_file_dir):
|
||||
try:
|
||||
os.makedirs(dir_)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
js_fragments = set()
|
||||
css_fragments = defaultdict(set)
|
||||
for _, descriptor in XModuleDescriptor.load_classes() + [(None, HiddenDescriptor)]:
|
||||
module_js = descriptor.module_class.get_javascript()
|
||||
for filetype in ('coffee', 'js'):
|
||||
for idx, fragment in enumerate(module_js.get(filetype, [])):
|
||||
js_fragments.add((idx, filetype, fragment))
|
||||
|
||||
module_css = descriptor.module_class.get_css()
|
||||
for filetype in ('sass', 'scss', 'css'):
|
||||
for idx, fragment in enumerate(module_css.get(filetype, [])):
|
||||
css_fragments[idx, filetype, fragment].add(descriptor.module_class.__name__)
|
||||
|
||||
module_js_sources = []
|
||||
for idx, filetype, fragment in sorted(js_fragments):
|
||||
path = js_file_dir / "{idx}-{hash}.{type}".format(
|
||||
idx=idx,
|
||||
hash=hashlib.md5(fragment).hexdigest(),
|
||||
type=filetype)
|
||||
with open(path, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
|
||||
|
||||
css_imports = defaultdict(set)
|
||||
for (idx, filetype, fragment), classes in sorted(css_fragments.items()):
|
||||
fragment_name = "{idx}-{hash}.{type}".format(
|
||||
idx=idx,
|
||||
hash=hashlib.md5(fragment).hexdigest(),
|
||||
type=filetype)
|
||||
# Prepend _ so that sass just includes the files into a single file
|
||||
with open(css_file_dir / '_' + fragment_name, 'w') as js_file:
|
||||
js_file.write(fragment)
|
||||
|
||||
for class_ in classes:
|
||||
css_imports[class_].add(fragment_name)
|
||||
|
||||
with open(module_styles_path, 'w') as module_styles:
|
||||
for class_, fragment_names in css_imports.items():
|
||||
imports = "\n".join('@import "{0}";'.format(name) for name in fragment_names)
|
||||
module_styles.write(""".xmodule_{class_} {{ {imports} }}""".format(
|
||||
class_=class_, imports=imports
|
||||
))
|
||||
|
||||
PIPELINE_JS = {
|
||||
'application': {
|
||||
# Application will contain all paths not in courseware_only_js or
|
||||
# discussion_js or staff_grading_js
|
||||
'source_filenames': [
|
||||
pth.replace(COMMON_ROOT / 'static/', '')
|
||||
for pth
|
||||
in sorted(glob2.glob(COMMON_ROOT / 'static/coffee/src/**/*.coffee'))
|
||||
] + [
|
||||
pth.replace(PROJECT_ROOT / 'static/', '')
|
||||
for pth in sorted(glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee'))\
|
||||
if (pth not in courseware_only_js and
|
||||
pth not in discussion_js and
|
||||
pth not in peer_grading_js and
|
||||
pth not in staff_grading_js)
|
||||
] + [
|
||||
|
||||
# Application will contain all paths not in courseware_only_js
|
||||
'source_filenames': sorted(
|
||||
set(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/**/*.coffee') +
|
||||
rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/**/*.coffee')) -
|
||||
set(courseware_js + discussion_js + staff_grading_js + peer_grading_js)
|
||||
) + [
|
||||
'js/form.ext.js',
|
||||
'js/my_courses_dropdown.js',
|
||||
'js/toggle_login_modal.js',
|
||||
@@ -534,7 +479,7 @@ PIPELINE_JS = {
|
||||
'output_filename': 'js/lms-application.js'
|
||||
},
|
||||
'courseware': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in courseware_only_js],
|
||||
'source_filenames': courseware_js,
|
||||
'output_filename': 'js/lms-courseware.js'
|
||||
},
|
||||
'main_vendor': {
|
||||
@@ -542,23 +487,19 @@ PIPELINE_JS = {
|
||||
'output_filename': 'js/lms-main_vendor.js',
|
||||
},
|
||||
'module-js': {
|
||||
'source_filenames': module_js_sources,
|
||||
'source_filenames': module_js,
|
||||
'output_filename': 'js/lms-modules.js',
|
||||
},
|
||||
'spec': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
|
||||
'output_filename': 'js/lms-spec.js'
|
||||
},
|
||||
'discussion' : {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in discussion_js],
|
||||
'discussion': {
|
||||
'source_filenames': discussion_js,
|
||||
'output_filename': 'js/discussion.js'
|
||||
},
|
||||
'staff_grading' : {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in staff_grading_js],
|
||||
'source_filenames': staff_grading_js,
|
||||
'output_filename': 'js/staff_grading.js'
|
||||
},
|
||||
'peer_grading' : {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in peer_grading_js],
|
||||
'source_filenames': peer_grading_js,
|
||||
'output_filename': 'js/peer_grading.js'
|
||||
}
|
||||
}
|
||||
@@ -649,7 +590,6 @@ INSTALLED_APPS = (
|
||||
'course_wiki.plugins.markdownedx',
|
||||
|
||||
# For testing
|
||||
'django_jasmine',
|
||||
'django.contrib.admin', # only used in DEBUG mode
|
||||
|
||||
# Discussion forums
|
||||
|
||||
@@ -8,7 +8,7 @@ sessions. Assumes structure:
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
from logsettings import get_logger_config
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = True
|
||||
@@ -100,6 +100,10 @@ SUBDOMAIN_BRANDING = {
|
||||
'harvard': 'HarvardX',
|
||||
}
|
||||
|
||||
# List of `university` landing pages to display, even though they may not
|
||||
# have an actual course with that org set
|
||||
VIRTUAL_UNIVERSITIES = []
|
||||
|
||||
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
|
||||
|
||||
################################# mitx revision string #####################
|
||||
|
||||
@@ -14,7 +14,7 @@ if 'eecs1' in socket.gethostname():
|
||||
MITX_ROOT_URL = '/mitx2'
|
||||
|
||||
from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
from logsettings import get_logger_config
|
||||
from .dev import *
|
||||
|
||||
if 'eecs1' in socket.gethostname():
|
||||
|
||||
@@ -8,7 +8,7 @@ sessions. Assumes structure:
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
from logsettings import get_logger_config
|
||||
from .dev import *
|
||||
import socket
|
||||
|
||||
|
||||
38
lms/envs/jasmine.py
Normal file
38
lms/envs/jasmine.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
This configuration is used for running jasmine tests
|
||||
"""
|
||||
|
||||
from .test import *
|
||||
from logsettings import get_logger_config
|
||||
|
||||
ENABLE_JASMINE = True
|
||||
DEBUG = True
|
||||
|
||||
LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
dev_env=True,
|
||||
debug=True,
|
||||
local_loglevel='ERROR',
|
||||
console_loglevel='ERROR')
|
||||
|
||||
PIPELINE_JS['js-test-source'] = {
|
||||
'source_filenames': sum([
|
||||
pipeline_group['source_filenames']
|
||||
for group_name, pipeline_group
|
||||
in PIPELINE_JS.items()
|
||||
if group_name != 'spec'
|
||||
], []),
|
||||
'output_filename': 'js/lms-test-source.js'
|
||||
}
|
||||
|
||||
PIPELINE_JS['spec'] = {
|
||||
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.coffee')),
|
||||
'output_filename': 'js/lms-spec.js'
|
||||
}
|
||||
|
||||
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
|
||||
|
||||
STATICFILES_DIRS.append(COMMON_ROOT / 'test' / 'phantom-jasmine' / 'lib')
|
||||
|
||||
INSTALLED_APPS += ('django_jasmine', )
|
||||
@@ -1,143 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from logging.handlers import SysLogHandler
|
||||
|
||||
|
||||
def get_logger_config(log_dir,
|
||||
logging_env="no_env",
|
||||
tracking_filename="tracking.log",
|
||||
edx_filename="edx.log",
|
||||
dev_env=False,
|
||||
syslog_addr=None,
|
||||
debug=False,
|
||||
local_loglevel='INFO'):
|
||||
|
||||
"""
|
||||
|
||||
Return the appropriate logging config dictionary. You should assign the
|
||||
result of this to the LOGGING var in your settings. The reason it's done
|
||||
this way instead of registering directly is because I didn't want to worry
|
||||
about resetting the logging state if this is called multiple times when
|
||||
settings are extended.
|
||||
|
||||
If dev_env is set to true logging will not be done via local rsyslogd,
|
||||
instead, tracking and application logs will be dropped in log_dir.
|
||||
|
||||
"tracking_filename" and "edx_filename" are ignored unless dev_env
|
||||
is set to true since otherwise logging is handled by rsyslogd.
|
||||
|
||||
"""
|
||||
|
||||
# Revert to INFO if an invalid string is passed in
|
||||
if local_loglevel not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
|
||||
local_loglevel = 'INFO'
|
||||
|
||||
hostname = platform.node().split(".")[0]
|
||||
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s "
|
||||
"[{hostname} %(process)d] [%(filename)s:%(lineno)d] "
|
||||
"- %(message)s").format(
|
||||
logging_env=logging_env, hostname=hostname)
|
||||
|
||||
handlers = ['console', 'local'] if debug else ['console',
|
||||
'syslogger-remote', 'local']
|
||||
|
||||
logger_config = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'standard': {
|
||||
'format': '%(asctime)s %(levelname)s %(process)d '
|
||||
'[%(name)s] %(filename)s:%(lineno)d - %(message)s',
|
||||
},
|
||||
'syslog_format': {'format': syslog_format},
|
||||
'raw': {'format': '%(message)s'},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'DEBUG' if debug else 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'standard',
|
||||
'stream': sys.stdout,
|
||||
},
|
||||
'syslogger-remote': {
|
||||
'level': 'INFO',
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': syslog_addr,
|
||||
'formatter': 'syslog_format',
|
||||
},
|
||||
'newrelic': {
|
||||
'level': 'ERROR',
|
||||
'class': 'newrelic_logging.NewRelicHandler',
|
||||
'formatter': 'raw',
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': handlers,
|
||||
'propagate': True,
|
||||
'level': 'INFO'
|
||||
},
|
||||
'tracking': {
|
||||
'handlers': ['tracking'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
},
|
||||
'': {
|
||||
'handlers': handlers,
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
'mitx': {
|
||||
'handlers': handlers,
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
'keyedcache': {
|
||||
'handlers': handlers,
|
||||
'level': 'DEBUG',
|
||||
'propagate': False
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if dev_env:
|
||||
tracking_file_loc = os.path.join(log_dir, tracking_filename)
|
||||
edx_file_loc = os.path.join(log_dir, edx_filename)
|
||||
logger_config['handlers'].update({
|
||||
'local': {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'level': local_loglevel,
|
||||
'formatter': 'standard',
|
||||
'filename': edx_file_loc,
|
||||
'maxBytes': 1024 * 1024 * 2,
|
||||
'backupCount': 5,
|
||||
},
|
||||
'tracking': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'filename': tracking_file_loc,
|
||||
'formatter': 'raw',
|
||||
'maxBytes': 1024 * 1024 * 2,
|
||||
'backupCount': 5,
|
||||
},
|
||||
})
|
||||
else:
|
||||
logger_config['handlers'].update({
|
||||
'local': {
|
||||
'level': local_loglevel,
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': '/dev/log',
|
||||
'formatter': 'syslog_format',
|
||||
'facility': SysLogHandler.LOG_LOCAL0,
|
||||
},
|
||||
'tracking': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': '/dev/log',
|
||||
'facility': SysLogHandler.LOG_LOCAL1,
|
||||
'formatter': 'raw',
|
||||
},
|
||||
})
|
||||
|
||||
return logger_config
|
||||
@@ -8,7 +8,7 @@ sessions. Assumes structure:
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
from logsettings import get_logger_config
|
||||
|
||||
STATIC_GRAB = True
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ sessions. Assumes structure:
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
from logsettings import get_logger_config
|
||||
import os
|
||||
from path import path
|
||||
|
||||
@@ -125,8 +125,15 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
################################## OPENID ######################################
|
||||
MITX_FEATURES['AUTH_USE_OPENID'] = True
|
||||
MITX_FEATURES['AUTH_USE_OPENID_PROVIDER'] = True
|
||||
|
||||
OPENID_CREATE_USERS = False
|
||||
OPENID_UPDATE_DETAILS_FROM_SREG = True
|
||||
OPENID_USE_AS_ADMIN_LOGIN = False
|
||||
OPENID_PROVIDER_TRUSTED_ROOTS = ['*']
|
||||
|
||||
INSTALLED_APPS += ('external_auth',)
|
||||
INSTALLED_APPS += ('django_openid_auth',)
|
||||
|
||||
############################ STATIC FILES #############################
|
||||
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
MEDIA_ROOT = TEST_ROOT / "uploads"
|
||||
|
||||
@@ -8,7 +8,7 @@ sessions. Assumes structure:
|
||||
/log # Where we're going to write log files
|
||||
"""
|
||||
from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
from logsettings import get_logger_config
|
||||
import os
|
||||
|
||||
DEBUG = True
|
||||
|
||||
@@ -154,8 +154,9 @@ def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False
|
||||
|
||||
class formula(object):
|
||||
'''
|
||||
Representation of a mathematical formula object. Accepts mathml math expression for constructing,
|
||||
and can produce sympy translation. The formula may or may not include an assignment (=).
|
||||
Representation of a mathematical formula object. Accepts mathml math expression
|
||||
for constructing, and can produce sympy translation. The formula may or may not
|
||||
include an assignment (=).
|
||||
'''
|
||||
def __init__(self, expr, asciimath='', options=None):
|
||||
self.expr = expr.strip()
|
||||
@@ -194,8 +195,12 @@ class formula(object):
|
||||
|
||||
def preprocess_pmathml(self, xml):
|
||||
'''
|
||||
Pre-process presentation MathML from ASCIIMathML to make it more acceptable for SnuggleTeX, and also
|
||||
to accomodate some sympy conventions (eg hat(i) for \hat{i}).
|
||||
Pre-process presentation MathML from ASCIIMathML to make it more
|
||||
acceptable for SnuggleTeX, and also to accomodate some sympy
|
||||
conventions (eg hat(i) for \hat{i}).
|
||||
|
||||
This method would be a good spot to look for an integral and convert
|
||||
it, if possible...
|
||||
'''
|
||||
|
||||
if type(xml) == str or type(xml) == unicode:
|
||||
@@ -266,6 +271,9 @@ class formula(object):
|
||||
'''
|
||||
Return sympy expression for the math formula.
|
||||
The math formula is converted to Content MathML then that is parsed.
|
||||
|
||||
This is a recursive function, called on every CMML node. Support for
|
||||
more functions can be added by modifying opdict, abould halfway down
|
||||
'''
|
||||
|
||||
if self.the_sympy: return self.the_sympy
|
||||
|
||||
@@ -157,13 +157,33 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
|
||||
'''
|
||||
Check a symbolic mathematical expression using sympy.
|
||||
The input may be presentation MathML. Uses formula.
|
||||
|
||||
This is the default Symbolic Response checking function
|
||||
|
||||
Desc of args:
|
||||
expect is a sympy string representing the correct answer. It is interpreted
|
||||
using my_sympify (from formula.py), which reads strings as sympy input
|
||||
(e.g. 'integrate(x^2, (x,1,2))' would be valid, and evaluate to give 1.5)
|
||||
|
||||
ans is student-typed answer. It is expected to be ascii math, but the code
|
||||
below would support a sympy string.
|
||||
|
||||
dynamath is the PMathML string converted by MathJax. It is used if
|
||||
evaluation with ans is not sufficient.
|
||||
|
||||
options is a string with these possible substrings, set as an xml property
|
||||
of the problem:
|
||||
-matrix - make a sympy matrix, rather than a list of lists, if possible
|
||||
-qubit - passed to my_sympify
|
||||
-imaginary - used in formla, presumably to signal to use i as sqrt(-1)?
|
||||
-numerical - force numerical comparison.
|
||||
'''
|
||||
|
||||
msg = ''
|
||||
# msg += '<p/>abname=%s' % abname
|
||||
# msg += '<p/>adict=%s' % (repr(adict).replace('<','<'))
|
||||
|
||||
threshold = 1.0e-3
|
||||
threshold = 1.0e-3 # for numerical comparison (also with matrices)
|
||||
DEBUG = debug
|
||||
|
||||
if xml is not None:
|
||||
@@ -184,13 +204,17 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
|
||||
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err, expect)
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
|
||||
###### Sympy input #######
|
||||
# if expected answer is a number, try parsing provided answer as a number also
|
||||
try:
|
||||
fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
|
||||
except Exception, err:
|
||||
fans = None
|
||||
|
||||
if hasattr(fexpect, 'is_number') and fexpect.is_number and fans and hasattr(fans, 'is_number') and fans.is_number:
|
||||
# do a numerical comparison if both expected and answer are numbers
|
||||
if (hasattr(fexpect, 'is_number') and fexpect.is_number and fans
|
||||
and hasattr(fans, 'is_number') and fans.is_number):
|
||||
if abs(abs(fans - fexpect) / fexpect) < threshold:
|
||||
return {'ok': True, 'msg': msg}
|
||||
else:
|
||||
@@ -208,6 +232,8 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
|
||||
msg += '<p>You entered: %s</p>' % to_latex(fans)
|
||||
return {'ok': True, 'msg': msg}
|
||||
|
||||
|
||||
###### PMathML input ######
|
||||
# convert mathml answer to formula
|
||||
try:
|
||||
if dynamath:
|
||||
@@ -216,6 +242,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
|
||||
mmlans = None
|
||||
if not mmlans:
|
||||
return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
|
||||
|
||||
f = formula(mmlans, options=options)
|
||||
|
||||
# get sympy representation of the formula
|
||||
@@ -238,7 +265,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
|
||||
msg += '<hr>'
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
# compare with expected
|
||||
# do numerical comparison with expected
|
||||
if hasattr(fexpect, 'is_number') and fexpect.is_number:
|
||||
if hasattr(fsym, 'is_number') and fsym.is_number:
|
||||
if abs(abs(fsym - fexpect) / fexpect) < threshold:
|
||||
@@ -250,6 +277,10 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
|
||||
# msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','<')
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
# Here is a good spot for adding calls to X.simplify() or X.expand(),
|
||||
# allowing equivalence over binomial expansion or trig identities
|
||||
|
||||
# exactly the same?
|
||||
if fexpect == fsym:
|
||||
return {'ok': True, 'msg': msg}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class @Courseware
|
||||
new Courseware
|
||||
|
||||
render: ->
|
||||
XModule.loadModules('display')
|
||||
XModule.loadModules()
|
||||
$('.course-content .histogram').each ->
|
||||
id = $(this).attr('id').replace(/histogram_/, '')
|
||||
try
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
if Backbone?
|
||||
class @Content extends Backbone.Model
|
||||
|
||||
@contents: {}
|
||||
@contentInfos: {}
|
||||
|
||||
template: -> DiscussionUtil.getTemplate('_content')
|
||||
|
||||
actions:
|
||||
editable: '.admin-edit'
|
||||
can_reply: '.discussion-reply'
|
||||
can_endorse: '.admin-endorse'
|
||||
can_delete: '.admin-delete'
|
||||
can_openclose: '.admin-openclose'
|
||||
|
||||
urlMappers: {}
|
||||
|
||||
urlFor: (name) ->
|
||||
@urlMappers[name].apply(@)
|
||||
|
||||
can: (action) ->
|
||||
(@get('ability') || {})[action]
|
||||
|
||||
updateInfo: (info) ->
|
||||
if info
|
||||
@set('ability', info.ability)
|
||||
@set('voted', info.voted)
|
||||
@set('subscribed', info.subscribed)
|
||||
|
||||
addComment: (comment, options) ->
|
||||
options ||= {}
|
||||
if not options.silent
|
||||
thread = @get('thread')
|
||||
comments_count = parseInt(thread.get('comments_count'))
|
||||
thread.set('comments_count', comments_count + 1)
|
||||
@get('children').push comment
|
||||
model = new Comment $.extend {}, comment, { thread: @get('thread') }
|
||||
@get('comments').add model
|
||||
@trigger "comment:add"
|
||||
model
|
||||
|
||||
removeComment: (comment) ->
|
||||
thread = @get('thread')
|
||||
comments_count = parseInt(thread.get('comments_count'))
|
||||
thread.set('comments_count', comments_count - 1 - comment.getCommentsCount())
|
||||
@trigger "comment:remove"
|
||||
|
||||
resetComments: (children) ->
|
||||
@set 'children', []
|
||||
@set 'comments', new Comments()
|
||||
for comment in (children || [])
|
||||
@addComment comment, { silent: true }
|
||||
|
||||
initialize: ->
|
||||
Content.addContent @id, @
|
||||
if Content.getInfo(@id)
|
||||
@updateInfo(Content.getInfo(@id))
|
||||
@set 'user_url', DiscussionUtil.urlFor('user_profile', @get('user_id'))
|
||||
@resetComments(@get('children'))
|
||||
|
||||
remove: ->
|
||||
|
||||
if @get('type') == 'comment'
|
||||
@get('thread').removeComment(@)
|
||||
@get('thread').trigger "comment:remove", @
|
||||
else
|
||||
@trigger "thread:remove", @
|
||||
|
||||
@addContent: (id, content) -> @contents[id] = content
|
||||
|
||||
@getContent: (id) -> @contents[id]
|
||||
|
||||
@getInfo: (id) ->
|
||||
@contentInfos[id]
|
||||
|
||||
@loadContentInfos: (infos) ->
|
||||
for id, info of infos
|
||||
if @getContent(id)
|
||||
@getContent(id).updateInfo(info)
|
||||
$.extend @contentInfos, infos
|
||||
|
||||
class @Thread extends @Content
|
||||
urlMappers:
|
||||
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
|
||||
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
|
||||
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
|
||||
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
|
||||
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
|
||||
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
|
||||
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
|
||||
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
|
||||
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
|
||||
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
|
||||
|
||||
initialize: ->
|
||||
@set('thread', @)
|
||||
super()
|
||||
|
||||
comment: ->
|
||||
@set("comments_count", parseInt(@get("comments_count")) + 1)
|
||||
|
||||
follow: ->
|
||||
@set('subscribed', true)
|
||||
|
||||
unfollow: ->
|
||||
@set('subscribed', false)
|
||||
|
||||
vote: ->
|
||||
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) + 1
|
||||
@trigger "change", @
|
||||
|
||||
unvote: ->
|
||||
@get("votes")["up_count"] = parseInt(@get("votes")["up_count"]) - 1
|
||||
@trigger "change", @
|
||||
|
||||
display_body: ->
|
||||
if @has("highlighted_body")
|
||||
String(@get("highlighted_body")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
|
||||
else
|
||||
@get("body")
|
||||
|
||||
display_title: ->
|
||||
if @has("highlighted_title")
|
||||
String(@get("highlighted_title")).replace(/<highlight>/g, '<mark>').replace(/<\/highlight>/g, '</mark>')
|
||||
else
|
||||
@get("title")
|
||||
|
||||
toJSON: ->
|
||||
json_attributes = _.clone(@attributes)
|
||||
_.extend(json_attributes, { title: @display_title(), body: @display_body() })
|
||||
|
||||
created_at_date: ->
|
||||
new Date(@get("created_at"))
|
||||
|
||||
created_at_time: ->
|
||||
new Date(@get("created_at")).getTime()
|
||||
|
||||
class @Comment extends @Content
|
||||
urlMappers:
|
||||
'reply': -> DiscussionUtil.urlFor('create_sub_comment', @id)
|
||||
'unvote': -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
|
||||
'upvote': -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
|
||||
'downvote': -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
|
||||
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
|
||||
'update': -> DiscussionUtil.urlFor('update_comment', @id)
|
||||
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
|
||||
|
||||
getCommentsCount: ->
|
||||
count = 0
|
||||
@get('comments').each (comment) ->
|
||||
count += comment.getCommentsCount() + 1
|
||||
count
|
||||
|
||||
class @Comments extends Backbone.Collection
|
||||
|
||||
model: Comment
|
||||
|
||||
initialize: ->
|
||||
@bind "add", (item) =>
|
||||
item.collection = @
|
||||
|
||||
find: (id) ->
|
||||
_.first @where(id: id)
|
||||
@@ -1,82 +0,0 @@
|
||||
if Backbone?
|
||||
class @Discussion extends Backbone.Collection
|
||||
model: Thread
|
||||
|
||||
initialize: (models, options={})->
|
||||
@pages = options['pages'] || 1
|
||||
@current_page = 1
|
||||
@bind "add", (item) =>
|
||||
item.discussion = @
|
||||
@comparator = @sortByDateRecentFirst
|
||||
@on "thread:remove", (thread) =>
|
||||
@remove(thread)
|
||||
|
||||
find: (id) ->
|
||||
_.first @where(id: id)
|
||||
|
||||
hasMorePages: ->
|
||||
@current_page < @pages
|
||||
|
||||
addThread: (thread, options) ->
|
||||
# TODO: Check for existing thread with same ID in a faster way
|
||||
if not @find(thread.id)
|
||||
options ||= {}
|
||||
model = new Thread thread
|
||||
@add model
|
||||
model
|
||||
|
||||
retrieveAnotherPage: (mode, options={}, sort_options={})->
|
||||
@current_page += 1
|
||||
data = { page: @current_page }
|
||||
switch mode
|
||||
when 'search'
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
data['text'] = options.search_text
|
||||
when 'commentables'
|
||||
url = DiscussionUtil.urlFor 'search'
|
||||
data['commentable_ids'] = options.commentable_ids
|
||||
when 'all'
|
||||
url = DiscussionUtil.urlFor 'threads'
|
||||
when 'followed'
|
||||
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
|
||||
data['sort_key'] = sort_options.sort_key || 'date'
|
||||
data['sort_order'] = sort_options.sort_order || 'desc'
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$el
|
||||
url: url
|
||||
data: data
|
||||
dataType: 'json'
|
||||
success: (response, textStatus) =>
|
||||
models = @models
|
||||
new_threads = [new Thread(data) for data in response.discussion_data][0]
|
||||
new_collection = _.union(models, new_threads)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@reset new_collection
|
||||
@pages = response.num_pages
|
||||
@current_page = response.page
|
||||
|
||||
sortByDate: (thread) ->
|
||||
thread.get("created_at")
|
||||
|
||||
sortByDateRecentFirst: (thread) ->
|
||||
-(new Date(thread.get("created_at")).getTime())
|
||||
#return String.fromCharCode.apply(String,
|
||||
# _.map(thread.get("created_at").split(""),
|
||||
# ((c) -> return 0xffff - c.charChodeAt()))
|
||||
#)
|
||||
|
||||
sortByVotes: (thread1, thread2) ->
|
||||
thread1_count = parseInt(thread1.get("votes")['up_count'])
|
||||
thread2_count = parseInt(thread2.get("votes")['up_count'])
|
||||
if thread2_count != thread1_count
|
||||
thread2_count - thread1_count
|
||||
else
|
||||
thread2.created_at_time() - thread1.created_at_time()
|
||||
|
||||
sortByComments: (thread1, thread2) ->
|
||||
thread1_count = parseInt(thread1.get("comments_count"))
|
||||
thread2_count = parseInt(thread2.get("comments_count"))
|
||||
if thread2_count != thread1_count
|
||||
thread2_count - thread1_count
|
||||
else
|
||||
thread2.created_at_time() - thread1.created_at_time()
|
||||
@@ -1,28 +0,0 @@
|
||||
class @DiscussionFilter
|
||||
@filterDrop: (e) ->
|
||||
$drop = $(e.target).parents('.topic_menu_wrapper, .browse-topic-drop-menu-wrapper')
|
||||
query = $(e.target).val()
|
||||
$items = $drop.find('a')
|
||||
|
||||
if(query.length == 0)
|
||||
$items.removeClass('hidden')
|
||||
return;
|
||||
|
||||
$items.addClass('hidden')
|
||||
$items.each (i) ->
|
||||
thisText = $(this).not('.unread').text()
|
||||
$(this).parents('ul').siblings('a').not('.unread').each (i) ->
|
||||
thisText = thisText + ' ' + $(this).text();
|
||||
|
||||
test = true
|
||||
terms = thisText.split(' ')
|
||||
|
||||
if(thisText.toLowerCase().search(query.toLowerCase()) == -1)
|
||||
test = false
|
||||
|
||||
if(test)
|
||||
$(this).removeClass('hidden')
|
||||
# show children
|
||||
$(this).parent().find('a').removeClass('hidden');
|
||||
# show parents
|
||||
$(this).parents('ul').siblings('a').removeClass('hidden');
|
||||
@@ -1,124 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionModuleView extends Backbone.View
|
||||
events:
|
||||
"click .discussion-show": "toggleDiscussion"
|
||||
"click .new-post-btn": "toggleNewPost"
|
||||
"click .new-post-cancel": "hideNewPost"
|
||||
"click .discussion-paginator a": "navigateToPage"
|
||||
|
||||
paginationTemplate: -> DiscussionUtil.getTemplate("_pagination")
|
||||
page_re: /\?discussion_page=(\d+)/
|
||||
initialize: ->
|
||||
@toggleDiscussionBtn = @$(".discussion-show")
|
||||
# Set the page if it was set in the URL. This is used to allow deep linking to pages
|
||||
match = @page_re.exec(window.location.href)
|
||||
if match
|
||||
@page = parseInt(match[1])
|
||||
else
|
||||
@page = 1
|
||||
|
||||
toggleNewPost: (event) ->
|
||||
event.preventDefault()
|
||||
if !@newPostForm
|
||||
@toggleDiscussion()
|
||||
@isWaitingOnNewPost = true;
|
||||
return
|
||||
if @showed
|
||||
@newPostForm.slideDown(300)
|
||||
else
|
||||
@newPostForm.show()
|
||||
@toggleDiscussionBtn.addClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
|
||||
@$("section.discussion").slideDown()
|
||||
@showed = true
|
||||
|
||||
hideNewPost: (event) ->
|
||||
event.preventDefault()
|
||||
@newPostForm.slideUp(300)
|
||||
|
||||
toggleDiscussion: (event) ->
|
||||
if @showed
|
||||
@$("section.discussion").slideUp()
|
||||
@toggleDiscussionBtn.removeClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html("Show Discussion")
|
||||
@showed = false
|
||||
else
|
||||
@toggleDiscussionBtn.addClass('shown')
|
||||
@toggleDiscussionBtn.find('.button-text').html("Hide Discussion")
|
||||
|
||||
if @retrieved
|
||||
@$("section.discussion").slideDown()
|
||||
@showed = true
|
||||
else
|
||||
$elem = @toggleDiscussionBtn
|
||||
@loadPage $elem
|
||||
|
||||
loadPage: ($elem)=>
|
||||
discussionId = @$el.data("discussion-id")
|
||||
url = DiscussionUtil.urlFor('retrieve_discussion', discussionId) + "?page=#{@page}"
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
$loading: $elem
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: 'json'
|
||||
success: (response, textStatus, jqXHR) => @renderDiscussion($elem, response, textStatus, discussionId)
|
||||
|
||||
renderDiscussion: ($elem, response, textStatus, discussionId) =>
|
||||
window.user = new DiscussionUser(response.user_info)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
DiscussionUtil.loadRoles(response.roles)
|
||||
allow_anonymous = response.allow_anonymous
|
||||
allow_anonymous_to_peers = response.allow_anonymous_to_peers
|
||||
# $elem.html("Hide Discussion")
|
||||
@discussion = new Discussion()
|
||||
@discussion.reset(response.discussion_data, {silent: false})
|
||||
$discussion = $(Mustache.render $("script#_inline_discussion").html(), {'threads':response.discussion_data, 'discussionId': discussionId, 'allow_anonymous_to_peers': allow_anonymous_to_peers, 'allow_anonymous': allow_anonymous})
|
||||
if @$('section.discussion').length
|
||||
@$('section.discussion').replaceWith($discussion)
|
||||
else
|
||||
$(".discussion-module").append($discussion)
|
||||
@newPostForm = $('.new-post-article')
|
||||
@threadviews = @discussion.map (thread) ->
|
||||
new DiscussionThreadInlineView el: @$("article#thread_#{thread.id}"), model: thread
|
||||
_.each @threadviews, (dtv) -> dtv.render()
|
||||
DiscussionUtil.bulkUpdateContentInfo(window.$$annotated_content_info)
|
||||
@newPostView = new NewPostInlineView el: @$('.new-post-article'), collection: @discussion
|
||||
@discussion.on "add", @addThread
|
||||
@retrieved = true
|
||||
@showed = true
|
||||
@renderPagination(2, response.num_pages)
|
||||
if @isWaitingOnNewPost
|
||||
@newPostForm.show()
|
||||
|
||||
addThread: (thread, collection, options) =>
|
||||
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
|
||||
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
|
||||
@$('section.discussion > .threads').prepend(article)
|
||||
threadView = new DiscussionThreadInlineView el: article, model: thread
|
||||
threadView.render()
|
||||
@threadviews.unshift threadView
|
||||
|
||||
renderPagination: (delta, numPages) =>
|
||||
minPage = Math.max(@page - delta, 1)
|
||||
maxPage = Math.min(@page + delta, numPages)
|
||||
pageUrl = (number) ->
|
||||
"?discussion_page=#{number}"
|
||||
params =
|
||||
page: @page
|
||||
lowPages: _.range(minPage, @page).map (n) -> {number: n, url: pageUrl(n)}
|
||||
highPages: _.range(@page+1, maxPage+1).map (n) -> {number: n, url: pageUrl(n)}
|
||||
previous: if @page-1 >= 1 then {url: pageUrl(@page-1), number: @page-1} else false
|
||||
next: if @page+1 <= numPages then {url: pageUrl(@page+1), number: @page+1} else false
|
||||
leftdots: minPage > 2
|
||||
rightdots: maxPage < numPages-1
|
||||
first: if minPage > 1 then {url: pageUrl(1)} else false
|
||||
last: if maxPage < numPages then {number: numPages, url: pageUrl(numPages)} else false
|
||||
thing = Mustache.render @paginationTemplate(), params
|
||||
@$('section.pagination').html(thing)
|
||||
|
||||
navigateToPage: (event) =>
|
||||
event.preventDefault()
|
||||
window.history.pushState({}, window.document.title, event.target.href)
|
||||
@page = $(event.target).data('page-number')
|
||||
@loadPage($(event.target))
|
||||
@@ -1,57 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionRouter extends Backbone.Router
|
||||
routes:
|
||||
"": "allThreads"
|
||||
":forum_name/threads/:thread_id" : "showThread"
|
||||
|
||||
initialize: (options) ->
|
||||
@discussion = options['discussion']
|
||||
@nav = new DiscussionThreadListView(collection: @discussion, el: $(".sidebar"))
|
||||
@nav.on "thread:selected", @navigateToThread
|
||||
@nav.on "thread:removed", @navigateToAllThreads
|
||||
@nav.on "threads:rendered", @setActiveThread
|
||||
@nav.render()
|
||||
|
||||
@newPostView = new NewPostView(el: $(".new-post-article"), collection: @discussion)
|
||||
@nav.on "thread:created", @navigateToThread
|
||||
@newPost = $('.new-post-article')
|
||||
$('.new-post-btn').bind "click", @showNewPost
|
||||
$('.new-post-cancel').bind "click", @hideNewPost
|
||||
|
||||
allThreads: ->
|
||||
@nav.updateSidebar()
|
||||
|
||||
setActiveThread: =>
|
||||
if @thread
|
||||
@nav.setActiveThread(@thread.get("id"))
|
||||
|
||||
showThread: (forum_name, thread_id) ->
|
||||
@thread = @discussion.get(thread_id)
|
||||
@thread.set("unread_comments_count", 0)
|
||||
@thread.set("read", true)
|
||||
@setActiveThread()
|
||||
if(@main)
|
||||
@main.cleanup()
|
||||
@main.undelegateEvents()
|
||||
|
||||
@main = new DiscussionThreadView(el: $(".discussion-column"), model: @thread)
|
||||
@main.render()
|
||||
@main.on "thread:responses:rendered", =>
|
||||
@nav.updateSidebar()
|
||||
@main.on "tag:selected", (tag) =>
|
||||
search = "[#{tag}]"
|
||||
@nav.setAndSearchFor(search)
|
||||
|
||||
navigateToThread: (thread_id) =>
|
||||
thread = @discussion.get(thread_id)
|
||||
@navigate("#{thread.get("commentable_id")}/threads/#{thread_id}", trigger: true)
|
||||
|
||||
navigateToAllThreads: =>
|
||||
@navigate("", trigger: true)
|
||||
|
||||
showNewPost: (event) =>
|
||||
@newPost.slideDown(300)
|
||||
$('.new-post-title').focus()
|
||||
|
||||
hideNewPost: (event) =>
|
||||
@newPost.slideUp(300)
|
||||
@@ -1,29 +0,0 @@
|
||||
if Backbone?
|
||||
DiscussionApp =
|
||||
start: (elem)->
|
||||
# TODO: Perhaps eliminate usage of global variables when possible
|
||||
DiscussionUtil.loadRolesFromContainer()
|
||||
element = $(elem)
|
||||
window.$$course_id = element.data("course-id")
|
||||
user_info = element.data("user-info")
|
||||
threads = element.data("threads")
|
||||
thread_pages = element.data("thread-pages")
|
||||
content_info = element.data("content-info")
|
||||
window.user = new DiscussionUser(user_info)
|
||||
Content.loadContentInfos(content_info)
|
||||
discussion = new Discussion(threads, pages: thread_pages)
|
||||
new DiscussionRouter({discussion: discussion})
|
||||
Backbone.history.start({pushState: true, root: "/courses/#{$$course_id}/discussion/forum/"})
|
||||
DiscussionProfileApp =
|
||||
start: (elem) ->
|
||||
element = $(elem)
|
||||
window.$$course_id = element.data("course-id")
|
||||
threads = element.data("threads")
|
||||
user_info = element.data("user-info")
|
||||
window.user = new DiscussionUser(user_info)
|
||||
new DiscussionUserProfileView(el: element, collection: threads)
|
||||
$ ->
|
||||
$("section.discussion").each (index, elem) ->
|
||||
DiscussionApp.start(elem)
|
||||
$("section.discussion-user-threads").each (index, elem) ->
|
||||
DiscussionProfileApp.start(elem)
|
||||
@@ -1,15 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionUser extends Backbone.Model
|
||||
following: (thread) ->
|
||||
_.include(@get('subscribed_thread_ids'), thread.id)
|
||||
|
||||
voted: (thread) ->
|
||||
_.include(@get('upvoted_ids'), thread.id)
|
||||
|
||||
vote: (thread) ->
|
||||
@get('upvoted_ids').push(thread.id)
|
||||
thread.vote()
|
||||
|
||||
unvote: (thread) ->
|
||||
@set('upvoted_ids', _.without(@get('upvoted_ids'), thread.id))
|
||||
thread.unvote()
|
||||
@@ -1,47 +0,0 @@
|
||||
$ ->
|
||||
new TooltipManager
|
||||
|
||||
class @TooltipManager
|
||||
constructor: () ->
|
||||
@$body = $('body')
|
||||
@$tooltip = $('<div class="tooltip"></div>')
|
||||
@$body.delegate '[data-tooltip]',
|
||||
'mouseover': @showTooltip,
|
||||
'mousemove': @moveTooltip,
|
||||
'mouseout': @hideTooltip,
|
||||
'click': @hideTooltip
|
||||
|
||||
showTooltip: (e) =>
|
||||
tooltipText = $(e.target).attr('data-tooltip')
|
||||
@$tooltip.html(tooltipText)
|
||||
@$body.append(@$tooltip)
|
||||
$(e.target).children().css('pointer-events', 'none')
|
||||
|
||||
tooltipCoords =
|
||||
x: e.pageX - (@$tooltip.outerWidth() / 2)
|
||||
y: e.pageY - (@$tooltip.outerHeight() + 15)
|
||||
|
||||
@$tooltip.css
|
||||
'left': tooltipCoords.x,
|
||||
'top': tooltipCoords.y
|
||||
|
||||
@tooltipTimer = setTimeout ()=>
|
||||
@$tooltip.show().css('opacity', 1)
|
||||
|
||||
@tooltipTimer = setTimeout ()=>
|
||||
@hideTooltip()
|
||||
, 3000
|
||||
, 500
|
||||
|
||||
moveTooltip: (e) =>
|
||||
tooltipCoords =
|
||||
x: e.pageX - (@$tooltip.outerWidth() / 2)
|
||||
y: e.pageY - (@$tooltip.outerHeight() + 15)
|
||||
|
||||
@$tooltip.css
|
||||
'left': tooltipCoords.x
|
||||
'top': tooltipCoords.y
|
||||
|
||||
hideTooltip: (e) =>
|
||||
@$tooltip.hide().css('opacity', 0)
|
||||
clearTimeout(@tooltipTimer)
|
||||
@@ -1,30 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionUserProfileView extends Backbone.View
|
||||
toggleModeratorStatus: (event) ->
|
||||
confirmValue = confirm("Are you sure?")
|
||||
if not confirmValue then return
|
||||
$elem = $(event.target)
|
||||
if $elem.hasClass("sidebar-promote-moderator-button")
|
||||
isModerator = true
|
||||
else if $elem.hasClass("sidebar-revoke-moderator-button")
|
||||
isModerator = false
|
||||
else
|
||||
console.error "unrecognized moderator status"
|
||||
return
|
||||
url = DiscussionUtil.urlFor('update_moderator_status', $$profiled_user_id)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
is_moderator: isModerator
|
||||
error: (response, textStatus, e) ->
|
||||
console.log e
|
||||
success: (response, textStatus) =>
|
||||
parent = @$el.parent()
|
||||
@$el.replaceWith(response.html)
|
||||
view = new DiscussionUserProfileView el: parent.children(".user-profile")
|
||||
|
||||
events:
|
||||
"click .sidebar-toggle-moderator-button": "toggleModeratorStatus"
|
||||
@@ -1,304 +0,0 @@
|
||||
$ ->
|
||||
if !window.$$contents
|
||||
window.$$contents = {}
|
||||
$.fn.extend
|
||||
loading: ->
|
||||
@$_loading = $("<div class='loading-animation'></div>")
|
||||
$(this).after(@$_loading)
|
||||
loaded: ->
|
||||
@$_loading.remove()
|
||||
|
||||
class @DiscussionUtil
|
||||
|
||||
@wmdEditors: {}
|
||||
|
||||
@getTemplate: (id) ->
|
||||
$("script##{id}").html()
|
||||
|
||||
@loadRoles: (roles)->
|
||||
@roleIds = roles
|
||||
|
||||
@loadRolesFromContainer: ->
|
||||
@loadRoles($("#discussion-container").data("roles"))
|
||||
|
||||
@isStaff: (user_id) ->
|
||||
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
|
||||
_.include(staff, parseInt(user_id))
|
||||
|
||||
@isTA: (user_id) ->
|
||||
ta = _.union(@roleIds['Community TA'])
|
||||
_.include(ta, parseInt(user_id))
|
||||
|
||||
@bulkUpdateContentInfo: (infos) ->
|
||||
for id, info of infos
|
||||
Content.getContent(id).updateInfo(info)
|
||||
|
||||
@generateDiscussionLink: (cls, txt, handler) ->
|
||||
$("<a>").addClass("discussion-link")
|
||||
.attr("href", "javascript:void(0)")
|
||||
.addClass(cls).html(txt)
|
||||
.click -> handler(this)
|
||||
|
||||
@urlFor: (name, param, param1, param2) ->
|
||||
{
|
||||
follow_discussion : "/courses/#{$$course_id}/discussion/#{param}/follow"
|
||||
unfollow_discussion : "/courses/#{$$course_id}/discussion/#{param}/unfollow"
|
||||
create_thread : "/courses/#{$$course_id}/discussion/#{param}/threads/create"
|
||||
search_similar_threads : "/courses/#{$$course_id}/discussion/#{param}/threads/search_similar"
|
||||
update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update"
|
||||
create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply"
|
||||
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
|
||||
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
|
||||
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
|
||||
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
|
||||
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
|
||||
unfollow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unfollow"
|
||||
update_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/update"
|
||||
endorse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/endorse"
|
||||
create_sub_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/reply"
|
||||
delete_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/delete"
|
||||
upvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/upvote"
|
||||
downvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/downvote"
|
||||
undo_vote_for_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unvote"
|
||||
upload : "/courses/#{$$course_id}/discussion/upload"
|
||||
search : "/courses/#{$$course_id}/discussion/forum/search"
|
||||
tags_autocomplete : "/courses/#{$$course_id}/discussion/threads/tags/autocomplete"
|
||||
retrieve_discussion : "/courses/#{$$course_id}/discussion/forum/#{param}/inline"
|
||||
retrieve_single_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
|
||||
update_moderator_status : "/courses/#{$$course_id}/discussion/users/#{param}/update_moderator_status"
|
||||
openclose_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/close"
|
||||
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
|
||||
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
|
||||
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
|
||||
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
|
||||
threads : "/courses/#{$$course_id}/discussion/forum"
|
||||
}[name]
|
||||
|
||||
@safeAjax: (params) ->
|
||||
$elem = params.$elem
|
||||
if $elem and $elem.attr("disabled")
|
||||
return
|
||||
params["url"] = URI(params["url"]).addSearch ajax: 1
|
||||
params["beforeSend"] = ->
|
||||
if $elem
|
||||
$elem.attr("disabled", "disabled")
|
||||
if params["$loading"]
|
||||
if params["loadingCallback"]?
|
||||
params["loadingCallback"].apply(params["$loading"])
|
||||
else
|
||||
params["$loading"].loading()
|
||||
request = $.ajax(params).always ->
|
||||
if $elem
|
||||
$elem.removeAttr("disabled")
|
||||
if params["$loading"]
|
||||
if params["loadedCallback"]?
|
||||
params["loadedCallback"].apply(params["$loading"])
|
||||
else
|
||||
params["$loading"].loaded()
|
||||
return request
|
||||
|
||||
@get: ($elem, url, data, success) ->
|
||||
@safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "GET"
|
||||
dataType: "json"
|
||||
data: data
|
||||
success: success
|
||||
|
||||
@post: ($elem, url, data, success) ->
|
||||
@safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: "json"
|
||||
data: data
|
||||
success: success
|
||||
|
||||
@bindLocalEvents: ($local, eventsHandler) ->
|
||||
for eventSelector, handler of eventsHandler
|
||||
[event, selector] = eventSelector.split(' ')
|
||||
$local(selector).unbind(event)[event] handler
|
||||
|
||||
@processTag: (text) ->
|
||||
text.toLowerCase()
|
||||
|
||||
@tagsInputOptions: ->
|
||||
autocomplete_url: @urlFor('tags_autocomplete')
|
||||
autocomplete:
|
||||
remoteDataType: 'json'
|
||||
interactive: true
|
||||
height: '30px'
|
||||
width: '100%'
|
||||
defaultText: "Tag your post: press enter after each tag"
|
||||
removeWithBackspace: true
|
||||
preprocessTag: @processTag
|
||||
|
||||
@formErrorHandler: (errorsField) ->
|
||||
(xhr, textStatus, error) ->
|
||||
response = JSON.parse(xhr.responseText)
|
||||
if response.errors? and response.errors.length > 0
|
||||
errorsField.empty()
|
||||
for error in response.errors
|
||||
errorsField.append($("<li>").addClass("new-post-form-error").html(error)).show()
|
||||
|
||||
@clearFormErrors: (errorsField) ->
|
||||
errorsField.empty()
|
||||
|
||||
@postMathJaxProcessor: (text) ->
|
||||
RE_INLINEMATH = /^\$([^\$]*)\$/g
|
||||
RE_DISPLAYMATH = /^\$\$([^\$]*)\$\$/g
|
||||
@processEachMathAndCode text, (s, type) ->
|
||||
if type == 'display'
|
||||
s.replace RE_DISPLAYMATH, ($0, $1) ->
|
||||
"\\[" + $1 + "\\]"
|
||||
else if type == 'inline'
|
||||
s.replace RE_INLINEMATH, ($0, $1) ->
|
||||
"\\(" + $1 + "\\)"
|
||||
else
|
||||
s
|
||||
|
||||
@makeWmdEditor: ($content, $local, cls_identifier) ->
|
||||
elem = $local(".#{cls_identifier}")
|
||||
placeholder = elem.data('placeholder')
|
||||
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
|
||||
appended_id = "-#{cls_identifier}-#{id}"
|
||||
imageUploadUrl = @urlFor('upload')
|
||||
_processor = (_this) ->
|
||||
(text) -> _this.postMathJaxProcessor(text)
|
||||
editor = Markdown.makeWmdEditor elem, appended_id, imageUploadUrl, _processor(@)
|
||||
@wmdEditors["#{cls_identifier}-#{id}"] = editor
|
||||
if placeholder?
|
||||
elem.find("#wmd-input#{appended_id}").attr('placeholder', placeholder)
|
||||
editor
|
||||
|
||||
@getWmdEditor: ($content, $local, cls_identifier) ->
|
||||
elem = $local(".#{cls_identifier}")
|
||||
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
|
||||
@wmdEditors["#{cls_identifier}-#{id}"]
|
||||
|
||||
@getWmdInput: ($content, $local, cls_identifier) ->
|
||||
elem = $local(".#{cls_identifier}")
|
||||
id = elem.attr("data-id") # use attr instead of data because we want to avoid type coercion
|
||||
$local("#wmd-input-#{cls_identifier}-#{id}")
|
||||
|
||||
@getWmdContent: ($content, $local, cls_identifier) ->
|
||||
@getWmdInput($content, $local, cls_identifier).val()
|
||||
|
||||
@setWmdContent: ($content, $local, cls_identifier, text) ->
|
||||
@getWmdInput($content, $local, cls_identifier).val(text)
|
||||
@getWmdEditor($content, $local, cls_identifier).refreshPreview()
|
||||
|
||||
@subscriptionLink: (type, id) ->
|
||||
followLink = ->
|
||||
@generateDiscussionLink("discussion-follow-#{type}", "Follow", handleFollow)
|
||||
|
||||
unfollowLink = ->
|
||||
@generateDiscussionLink("discussion-unfollow-#{type}", "Unfollow", handleUnfollow)
|
||||
|
||||
handleFollow = (elem) ->
|
||||
@safeAjax
|
||||
$elem: $(elem)
|
||||
url: @urlFor("follow_#{type}", id)
|
||||
type: "POST"
|
||||
success: (response, textStatus) ->
|
||||
if textStatus == "success"
|
||||
$(elem).replaceWith unfollowLink()
|
||||
dataType: 'json'
|
||||
|
||||
handleUnfollow = (elem) ->
|
||||
@safeAjax
|
||||
$elem: $(elem)
|
||||
url: @urlFor("unfollow_#{type}", id)
|
||||
type: "POST"
|
||||
success: (response, textStatus) ->
|
||||
if textStatus == "success"
|
||||
$(elem).replaceWith followLink()
|
||||
dataType: 'json'
|
||||
|
||||
if @isSubscribed(id, type)
|
||||
unfollowLink()
|
||||
else
|
||||
followLink()
|
||||
|
||||
@processEachMathAndCode: (text, processor) ->
|
||||
|
||||
codeArchive = []
|
||||
|
||||
RE_DISPLAYMATH = /^([^\$]*?)\$\$([^\$]*?)\$\$(.*)$/m
|
||||
RE_INLINEMATH = /^([^\$]*?)\$([^\$]+?)\$(.*)$/m
|
||||
|
||||
ESCAPED_DOLLAR = '@@ESCAPED_D@@'
|
||||
ESCAPED_BACKSLASH = '@@ESCAPED_B@@'
|
||||
|
||||
processedText = ""
|
||||
|
||||
$div = $("<div>").html(text)
|
||||
|
||||
$div.find("code").each (index, code) ->
|
||||
codeArchive.push $(code).html()
|
||||
$(code).html(codeArchive.length - 1)
|
||||
|
||||
text = $div.html()
|
||||
text = text.replace /\\\$/g, ESCAPED_DOLLAR
|
||||
|
||||
while true
|
||||
if RE_INLINEMATH.test(text)
|
||||
text = text.replace RE_INLINEMATH, ($0, $1, $2, $3) ->
|
||||
processedText += $1 + processor("$" + $2 + "$", 'inline')
|
||||
$3
|
||||
else if RE_DISPLAYMATH.test(text)
|
||||
text = text.replace RE_DISPLAYMATH, ($0, $1, $2, $3) ->
|
||||
#processedText += $1 + processor("$$" + $2 + "$$", 'display')
|
||||
#bug fix, ordering is off
|
||||
processedText = processor("$$" + $2 + "$$", 'display') + processedText
|
||||
processedText = $1 + processedText
|
||||
$3
|
||||
else
|
||||
processedText += text
|
||||
break
|
||||
|
||||
text = processedText
|
||||
text = text.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$')
|
||||
|
||||
text = text.replace /\\\\\\\\/g, ESCAPED_BACKSLASH
|
||||
text = text.replace /\\begin\{([a-z]*\*?)\}([\s\S]*?)\\end\{\1\}/img, ($0, $1, $2) ->
|
||||
processor("\\begin{#{$1}}" + $2 + "\\end{#{$1}}")
|
||||
text = text.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\')
|
||||
|
||||
$div = $("<div>").html(text)
|
||||
cnt = 0
|
||||
$div.find("code").each (index, code) ->
|
||||
$(code).html(processor(codeArchive[cnt], 'code'))
|
||||
cnt += 1
|
||||
|
||||
text = $div.html()
|
||||
|
||||
text
|
||||
|
||||
@unescapeHighlightTag: (text) ->
|
||||
text.replace(/\<\;highlight\>\;/g, "<span class='search-highlight'>")
|
||||
.replace(/\<\;\/highlight\>\;/g, "</span>")
|
||||
|
||||
@stripHighlight: (text) ->
|
||||
text.replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "")
|
||||
.replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, "")
|
||||
|
||||
@stripLatexHighlight: (text) ->
|
||||
@processEachMathAndCode text, @stripHighlight
|
||||
|
||||
@markdownWithHighlight: (text) ->
|
||||
text = text.replace(/^\>\;/gm, ">")
|
||||
converter = Markdown.getMathCompatibleConverter()
|
||||
text = @unescapeHighlightTag @stripLatexHighlight converter.makeHtml text
|
||||
return text.replace(/^>/gm,">")
|
||||
|
||||
@abbreviateString: (text, minLength) ->
|
||||
# Abbreviates a string to at least minLength characters, stopping at word boundaries
|
||||
if text.length<minLength
|
||||
return text
|
||||
else
|
||||
while minLength < text.length && text[minLength] != ' '
|
||||
minLength++
|
||||
return text.substr(0, minLength) + '...'
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionContentView extends Backbone.View
|
||||
|
||||
attrRenderer:
|
||||
endorsed: (endorsed) ->
|
||||
if endorsed
|
||||
@$(".action-endorse").show().addClass("is-endorsed")
|
||||
else
|
||||
if @model.get('ability')?.can_endorse
|
||||
@$(".action-endorse").show()
|
||||
else
|
||||
@$(".action-endorse").hide()
|
||||
@$(".action-endorse").removeClass("is-endorsed")
|
||||
|
||||
closed: (closed) ->
|
||||
return if not @$(".action-openclose").length
|
||||
return if not @$(".post-status-closed").length
|
||||
if closed
|
||||
@$(".post-status-closed").show()
|
||||
@$(".action-openclose").html(@$(".action-openclose").html().replace("Close", "Open"))
|
||||
@$(".discussion-reply-new").hide()
|
||||
else
|
||||
@$(".post-status-closed").hide()
|
||||
@$(".action-openclose").html(@$(".action-openclose").html().replace("Open", "Close"))
|
||||
@$(".discussion-reply-new").show()
|
||||
|
||||
voted: (voted) ->
|
||||
|
||||
votes_point: (votes_point) ->
|
||||
|
||||
comments_count: (comments_count) ->
|
||||
|
||||
subscribed: (subscribed) ->
|
||||
if subscribed
|
||||
@$(".dogear").addClass("is-followed")
|
||||
else
|
||||
@$(".dogear").removeClass("is-followed")
|
||||
|
||||
ability: (ability) ->
|
||||
for action, selector of @abilityRenderer
|
||||
if not ability[action]
|
||||
selector.disable.apply(@)
|
||||
else
|
||||
selector.enable.apply(@)
|
||||
|
||||
abilityRenderer:
|
||||
editable:
|
||||
enable: -> @$(".action-edit").closest("li").show()
|
||||
disable: -> @$(".action-edit").closest("li").hide()
|
||||
can_delete:
|
||||
enable: -> @$(".action-delete").closest("li").show()
|
||||
disable: -> @$(".action-delete").closest("li").hide()
|
||||
can_endorse:
|
||||
enable: ->
|
||||
@$(".action-endorse").show().css("cursor", "auto")
|
||||
disable: ->
|
||||
@$(".action-endorse").css("cursor", "default")
|
||||
if not @model.get('endorsed')
|
||||
@$(".action-endorse").hide()
|
||||
else
|
||||
@$(".action-endorse").show()
|
||||
can_openclose:
|
||||
enable: -> @$(".action-openclose").closest("li").show()
|
||||
disable: -> @$(".action-openclose").closest("li").hide()
|
||||
|
||||
renderPartialAttrs: ->
|
||||
for attr, value of @model.changedAttributes()
|
||||
if @attrRenderer[attr]
|
||||
@attrRenderer[attr].apply(@, [value])
|
||||
|
||||
renderAttrs: ->
|
||||
for attr, value of @model.attributes
|
||||
if @attrRenderer[attr]
|
||||
@attrRenderer[attr].apply(@, [value])
|
||||
|
||||
$: (selector) ->
|
||||
@$local.find(selector)
|
||||
|
||||
initLocal: ->
|
||||
@$local = @$el.children(".local")
|
||||
if not @$local.length
|
||||
@$local = @$el
|
||||
@$delegateElement = @$local
|
||||
|
||||
makeWmdEditor: (cls_identifier) =>
|
||||
if not @$el.find(".wmd-panel").length
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), cls_identifier
|
||||
|
||||
getWmdEditor: (cls_identifier) =>
|
||||
DiscussionUtil.getWmdEditor @$el, $.proxy(@$, @), cls_identifier
|
||||
|
||||
getWmdContent: (cls_identifier) =>
|
||||
DiscussionUtil.getWmdContent @$el, $.proxy(@$, @), cls_identifier
|
||||
|
||||
setWmdContent: (cls_identifier, text) =>
|
||||
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), cls_identifier, text
|
||||
|
||||
initialize: ->
|
||||
@initLocal()
|
||||
@model.bind('change', @renderPartialAttrs, @)
|
||||
@@ -1,26 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadEditView extends Backbone.View
|
||||
|
||||
events:
|
||||
"click .post-update": "update"
|
||||
"click .post-cancel": "cancel_edit"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#thread-edit-template").html())
|
||||
@$el.html(@template(@model.toJSON()))
|
||||
@delegateEvents()
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
|
||||
@$(".edit-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
|
||||
@
|
||||
|
||||
update: (event) ->
|
||||
@trigger "thread:update", event
|
||||
|
||||
cancel_edit: (event) ->
|
||||
@trigger "thread:cancel_edit", event
|
||||
@@ -1,386 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadListView extends Backbone.View
|
||||
events:
|
||||
"click .search": "showSearch"
|
||||
"click .browse": "toggleTopicDrop"
|
||||
"keydown .post-search-field": "performSearch"
|
||||
"click .sort-bar a": "sortThreads"
|
||||
"click .browse-topic-drop-menu": "filterTopic"
|
||||
"click .browse-topic-drop-search-input": "ignoreClick"
|
||||
"click .post-list .list-item a": "threadSelected"
|
||||
"click .post-list .more-pages a": "loadMorePages"
|
||||
'keyup .browse-topic-drop-search-input': DiscussionFilter.filterDrop
|
||||
|
||||
initialize: ->
|
||||
@displayedCollection = new Discussion(@collection.models, pages: @collection.pages)
|
||||
@collection.on "change", @reloadDisplayedCollection
|
||||
@sortBy = "date"
|
||||
@discussionIds=""
|
||||
@collection.on "reset", (discussion) =>
|
||||
board = $(".current-board").html()
|
||||
@displayedCollection.current_page = discussion.current_page
|
||||
@displayedCollection.pages = discussion.pages
|
||||
@displayedCollection.reset discussion.models
|
||||
# TODO: filter correctly
|
||||
# target = _.filter($("a.topic:contains('#{board}')"), (el) -> el.innerText == "General" || el.innerHTML == "General")
|
||||
# if target.length > 0
|
||||
# @filterTopic($.Event("filter", {'target': target[0]}))
|
||||
@collection.on "add", @addAndSelectThread
|
||||
@sidebar_padding = 10
|
||||
@sidebar_header_height = 87
|
||||
@boardName
|
||||
@template = _.template($("#thread-list-template").html())
|
||||
@current_search = ""
|
||||
@mode = 'all'
|
||||
|
||||
reloadDisplayedCollection: (thread) =>
|
||||
thread_id = thread.get('id')
|
||||
content = @renderThread(thread)
|
||||
current_el = @$("a[data-id=#{thread_id}]")
|
||||
active = current_el.hasClass("active")
|
||||
current_el.replaceWith(content)
|
||||
if active
|
||||
@setActiveThread(thread_id)
|
||||
|
||||
#TODO fix this entire chain of events
|
||||
addAndSelectThread: (thread) =>
|
||||
commentable_id = thread.get("commentable_id")
|
||||
commentable = @$(".board-name[data-discussion_id]").filter(-> $(this).data("discussion_id").id == commentable_id)
|
||||
@setTopicHack(commentable)
|
||||
@retrieveDiscussion commentable_id, =>
|
||||
@trigger "thread:created", thread.get('id')
|
||||
|
||||
updateSidebar: =>
|
||||
|
||||
scrollTop = $(window).scrollTop();
|
||||
windowHeight = $(window).height();
|
||||
|
||||
discussionBody = $(".discussion-article")
|
||||
discussionsBodyTop = if discussionBody[0] then discussionBody.offset().top
|
||||
discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight()
|
||||
|
||||
sidebar = $(".sidebar")
|
||||
if scrollTop > discussionsBodyTop - @sidebar_padding
|
||||
sidebar.addClass('fixed');
|
||||
sidebar.css('top', @sidebar_padding);
|
||||
else
|
||||
sidebar.removeClass('fixed');
|
||||
sidebar.css('top', '0');
|
||||
|
||||
sidebarWidth = .31 * $(".discussion-body").width();
|
||||
sidebar.css('width', sidebarWidth + 'px');
|
||||
|
||||
sidebarHeight = windowHeight - Math.max(discussionsBodyTop - scrollTop, @sidebar_padding)
|
||||
|
||||
topOffset = scrollTop + windowHeight
|
||||
discussionBottomOffset = discussionsBodyBottom + @sidebar_padding
|
||||
amount = Math.max(topOffset - discussionBottomOffset, 0)
|
||||
|
||||
sidebarHeight = sidebarHeight - @sidebar_padding - amount
|
||||
sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight())
|
||||
sidebar.css 'height', sidebarHeight
|
||||
|
||||
postListWrapper = @$('.post-list-wrapper')
|
||||
postListWrapper.css('height', (sidebarHeight - @sidebar_header_height - 4) + 'px')
|
||||
|
||||
|
||||
# Because we want the behavior that when the body is clicked the menu is
|
||||
# closed, we need to ignore clicks in the search field and stop propagation.
|
||||
# Without this, clicking the search field would also close the menu.
|
||||
ignoreClick: (event) ->
|
||||
event.stopPropagation()
|
||||
|
||||
render: ->
|
||||
@timer = 0
|
||||
@$el.html(@template())
|
||||
|
||||
$(window).bind "scroll", @updateSidebar
|
||||
$(window).bind "resize", @updateSidebar
|
||||
|
||||
@displayedCollection.on "reset", @renderThreads
|
||||
@displayedCollection.on "thread:remove", @renderThreads
|
||||
@renderThreads()
|
||||
@
|
||||
|
||||
renderThreads: =>
|
||||
@$(".post-list").html("")
|
||||
rendered = $("<div></div>")
|
||||
for thread in @displayedCollection.models
|
||||
content = @renderThread(thread)
|
||||
rendered.append content
|
||||
content.wrap("<li class='list-item' data-id='\"#{thread.get('id')}\"' />")
|
||||
|
||||
@$(".post-list").html(rendered.html())
|
||||
@renderMorePages()
|
||||
@updateSidebar()
|
||||
@trigger "threads:rendered"
|
||||
|
||||
renderMorePages: ->
|
||||
if @displayedCollection.hasMorePages()
|
||||
@$(".post-list").append("<li class='more-pages'><a href='#'>Load more</a></li>")
|
||||
|
||||
loadMorePages: (event) ->
|
||||
if event
|
||||
event.preventDefault()
|
||||
@$(".more-pages").html('<div class="loading-animation"></div>')
|
||||
@$(".more-pages").addClass("loading")
|
||||
options = {}
|
||||
switch @mode
|
||||
when 'search'
|
||||
options.search_text = @current_search
|
||||
when 'followed'
|
||||
options.user_id = window.user.id
|
||||
when 'commentables'
|
||||
options.commentable_ids = @discussionIds
|
||||
@collection.retrieveAnotherPage(@mode, options, {sort_key: @sortBy})
|
||||
|
||||
renderThread: (thread) =>
|
||||
content = $(_.template($("#thread-list-item-template").html())(thread.toJSON()))
|
||||
if thread.get('subscribed')
|
||||
content.addClass("followed")
|
||||
if thread.get('endorsed')
|
||||
content.addClass("resolved")
|
||||
if thread.get('read')
|
||||
content.addClass("read")
|
||||
@highlight(content)
|
||||
|
||||
|
||||
highlight: (el) ->
|
||||
el.html(el.html().replace(/<mark>/g, "<mark>").replace(/<\/mark>/g, "</mark>"))
|
||||
|
||||
renderThreadListItem: (thread) =>
|
||||
view = new ThreadListItemView(model: thread)
|
||||
view.on "thread:selected", @threadSelected
|
||||
view.on "thread:removed", @threadRemoved
|
||||
view.render()
|
||||
@$(".post-list").append(view.el)
|
||||
|
||||
threadSelected: (e) =>
|
||||
# Use .attr('data-id') rather than .data('id') because .data does type
|
||||
# coercion. Usually, this is fine, but when Mongo gives an object id with
|
||||
# no letters, it casts it to a Number.
|
||||
|
||||
thread_id = $(e.target).closest("a").attr("data-id")
|
||||
@setActiveThread(thread_id)
|
||||
@trigger("thread:selected", thread_id) # This triggers a callback in the DiscussionRouter which calls the line above...
|
||||
false
|
||||
|
||||
threadRemoved: (thread_id) =>
|
||||
@trigger("thread:removed", thread_id)
|
||||
|
||||
setActiveThread: (thread_id) ->
|
||||
@$(".post-list a[data-id!='#{thread_id}']").removeClass("active")
|
||||
@$(".post-list a[data-id='#{thread_id}']").addClass("active")
|
||||
|
||||
showSearch: ->
|
||||
@$(".browse").removeClass('is-dropped')
|
||||
@hideTopicDrop()
|
||||
|
||||
@$(".search").addClass('is-open')
|
||||
@$(".browse").removeClass('is-open')
|
||||
setTimeout (-> @$(".post-search-field").focus()), 200
|
||||
|
||||
toggleTopicDrop: (event) =>
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if @current_search != ""
|
||||
@clearSearch()
|
||||
@$(".search").removeClass('is-open')
|
||||
@$(".browse").addClass('is-open')
|
||||
@$(".browse").toggleClass('is-dropped')
|
||||
|
||||
if @$(".browse").hasClass('is-dropped')
|
||||
@$(".browse-topic-drop-menu-wrapper").show()
|
||||
$(".browse-topic-drop-search-input").focus()
|
||||
$("body").bind "click", @toggleTopicDrop
|
||||
$("body").bind "keydown", @setActiveItem
|
||||
else
|
||||
@hideTopicDrop()
|
||||
|
||||
hideTopicDrop: ->
|
||||
@$(".browse-topic-drop-menu-wrapper").hide()
|
||||
$("body").unbind "click", @toggleTopicDrop
|
||||
$("body").unbind "keydown", @setActiveItem
|
||||
|
||||
# TODO get rid of this asap
|
||||
setTopicHack: (boardNameContainer) ->
|
||||
item = $(boardNameContainer).closest('a')
|
||||
boardName = item.find(".board-name").html()
|
||||
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) ->
|
||||
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName
|
||||
@$(".current-board").html(@fitName(boardName))
|
||||
|
||||
setTopic: (event) ->
|
||||
item = $(event.target).closest('a')
|
||||
boardName = item.find(".board-name").html()
|
||||
_.each item.parents('ul').not('.browse-topic-drop-menu'), (parent) ->
|
||||
boardName = $(parent).siblings('a').find('.board-name').html() + ' / ' + boardName
|
||||
@$(".current-board").html(@fitName(boardName))
|
||||
|
||||
setSelectedTopic: (name) ->
|
||||
@$(".current-board").html(@fitName(name))
|
||||
|
||||
getNameWidth: (name) ->
|
||||
test = $("<div>")
|
||||
test.css
|
||||
"font-size": @$(".current-board").css('font-size')
|
||||
opacity: 0
|
||||
position: 'absolute'
|
||||
left: -1000
|
||||
top: -1000
|
||||
$("body").append(test)
|
||||
test.html(name)
|
||||
width = test.width()
|
||||
test.remove()
|
||||
return width
|
||||
|
||||
fitName: (name) ->
|
||||
@maxNameWidth = (@$el.width() * .8) - 50
|
||||
width = @getNameWidth(name)
|
||||
if width < @maxNameWidth
|
||||
return name
|
||||
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
|
||||
while path.length > 1
|
||||
path.shift()
|
||||
partialName = "…/" + path.join("/")
|
||||
if @getNameWidth(partialName) < @maxNameWidth
|
||||
return partialName
|
||||
rawName = path[0]
|
||||
name = "…/" + rawName
|
||||
while @getNameWidth(name) > @maxNameWidth
|
||||
rawName = rawName[0...rawName.length-1]
|
||||
name = "…/" + rawName + "…"
|
||||
return name
|
||||
|
||||
filterTopic: (event) ->
|
||||
if @current_search != ""
|
||||
@setTopic(event)
|
||||
@clearSearch @filterTopic, event
|
||||
else
|
||||
@setTopic(event) # just sets the title for the dropdown
|
||||
item = $(event.target).closest('li')
|
||||
discussionId = item.find("span.board-name").data("discussion_id")
|
||||
if discussionId == "#all"
|
||||
@discussionIds = ""
|
||||
@$(".post-search-field").val("")
|
||||
@retrieveAllThreads()
|
||||
else if discussionId == "#following"
|
||||
@retrieveFollowed(event)
|
||||
else
|
||||
discussionIds = _.map item.find(".board-name[data-discussion_id]"), (board) -> $(board).data("discussion_id").id
|
||||
@retrieveDiscussions(discussionIds)
|
||||
|
||||
retrieveDiscussion: (discussion_id, callback=null) ->
|
||||
url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id)
|
||||
DiscussionUtil.safeAjax
|
||||
url: url
|
||||
type: "GET"
|
||||
success: (response, textStatus) =>
|
||||
@collection.current_page = response.page
|
||||
@collection.pages = response.num_pages
|
||||
@collection.reset(response.discussion_data)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@displayedCollection.reset(@collection.models)# Don't think this is necessary because it's called on collection.reset
|
||||
if callback?
|
||||
callback()
|
||||
|
||||
retrieveDiscussions: (discussion_ids) ->
|
||||
@discussionIds = discussion_ids.join(',')
|
||||
@mode = 'commentables'
|
||||
@retrieveFirstPage()
|
||||
|
||||
retrieveAllThreads: () ->
|
||||
@mode = 'all'
|
||||
@retrieveFirstPage()
|
||||
|
||||
retrieveFirstPage: (event)->
|
||||
@collection.current_page = 0
|
||||
@collection.reset()
|
||||
@loadMorePages(event)
|
||||
|
||||
sortThreads: (event) ->
|
||||
@$(".sort-bar a").removeClass("active")
|
||||
$(event.target).addClass("active")
|
||||
@sortBy = $(event.target).data("sort")
|
||||
|
||||
@displayedCollection.comparator = switch @sortBy
|
||||
when 'date' then @displayedCollection.sortByDateRecentFirst
|
||||
when 'votes' then @displayedCollection.sortByVotes
|
||||
when 'comments' then @displayedCollection.sortByComments
|
||||
@retrieveFirstPage(event)
|
||||
|
||||
performSearch: (event) ->
|
||||
if event.which == 13
|
||||
event.preventDefault()
|
||||
text = @$(".post-search-field").val()
|
||||
@searchFor(text)
|
||||
|
||||
setAndSearchFor: (text) ->
|
||||
@showSearch()
|
||||
@$(".post-search-field").val(text)
|
||||
@searchFor(text)
|
||||
|
||||
searchFor: (text, callback, value) ->
|
||||
@mode = 'search'
|
||||
@current_search = text
|
||||
url = DiscussionUtil.urlFor("search")
|
||||
#TODO: This might be better done by setting discussion.current_page=0 and calling discussion.loadMorePages
|
||||
# Mainly because this currently does not reset any pagination variables which could cause problems.
|
||||
# This doesn't use pagination either.
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".post-search-field")
|
||||
data: { text: text }
|
||||
url: url
|
||||
type: "GET"
|
||||
$loading: $
|
||||
loadingCallback: =>
|
||||
@$(".post-list").html('<li class="loading"><div class="loading-animation"></div></li>')
|
||||
loadedCallback: =>
|
||||
if callback
|
||||
callback.apply @, [value]
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
# TODO: Augment existing collection?
|
||||
@collection.reset(response.discussion_data)
|
||||
Content.loadContentInfos(response.annotated_content_info)
|
||||
@collection.current_page = response.page
|
||||
@collection.pages = response.num_pages
|
||||
# TODO: Perhaps reload user info so that votes can be updated.
|
||||
# In the future we might not load all of a user's votes at once
|
||||
# so this would probably be necessary anyway
|
||||
@displayedCollection.reset(@collection.models) # Don't think this is necessary
|
||||
|
||||
clearSearch: (callback, value) ->
|
||||
@$(".post-search-field").val("")
|
||||
@searchFor("", callback, value)
|
||||
|
||||
setActiveItem: (event) ->
|
||||
if event.which == 13
|
||||
$(".browse-topic-drop-menu-wrapper .focused").click()
|
||||
return
|
||||
if event.which != 40 && event.which != 38
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
items = $.makeArray($(".browse-topic-drop-menu-wrapper a").not(".hidden"))
|
||||
index = items.indexOf($('.browse-topic-drop-menu-wrapper .focused')[0])
|
||||
|
||||
if event.which == 40
|
||||
index = Math.min(index + 1, items.length - 1)
|
||||
if event.which == 38
|
||||
index = Math.max(index - 1, 0)
|
||||
|
||||
$(".browse-topic-drop-menu-wrapper .focused").removeClass("focused")
|
||||
$(items[index]).addClass("focused")
|
||||
|
||||
itemTop = $(items[index]).parent().offset().top
|
||||
scrollTop = $(".browse-topic-drop-menu").scrollTop()
|
||||
itemFromTop = $(".browse-topic-drop-menu").offset().top - itemTop
|
||||
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
|
||||
scrollTarget = Math.max(scrollTop - itemFromTop - $(".browse-topic-drop-menu").height() + $(items[index]).height(), scrollTarget)
|
||||
$(".browse-topic-drop-menu").scrollTop(scrollTarget)
|
||||
|
||||
retrieveFollowed: (event)=>
|
||||
@mode = 'followed'
|
||||
@retrieveFirstPage(event)
|
||||
@@ -1,144 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadProfileView extends DiscussionContentView
|
||||
expanded = false
|
||||
events:
|
||||
"click .discussion-vote": "toggleVote"
|
||||
"click .action-follow": "toggleFollowing"
|
||||
"click .expand-post": "expandPost"
|
||||
"click .collapse-post": "collapsePost"
|
||||
|
||||
initLocal: ->
|
||||
@$local = @$el.children(".discussion-article").children(".local")
|
||||
@$delegateElement = @$local
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
|
||||
render: ->
|
||||
@template = DiscussionUtil.getTemplate("_profile_thread")
|
||||
if not @model.has('abbreviatedBody')
|
||||
@abbreviateBody()
|
||||
params = $.extend(@model.toJSON(),{expanded: @expanded, permalink: @model.urlFor('retrieve')})
|
||||
if not @model.get('anonymous')
|
||||
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
|
||||
@$el.html(Mustache.render(@template, params))
|
||||
@initLocal()
|
||||
@delegateEvents()
|
||||
@renderDogear()
|
||||
@renderVoted()
|
||||
@renderAttrs()
|
||||
@$("span.timeago").timeago()
|
||||
@convertMath()
|
||||
if @expanded
|
||||
@renderResponses()
|
||||
@
|
||||
|
||||
renderDogear: ->
|
||||
if window.user.following(@model)
|
||||
@$(".dogear").addClass("is-followed")
|
||||
|
||||
renderVoted: =>
|
||||
if window.user.voted(@model)
|
||||
@$("[data-role=discussion-vote]").addClass("is-cast")
|
||||
else
|
||||
@$("[data-role=discussion-vote]").removeClass("is-cast")
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderVoted()
|
||||
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".post-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
|
||||
renderResponses: ->
|
||||
DiscussionUtil.safeAjax
|
||||
url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}"
|
||||
$loading: @$el
|
||||
success: (data, textStatus, xhr) =>
|
||||
@$el.find(".loading").remove()
|
||||
Content.loadContentInfos(data['annotated_content_info'])
|
||||
comments = new Comments(data['content']['children'])
|
||||
comments.each @renderResponse
|
||||
@trigger "thread:responses:rendered"
|
||||
|
||||
renderResponse: (response) =>
|
||||
response.set('thread', @model)
|
||||
view = new ThreadResponseView(model: response)
|
||||
view.on "comment:add", @addComment
|
||||
view.render()
|
||||
@$el.find(".responses").append(view.el)
|
||||
|
||||
addComment: =>
|
||||
@model.comment()
|
||||
|
||||
toggleVote: (event) ->
|
||||
event.preventDefault()
|
||||
if window.user.voted(@model)
|
||||
@unvote()
|
||||
else
|
||||
@vote()
|
||||
|
||||
toggleFollowing: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = null
|
||||
if not @model.get('subscribed')
|
||||
@model.follow()
|
||||
url = @model.urlFor("follow")
|
||||
else
|
||||
@model.unfollow()
|
||||
url = @model.urlFor("unfollow")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
|
||||
vote: ->
|
||||
window.user.vote(@model)
|
||||
url = @model.urlFor("upvote")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-vote")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
|
||||
unvote: ->
|
||||
window.user.unvote(@model)
|
||||
url = @model.urlFor("unvote")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-vote")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
|
||||
edit: ->
|
||||
|
||||
abbreviateBody: ->
|
||||
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
|
||||
@model.set('abbreviatedBody', abbreviated)
|
||||
|
||||
expandPost: (event) ->
|
||||
@expanded = true
|
||||
@$el.addClass('expanded')
|
||||
@$el.find('.post-body').html(@model.get('body'))
|
||||
@convertMath()
|
||||
@$el.find('.expand-post').css('display', 'none')
|
||||
@$el.find('.collapse-post').css('display', 'block')
|
||||
@$el.find('.post-extended-content').show()
|
||||
if @$el.find('.loading').length
|
||||
@renderResponses()
|
||||
|
||||
collapsePost: (event) ->
|
||||
@expanded = false
|
||||
@$el.removeClass('expanded')
|
||||
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
|
||||
@convertMath()
|
||||
@$el.find('.collapse-post').css('display', 'none')
|
||||
@$el.find('.post-extended-content').hide()
|
||||
@$el.find('.expand-post').css('display', 'block')
|
||||
@@ -1,139 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadShowView extends DiscussionContentView
|
||||
|
||||
events:
|
||||
"click .discussion-vote": "toggleVote"
|
||||
"click .action-follow": "toggleFollowing"
|
||||
"click .action-edit": "edit"
|
||||
"click .action-delete": "delete"
|
||||
"click .action-openclose": "toggleClosed"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-show-template").html())
|
||||
@template(@model.toJSON())
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
@renderDogear()
|
||||
@renderVoted()
|
||||
@renderAttrs()
|
||||
@$("span.timeago").timeago()
|
||||
@convertMath()
|
||||
@highlight @$(".post-body")
|
||||
@highlight @$("h1,h3")
|
||||
@
|
||||
|
||||
renderDogear: ->
|
||||
if window.user.following(@model)
|
||||
@$(".dogear").addClass("is-followed")
|
||||
|
||||
renderVoted: =>
|
||||
if window.user.voted(@model)
|
||||
@$("[data-role=discussion-vote]").addClass("is-cast")
|
||||
else
|
||||
@$("[data-role=discussion-vote]").removeClass("is-cast")
|
||||
|
||||
updateModelDetails: =>
|
||||
@renderVoted()
|
||||
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".post-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
|
||||
toggleVote: (event) ->
|
||||
event.preventDefault()
|
||||
if window.user.voted(@model)
|
||||
@unvote()
|
||||
else
|
||||
@vote()
|
||||
|
||||
toggleFollowing: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = null
|
||||
if not @model.get('subscribed')
|
||||
@model.follow()
|
||||
url = @model.urlFor("follow")
|
||||
else
|
||||
@model.unfollow()
|
||||
url = @model.urlFor("unfollow")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
|
||||
vote: ->
|
||||
window.user.vote(@model)
|
||||
url = @model.urlFor("upvote")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-vote")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response, {silent: true})
|
||||
|
||||
unvote: ->
|
||||
window.user.unvote(@model)
|
||||
url = @model.urlFor("unvote")
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-vote")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response, {silent: true})
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "thread:edit", event
|
||||
|
||||
delete: (event) ->
|
||||
@trigger "thread:delete", event
|
||||
|
||||
toggleClosed: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('close')
|
||||
closed = @model.get('closed')
|
||||
data = { closed: not closed }
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.set('closed', not closed)
|
||||
@model.set('ability', response.ability)
|
||||
|
||||
toggleEndorse: (event) ->
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('endorse')
|
||||
endorsed = @model.get('endorsed')
|
||||
data = { endorsed: not endorsed }
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.set('endorsed', not endorsed)
|
||||
|
||||
highlight: (el) ->
|
||||
if el.html()
|
||||
el.html(el.html().replace(/<mark>/g, "<mark>").replace(/<\/mark>/g, "</mark>"))
|
||||
|
||||
class @DiscussionThreadInlineShowView extends DiscussionThreadShowView
|
||||
renderTemplate: ->
|
||||
@template = DiscussionUtil.getTemplate('_inline_thread_show')
|
||||
params = @model.toJSON()
|
||||
if @model.get('username')?
|
||||
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
|
||||
Mustache.render(@template, params)
|
||||
@@ -1,215 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadView extends DiscussionContentView
|
||||
|
||||
events:
|
||||
"click .discussion-submit-post": "submitComment"
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w/ tags, removing them.
|
||||
#"click .thread-tag": "tagSelected"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@createShowView()
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-template").html())
|
||||
@template(@model.toJSON())
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@$el.find(".loading").hide()
|
||||
@delegateEvents()
|
||||
|
||||
@renderShowView()
|
||||
@renderAttrs()
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w/ tags, removing them.
|
||||
#@renderTags()
|
||||
|
||||
@$("span.timeago").timeago()
|
||||
@makeWmdEditor "reply-body"
|
||||
@renderResponses()
|
||||
@
|
||||
|
||||
cleanup: ->
|
||||
if @responsesRequest?
|
||||
@responsesRequest.abort()
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w/ tags, removing them.
|
||||
#renderTags: ->
|
||||
# # tags
|
||||
# for tag in @model.get("tags")
|
||||
# if !tags
|
||||
# tags = $('<div class="thread-tags">')
|
||||
# tags.append("<a href='#' class='thread-tag'>#{tag}</a>")
|
||||
# @$(".post-body").after(tags)
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w tags, removing them.
|
||||
#tagSelected: (e) ->
|
||||
# @trigger "tag:selected", $(e.target).html()
|
||||
|
||||
renderResponses: ->
|
||||
setTimeout(=>
|
||||
@$el.find(".loading").show()
|
||||
, 200)
|
||||
@responsesRequest = DiscussionUtil.safeAjax
|
||||
url: DiscussionUtil.urlFor('retrieve_single_thread', @model.get('commentable_id'), @model.id)
|
||||
success: (data, textStatus, xhr) =>
|
||||
@responsesRequest = null
|
||||
@$el.find(".loading").remove()
|
||||
Content.loadContentInfos(data['annotated_content_info'])
|
||||
comments = new Comments(data['content']['children'])
|
||||
comments.each @renderResponse
|
||||
@trigger "thread:responses:rendered"
|
||||
|
||||
renderResponse: (response) =>
|
||||
response.set('thread', @model)
|
||||
view = new ThreadResponseView(model: response)
|
||||
view.on "comment:add", @addComment
|
||||
view.on "comment:endorse", @endorseThread
|
||||
view.render()
|
||||
@$el.find(".responses").append(view.el)
|
||||
view.afterInsert()
|
||||
|
||||
addComment: =>
|
||||
@model.comment()
|
||||
|
||||
endorseThread: (endorsed) =>
|
||||
is_endorsed = @$el.find(".is-endorsed").length
|
||||
@model.set 'endorsed', is_endorsed
|
||||
|
||||
submitComment: (event) ->
|
||||
event.preventDefault()
|
||||
url = @model.urlFor('reply')
|
||||
body = @getWmdContent("reply-body")
|
||||
return if not body.trim().length
|
||||
@setWmdContent("reply-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, endorsed: false, user_id: window.user.get("id"))
|
||||
comment.set('thread', @model.get('thread'))
|
||||
@renderResponse(comment)
|
||||
@model.addComment()
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
body: body
|
||||
success: (data, textStatus) =>
|
||||
comment.updateInfo(data.annotated_content_info)
|
||||
comment.set(data.content)
|
||||
|
||||
edit: (event) =>
|
||||
@createEditView()
|
||||
@renderEditView()
|
||||
|
||||
update: (event) =>
|
||||
|
||||
newTitle = @editView.$(".edit-post-title").val()
|
||||
newBody = @editView.$(".edit-post-body textarea").val()
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w/ tags, removing them.
|
||||
#newTags = @editView.$(".edit-post-tags").val()
|
||||
|
||||
url = DiscussionUtil.urlFor('update_thread', @model.id)
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target) if event
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
async: false # TODO when the rest of the stuff below is made to work properly..
|
||||
data:
|
||||
title: newTitle
|
||||
body: newBody
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w/ tags, removing them.
|
||||
#tags: newTags
|
||||
|
||||
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
# TODO: Move this out of the callback, this makes it feel sluggish
|
||||
@editView.$(".edit-post-title").val("").attr("prev-text", "")
|
||||
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
|
||||
@editView.$(".edit-post-tags").val("")
|
||||
@editView.$(".edit-post-tags").importTags("")
|
||||
@editView.$(".wmd-preview p").html("")
|
||||
|
||||
@model.set
|
||||
title: newTitle
|
||||
body: newBody
|
||||
tags: response.content.tags
|
||||
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
|
||||
# TODO tags
|
||||
# Until we decide what to do w/ tags, removing them.
|
||||
#@renderTags()
|
||||
|
||||
createEditView: () ->
|
||||
|
||||
if @showView?
|
||||
@showView.undelegateEvents()
|
||||
@showView.$el.empty()
|
||||
@showView = null
|
||||
|
||||
@editView = new DiscussionThreadEditView(model: @model)
|
||||
@editView.bind "thread:update", @update
|
||||
@editView.bind "thread:cancel_edit", @cancelEdit
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$('.thread-content-wrapper'))
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderEditView: () ->
|
||||
@renderSubView(@editView)
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
|
||||
@showView = new DiscussionThreadShowView(model: @model)
|
||||
@showView.bind "thread:delete", @delete
|
||||
@showView.bind "thread:edit", @edit
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
|
||||
cancelEdit: (event) =>
|
||||
event.preventDefault()
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
|
||||
|
||||
delete: (event) =>
|
||||
url = @model.urlFor('delete')
|
||||
if not @model.can('can_delete')
|
||||
return
|
||||
if not confirm "Are you sure to delete thread \"#{@model.get('title')}\"?"
|
||||
return
|
||||
@model.remove()
|
||||
@showView.undelegateEvents()
|
||||
@undelegateEvents()
|
||||
@$el.empty()
|
||||
$elem = $(event.target)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@@ -1,124 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionThreadInlineView extends DiscussionThreadView
|
||||
expanded = false
|
||||
events:
|
||||
"click .discussion-submit-post": "submitComment"
|
||||
"click .expand-post": "expandPost"
|
||||
"click .collapse-post": "collapsePost"
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
initLocal: ->
|
||||
@$local = @$el.children(".discussion-article").children(".local")
|
||||
if not @$local.length
|
||||
@$local = @$el
|
||||
@$delegateElement = @$local
|
||||
|
||||
render: ->
|
||||
@template = DiscussionUtil.getTemplate("_inline_thread")
|
||||
|
||||
if not @model.has('abbreviatedBody')
|
||||
@abbreviateBody()
|
||||
params = @model.toJSON()
|
||||
@$el.html(Mustache.render(@template, params))
|
||||
#@createShowView()
|
||||
|
||||
@initLocal()
|
||||
@delegateEvents()
|
||||
@renderShowView()
|
||||
@renderAttrs()
|
||||
|
||||
# TODO tags commenting out til we decide what to do with tags
|
||||
#@renderTags()
|
||||
|
||||
@$("span.timeago").timeago()
|
||||
@$el.find('.post-extended-content').hide()
|
||||
if @expanded
|
||||
@makeWmdEditor "reply-body"
|
||||
@renderResponses()
|
||||
@
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
@showView = new DiscussionThreadInlineShowView(model: @model)
|
||||
@showView.bind "thread:delete", @delete
|
||||
@showView.bind "thread:edit", @edit
|
||||
|
||||
renderResponses: ->
|
||||
#TODO: threadview
|
||||
DiscussionUtil.safeAjax
|
||||
url: "/courses/#{$$course_id}/discussion/forum/#{@model.get('commentable_id')}/threads/#{@model.id}"
|
||||
$loading: @$el
|
||||
success: (data, textStatus, xhr) =>
|
||||
# @$el.find(".loading").remove()
|
||||
Content.loadContentInfos(data['annotated_content_info'])
|
||||
comments = new Comments(data['content']['children'])
|
||||
comments.each @renderResponse
|
||||
@trigger "thread:responses:rendered"
|
||||
@$('.loading').remove()
|
||||
|
||||
|
||||
toggleClosed: (event) ->
|
||||
#TODO: showview
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('close')
|
||||
closed = @model.get('closed')
|
||||
data = { closed: not closed }
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.set('closed', not closed)
|
||||
@model.set('ability', response.ability)
|
||||
|
||||
toggleEndorse: (event) ->
|
||||
#TODO: showview
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('endorse')
|
||||
endorsed = @model.get('endorsed')
|
||||
data = { endorsed: not endorsed }
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
@model.set('endorsed', not endorsed)
|
||||
|
||||
abbreviateBody: ->
|
||||
abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140
|
||||
@model.set('abbreviatedBody', abbreviated)
|
||||
|
||||
expandPost: (event) =>
|
||||
@expanded = true
|
||||
@$el.addClass('expanded')
|
||||
@$el.find('.post-body').html(@model.get('body'))
|
||||
@showView.convertMath()
|
||||
@$el.find('.expand-post').css('display', 'none')
|
||||
@$el.find('.collapse-post').css('display', 'block')
|
||||
@$el.find('.post-extended-content').show()
|
||||
@makeWmdEditor "reply-body"
|
||||
@renderAttrs()
|
||||
if @$el.find('.loading').length
|
||||
@renderResponses()
|
||||
|
||||
collapsePost: (event) ->
|
||||
@expanded = false
|
||||
@$el.removeClass('expanded')
|
||||
@$el.find('.post-body').html(@model.get('abbreviatedBody'))
|
||||
@showView.convertMath()
|
||||
@$el.find('.collapse-post').css('display', 'none')
|
||||
@$el.find('.post-extended-content').hide()
|
||||
@$el.find('.expand-post').css('display', 'block')
|
||||
|
||||
createEditView: () ->
|
||||
super()
|
||||
@editView.bind "thread:update", @expandPost
|
||||
@editView.bind "thread:update", @abbreviateBody
|
||||
@editView.bind "thread:cancel_edit", @expandPost
|
||||
@@ -1,23 +0,0 @@
|
||||
if Backbone?
|
||||
class @DiscussionUserProfileView extends Backbone.View
|
||||
# events:
|
||||
# "":""
|
||||
initialize: (options) ->
|
||||
@renderThreads @$el, @collection
|
||||
renderThreads: ($elem, threads) =>
|
||||
#Content.loadContentInfos(response.annotated_content_info)
|
||||
@discussion = new Discussion()
|
||||
@discussion.reset(threads, {silent: false})
|
||||
$discussion = $(Mustache.render $("script#_user_profile").html(), {'threads':threads})
|
||||
$elem.append($discussion)
|
||||
@threadviews = @discussion.map (thread) ->
|
||||
new DiscussionThreadProfileView el: @$("article#thread_#{thread.id}"), model: thread
|
||||
_.each @threadviews, (dtv) -> dtv.render()
|
||||
|
||||
addThread: (thread, collection, options) =>
|
||||
# TODO: When doing pagination, this will need to repaginate. Perhaps just reload page 1?
|
||||
article = $("<article class='discussion-thread' id='thread_#{thread.id}'></article>")
|
||||
@$('section.discussion > .threads').prepend(article)
|
||||
threadView = new DiscussionThreadInlineView el: article, model: thread
|
||||
threadView.render()
|
||||
@threadviews.unshift threadView
|
||||
@@ -1,68 +0,0 @@
|
||||
if Backbone?
|
||||
class @NewPostInlineView extends Backbone.View
|
||||
|
||||
initialize: () ->
|
||||
|
||||
@topicId = @$(".topic").first().data("discussion-id")
|
||||
|
||||
@maxNameWidth = 100
|
||||
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
|
||||
|
||||
# TODO tags: commenting out til we know what to do with them
|
||||
#@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
|
||||
|
||||
events:
|
||||
"submit .new-post-form": "createPost"
|
||||
|
||||
# Because we want the behavior that when the body is clicked the menu is
|
||||
# closed, we need to ignore clicks in the search field and stop propagation.
|
||||
# Without this, clicking the search field would also close the menu.
|
||||
ignoreClick: (event) ->
|
||||
event.stopPropagation()
|
||||
|
||||
createPost: (event) ->
|
||||
event.preventDefault()
|
||||
title = @$(".new-post-title").val()
|
||||
body = @$(".new-post-body").find(".wmd-input").val()
|
||||
|
||||
# TODO tags: commenting out til we know what to do with them
|
||||
#tags = @$(".new-post-tags").val()
|
||||
|
||||
anonymous = false || @$("input.discussion-anonymous").is(":checked")
|
||||
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
|
||||
follow = false || @$("input.discussion-follow").is(":checked")
|
||||
|
||||
url = DiscussionUtil.urlFor('create_thread', @topicId)
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target) if event
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
async: false # TODO when the rest of the stuff below is made to work properly..
|
||||
data:
|
||||
title: title
|
||||
body: body
|
||||
|
||||
# TODO tags: commenting out til we know what to do with them
|
||||
#tags: tags
|
||||
|
||||
anonymous: anonymous
|
||||
anonymous_to_peers: anonymous_to_peers
|
||||
auto_subscribe: follow
|
||||
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
# TODO: Move this out of the callback, this makes it feel sluggish
|
||||
thread = new Thread response['content']
|
||||
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
|
||||
@$el.hide()
|
||||
@$(".new-post-title").val("").attr("prev-text", "")
|
||||
@$(".new-post-body textarea").val("").attr("prev-text", "")
|
||||
|
||||
# TODO tags, commenting out til we know what to do with them
|
||||
#@$(".new-post-tags").val("")
|
||||
#@$(".new-post-tags").importTags("")
|
||||
|
||||
@collection.add thread
|
||||
@@ -1,177 +0,0 @@
|
||||
if Backbone?
|
||||
class @NewPostView extends Backbone.View
|
||||
|
||||
initialize: () ->
|
||||
@dropdownButton = @$(".topic_dropdown_button")
|
||||
@topicMenu = @$(".topic_menu_wrapper")
|
||||
|
||||
@menuOpen = @dropdownButton.hasClass('dropped')
|
||||
|
||||
@topicId = @$(".topic").first().data("discussion_id")
|
||||
@topicText = @getFullTopicName(@$(".topic").first())
|
||||
|
||||
@maxNameWidth = 100
|
||||
@setSelectedTopic()
|
||||
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "new-post-body"
|
||||
@$(".new-post-tags").tagsInput DiscussionUtil.tagsInputOptions()
|
||||
|
||||
events:
|
||||
"submit .new-post-form": "createPost"
|
||||
"click .topic_dropdown_button": "toggleTopicDropdown"
|
||||
"click .topic_menu_wrapper": "setTopic"
|
||||
"click .topic_menu_search": "ignoreClick"
|
||||
"keyup .form-topic-drop-search-input": DiscussionFilter.filterDrop
|
||||
|
||||
# Because we want the behavior that when the body is clicked the menu is
|
||||
# closed, we need to ignore clicks in the search field and stop propagation.
|
||||
# Without this, clicking the search field would also close the menu.
|
||||
ignoreClick: (event) ->
|
||||
event.stopPropagation()
|
||||
|
||||
toggleTopicDropdown: (event) ->
|
||||
event.stopPropagation()
|
||||
if @menuOpen
|
||||
@hideTopicDropdown()
|
||||
else
|
||||
@showTopicDropdown()
|
||||
|
||||
showTopicDropdown: () ->
|
||||
@menuOpen = true
|
||||
@dropdownButton.addClass('dropped')
|
||||
@topicMenu.show()
|
||||
$(".form-topic-drop-search-input").focus()
|
||||
|
||||
$("body").bind "keydown", @setActiveItem
|
||||
$("body").bind "click", @hideTopicDropdown
|
||||
|
||||
# Set here because 1) the window might get resized and things could
|
||||
# change and 2) can't set in initialize because the button is hidden
|
||||
@maxNameWidth = @dropdownButton.width() * 0.9
|
||||
|
||||
# Need a fat arrow because hideTopicDropdown is passed as a callback to bind
|
||||
hideTopicDropdown: () =>
|
||||
@menuOpen = false
|
||||
@dropdownButton.removeClass('dropped')
|
||||
@topicMenu.hide()
|
||||
|
||||
$("body").unbind "keydown", @setActiveItem
|
||||
$("body").unbind "click", @hideTopicDropdown
|
||||
|
||||
setTopic: (event) ->
|
||||
$target = $(event.target)
|
||||
if $target.data('discussion_id')
|
||||
@topicText = $target.html()
|
||||
@topicText = @getFullTopicName($target)
|
||||
@topicId = $target.data('discussion_id')
|
||||
@setSelectedTopic()
|
||||
|
||||
setSelectedTopic: ->
|
||||
@dropdownButton.html(@fitName(@topicText) + ' <span class="drop-arrow">▾</span>')
|
||||
|
||||
getFullTopicName: (topicElement) ->
|
||||
name = topicElement.html()
|
||||
topicElement.parents('ul').not('.topic_menu').each ->
|
||||
name = $(this).siblings('a').html() + ' / ' + name
|
||||
return name
|
||||
|
||||
getNameWidth: (name) ->
|
||||
test = $("<div>")
|
||||
test.css
|
||||
"font-size": @dropdownButton.css('font-size')
|
||||
opacity: 0
|
||||
position: 'absolute'
|
||||
left: -1000
|
||||
top: -1000
|
||||
$("body").append(test)
|
||||
test.html(name)
|
||||
width = test.width()
|
||||
test.remove()
|
||||
return width
|
||||
|
||||
fitName: (name) ->
|
||||
width = @getNameWidth(name)
|
||||
if width < @maxNameWidth
|
||||
return name
|
||||
path = (x.replace /^\s+|\s+$/g, "" for x in name.split("/"))
|
||||
while path.length > 1
|
||||
path.shift()
|
||||
partialName = "... / " + path.join(" / ")
|
||||
if @getNameWidth(partialName) < @maxNameWidth
|
||||
return partialName
|
||||
|
||||
rawName = path[0]
|
||||
|
||||
name = "... / " + rawName
|
||||
|
||||
while @getNameWidth(name) > @maxNameWidth
|
||||
rawName = rawName[0...rawName.length-1]
|
||||
name = "... / " + rawName + " ..."
|
||||
|
||||
return name
|
||||
|
||||
|
||||
createPost: (event) ->
|
||||
event.preventDefault()
|
||||
title = @$(".new-post-title").val()
|
||||
body = @$(".new-post-body").find(".wmd-input").val()
|
||||
tags = @$(".new-post-tags").val()
|
||||
|
||||
anonymous = false || @$("input.discussion-anonymous").is(":checked")
|
||||
anonymous_to_peers = false || @$("input.discussion-anonymous-to-peers").is(":checked")
|
||||
follow = false || @$("input.discussion-follow").is(":checked")
|
||||
|
||||
url = DiscussionUtil.urlFor('create_thread', @topicId)
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target) if event
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
async: false # TODO when the rest of the stuff below is made to work properly..
|
||||
data:
|
||||
title: title
|
||||
body: body
|
||||
tags: tags
|
||||
anonymous: anonymous
|
||||
anonymous_to_peers: anonymous_to_peers
|
||||
auto_subscribe: follow
|
||||
error: DiscussionUtil.formErrorHandler(@$(".new-post-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
# TODO: Move this out of the callback, this makes it feel sluggish
|
||||
thread = new Thread response['content']
|
||||
DiscussionUtil.clearFormErrors(@$(".new-post-form-errors"))
|
||||
@$el.hide()
|
||||
@$(".new-post-title").val("").attr("prev-text", "")
|
||||
@$(".new-post-body textarea").val("").attr("prev-text", "")
|
||||
@$(".new-post-tags").val("")
|
||||
@$(".new-post-tags").importTags("")
|
||||
@$(".wmd-preview p").html("")
|
||||
@collection.add thread
|
||||
|
||||
setActiveItem: (event) ->
|
||||
if event.which == 13
|
||||
$(".topic_menu_wrapper .focused").click()
|
||||
return
|
||||
if event.which != 40 && event.which != 38
|
||||
return
|
||||
event.preventDefault()
|
||||
|
||||
items = $.makeArray($(".topic_menu_wrapper a").not(".hidden"))
|
||||
index = items.indexOf($('.topic_menu_wrapper .focused')[0])
|
||||
|
||||
if event.which == 40
|
||||
index = Math.min(index + 1, items.length - 1)
|
||||
if event.which == 38
|
||||
index = Math.max(index - 1, 0)
|
||||
|
||||
$(".topic_menu_wrapper .focused").removeClass("focused")
|
||||
$(items[index]).addClass("focused")
|
||||
|
||||
itemTop = $(items[index]).parent().offset().top
|
||||
scrollTop = $(".topic_menu").scrollTop()
|
||||
itemFromTop = $(".topic_menu").offset().top - itemTop
|
||||
scrollTarget = Math.min(scrollTop - itemFromTop, scrollTop)
|
||||
scrollTarget = Math.max(scrollTop - itemFromTop - $(".topic_menu").height() + $(items[index]).height() + 20, scrollTarget)
|
||||
$(".topic_menu").scrollTop(scrollTarget)
|
||||
@@ -1,36 +0,0 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentShowView extends DiscussionContentView
|
||||
|
||||
tagName: "li"
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#response-comment-show-template").html())
|
||||
params = @model.toJSON()
|
||||
|
||||
@$el.html(@template(params))
|
||||
@initLocal()
|
||||
@delegateEvents()
|
||||
@renderAttrs()
|
||||
@markAsStaff()
|
||||
@$el.find(".timeago").timeago()
|
||||
@convertMath()
|
||||
@addReplyLink()
|
||||
@
|
||||
|
||||
addReplyLink: () ->
|
||||
if @model.hasOwnProperty('parent')
|
||||
name = @model.parent.get('username') ? "anonymous"
|
||||
html = "<a href='#comment_#{@model.parent.id}'>@#{name}</a>: "
|
||||
p = @$('.response-body p:first')
|
||||
p.prepend(html)
|
||||
|
||||
convertMath: ->
|
||||
body = @$el.find(".response-body")
|
||||
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
|
||||
|
||||
markAsStaff: ->
|
||||
if DiscussionUtil.isStaff(@model.get("user_id"))
|
||||
@$el.find("a.profile-link").after('<span class="staff-label">staff</span>')
|
||||
else if DiscussionUtil.isTA(@model.get("user_id"))
|
||||
@$el.find("a.profile-link").after('<span class="community-ta-label">Community TA</span>')
|
||||
@@ -1,31 +0,0 @@
|
||||
if Backbone?
|
||||
class @ResponseCommentView extends DiscussionContentView
|
||||
tagName: "li"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@createShowView()
|
||||
|
||||
render: ->
|
||||
@renderShowView()
|
||||
@
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
|
||||
@showView = new ResponseCommentShowView(model: @model)
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$el)
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
@@ -1,25 +0,0 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseEditView extends Backbone.View
|
||||
|
||||
events:
|
||||
"click .post-update": "update"
|
||||
"click .post-cancel": "cancel_edit"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
|
||||
render: ->
|
||||
@template = _.template($("#thread-response-edit-template").html())
|
||||
@$el.html(@template(@model.toJSON()))
|
||||
@delegateEvents()
|
||||
DiscussionUtil.makeWmdEditor @$el, $.proxy(@$, @), "edit-post-body"
|
||||
@
|
||||
|
||||
update: (event) ->
|
||||
@trigger "response:update", event
|
||||
|
||||
cancel_edit: (event) ->
|
||||
@trigger "response:cancel_edit", event
|
||||
@@ -1,94 +0,0 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseShowView extends DiscussionContentView
|
||||
events:
|
||||
"click .vote-btn": "toggleVote"
|
||||
"click .action-endorse": "toggleEndorse"
|
||||
"click .action-delete": "delete"
|
||||
"click .action-edit": "edit"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@model.on "change", @updateModelDetails
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-response-show-template").html())
|
||||
@template(@model.toJSON())
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
if window.user.voted(@model)
|
||||
@$(".vote-btn").addClass("is-cast")
|
||||
@renderAttrs()
|
||||
@$el.find(".posted-details").timeago()
|
||||
@convertMath()
|
||||
@markAsStaff()
|
||||
@
|
||||
|
||||
convertMath: ->
|
||||
element = @$(".response-body")
|
||||
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
|
||||
|
||||
markAsStaff: ->
|
||||
if DiscussionUtil.isStaff(@model.get("user_id"))
|
||||
@$el.addClass("staff")
|
||||
@$el.prepend('<div class="staff-banner">staff</div>')
|
||||
else if DiscussionUtil.isTA(@model.get("user_id"))
|
||||
@$el.addClass("community-ta")
|
||||
@$el.prepend('<div class="community-ta-banner">Community TA</div>')
|
||||
|
||||
toggleVote: (event) ->
|
||||
event.preventDefault()
|
||||
@$(".vote-btn").toggleClass("is-cast")
|
||||
if @$(".vote-btn").hasClass("is-cast")
|
||||
@vote()
|
||||
else
|
||||
@unvote()
|
||||
|
||||
vote: ->
|
||||
url = @model.urlFor("upvote")
|
||||
@$(".votes-count-number").html(parseInt(@$(".votes-count-number").html()) + 1)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-vote")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
|
||||
unvote: ->
|
||||
url = @model.urlFor("unvote")
|
||||
@$(".votes-count-number").html(parseInt(@$(".votes-count-number").html()) - 1)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: @$(".discussion-vote")
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
if textStatus == 'success'
|
||||
@model.set(response)
|
||||
|
||||
edit: (event) ->
|
||||
@trigger "response:edit", event
|
||||
|
||||
delete: (event) ->
|
||||
@trigger "response:delete", event
|
||||
|
||||
toggleEndorse: (event) ->
|
||||
event.preventDefault()
|
||||
if not @model.can('can_endorse')
|
||||
return
|
||||
$elem = $(event.target)
|
||||
url = @model.urlFor('endorse')
|
||||
endorsed = @model.get('endorsed')
|
||||
data = { endorsed: not endorsed }
|
||||
@model.set('endorsed', not endorsed)
|
||||
@trigger "comment:endorse", not endorsed
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
data: data
|
||||
type: "POST"
|
||||
@@ -1,188 +0,0 @@
|
||||
if Backbone?
|
||||
class @ThreadResponseView extends DiscussionContentView
|
||||
tagName: "li"
|
||||
|
||||
events:
|
||||
"click .discussion-submit-comment": "submitComment"
|
||||
"focus .wmd-input": "showEditorChrome"
|
||||
|
||||
$: (selector) ->
|
||||
@$el.find(selector)
|
||||
|
||||
initialize: ->
|
||||
@createShowView()
|
||||
|
||||
renderTemplate: ->
|
||||
@template = _.template($("#thread-response-template").html())
|
||||
|
||||
templateData = @model.toJSON()
|
||||
templateData.wmdId = @model.id ? (new Date()).getTime()
|
||||
@template(templateData)
|
||||
|
||||
render: ->
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
|
||||
@renderShowView()
|
||||
@renderAttrs()
|
||||
|
||||
@renderComments()
|
||||
@
|
||||
|
||||
afterInsert: ->
|
||||
@makeWmdEditor "comment-body"
|
||||
@hideEditorChrome()
|
||||
|
||||
hideEditorChrome: ->
|
||||
@$('.wmd-button-row').hide()
|
||||
@$('.wmd-preview').hide()
|
||||
@$('.wmd-input').css({
|
||||
height: '35px',
|
||||
padding: '5px'
|
||||
})
|
||||
@$('.comment-post-control').hide()
|
||||
|
||||
showEditorChrome: ->
|
||||
@$('.wmd-button-row').show()
|
||||
@$('.wmd-preview').show()
|
||||
@$('.comment-post-control').show()
|
||||
@$('.wmd-input').css({
|
||||
height: '125px',
|
||||
padding: '10px'
|
||||
})
|
||||
|
||||
renderComments: ->
|
||||
comments = new Comments()
|
||||
comments.comparator = (comment) ->
|
||||
comment.get('created_at')
|
||||
collectComments = (comment) ->
|
||||
comments.add(comment)
|
||||
children = new Comments(comment.get('children'))
|
||||
children.each (child) ->
|
||||
child.parent = comment
|
||||
collectComments(child)
|
||||
@model.get('comments').each collectComments
|
||||
comments.each (comment) => @renderComment(comment, false, null)
|
||||
|
||||
renderComment: (comment) =>
|
||||
comment.set('thread', @model.get('thread'))
|
||||
view = new ResponseCommentView(model: comment)
|
||||
view.render()
|
||||
@$el.find(".comments .new-comment").before(view.el)
|
||||
view
|
||||
|
||||
submitComment: (event) ->
|
||||
event.preventDefault()
|
||||
url = @model.urlFor('reply')
|
||||
body = @getWmdContent("comment-body")
|
||||
return if not body.trim().length
|
||||
@setWmdContent("comment-body", "")
|
||||
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), user_id: window.user.get("id"), id:"unsaved")
|
||||
view = @renderComment(comment)
|
||||
@hideEditorChrome()
|
||||
@trigger "comment:add", comment
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
data:
|
||||
body: body
|
||||
success: (response, textStatus) ->
|
||||
comment.set(response.content)
|
||||
view.render() # This is just to update the id for the most part, but might be useful in general
|
||||
|
||||
delete: (event) =>
|
||||
event.preventDefault()
|
||||
if not @model.can('can_delete')
|
||||
return
|
||||
if not confirm "Are you sure to delete this response? "
|
||||
return
|
||||
url = @model.urlFor('delete')
|
||||
@model.remove()
|
||||
@$el.remove()
|
||||
$elem = $(event.target)
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $elem
|
||||
url: url
|
||||
type: "POST"
|
||||
success: (response, textStatus) =>
|
||||
|
||||
createEditView: () ->
|
||||
if @showView?
|
||||
@showView.undelegateEvents()
|
||||
@showView.$el.empty()
|
||||
@showView = null
|
||||
|
||||
@editView = new ThreadResponseEditView(model: @model)
|
||||
@editView.bind "response:update", @update
|
||||
@editView.bind "response:cancel_edit", @cancelEdit
|
||||
|
||||
renderSubView: (view) ->
|
||||
view.setElement(@$('.discussion-response'))
|
||||
view.render()
|
||||
view.delegateEvents()
|
||||
|
||||
renderEditView: () ->
|
||||
@renderSubView(@editView)
|
||||
|
||||
hideCommentForm: () ->
|
||||
@$('.comment-form').closest('li').hide()
|
||||
|
||||
showCommentForm: () ->
|
||||
@$('.comment-form').closest('li').show()
|
||||
|
||||
createShowView: () ->
|
||||
|
||||
if @editView?
|
||||
@editView.undelegateEvents()
|
||||
@editView.$el.empty()
|
||||
@editView = null
|
||||
|
||||
@showView = new ThreadResponseShowView(model: @model)
|
||||
@showView.bind "response:delete", @delete
|
||||
@showView.bind "response:edit", @edit
|
||||
|
||||
renderShowView: () ->
|
||||
@renderSubView(@showView)
|
||||
|
||||
cancelEdit: (event) =>
|
||||
event.preventDefault()
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
@showCommentForm()
|
||||
|
||||
edit: (event) =>
|
||||
@createEditView()
|
||||
@renderEditView()
|
||||
@hideCommentForm()
|
||||
|
||||
update: (event) =>
|
||||
|
||||
newBody = @editView.$(".edit-post-body textarea").val()
|
||||
|
||||
url = DiscussionUtil.urlFor('update_comment', @model.id)
|
||||
|
||||
DiscussionUtil.safeAjax
|
||||
$elem: $(event.target)
|
||||
$loading: $(event.target) if event
|
||||
url: url
|
||||
type: "POST"
|
||||
dataType: 'json'
|
||||
async: false # TODO when the rest of the stuff below is made to work properly..
|
||||
data:
|
||||
body: newBody
|
||||
error: DiscussionUtil.formErrorHandler(@$(".edit-post-form-errors"))
|
||||
success: (response, textStatus) =>
|
||||
# TODO: Move this out of the callback, this makes it feel sluggish
|
||||
@editView.$(".edit-post-body textarea").val("").attr("prev-text", "")
|
||||
@editView.$(".wmd-preview p").html("")
|
||||
|
||||
@model.set
|
||||
body: newBody
|
||||
|
||||
@createShowView()
|
||||
@renderShowView()
|
||||
@showCommentForm()
|
||||
|
||||
1
lms/static/css/vendor/jquery.qtip.min.css
vendored
1
lms/static/css/vendor/jquery.qtip.min.css
vendored
@@ -1 +0,0 @@
|
||||
.ui-tooltip,.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;}.ui-tooltip-fluid{display:block;visibility:hidden;position:static!important;float:left!important;}.ui-tooltip-content{position:relative;padding:5px 9px;overflow:hidden;border:1px solid #000001;text-align:left;word-wrap:break-word;overflow:hidden;}.ui-tooltip-titlebar{position:relative;min-height:14px;padding:5px 35px 5px 10px;overflow:hidden;border:1px solid #000001;border-width:1px 1px 0;font-weight:bold;}.ui-tooltip-titlebar+.ui-tooltip-content{border-top-width:0!important;}/*!Default close button class */ .ui-tooltip-titlebar .ui-state-default{position:absolute;right:4px;top:50%;margin-top:-9px;cursor:pointer;outline:medium none;border-width:1px;border-style:solid;}* html .ui-tooltip-titlebar .ui-state-default{top:16px;}.ui-tooltip-titlebar .ui-icon,.ui-tooltip-icon .ui-icon{display:block;text-indent:-1000em;}.ui-tooltip-icon,.ui-tooltip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;}.ui-tooltip-icon .ui-icon{width:18px;height:14px;text-align:center;text-indent:0;font:normal bold 10px/13px Tahoma,sans-serif;color:inherit;background:transparent none no-repeat -100em -100em;}/*!Default tooltip style */ .ui-tooltip-default .ui-tooltip-titlebar,.ui-tooltip-default .ui-tooltip-content{border-color:#F1D031;background-color:#FFFFA3;color:#555;}.ui-tooltip-default .ui-tooltip-titlebar{background-color:#FFEF93;}.ui-tooltip-default .ui-tooltip-icon{border-color:#CCC;background:#F1F1F1;color:#777;}.ui-tooltip-default .ui-tooltip-titlebar .ui-state-hover{border-color:#AAA;color:#111;}
|
||||
BIN
lms/static/images/edge-on-edx-logo.png
Normal file
BIN
lms/static/images/edge-on-edx-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1 @@
|
||||
b4d043bb1ca4a8815d4a388a2c9d96038211417b
|
||||
BIN
lms/static/images/press/releases/dr-lewin-276_240x180.jpg
Normal file
BIN
lms/static/images/press/releases/dr-lewin-276_240x180.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
lms/static/images/press/releases/dr-lewin-316_240x180.jpg
Normal file
BIN
lms/static/images/press/releases/dr-lewin-316_240x180.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
@@ -29,6 +29,7 @@
|
||||
@import 'multicourse/password_reset';
|
||||
@import 'multicourse/error-pages';
|
||||
@import 'multicourse/help';
|
||||
@import 'multicourse/edge';
|
||||
|
||||
@import 'discussion';
|
||||
@import 'news';
|
||||
|
||||
@@ -15,6 +15,8 @@ $monospace: Monaco, 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
|
||||
$body-font-size: em(14);
|
||||
$body-line-height: golden-ratio(.875em, 1);
|
||||
$base-font-color: rgb(60,60,60);
|
||||
$baseFontColor: rgb(60,60,60);
|
||||
$base-font-color: rgb(60,60,60);
|
||||
$lighter-base-font-color: rgb(100,100,100);
|
||||
|
||||
$blue: rgb(29,157,217);
|
||||
|
||||
1
lms/static/sass/bourbon
Symbolic link
1
lms/static/sass/bourbon
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../common/static/sass/bourbon/
|
||||
35
lms/static/sass/bourbon/_bourbon.scss
vendored
35
lms/static/sass/bourbon/_bourbon.scss
vendored
@@ -1,35 +0,0 @@
|
||||
// Custom Functions
|
||||
@import "functions/deprecated-webkit-gradient";
|
||||
@import "functions/flex-grid";
|
||||
@import "functions/grid-width";
|
||||
@import "functions/linear-gradient";
|
||||
@import "functions/modular-scale";
|
||||
@import "functions/radial-gradient";
|
||||
@import "functions/render-gradients";
|
||||
@import "functions/tint-shade";
|
||||
|
||||
// CSS3 Mixins
|
||||
@import "css3/animation";
|
||||
@import "css3/appearance";
|
||||
@import "css3/background-image";
|
||||
@import "css3/background-size";
|
||||
@import "css3/border-image";
|
||||
@import "css3/border-radius";
|
||||
@import "css3/box-shadow";
|
||||
@import "css3/box-sizing";
|
||||
@import "css3/columns";
|
||||
@import "css3/flex-box";
|
||||
@import "css3/inline-block";
|
||||
@import "css3/linear-gradient";
|
||||
@import "css3/radial-gradient";
|
||||
@import "css3/transform";
|
||||
@import "css3/transition";
|
||||
@import "css3/user-select";
|
||||
|
||||
// Addons & other mixins
|
||||
@import "addons/button";
|
||||
@import "addons/clearfix";
|
||||
@import "addons/font-family";
|
||||
@import "addons/html5-input-types";
|
||||
@import "addons/position";
|
||||
@import "addons/timing-functions";
|
||||
267
lms/static/sass/bourbon/addons/_button.scss
vendored
267
lms/static/sass/bourbon/addons/_button.scss
vendored
@@ -1,267 +0,0 @@
|
||||
@mixin button ($style: simple, $base-color: #4294f0) {
|
||||
|
||||
@if type-of($style) == color {
|
||||
$base-color: $style;
|
||||
$style: simple;
|
||||
}
|
||||
|
||||
// Grayscale button
|
||||
@if $base-color == grayscale($base-color) {
|
||||
@if $style == simple {
|
||||
@include simple($base-color, $grayscale: true);
|
||||
}
|
||||
|
||||
@else if $style == shiny {
|
||||
@include shiny($base-color, $grayscale: true);
|
||||
}
|
||||
|
||||
@else if $style == pill {
|
||||
@include pill($base-color, $grayscale: true);
|
||||
}
|
||||
}
|
||||
|
||||
// Colored button
|
||||
@else {
|
||||
@if $style == simple {
|
||||
@include simple($base-color);
|
||||
}
|
||||
|
||||
@else if $style == shiny {
|
||||
@include shiny($base-color);
|
||||
}
|
||||
|
||||
@else if $style == pill {
|
||||
@include pill($base-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Simple Button
|
||||
//************************************************************************//
|
||||
@mixin simple($base-color, $grayscale: false) {
|
||||
$color: hsl(0, 0, 100%);
|
||||
$border: adjust-color($base-color, $saturation: 9%, $lightness: -14%);
|
||||
$inset-shadow: adjust-color($base-color, $saturation: -8%, $lightness: 15%);
|
||||
$stop-gradient: adjust-color($base-color, $saturation: 9%, $lightness: -11%);
|
||||
$text-shadow: adjust-color($base-color, $saturation: 15%, $lightness: -18%);
|
||||
|
||||
@if lightness($base-color) > 70% {
|
||||
$color: hsl(0, 0, 20%);
|
||||
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
|
||||
}
|
||||
|
||||
@if $grayscale == true {
|
||||
$border: grayscale($border);
|
||||
$inset-shadow: grayscale($inset-shadow);
|
||||
$stop-gradient: grayscale($stop-gradient);
|
||||
$text-shadow: grayscale($text-shadow);
|
||||
}
|
||||
|
||||
border: 1px solid $border;
|
||||
@include border-radius (3px);
|
||||
@include box-shadow (inset 0 1px 0 0 $inset-shadow);
|
||||
color: $color;
|
||||
display: inline;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
@include linear-gradient ($base-color, $stop-gradient);
|
||||
padding: 6px 18px 7px;
|
||||
text-shadow: 0 1px 0 $text-shadow;
|
||||
-webkit-background-clip: padding-box;
|
||||
|
||||
&:hover {
|
||||
$base-color-hover: adjust-color($base-color, $saturation: -4%, $lightness: -5%);
|
||||
$inset-shadow-hover: adjust-color($base-color, $saturation: -7%, $lightness: 5%);
|
||||
$stop-gradient-hover: adjust-color($base-color, $saturation: 8%, $lightness: -14%);
|
||||
|
||||
@if $grayscale == true {
|
||||
$base-color-hover: grayscale($base-color-hover);
|
||||
$inset-shadow-hover: grayscale($inset-shadow-hover);
|
||||
$stop-gradient-hover: grayscale($stop-gradient-hover);
|
||||
}
|
||||
|
||||
@include box-shadow (inset 0 1px 0 0 $inset-shadow-hover);
|
||||
cursor: pointer;
|
||||
@include linear-gradient ($base-color-hover, $stop-gradient-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
$border-active: adjust-color($base-color, $saturation: 9%, $lightness: -14%);
|
||||
$inset-shadow-active: adjust-color($base-color, $saturation: 7%, $lightness: -17%);
|
||||
|
||||
@if $grayscale == true {
|
||||
$border-active: grayscale($border-active);
|
||||
$inset-shadow-active: grayscale($inset-shadow-active);
|
||||
}
|
||||
|
||||
border: 1px solid $border-active;
|
||||
@include box-shadow (inset 0 0 8px 4px $inset-shadow-active, inset 0 0 8px 4px $inset-shadow-active, 0 1px 1px 0 #eee);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Shiny Button
|
||||
//************************************************************************//
|
||||
@mixin shiny($base-color, $grayscale: false) {
|
||||
$color: hsl(0, 0, 100%);
|
||||
$border: adjust-color($base-color, $red: -117, $green: -111, $blue: -81);
|
||||
$border-bottom: adjust-color($base-color, $red: -126, $green: -127, $blue: -122);
|
||||
$fourth-stop: adjust-color($base-color, $red: -79, $green: -70, $blue: -46);
|
||||
$inset-shadow: adjust-color($base-color, $red: 37, $green: 29, $blue: 12);
|
||||
$second-stop: adjust-color($base-color, $red: -56, $green: -50, $blue: -33);
|
||||
$text-shadow: adjust-color($base-color, $red: -140, $green: -141, $blue: -114);
|
||||
$third-stop: adjust-color($base-color, $red: -86, $green: -75, $blue: -48);
|
||||
|
||||
@if lightness($base-color) > 70% {
|
||||
$color: hsl(0, 0, 20%);
|
||||
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
|
||||
}
|
||||
|
||||
@if $grayscale == true {
|
||||
$border: grayscale($border);
|
||||
$border-bottom: grayscale($border-bottom);
|
||||
$fourth-stop: grayscale($fourth-stop);
|
||||
$inset-shadow: grayscale($inset-shadow);
|
||||
$second-stop: grayscale($second-stop);
|
||||
$text-shadow: grayscale($text-shadow);
|
||||
$third-stop: grayscale($third-stop);
|
||||
}
|
||||
|
||||
border: 1px solid $border;
|
||||
border-bottom: 1px solid $border-bottom;
|
||||
@include border-radius(5px);
|
||||
@include box-shadow(inset 0 1px 0 0 $inset-shadow);
|
||||
color: $color;
|
||||
display: inline;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
@include linear-gradient(top, $base-color 0%, $second-stop 50%, $third-stop 50%, $fourth-stop 100%);
|
||||
padding: 7px 20px 8px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 -1px 1px $text-shadow;
|
||||
|
||||
&:hover {
|
||||
$first-stop-hover: adjust-color($base-color, $red: -13, $green: -15, $blue: -18);
|
||||
$second-stop-hover: adjust-color($base-color, $red: -66, $green: -62, $blue: -51);
|
||||
$third-stop-hover: adjust-color($base-color, $red: -93, $green: -85, $blue: -66);
|
||||
$fourth-stop-hover: adjust-color($base-color, $red: -86, $green: -80, $blue: -63);
|
||||
|
||||
@if $grayscale == true {
|
||||
$first-stop-hover: grayscale($first-stop-hover);
|
||||
$second-stop-hover: grayscale($second-stop-hover);
|
||||
$third-stop-hover: grayscale($third-stop-hover);
|
||||
$fourth-stop-hover: grayscale($fourth-stop-hover);
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
@include linear-gradient(top, $first-stop-hover 0%,
|
||||
$second-stop-hover 50%,
|
||||
$third-stop-hover 50%,
|
||||
$fourth-stop-hover 100%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
$inset-shadow-active: adjust-color($base-color, $red: -111, $green: -116, $blue: -122);
|
||||
|
||||
@if $grayscale == true {
|
||||
$inset-shadow-active: grayscale($inset-shadow-active);
|
||||
}
|
||||
|
||||
@include box-shadow(inset 0 0 20px 0 $inset-shadow-active, 0 1px 0 #fff);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Pill Button
|
||||
//************************************************************************//
|
||||
@mixin pill($base-color, $grayscale: false) {
|
||||
$color: hsl(0, 0, 100%);
|
||||
$border-bottom: adjust-color($base-color, $hue: 8, $saturation: -11%, $lightness: -26%);
|
||||
$border-sides: adjust-color($base-color, $hue: 4, $saturation: -21%, $lightness: -21%);
|
||||
$border-top: adjust-color($base-color, $hue: -1, $saturation: -30%, $lightness: -15%);
|
||||
$inset-shadow: adjust-color($base-color, $hue: -1, $saturation: -1%, $lightness: 7%);
|
||||
$stop-gradient: adjust-color($base-color, $hue: 8, $saturation: 14%, $lightness: -10%);
|
||||
$text-shadow: adjust-color($base-color, $hue: 5, $saturation: -19%, $lightness: -15%);
|
||||
|
||||
@if lightness($base-color) > 70% {
|
||||
$color: hsl(0, 0, 20%);
|
||||
$text-shadow: adjust-color($base-color, $saturation: 10%, $lightness: 4%);
|
||||
}
|
||||
|
||||
@if $grayscale == true {
|
||||
$border-bottom: grayscale($border-bottom);
|
||||
$border-sides: grayscale($border-sides);
|
||||
$border-top: grayscale($border-top);
|
||||
$inset-shadow: grayscale($inset-shadow);
|
||||
$stop-gradient: grayscale($stop-gradient);
|
||||
$text-shadow: grayscale($text-shadow);
|
||||
}
|
||||
|
||||
border: 1px solid $border-top;
|
||||
border-color: $border-top $border-sides $border-bottom;
|
||||
@include border-radius(16px);
|
||||
@include box-shadow(inset 0 1px 0 0 $inset-shadow, 0 1px 2px 0 #b3b3b3);
|
||||
color: $color;
|
||||
display: inline;
|
||||
font-size: 11px;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
@include linear-gradient ($base-color, $stop-gradient);
|
||||
padding: 3px 16px 5px;
|
||||
text-align: center;
|
||||
text-shadow: 0 -1px 1px $text-shadow;
|
||||
-webkit-background-clip: padding-box;
|
||||
|
||||
&:hover {
|
||||
$base-color-hover: adjust-color($base-color, $lightness: -4.5%);
|
||||
$border-bottom: adjust-color($base-color, $hue: 8, $saturation: 13.5%, $lightness: -32%);
|
||||
$border-sides: adjust-color($base-color, $hue: 4, $saturation: -2%, $lightness: -27%);
|
||||
$border-top: adjust-color($base-color, $hue: -1, $saturation: -17%, $lightness: -21%);
|
||||
$inset-shadow-hover: adjust-color($base-color, $saturation: -1%, $lightness: 3%);
|
||||
$stop-gradient-hover: adjust-color($base-color, $hue: 8, $saturation: -4%, $lightness: -15.5%);
|
||||
$text-shadow-hover: adjust-color($base-color, $hue: 5, $saturation: -5%, $lightness: -22%);
|
||||
|
||||
@if $grayscale == true {
|
||||
$base-color-hover: grayscale($base-color-hover);
|
||||
$border-bottom: grayscale($border-bottom);
|
||||
$border-sides: grayscale($border-sides);
|
||||
$border-top: grayscale($border-top);
|
||||
$inset-shadow-hover: grayscale($inset-shadow-hover);
|
||||
$stop-gradient-hover: grayscale($stop-gradient-hover);
|
||||
$text-shadow-hover: grayscale($text-shadow-hover);
|
||||
}
|
||||
|
||||
border: 1px solid $border-top;
|
||||
border-color: $border-top $border-sides $border-bottom;
|
||||
@include box-shadow(inset 0 1px 0 0 $inset-shadow-hover);
|
||||
cursor: pointer;
|
||||
@include linear-gradient ($base-color-hover, $stop-gradient-hover);
|
||||
text-shadow: 0 -1px 1px $text-shadow-hover;
|
||||
-webkit-background-clip: padding-box;
|
||||
}
|
||||
|
||||
&:active {
|
||||
$active-color: adjust-color($base-color, $hue: 4, $saturation: -12%, $lightness: -10%);
|
||||
$border-active: adjust-color($base-color, $hue: 6, $saturation: -2.5%, $lightness: -30%);
|
||||
$border-bottom-active: adjust-color($base-color, $hue: 11, $saturation: 6%, $lightness: -31%);
|
||||
$inset-shadow-active: adjust-color($base-color, $hue: 9, $saturation: 2%, $lightness: -21.5%);
|
||||
$text-shadow-active: adjust-color($base-color, $hue: 5, $saturation: -12%, $lightness: -21.5%);
|
||||
|
||||
@if $grayscale == true {
|
||||
$active-color: grayscale($active-color);
|
||||
$border-active: grayscale($border-active);
|
||||
$border-bottom-active: grayscale($border-bottom-active);
|
||||
$inset-shadow-active: grayscale($inset-shadow-active);
|
||||
$text-shadow-active: grayscale($text-shadow-active);
|
||||
}
|
||||
|
||||
background: $active-color;
|
||||
border: 1px solid $border-active;
|
||||
border-bottom: 1px solid $border-bottom-active;
|
||||
@include box-shadow(inset 0 0 6px 3px $inset-shadow-active, 0 1px 0 0 #fff);
|
||||
text-shadow: 0 -1px 1px $text-shadow-active;
|
||||
}
|
||||
}
|
||||
|
||||
29
lms/static/sass/bourbon/addons/_clearfix.scss
vendored
29
lms/static/sass/bourbon/addons/_clearfix.scss
vendored
@@ -1,29 +0,0 @@
|
||||
// Micro clearfix provides an easy way to contain floats without adding additional markup
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Contain all floats within .wrapper
|
||||
// .wrapper {
|
||||
// @include clearfix;
|
||||
// .content,
|
||||
// .sidebar {
|
||||
// float : left;
|
||||
// }
|
||||
// }
|
||||
|
||||
@mixin clearfix {
|
||||
zoom: 1;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
|
||||
&:after {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// Acknowledgements
|
||||
// Micro clearfix: [Nicolas Gallagher](http://nicolasgallagher.com/micro-clearfix-hack/)
|
||||
@@ -1,5 +0,0 @@
|
||||
$georgia: Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
$helvetica: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$lucida-grande: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif;
|
||||
$monospace: "Bitstream Vera Sans Mono", Consolas, Courier, monospace;
|
||||
$verdana: Verdana, Geneva, sans-serif;
|
||||
@@ -1,36 +0,0 @@
|
||||
//************************************************************************//
|
||||
// Generate a variable ($all-text-inputs) with a list of all html5
|
||||
// input types that have a text-based input, excluding textarea.
|
||||
// http://diveintohtml5.org/forms.html
|
||||
//************************************************************************//
|
||||
$inputs-list: 'input[type="email"]',
|
||||
'input[type="number"]',
|
||||
'input[type="password"]',
|
||||
'input[type="search"]',
|
||||
'input[type="tel"]',
|
||||
'input[type="text"]',
|
||||
'input[type="url"]',
|
||||
|
||||
// Webkit & Gecko may change the display of these in the future
|
||||
'input[type="color"]',
|
||||
'input[type="date"]',
|
||||
'input[type="datetime"]',
|
||||
'input[type="datetime-local"]',
|
||||
'input[type="month"]',
|
||||
'input[type="time"]',
|
||||
'input[type="week"]';
|
||||
|
||||
$unquoted-inputs-list: ();
|
||||
|
||||
@each $input-type in $inputs-list {
|
||||
$unquoted-inputs-list: append($unquoted-inputs-list, unquote($input-type), comma);
|
||||
}
|
||||
|
||||
$all-text-inputs: $unquoted-inputs-list;
|
||||
|
||||
// You must use interpolation on the variable:
|
||||
// #{$all-text-inputs}
|
||||
//************************************************************************//
|
||||
// #{$all-text-inputs}, textarea {
|
||||
// border: 1px solid red;
|
||||
// }
|
||||
30
lms/static/sass/bourbon/addons/_position.scss
vendored
30
lms/static/sass/bourbon/addons/_position.scss
vendored
@@ -1,30 +0,0 @@
|
||||
@mixin position ($position: relative, $coordinates: 0 0 0 0) {
|
||||
|
||||
@if type-of($position) == list {
|
||||
$coordinates: $position;
|
||||
$position: relative;
|
||||
}
|
||||
|
||||
$top: nth($coordinates, 1);
|
||||
$right: nth($coordinates, 2);
|
||||
$bottom: nth($coordinates, 3);
|
||||
$left: nth($coordinates, 4);
|
||||
|
||||
position: $position;
|
||||
|
||||
@if not(unitless($top)) {
|
||||
top: $top;
|
||||
}
|
||||
|
||||
@if not(unitless($right)) {
|
||||
right: $right;
|
||||
}
|
||||
|
||||
@if not(unitless($bottom)) {
|
||||
bottom: $bottom;
|
||||
}
|
||||
|
||||
@if not(unitless($left)) {
|
||||
left: $left;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// CSS cubic-bezier timing functions. Timing functions courtesy of jquery.easie (github.com/jaukia/easie)
|
||||
// Timing functions are the same as demo'ed here: http://jqueryui.com/demos/effect/easing.html
|
||||
|
||||
// EASE IN
|
||||
$ease-in-quad: cubic-bezier(0.550, 0.085, 0.680, 0.530);
|
||||
$ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190);
|
||||
$ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220);
|
||||
$ease-in-quint: cubic-bezier(0.755, 0.050, 0.855, 0.060);
|
||||
$ease-in-sine: cubic-bezier(0.470, 0.000, 0.745, 0.715);
|
||||
$ease-in-expo: cubic-bezier(0.950, 0.050, 0.795, 0.035);
|
||||
$ease-in-circ: cubic-bezier(0.600, 0.040, 0.980, 0.335);
|
||||
$ease-in-back: cubic-bezier(0.600, -0.280, 0.735, 0.045);
|
||||
|
||||
// EASE OUT
|
||||
$ease-out-quad: cubic-bezier(0.250, 0.460, 0.450, 0.940);
|
||||
$ease-out-cubic: cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
$ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000);
|
||||
$ease-out-quint: cubic-bezier(0.230, 1.000, 0.320, 1.000);
|
||||
$ease-out-sine: cubic-bezier(0.390, 0.575, 0.565, 1.000);
|
||||
$ease-out-expo: cubic-bezier(0.190, 1.000, 0.220, 1.000);
|
||||
$ease-out-circ: cubic-bezier(0.075, 0.820, 0.165, 1.000);
|
||||
$ease-out-back: cubic-bezier(0.175, 0.885, 0.320, 1.275);
|
||||
|
||||
// EASE IN OUT
|
||||
$ease-in-out-quad: cubic-bezier(0.455, 0.030, 0.515, 0.955);
|
||||
$ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
$ease-in-out-quart: cubic-bezier(0.770, 0.000, 0.175, 1.000);
|
||||
$ease-in-out-quint: cubic-bezier(0.860, 0.000, 0.070, 1.000);
|
||||
$ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950);
|
||||
$ease-in-out-expo: cubic-bezier(1.000, 0.000, 0.000, 1.000);
|
||||
$ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.150, 0.860);
|
||||
$ease-in-out-back: cubic-bezier(0.680, -0.550, 0.265, 1.550);
|
||||
171
lms/static/sass/bourbon/css3/_animation.scss
vendored
171
lms/static/sass/bourbon/css3/_animation.scss
vendored
@@ -1,171 +0,0 @@
|
||||
// http://www.w3.org/TR/css3-animations/#the-animation-name-property-
|
||||
// Each of these mixins support comma separated lists of values, which allows different transitions for individual properties to be described in a single style rule. Each value in the list corresponds to the value at that same position in the other properties.
|
||||
|
||||
// Official animation shorthand property.
|
||||
@mixin animation ($animation-1,
|
||||
$animation-2: false, $animation-3: false,
|
||||
$animation-4: false, $animation-5: false,
|
||||
$animation-6: false, $animation-7: false,
|
||||
$animation-8: false, $animation-9: false)
|
||||
{
|
||||
$full: compact($animation-1, $animation-2, $animation-3, $animation-4,
|
||||
$animation-5, $animation-6, $animation-7, $animation-8, $animation-9);
|
||||
|
||||
-webkit-animation: $full;
|
||||
-moz-animation: $full;
|
||||
animation: $full;
|
||||
}
|
||||
|
||||
// Individual Animation Properties
|
||||
@mixin animation-name ($name-1,
|
||||
$name-2: false, $name-3: false,
|
||||
$name-4: false, $name-5: false,
|
||||
$name-6: false, $name-7: false,
|
||||
$name-8: false, $name-9: false)
|
||||
{
|
||||
$full: compact($name-1, $name-2, $name-3, $name-4,
|
||||
$name-5, $name-6, $name-7, $name-8, $name-9);
|
||||
|
||||
-webkit-animation-name: $full;
|
||||
-moz-animation-name: $full;
|
||||
animation-name: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-duration ($time-1: 0,
|
||||
$time-2: false, $time-3: false,
|
||||
$time-4: false, $time-5: false,
|
||||
$time-6: false, $time-7: false,
|
||||
$time-8: false, $time-9: false)
|
||||
{
|
||||
$full: compact($time-1, $time-2, $time-3, $time-4,
|
||||
$time-5, $time-6, $time-7, $time-8, $time-9);
|
||||
|
||||
-webkit-animation-duration: $full;
|
||||
-moz-animation-duration: $full;
|
||||
animation-duration: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-timing-function ($motion-1: ease,
|
||||
// ease | linear | ease-in | ease-out | ease-in-out
|
||||
$motion-2: false, $motion-3: false,
|
||||
$motion-4: false, $motion-5: false,
|
||||
$motion-6: false, $motion-7: false,
|
||||
$motion-8: false, $motion-9: false)
|
||||
{
|
||||
$full: compact($motion-1, $motion-2, $motion-3, $motion-4,
|
||||
$motion-5, $motion-6, $motion-7, $motion-8, $motion-9);
|
||||
|
||||
-webkit-animation-timing-function: $full;
|
||||
-moz-animation-timing-function: $full;
|
||||
animation-timing-function: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-iteration-count ($value-1: 1,
|
||||
// infinite | <number>
|
||||
$value-2: false, $value-3: false,
|
||||
$value-4: false, $value-5: false,
|
||||
$value-6: false, $value-7: false,
|
||||
$value-8: false, $value-9: false)
|
||||
{
|
||||
$full: compact($value-1, $value-2, $value-3, $value-4,
|
||||
$value-5, $value-6, $value-7, $value-8, $value-9);
|
||||
|
||||
-webkit-animation-iteration-count: $full;
|
||||
-moz-animation-iteration-count: $full;
|
||||
animation-iteration-count: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-direction ($direction-1: normal,
|
||||
// normal | alternate
|
||||
$direction-2: false, $direction-3: false,
|
||||
$direction-4: false, $direction-5: false,
|
||||
$direction-6: false, $direction-7: false,
|
||||
$direction-8: false, $direction-9: false)
|
||||
{
|
||||
$full: compact($direction-1, $direction-2, $direction-3, $direction-4,
|
||||
$direction-5, $direction-6, $direction-7, $direction-8, $direction-9);
|
||||
|
||||
-webkit-animation-direction: $full;
|
||||
-moz-animation-direction: $full;
|
||||
animation-direction: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-play-state ($state-1: running,
|
||||
// running | paused
|
||||
$state-2: false, $state-3: false,
|
||||
$state-4: false, $state-5: false,
|
||||
$state-6: false, $state-7: false,
|
||||
$state-8: false, $state-9: false)
|
||||
{
|
||||
$full: compact($state-1, $state-2, $state-3, $state-4,
|
||||
$state-5, $state-6, $state-7, $state-8, $state-9);
|
||||
|
||||
-webkit-animation-play-state: $full;
|
||||
-moz-animation-play-state: $full;
|
||||
animation-play-state: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-delay ($time-1: 0,
|
||||
$time-2: false, $time-3: false,
|
||||
$time-4: false, $time-5: false,
|
||||
$time-6: false, $time-7: false,
|
||||
$time-8: false, $time-9: false)
|
||||
{
|
||||
$full: compact($time-1, $time-2, $time-3, $time-4,
|
||||
$time-5, $time-6, $time-7, $time-8, $time-9);
|
||||
|
||||
-webkit-animation-delay: $full;
|
||||
-moz-animation-delay: $full;
|
||||
animation-delay: $full;
|
||||
}
|
||||
|
||||
|
||||
@mixin animation-fill-mode ($mode-1: none,
|
||||
// http://goo.gl/l6ckm
|
||||
// none | forwards | backwards | both
|
||||
$mode-2: false, $mode-3: false,
|
||||
$mode-4: false, $mode-5: false,
|
||||
$mode-6: false, $mode-7: false,
|
||||
$mode-8: false, $mode-9: false)
|
||||
{
|
||||
$full: compact($mode-1, $mode-2, $mode-3, $mode-4,
|
||||
$mode-5, $mode-6, $mode-7, $mode-8, $mode-9);
|
||||
|
||||
-webkit-animation-fill-mode: $full;
|
||||
-moz-animation-fill-mode: $full;
|
||||
animation-fill-mode: $full;
|
||||
}
|
||||
|
||||
|
||||
// Deprecated
|
||||
@mixin animation-basic ($name, $time: 0, $motion: ease) {
|
||||
$length-of-name: length($name);
|
||||
$length-of-time: length($time);
|
||||
$length-of-motion: length($motion);
|
||||
|
||||
@if $length-of-name > 1 {
|
||||
@include animation-name(zip($name));
|
||||
} @else {
|
||||
@include animation-name( $name);
|
||||
}
|
||||
|
||||
@if $length-of-time > 1 {
|
||||
@include animation-duration(zip($time));
|
||||
} @else {
|
||||
@include animation-duration( $time);
|
||||
}
|
||||
|
||||
@if $length-of-motion > 1 {
|
||||
@include animation-timing-function(zip($motion));
|
||||
} @else {
|
||||
@include animation-timing-function( $motion);
|
||||
}
|
||||
@warn "The animation-basic mixin is deprecated. Use the animation mixin instead.";
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
@mixin appearance ($value) {
|
||||
-webkit-appearance: $value;
|
||||
-moz-appearance: $value;
|
||||
-ms-appearance: $value;
|
||||
-o-appearance: $value;
|
||||
appearance: $value;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//************************************************************************//
|
||||
// Background-image property for adding multiple background images with
|
||||
// gradients, or for stringing multiple gradients together.
|
||||
//************************************************************************//
|
||||
|
||||
@mixin background-image(
|
||||
$image-1 , $image-2: false,
|
||||
$image-3: false, $image-4: false,
|
||||
$image-5: false, $image-6: false,
|
||||
$image-7: false, $image-8: false,
|
||||
$image-9: false, $image-10: false
|
||||
) {
|
||||
$images: compact($image-1, $image-2,
|
||||
$image-3, $image-4,
|
||||
$image-5, $image-6,
|
||||
$image-7, $image-8,
|
||||
$image-9, $image-10);
|
||||
|
||||
background-image: add-prefix($images, webkit);
|
||||
background-image: add-prefix($images, moz);
|
||||
background-image: add-prefix($images, ms);
|
||||
background-image: add-prefix($images, o);
|
||||
background-image: add-prefix($images);
|
||||
}
|
||||
|
||||
|
||||
@function add-prefix($images, $vendor: false) {
|
||||
$images-prefixed: ();
|
||||
|
||||
@for $i from 1 through length($images) {
|
||||
$type: type-of(nth($images, $i)); // Get type of variable - List or String
|
||||
|
||||
// If variable is a list - Gradient
|
||||
@if $type == list {
|
||||
$gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial)
|
||||
$gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue)
|
||||
|
||||
$gradient: render-gradients($gradient-args, $gradient-type, $vendor);
|
||||
$images-prefixed: append($images-prefixed, $gradient, comma);
|
||||
}
|
||||
|
||||
// If variable is a string - Image
|
||||
@else if $type == string {
|
||||
$images-prefixed: join($images-prefixed, nth($images, $i), comma);
|
||||
}
|
||||
}
|
||||
@return $images-prefixed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Examples:
|
||||
//@include background-image(linear-gradient(top, orange, red));
|
||||
//@include background-image(radial-gradient(50% 50%, cover circle, orange, red));
|
||||
//@include background-image(url("/images/a.png"), linear-gradient(orange, red));
|
||||
//@include background-image(url("image.png"), linear-gradient(orange, red), url("image.png"));
|
||||
//@include background-image(linear-gradient(hsla(0, 100%, 100%, 0.25) 0%, hsla(0, 100%, 100%, 0.08) 50%, transparent 50%), linear-gradient(orange, red);
|
||||
@@ -1,15 +0,0 @@
|
||||
@mixin background-size ($length-1,
|
||||
$length-2: false, $length-3: false,
|
||||
$length-4: false, $length-5: false,
|
||||
$length-6: false, $length-7: false,
|
||||
$length-8: false, $length-9: false)
|
||||
{
|
||||
$full: compact($length-1, $length-2, $length-3, $length-4,
|
||||
$length-5, $length-6, $length-7, $length-8, $length-9);
|
||||
|
||||
-webkit-background-size: $full;
|
||||
-moz-background-size: $full;
|
||||
-ms-background-size: $full;
|
||||
-o-background-size: $full;
|
||||
background-size: $full;
|
||||
}
|
||||
56
lms/static/sass/bourbon/css3/_border-image.scss
vendored
56
lms/static/sass/bourbon/css3/_border-image.scss
vendored
@@ -1,56 +0,0 @@
|
||||
@mixin border-image($images) {
|
||||
-webkit-border-image: border-add-prefix($images, webkit);
|
||||
-moz-border-image: border-add-prefix($images, moz);
|
||||
-o-border-image: border-add-prefix($images, o);
|
||||
border-image: border-add-prefix($images);
|
||||
}
|
||||
|
||||
@function border-add-prefix($images, $vendor: false) {
|
||||
$border-image: ();
|
||||
$images-type: type-of(nth($images, 1));
|
||||
$first-var: nth(nth($images, 1), 1); // Get type of Gradient (Linear || radial)
|
||||
|
||||
// If input is a gradient
|
||||
@if $images-type == string {
|
||||
@if ($first-var == "linear") or ($first-var == "radial") {
|
||||
@for $i from 2 through length($images) {
|
||||
$gradient-type: nth($images, 1); // Get type of gradient (linear || radial)
|
||||
$gradient-args: nth($images, $i); // Get actual gradient (red, blue)
|
||||
$border-image: render-gradients($gradient-args, $gradient-type, $vendor);
|
||||
}
|
||||
}
|
||||
|
||||
// If input is a URL
|
||||
@else {
|
||||
$border-image: $images;
|
||||
}
|
||||
}
|
||||
|
||||
// If input is gradient or url + additional args
|
||||
@else if $images-type == list {
|
||||
@for $i from 1 through length($images) {
|
||||
$type: type-of(nth($images, $i)); // Get type of variable - List or String
|
||||
|
||||
// If variable is a list - Gradient
|
||||
@if $type == list {
|
||||
$gradient-type: nth(nth($images, $i), 1); // Get type of gradient (linear || radial)
|
||||
$gradient-args: nth(nth($images, $i), 2); // Get actual gradient (red, blue)
|
||||
$border-image: render-gradients($gradient-args, $gradient-type, $vendor);
|
||||
}
|
||||
|
||||
// If variable is a string - Image or number
|
||||
@else if ($type == string) or ($type == number) {
|
||||
$border-image: append($border-image, nth($images, $i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@return $border-image;
|
||||
}
|
||||
|
||||
//Examples:
|
||||
// @include border-image(url("image.png"));
|
||||
// @include border-image(url("image.png") 20 stretch);
|
||||
// @include border-image(linear-gradient(45deg, orange, yellow));
|
||||
// @include border-image(linear-gradient(45deg, orange, yellow) stretch);
|
||||
// @include border-image(linear-gradient(45deg, orange, yellow) 20 30 40 50 stretch round);
|
||||
// @include border-image(radial-gradient(top, cover, orange, yellow, orange));
|
||||
63
lms/static/sass/bourbon/css3/_border-radius.scss
vendored
63
lms/static/sass/bourbon/css3/_border-radius.scss
vendored
@@ -1,63 +0,0 @@
|
||||
@mixin border-radius ($radii) {
|
||||
-webkit-border-radius: $radii;
|
||||
-moz-border-radius: $radii;
|
||||
-ms-border-radius: $radii;
|
||||
-o-border-radius: $radii;
|
||||
border-radius: $radii;
|
||||
}
|
||||
|
||||
@mixin border-top-left-radius($radii) {
|
||||
-webkit-border-top-left-radius: $radii;
|
||||
-moz-border-top-left-radius: $radii;
|
||||
-moz-border-radius-topleft: $radii;
|
||||
-ms-border-top-left-radius: $radii;
|
||||
-o-border-top-left-radius: $radii;
|
||||
border-top-left-radius: $radii;
|
||||
}
|
||||
|
||||
@mixin border-top-right-radius($radii) {
|
||||
-webkit-border-top-right-radius: $radii;
|
||||
-moz-border-top-right-radius: $radii;
|
||||
-moz-border-radius-topright: $radii;
|
||||
-ms-border-top-right-radius: $radii;
|
||||
-o-border-top-right-radius: $radii;
|
||||
border-top-right-radius: $radii;
|
||||
}
|
||||
|
||||
@mixin border-bottom-left-radius($radii) {
|
||||
-webkit-border-bottom-left-radius: $radii;
|
||||
-moz-border-bottom-left-radius: $radii;
|
||||
-moz-border-radius-bottomleft: $radii;
|
||||
-ms-border-bottom-left-radius: $radii;
|
||||
-o-border-bottom-left-radius: $radii;
|
||||
border-bottom-left-radius: $radii;
|
||||
}
|
||||
|
||||
@mixin border-bottom-right-radius($radii) {
|
||||
-webkit-border-bottom-right-radius: $radii;
|
||||
-moz-border-bottom-right-radius: $radii;
|
||||
-moz-border-radius-bottomright: $radii;
|
||||
-ms-border-bottom-right-radius: $radii;
|
||||
-o-border-bottom-right-radius: $radii;
|
||||
border-bottom-right-radius: $radii;
|
||||
}
|
||||
|
||||
@mixin border-top-radius($radii) {
|
||||
@include border-top-left-radius($radii);
|
||||
@include border-top-right-radius($radii);
|
||||
}
|
||||
|
||||
@mixin border-right-radius($radii) {
|
||||
@include border-top-right-radius($radii);
|
||||
@include border-bottom-right-radius($radii);
|
||||
}
|
||||
|
||||
@mixin border-bottom-radius($radii) {
|
||||
@include border-bottom-left-radius($radii);
|
||||
@include border-bottom-right-radius($radii);
|
||||
}
|
||||
|
||||
@mixin border-left-radius($radii) {
|
||||
@include border-top-left-radius($radii);
|
||||
@include border-bottom-left-radius($radii);
|
||||
}
|
||||
14
lms/static/sass/bourbon/css3/_box-shadow.scss
vendored
14
lms/static/sass/bourbon/css3/_box-shadow.scss
vendored
@@ -1,14 +0,0 @@
|
||||
// Box-Shadow Mixin Requires Sass v3.1.1+
|
||||
@mixin box-shadow ($shadow-1,
|
||||
$shadow-2: false, $shadow-3: false,
|
||||
$shadow-4: false, $shadow-5: false,
|
||||
$shadow-6: false, $shadow-7: false,
|
||||
$shadow-8: false, $shadow-9: false)
|
||||
{
|
||||
$full: compact($shadow-1, $shadow-2, $shadow-3, $shadow-4,
|
||||
$shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9);
|
||||
|
||||
-webkit-box-shadow: $full;
|
||||
-moz-box-shadow: $full;
|
||||
box-shadow: $full;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
@mixin box-sizing ($box) {
|
||||
// content-box | border-box | inherit
|
||||
-webkit-box-sizing: $box;
|
||||
-moz-box-sizing: $box;
|
||||
box-sizing: $box; *behavior: url(/static/scripts/boxsizing.htc);
|
||||
}
|
||||
67
lms/static/sass/bourbon/css3/_columns.scss
vendored
67
lms/static/sass/bourbon/css3/_columns.scss
vendored
@@ -1,67 +0,0 @@
|
||||
@mixin columns($arg: auto) {
|
||||
// <column-count> || <column-width>
|
||||
-webkit-columns: $arg;
|
||||
-moz-columns: $arg;
|
||||
columns: $arg;
|
||||
}
|
||||
|
||||
@mixin column-count($int: auto) {
|
||||
// auto || integer
|
||||
-webkit-column-count: $int;
|
||||
-moz-column-count: $int;
|
||||
column-count: $int;
|
||||
}
|
||||
|
||||
@mixin column-gap($length: normal) {
|
||||
// normal || length
|
||||
-webkit-column-gap: $length;
|
||||
-moz-column-gap: $length;
|
||||
column-gap: $length;
|
||||
}
|
||||
|
||||
@mixin column-fill($arg: auto) {
|
||||
// auto || length
|
||||
-webkit-columns-fill: $arg;
|
||||
-moz-columns-fill: $arg;
|
||||
columns-fill: $arg;
|
||||
}
|
||||
|
||||
@mixin column-rule($arg) {
|
||||
// <border-width> || <border-style> || <color>
|
||||
-webkit-column-rule: $arg;
|
||||
-moz-column-rule: $arg;
|
||||
column-rule: $arg;
|
||||
}
|
||||
|
||||
@mixin column-rule-color($color) {
|
||||
-webkit-column-rule-color: $color;
|
||||
-moz-column-rule-color: $color;
|
||||
column-rule-color: $color;
|
||||
}
|
||||
|
||||
@mixin column-rule-style($style: none) {
|
||||
// none | hidden | dashed | dotted | double | groove | inset | inset | outset | ridge | solid
|
||||
-webkit-column-rule-style: $style;
|
||||
-moz-column-rule-style: $style;
|
||||
column-rule-style: $style;
|
||||
}
|
||||
|
||||
@mixin column-rule-width ($width: none) {
|
||||
-webkit-column-rule-width: $width;
|
||||
-moz-column-rule-width: $width;
|
||||
column-rule-width: $width;
|
||||
}
|
||||
|
||||
@mixin column-span($arg: none) {
|
||||
// none || all
|
||||
-webkit-column-span: $arg;
|
||||
-moz-column-span: $arg;
|
||||
column-span: $arg;
|
||||
}
|
||||
|
||||
@mixin column-width($length: auto) {
|
||||
// auto || length
|
||||
-webkit-column-width: $length;
|
||||
-moz-column-width: $length;
|
||||
column-width: $length;
|
||||
}
|
||||
67
lms/static/sass/bourbon/css3/_flex-box.scss
vendored
67
lms/static/sass/bourbon/css3/_flex-box.scss
vendored
@@ -1,67 +0,0 @@
|
||||
// CSS3 Flexible Box Model and property defaults
|
||||
|
||||
// Custom shorthand notation for flexbox
|
||||
@mixin box($orient: inline-axis, $pack: start, $align: stretch) {
|
||||
@include display-box;
|
||||
@include box-orient($orient);
|
||||
@include box-pack($pack);
|
||||
@include box-align($align);
|
||||
}
|
||||
|
||||
@mixin display-box {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: box;
|
||||
}
|
||||
|
||||
@mixin box-orient($orient: inline-axis) {
|
||||
// horizontal|vertical|inline-axis|block-axis|inherit
|
||||
-webkit-box-orient: $orient;
|
||||
-moz-box-orient: $orient;
|
||||
box-orient: $orient;
|
||||
}
|
||||
|
||||
@mixin box-pack($pack: start) {
|
||||
// start|end|center|justify
|
||||
-webkit-box-pack: $pack;
|
||||
-moz-box-pack: $pack;
|
||||
box-pack: $pack;
|
||||
}
|
||||
|
||||
@mixin box-align($align: stretch) {
|
||||
// start|end|center|baseline|stretch
|
||||
-webkit-box-align: $align;
|
||||
-moz-box-align: $align;
|
||||
box-align: $align;
|
||||
}
|
||||
|
||||
@mixin box-direction($direction: normal) {
|
||||
// normal|reverse|inherit
|
||||
-webkit-box-direction: $direction;
|
||||
-moz-box-direction: $direction;
|
||||
box-direction: $direction;
|
||||
}
|
||||
@mixin box-lines($lines: single) {
|
||||
// single|multiple
|
||||
-webkit-box-lines: $lines;
|
||||
-moz-box-lines: $lines;
|
||||
box-lines: $lines;
|
||||
}
|
||||
|
||||
@mixin box-ordinal-group($integer: 1) {
|
||||
-webkit-box-ordinal-group: $integer;
|
||||
-moz-box-ordinal-group: $integer;
|
||||
box-ordinal-group: $integer;
|
||||
}
|
||||
|
||||
@mixin box-flex($value: 0.0) {
|
||||
-webkit-box-flex: $value;
|
||||
-moz-box-flex: $value;
|
||||
box-flex: $value;
|
||||
}
|
||||
|
||||
@mixin box-flex-group($integer: 1) {
|
||||
-webkit-box-flex-group: $integer;
|
||||
-moz-box-flex-group: $integer;
|
||||
box-flex-group: $integer;
|
||||
}
|
||||
10
lms/static/sass/bourbon/css3/_inline-block.scss
vendored
10
lms/static/sass/bourbon/css3/_inline-block.scss
vendored
@@ -1,10 +0,0 @@
|
||||
// Legacy support for inline-block in IE7 (maybe IE6)
|
||||
@mixin inline-block {
|
||||
display: -moz-inline-box;
|
||||
-moz-box-orient: vertical;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
*vertical-align: auto;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
@mixin linear-gradient($pos, $G1, $G2: false,
|
||||
$G3: false, $G4: false,
|
||||
$G5: false, $G6: false,
|
||||
$G7: false, $G8: false,
|
||||
$G9: false, $G10: false,
|
||||
$fallback: false) {
|
||||
// Detect what type of value exists in $pos
|
||||
$pos-type: type-of(nth($pos, 1));
|
||||
|
||||
// If $pos is missing from mixin, reassign vars and add default position
|
||||
@if ($pos-type == color) or (nth($pos, 1) == "transparent") {
|
||||
$G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5;
|
||||
$G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos;
|
||||
$pos: top; // Default position
|
||||
}
|
||||
|
||||
$full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10);
|
||||
|
||||
// Set $G1 as the default fallback color
|
||||
$fallback-color: nth($G1, 1);
|
||||
|
||||
// If $fallback is a color use that color as the fallback color
|
||||
@if (type-of($fallback) == color) or ($fallback == "transparent") {
|
||||
$fallback-color: $fallback;
|
||||
}
|
||||
|
||||
background-color: $fallback-color;
|
||||
background-image: deprecated-webkit-gradient(linear, $full); // Safari <= 5.0
|
||||
background-image: -webkit-linear-gradient($pos, $full); // Safari 5.1+, Chrome
|
||||
background-image: -moz-linear-gradient($pos, $full);
|
||||
background-image: -ms-linear-gradient($pos, $full);
|
||||
background-image: -o-linear-gradient($pos, $full);
|
||||
background-image: unquote("linear-gradient(#{$pos}, #{$full})");
|
||||
}
|
||||
|
||||
|
||||
// Usage: Gradient position is optional, default is top. Position can be a degree. Color stops are optional as well.
|
||||
// @include linear-gradient(#1e5799, #2989d8);
|
||||
// @include linear-gradient(#1e5799, #2989d8, $fallback:#2989d8);
|
||||
// @include linear-gradient(top, #1e5799 0%, #2989d8 50%);
|
||||
// @include linear-gradient(50deg, rgba(10, 10, 10, 0.5) 0%, #2989d8 50%, #207cca 51%, #7db9e8 100%);
|
||||
@@ -1,31 +0,0 @@
|
||||
// Requires Sass 3.1+
|
||||
@mixin radial-gradient($pos, $shape-size,
|
||||
$G1, $G2,
|
||||
$G3: false, $G4: false,
|
||||
$G5: false, $G6: false,
|
||||
$G7: false, $G8: false,
|
||||
$G9: false, $G10: false,
|
||||
$fallback: false) {
|
||||
|
||||
$full: compact($G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10);
|
||||
|
||||
// Set $G1 as the default fallback color
|
||||
$fallback-color: nth($G1, 1);
|
||||
|
||||
// If $fallback is a color use that color as the fallback color
|
||||
@if (type-of($fallback) == color) or ($fallback == "transparent") {
|
||||
$fallback-color: $fallback;
|
||||
}
|
||||
|
||||
background-color: $fallback-color;
|
||||
background-image: deprecated-webkit-gradient(radial, $full); // Safari <= 5.0
|
||||
background-image: -webkit-radial-gradient($pos, $shape-size, $full);
|
||||
background-image: -moz-radial-gradient($pos, $shape-size, $full);
|
||||
background-image: -ms-radial-gradient($pos, $shape-size, $full);
|
||||
background-image: -o-radial-gradient($pos, $shape-size, $full);
|
||||
background-image: unquote("radial-gradient(#{$pos}, #{$shape-size}, #{$full})");
|
||||
}
|
||||
|
||||
// Usage: Gradient position and shape-size are required. Color stops are optional.
|
||||
// @include radial-gradient(50% 50%, circle cover, #1e5799, #efefef);
|
||||
// @include radial-gradient(50% 50%, circle cover, #eee 10%, #1e5799 30%, #efefef);
|
||||
19
lms/static/sass/bourbon/css3/_transform.scss
vendored
19
lms/static/sass/bourbon/css3/_transform.scss
vendored
@@ -1,19 +0,0 @@
|
||||
@mixin transform($property: none) {
|
||||
// none | <transform-function>
|
||||
-webkit-transform: $property;
|
||||
-moz-transform: $property;
|
||||
-ms-transform: $property;
|
||||
-o-transform: $property;
|
||||
transform: $property;
|
||||
}
|
||||
|
||||
@mixin transform-origin($axes: 50%) {
|
||||
// x-axis - left | center | right | length | %
|
||||
// y-axis - top | center | bottom | length | %
|
||||
// z-axis - length
|
||||
-webkit-transform-origin: $axes;
|
||||
-moz-transform-origin: $axes;
|
||||
-ms-transform-origin: $axes;
|
||||
-o-transform-origin: $axes;
|
||||
transform-origin: $axes;
|
||||
}
|
||||
104
lms/static/sass/bourbon/css3/_transition.scss
vendored
104
lms/static/sass/bourbon/css3/_transition.scss
vendored
@@ -1,104 +0,0 @@
|
||||
// Shorthand mixin. Supports multiple parentheses-deliminated values for each variable.
|
||||
// Example: @include transition (all, 2.0s, ease-in-out);
|
||||
// @include transition ((opacity, width), (1.0s, 2.0s), ease-in, (0, 2s));
|
||||
// @include transition ($property:(opacity, width), $delay: (1.5s, 2.5s));
|
||||
|
||||
@mixin transition ($property: all, $duration: 0.15s, $timing-function: ease-out, $delay: 0) {
|
||||
|
||||
// Detect # of args passed into each variable
|
||||
$length-of-property: length($property);
|
||||
$length-of-duration: length($duration);
|
||||
$length-of-timing-function: length($timing-function);
|
||||
$length-of-delay: length($delay);
|
||||
|
||||
@if $length-of-property > 1 {
|
||||
@include transition-property(zip($property)); }
|
||||
@else {
|
||||
@include transition-property( $property);
|
||||
}
|
||||
|
||||
@if $length-of-duration > 1 {
|
||||
@include transition-duration(zip($duration)); }
|
||||
@else {
|
||||
@include transition-duration( $duration);
|
||||
}
|
||||
|
||||
@if $length-of-timing-function > 1 {
|
||||
@include transition-timing-function(zip($timing-function)); }
|
||||
@else {
|
||||
@include transition-timing-function( $timing-function);
|
||||
}
|
||||
|
||||
@if $length-of-delay > 1 {
|
||||
@include transition-delay(zip($delay)); }
|
||||
@else {
|
||||
@include transition-delay( $delay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin transition-property ($prop-1: all,
|
||||
$prop-2: false, $prop-3: false,
|
||||
$prop-4: false, $prop-5: false,
|
||||
$prop-6: false, $prop-7: false,
|
||||
$prop-8: false, $prop-9: false)
|
||||
{
|
||||
$full: compact($prop-1, $prop-2, $prop-3, $prop-4, $prop-5,
|
||||
$prop-6, $prop-7, $prop-8, $prop-9);
|
||||
|
||||
-webkit-transition-property: $full;
|
||||
-moz-transition-property: $full;
|
||||
-ms-transition-property: $full;
|
||||
-o-transition-property: $full;
|
||||
transition-property: $full;
|
||||
}
|
||||
|
||||
@mixin transition-duration ($time-1: 0,
|
||||
$time-2: false, $time-3: false,
|
||||
$time-4: false, $time-5: false,
|
||||
$time-6: false, $time-7: false,
|
||||
$time-8: false, $time-9: false)
|
||||
{
|
||||
$full: compact($time-1, $time-2, $time-3, $time-4, $time-5,
|
||||
$time-6, $time-7, $time-8, $time-9);
|
||||
|
||||
-webkit-transition-duration: $full;
|
||||
-moz-transition-duration: $full;
|
||||
-ms-transition-duration: $full;
|
||||
-o-transition-duration: $full;
|
||||
transition-duration: $full;
|
||||
}
|
||||
|
||||
@mixin transition-timing-function ($motion-1: ease,
|
||||
$motion-2: false, $motion-3: false,
|
||||
$motion-4: false, $motion-5: false,
|
||||
$motion-6: false, $motion-7: false,
|
||||
$motion-8: false, $motion-9: false)
|
||||
{
|
||||
$full: compact($motion-1, $motion-2, $motion-3, $motion-4, $motion-5,
|
||||
$motion-6, $motion-7, $motion-8, $motion-9);
|
||||
|
||||
// ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier()
|
||||
-webkit-transition-timing-function: $full;
|
||||
-moz-transition-timing-function: $full;
|
||||
-ms-transition-timing-function: $full;
|
||||
-o-transition-timing-function: $full;
|
||||
transition-timing-function: $full;
|
||||
}
|
||||
|
||||
@mixin transition-delay ($time-1: 0,
|
||||
$time-2: false, $time-3: false,
|
||||
$time-4: false, $time-5: false,
|
||||
$time-6: false, $time-7: false,
|
||||
$time-8: false, $time-9: false)
|
||||
{
|
||||
$full: compact($time-1, $time-2, $time-3, $time-4, $time-5,
|
||||
$time-6, $time-7, $time-8, $time-9);
|
||||
|
||||
-webkit-transition-delay: $full;
|
||||
-moz-transition-delay: $full;
|
||||
-ms-transition-delay: $full;
|
||||
-o-transition-delay: $full;
|
||||
transition-delay: $full;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@mixin user-select($arg: none) {
|
||||
-webkit-user-select: $arg;
|
||||
-moz-user-select: $arg;
|
||||
-ms-user-select: $arg;
|
||||
user-select: $arg;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// Render Deprecated Webkit Gradient - Linear || Radial
|
||||
//************************************************************************//
|
||||
@function deprecated-webkit-gradient($type, $full) {
|
||||
$gradient-list: ();
|
||||
$gradient: false;
|
||||
$full-length: length($full);
|
||||
$percentage: false;
|
||||
$gradient-type: $type;
|
||||
|
||||
@for $i from 1 through $full-length {
|
||||
$gradient: nth($full, $i);
|
||||
|
||||
@if length($gradient) == 2 {
|
||||
$color-stop: color-stop(nth($gradient, 2), nth($gradient, 1));
|
||||
$gradient-list: join($gradient-list, $color-stop, comma);
|
||||
}
|
||||
@else {
|
||||
@if $i == $full-length {
|
||||
$percentage: 100%;
|
||||
}
|
||||
@else {
|
||||
$percentage: ($i - 1) * (100 / ($full-length - 1)) + "%";
|
||||
}
|
||||
$color-stop: color-stop(unquote($percentage), $gradient);
|
||||
$gradient-list: join($gradient-list, $color-stop, comma);
|
||||
}
|
||||
}
|
||||
|
||||
@if $type == radial {
|
||||
$gradient: -webkit-gradient(radial, center center, 0, center center, 460, $gradient-list);
|
||||
}
|
||||
@else if $type == linear {
|
||||
$gradient: -webkit-gradient(linear, left top, left bottom, $gradient-list);
|
||||
}
|
||||
@return $gradient;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Flexible grid
|
||||
@function flex-grid($columns, $container-columns: $fg-max-columns) {
|
||||
$width: $columns * $fg-column + ($columns - 1) * $fg-gutter;
|
||||
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
|
||||
@return percentage($width / $container-width);
|
||||
}
|
||||
|
||||
// Flexible gutter
|
||||
@function flex-gutter($container-columns: $fg-max-columns, $gutter: $fg-gutter) {
|
||||
$container-width: $container-columns * $fg-column + ($container-columns - 1) * $fg-gutter;
|
||||
@return percentage($gutter / $container-width);
|
||||
}
|
||||
|
||||
// The $fg-column, $fg-gutter and $fg-max-columns variables must be defined in your base stylesheet to properly use the flex-grid function.
|
||||
// This function takes the fluid grid equation (target / context = result) and uses columns to help define each.
|
||||
//
|
||||
// $fg-column: 60px; // Column Width
|
||||
// $fg-gutter: 25px; // Gutter Width
|
||||
// $fg-max-columns: 12; // Total Columns For Main Container
|
||||
//
|
||||
// div {
|
||||
// width: flex-grid(4); // returns (315px / 1020px) = 30.882353%;
|
||||
// margin-left: flex-gutter(); // returns (25px / 1020px) = 2.45098%;
|
||||
//
|
||||
// p {
|
||||
// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%;
|
||||
// float: left;
|
||||
// margin: flex-gutter(4); // returns (25px / 315px) = 7.936508%;
|
||||
// }
|
||||
//
|
||||
// blockquote {
|
||||
// float: left;
|
||||
// width: flex-grid(2, 4); // returns (145px / 315px) = 46.031746%;
|
||||
// }
|
||||
// }
|
||||
@@ -1,13 +0,0 @@
|
||||
@function grid-width($n) {
|
||||
@return $n * $gw-column + ($n - 1) * $gw-gutter;
|
||||
}
|
||||
|
||||
// The $gw-column and $gw-gutter variables must be defined in your base stylesheet to properly use the grid-width function.
|
||||
//
|
||||
// $gw-column: 100px; // Column Width
|
||||
// $gw-gutter: 40px; // Gutter Width
|
||||
//
|
||||
// div {
|
||||
// width: grid-width(4); // returns 520px;
|
||||
// margin-left: $gw-gutter; // returns 40px;
|
||||
// }
|
||||
@@ -1,23 +0,0 @@
|
||||
@function linear-gradient($pos: top, $G1: false, $G2: false,
|
||||
$G3: false, $G4: false,
|
||||
$G5: false, $G6: false,
|
||||
$G7: false, $G8: false,
|
||||
$G9: false, $G10: false) {
|
||||
|
||||
// Detect what type of value exists in $pos
|
||||
$pos-type: type-of(nth($pos, 1));
|
||||
|
||||
// If $pos is missing from mixin, reassign vars and add default position
|
||||
@if ($pos-type == color) or (nth($pos, 1) == "transparent") {
|
||||
$G10: $G9; $G9: $G8; $G8: $G7; $G7: $G6; $G6: $G5;
|
||||
$G5: $G4; $G4: $G3; $G3: $G2; $G2: $G1; $G1: $pos;
|
||||
$pos: top; // Default position
|
||||
}
|
||||
|
||||
$type: linear;
|
||||
$gradient: compact($pos, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10);
|
||||
$type-gradient: append($type, $gradient, comma);
|
||||
|
||||
@return $type-gradient;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user