From 05436e591b71c34676d4bb5a143955cb7c1cf747 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 8 Jun 2012 11:27:22 -0400 Subject: [PATCH 001/252] Make mitxmako available to both the lms and the cms --- {lms => common}/lib/mitxmako/README | 0 {lms => common}/lib/mitxmako/__init__.py | 0 {lms => common}/lib/mitxmako/middleware.py | 10 +++++----- {lms => common}/lib/mitxmako/shortcuts.py | 10 +++------- {lms => common}/lib/mitxmako/template.py | 9 +++++---- 5 files changed, 13 insertions(+), 16 deletions(-) rename {lms => common}/lib/mitxmako/README (100%) rename {lms => common}/lib/mitxmako/__init__.py (100%) rename {lms => common}/lib/mitxmako/middleware.py (88%) rename {lms => common}/lib/mitxmako/shortcuts.py (84%) rename {lms => common}/lib/mitxmako/template.py (87%) diff --git a/lms/lib/mitxmako/README b/common/lib/mitxmako/README similarity index 100% rename from lms/lib/mitxmako/README rename to common/lib/mitxmako/README diff --git a/lms/lib/mitxmako/__init__.py b/common/lib/mitxmako/__init__.py similarity index 100% rename from lms/lib/mitxmako/__init__.py rename to common/lib/mitxmako/__init__.py diff --git a/lms/lib/mitxmako/middleware.py b/common/lib/mitxmako/middleware.py similarity index 88% rename from lms/lib/mitxmako/middleware.py rename to common/lib/mitxmako/middleware.py index 1d175abbf7..50f2840a05 100644 --- a/lms/lib/mitxmako/middleware.py +++ b/common/lib/mitxmako/middleware.py @@ -20,10 +20,10 @@ from django.conf import settings requestcontext = None lookup = {} + class MakoMiddleware(object): def __init__(self): """Setup mako variables and lookup object""" - from django.conf import settings # Set all mako variables based on django settings template_locations = settings.MAKO_TEMPLATES module_directory = getattr(settings, 'MAKO_MODULE_DIR', None) @@ -32,17 +32,17 @@ class MakoMiddleware(object): module_directory = tempfile.mkdtemp() for location in template_locations: - lookup[location] = TemplateLookup(directories=template_locations[location], + lookup[location] = TemplateLookup(directories=template_locations[location], module_directory=module_directory, - output_encoding='utf-8', - input_encoding='utf-8', + output_encoding='utf-8', + input_encoding='utf-8', encoding_errors='replace', ) import mitxmako mitxmako.lookup = lookup - def process_request (self, request): + def process_request(self, request): global requestcontext requestcontext = RequestContext(request) requestcontext['is_secure'] = request.is_secure() diff --git a/lms/lib/mitxmako/shortcuts.py b/common/lib/mitxmako/shortcuts.py similarity index 84% rename from lms/lib/mitxmako/shortcuts.py rename to common/lib/mitxmako/shortcuts.py index 7286a4e259..9f6044b81e 100644 --- a/lms/lib/mitxmako/shortcuts.py +++ b/common/lib/mitxmako/shortcuts.py @@ -15,10 +15,9 @@ from django.template import Context from django.http import HttpResponse -import mitxmako.middleware as middleware +from . import middleware from django.conf import settings -import mitxmako.middleware def render_to_string(template_name, dictionary, context=None, namespace='main'): context_instance = Context(dictionary) @@ -28,15 +27,12 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): context_dictionary = {} context_instance['settings'] = settings context_instance['MITX_ROOT_URL'] = settings.MITX_ROOT_URL - for d in mitxmako.middleware.requestcontext: + for d in middleware.requestcontext: context_dictionary.update(d) for d in context_instance: context_dictionary.update(d) - if context: + if context: context_dictionary.update(context) - ## HACK - ## We should remove this, and possible set COURSE_TITLE in the middleware from the session. - if 'COURSE_TITLE' not in context_dictionary: context_dictionary['COURSE_TITLE'] = '' # fetch and render template template = middleware.lookup[namespace].get_template(template_name) return template.render(**context_dictionary) diff --git a/lms/lib/mitxmako/template.py b/common/lib/mitxmako/template.py similarity index 87% rename from lms/lib/mitxmako/template.py rename to common/lib/mitxmako/template.py index 9e5897ef25..911f5a5b28 100644 --- a/lms/lib/mitxmako/template.py +++ b/common/lib/mitxmako/template.py @@ -14,10 +14,11 @@ from mako.template import Template as MakoTemplate -import middleware +from . import middleware + +django_variables = ['lookup', 'template_dirs', 'output_encoding', + 'module_directory', 'encoding_errors'] -django_variables = ['lookup', 'template_dirs', 'output_encoding', - 'module_directory', 'encoding_errors',] class Template(MakoTemplate): def __init__(self, *args, **kwargs): @@ -25,4 +26,4 @@ class Template(MakoTemplate): if not kwargs.get('no_django', False): overrides = dict([(k, getattr(middleware, k, None),) for k in django_variables]) kwargs.update(overrides) - super(Template, self).__init__(*args, **kwargs) + super(Template, self).__init__(*args, **kwargs) From 8134bcbc3df80595b9fcdfa11991772148ca1922 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 8 Jun 2012 14:01:41 -0400 Subject: [PATCH 002/252] Get the cms up to the point of rendering a template --- {lms/djangoapps/track => cms}/__init__.py | 0 .../templatetags => cms/envs}/__init__.py | 0 cms/envs/common.py | 151 ++++++++++++++++++ cms/envs/dev.py | 37 +++++ cms/manage.py | 14 ++ lms/lib/util/__init__.py => cms/models.py | 0 cms/templates/calendar.html | 3 + cms/urls.py | 9 ++ cms/views.py | 5 + common/djangoapps/track/__init__.py | 0 .../djangoapps/track/middleware.py | 0 {lms => common}/djangoapps/track/models.py | 0 {lms => common}/djangoapps/track/tests.py | 0 {lms => common}/djangoapps/track/views.py | 0 {lms => common}/lib/cache_toolbox/COPYING | 0 {lms => common}/lib/cache_toolbox/README.rst | 0 {lms => common}/lib/cache_toolbox/__init__.py | 0 .../lib/cache_toolbox/app_settings.py | 0 {lms => common}/lib/cache_toolbox/core.py | 0 .../lib/cache_toolbox/middleware.py | 0 {lms => common}/lib/cache_toolbox/model.py | 0 {lms => common}/lib/cache_toolbox/relation.py | 0 .../cache_toolbox/templatetags/__init__.py | 0 .../templatetags/cache_toolbox.py | 0 common/lib/monitoring/__init__.py | 0 common/lib/monitoring/exceptions.py | 10 ++ common/lib/util/__init__.py | 0 {lms => common}/lib/util/cache.py | 0 {lms => common}/lib/util/memcache.py | 0 {lms => common}/lib/util/middleware.py | 0 {lms => common}/lib/util/models.py | 0 {lms => common}/lib/util/tests.py | 0 {lms => common}/lib/util/views.py | 0 lms/envs/common.py | 1 + rakefile | 20 +-- 35 files changed, 240 insertions(+), 10 deletions(-) rename {lms/djangoapps/track => cms}/__init__.py (100%) rename {lms/lib/cache_toolbox/templatetags => cms/envs}/__init__.py (100%) create mode 100644 cms/envs/common.py create mode 100644 cms/envs/dev.py create mode 100644 cms/manage.py rename lms/lib/util/__init__.py => cms/models.py (100%) create mode 100644 cms/templates/calendar.html create mode 100644 cms/urls.py create mode 100644 cms/views.py create mode 100644 common/djangoapps/track/__init__.py rename {lms => common}/djangoapps/track/middleware.py (100%) rename {lms => common}/djangoapps/track/models.py (100%) rename {lms => common}/djangoapps/track/tests.py (100%) rename {lms => common}/djangoapps/track/views.py (100%) rename {lms => common}/lib/cache_toolbox/COPYING (100%) rename {lms => common}/lib/cache_toolbox/README.rst (100%) rename {lms => common}/lib/cache_toolbox/__init__.py (100%) rename {lms => common}/lib/cache_toolbox/app_settings.py (100%) rename {lms => common}/lib/cache_toolbox/core.py (100%) rename {lms => common}/lib/cache_toolbox/middleware.py (100%) rename {lms => common}/lib/cache_toolbox/model.py (100%) rename {lms => common}/lib/cache_toolbox/relation.py (100%) create mode 100644 common/lib/cache_toolbox/templatetags/__init__.py rename {lms => common}/lib/cache_toolbox/templatetags/cache_toolbox.py (100%) create mode 100644 common/lib/monitoring/__init__.py create mode 100644 common/lib/monitoring/exceptions.py create mode 100644 common/lib/util/__init__.py rename {lms => common}/lib/util/cache.py (100%) rename {lms => common}/lib/util/memcache.py (100%) rename {lms => common}/lib/util/middleware.py (100%) rename {lms => common}/lib/util/models.py (100%) rename {lms => common}/lib/util/tests.py (100%) rename {lms => common}/lib/util/views.py (100%) diff --git a/lms/djangoapps/track/__init__.py b/cms/__init__.py similarity index 100% rename from lms/djangoapps/track/__init__.py rename to cms/__init__.py diff --git a/lms/lib/cache_toolbox/templatetags/__init__.py b/cms/envs/__init__.py similarity index 100% rename from lms/lib/cache_toolbox/templatetags/__init__.py rename to cms/envs/__init__.py diff --git a/cms/envs/common.py b/cms/envs/common.py new file mode 100644 index 0000000000..9b349a06d0 --- /dev/null +++ b/cms/envs/common.py @@ -0,0 +1,151 @@ +""" +This is the common settings file, intended to set sane defaults. If you have a +piece of configuration that's dependent on a set of feature flags being set, +then create a function that returns the calculated value based on the value of +MITX_FEATURES[...]. Modules that extend this one can change the feature +configuration in an environment specific config file and re-calculate those +values. + +We should make a method that calls all these config methods so that you just +make one call at the end of your site-specific dev file to reset all the +dependent variables (like INSTALLED_APPS) for you. + +Longer TODO: +1. Right now our treatment of static content in general and in particular + course-specific static content is haphazard. +2. We should have a more disciplined approach to feature flagging, even if it + just means that we stick them in a dict called MITX_FEATURES. +3. We need to handle configuration for multiple courses. This could be as + multiple sites, but we do need a way to map their data assets. +""" + +import sys +import tempfile +from path import path + +############################# SET PATH INFORMATION ############################# +PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms +COMMON_ROOT = PROJECT_ROOT.dirname() / "common" +ENV_ROOT = PROJECT_ROOT.dirname().dirname() # virtualenv dir /mitx is in +ASKBOT_ROOT = ENV_ROOT / "askbot-devel" +COURSES_ROOT = ENV_ROOT / "data" + +# FIXME: To support multiple courses, we should walk the courses dir at startup +DATA_DIR = COURSES_ROOT + +sys.path.append(ENV_ROOT) +sys.path.append(ASKBOT_ROOT) +sys.path.append(ASKBOT_ROOT / "askbot" / "deps") +sys.path.append(PROJECT_ROOT / 'djangoapps') +sys.path.append(PROJECT_ROOT / 'lib') +sys.path.append(COMMON_ROOT / 'djangoapps') +sys.path.append(COMMON_ROOT / 'lib') + + +############################# WEB CONFIGURATION ############################# +# This is where we stick our compiled template files. +MAKO_MODULE_DIR = tempfile.mkdtemp('mako') +MAKO_TEMPLATES = {} +MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates'] + +MITX_ROOT_URL = '' + +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.request', + 'django.core.context_processors.static', + 'django.contrib.messages.context_processors.messages', + 'django.core.context_processors.auth', # this is required for admin + 'django.core.context_processors.csrf', # necessary for csrf protection +) + +################################# Middleware ################################### +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + + # Instead of AuthenticationMiddleware, we use a cached backed version + 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', + + 'django.contrib.messages.middleware.MessageMiddleware', + 'track.middleware.TrackMiddleware', + 'mitxmako.middleware.MakoMiddleware', + + 'django.middleware.transaction.TransactionMiddleware', +) + +############################ SIGNAL HANDLERS ################################ +import monitoring.exceptions # noqa + +############################ DJANGO_BUILTINS ################################ +# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here +DEBUG = False +TEMPLATE_DEBUG = False + +# Site info +SITE_ID = 1 +SITE_NAME = "localhost:8000" +HTTPS = 'on' +ROOT_URLCONF = 'mitx.cms.urls' +IGNORABLE_404_ENDS = ('favicon.ico') + +# Email +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DEFAULT_FROM_EMAIL = 'registration@mitx.mit.edu' +DEFAULT_FEEDBACK_EMAIL = 'feedback@mitx.mit.edu' +ADMINS = ( + ('MITx Admins', 'admin@mitx.mit.edu'), +) +MANAGERS = ADMINS + +# Static content +STATIC_URL = '/static/' +ADMIN_MEDIA_PREFIX = '/static/admin/' +STATIC_ROOT = ENV_ROOT / "staticfiles" + +# FIXME: We should iterate through the courses we have, adding the static +# contents for each of them. (Right now we just use symlinks.) +STATICFILES_DIRS = [ + PROJECT_ROOT / "static", + +# This is how you would use the textbook images locally +# ("book", ENV_ROOT / "book_images") +] + +# Locale/Internationalization +TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html +USE_I18N = True +USE_L10N = True + +# Messages +MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' + +############################ APPS ##################################### + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) diff --git a/cms/envs/dev.py b/cms/envs/dev.py new file mode 100644 index 0000000000..72a5e512c4 --- /dev/null +++ b/cms/envs/dev.py @@ -0,0 +1,37 @@ +""" +This config file runs the simplest dev environment""" + +from .common import * + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ENV_ROOT / "db" / "mitx.db", + } +} + +CACHES = { + # This is the cache used for most things. Askbot will not work without a + # functioning cache -- it relies on caching to load its settings in places. + # In staging/prod envs, the sessions also live here. + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'mitx_loc_mem_cache', + 'KEY_FUNCTION': 'util.memcache.safe_key', + }, + + # The general cache is what you get if you use our util.cache. It's used for + # things like caching the course.xml file for different A/B test groups. + # We set it to be a DummyCache to force reloading of course.xml in dev. + # In staging environments, we would grab VERSION from data uploaded by the + # push process. + 'general': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + 'KEY_PREFIX': 'general', + 'VERSION': 4, + 'KEY_FUNCTION': 'util.cache.memcache_safe_key', + } +} diff --git a/cms/manage.py b/cms/manage.py new file mode 100644 index 0000000000..3e4eedc9ff --- /dev/null +++ b/cms/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/lms/lib/util/__init__.py b/cms/models.py similarity index 100% rename from lms/lib/util/__init__.py rename to cms/models.py diff --git a/cms/templates/calendar.html b/cms/templates/calendar.html new file mode 100644 index 0000000000..05b2f88806 --- /dev/null +++ b/cms/templates/calendar.html @@ -0,0 +1,3 @@ +% for week in weeks: +${week} +% endfor diff --git a/cms/urls.py b/cms/urls.py new file mode 100644 index 0000000000..80e2b19e9d --- /dev/null +++ b/cms/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import patterns, url + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() + +urlpatterns = patterns('cms.views', + url(r'^(?P[^/]+)/calendar/', 'calendar', name='calendar'), +) diff --git a/cms/views.py b/cms/views.py new file mode 100644 index 0000000000..d0d4f4871c --- /dev/null +++ b/cms/views.py @@ -0,0 +1,5 @@ +from mitxmako.shortcuts import render_to_response + + +def calendar(request, course): + return render_to_response('calendar.html', {}) diff --git a/common/djangoapps/track/__init__.py b/common/djangoapps/track/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/track/middleware.py b/common/djangoapps/track/middleware.py similarity index 100% rename from lms/djangoapps/track/middleware.py rename to common/djangoapps/track/middleware.py diff --git a/lms/djangoapps/track/models.py b/common/djangoapps/track/models.py similarity index 100% rename from lms/djangoapps/track/models.py rename to common/djangoapps/track/models.py diff --git a/lms/djangoapps/track/tests.py b/common/djangoapps/track/tests.py similarity index 100% rename from lms/djangoapps/track/tests.py rename to common/djangoapps/track/tests.py diff --git a/lms/djangoapps/track/views.py b/common/djangoapps/track/views.py similarity index 100% rename from lms/djangoapps/track/views.py rename to common/djangoapps/track/views.py diff --git a/lms/lib/cache_toolbox/COPYING b/common/lib/cache_toolbox/COPYING similarity index 100% rename from lms/lib/cache_toolbox/COPYING rename to common/lib/cache_toolbox/COPYING diff --git a/lms/lib/cache_toolbox/README.rst b/common/lib/cache_toolbox/README.rst similarity index 100% rename from lms/lib/cache_toolbox/README.rst rename to common/lib/cache_toolbox/README.rst diff --git a/lms/lib/cache_toolbox/__init__.py b/common/lib/cache_toolbox/__init__.py similarity index 100% rename from lms/lib/cache_toolbox/__init__.py rename to common/lib/cache_toolbox/__init__.py diff --git a/lms/lib/cache_toolbox/app_settings.py b/common/lib/cache_toolbox/app_settings.py similarity index 100% rename from lms/lib/cache_toolbox/app_settings.py rename to common/lib/cache_toolbox/app_settings.py diff --git a/lms/lib/cache_toolbox/core.py b/common/lib/cache_toolbox/core.py similarity index 100% rename from lms/lib/cache_toolbox/core.py rename to common/lib/cache_toolbox/core.py diff --git a/lms/lib/cache_toolbox/middleware.py b/common/lib/cache_toolbox/middleware.py similarity index 100% rename from lms/lib/cache_toolbox/middleware.py rename to common/lib/cache_toolbox/middleware.py diff --git a/lms/lib/cache_toolbox/model.py b/common/lib/cache_toolbox/model.py similarity index 100% rename from lms/lib/cache_toolbox/model.py rename to common/lib/cache_toolbox/model.py diff --git a/lms/lib/cache_toolbox/relation.py b/common/lib/cache_toolbox/relation.py similarity index 100% rename from lms/lib/cache_toolbox/relation.py rename to common/lib/cache_toolbox/relation.py diff --git a/common/lib/cache_toolbox/templatetags/__init__.py b/common/lib/cache_toolbox/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/lib/cache_toolbox/templatetags/cache_toolbox.py b/common/lib/cache_toolbox/templatetags/cache_toolbox.py similarity index 100% rename from lms/lib/cache_toolbox/templatetags/cache_toolbox.py rename to common/lib/cache_toolbox/templatetags/cache_toolbox.py diff --git a/common/lib/monitoring/__init__.py b/common/lib/monitoring/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/lib/monitoring/exceptions.py b/common/lib/monitoring/exceptions.py new file mode 100644 index 0000000000..6a34d9a38f --- /dev/null +++ b/common/lib/monitoring/exceptions.py @@ -0,0 +1,10 @@ +from django.core.signals import got_request_exception +from django.dispatch import receiver +import logging + + +@receiver(got_request_exception) +def record_request_exception(sender, **kwargs): + logging.exception("Uncaught exception from {sender}".format( + sender=sender + )) diff --git a/common/lib/util/__init__.py b/common/lib/util/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/lib/util/cache.py b/common/lib/util/cache.py similarity index 100% rename from lms/lib/util/cache.py rename to common/lib/util/cache.py diff --git a/lms/lib/util/memcache.py b/common/lib/util/memcache.py similarity index 100% rename from lms/lib/util/memcache.py rename to common/lib/util/memcache.py diff --git a/lms/lib/util/middleware.py b/common/lib/util/middleware.py similarity index 100% rename from lms/lib/util/middleware.py rename to common/lib/util/middleware.py diff --git a/lms/lib/util/models.py b/common/lib/util/models.py similarity index 100% rename from lms/lib/util/models.py rename to common/lib/util/models.py diff --git a/lms/lib/util/tests.py b/common/lib/util/tests.py similarity index 100% rename from lms/lib/util/tests.py rename to common/lib/util/tests.py diff --git a/lms/lib/util/views.py b/common/lib/util/views.py similarity index 100% rename from lms/lib/util/views.py rename to common/lib/util/views.py diff --git a/lms/envs/common.py b/lms/envs/common.py index 806c379747..3ac026fd81 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -60,6 +60,7 @@ sys.path.append(ASKBOT_ROOT) sys.path.append(ASKBOT_ROOT / "askbot" / "deps") sys.path.append(PROJECT_ROOT / 'djangoapps') sys.path.append(PROJECT_ROOT / 'lib') +sys.path.append(COMMON_ROOT / 'djangoapps') sys.path.append(COMMON_ROOT / 'lib') ################################## MITXWEB ##################################### diff --git a/rakefile b/rakefile index 8f49668506..6bd582783e 100644 --- a/rakefile +++ b/rakefile @@ -58,7 +58,7 @@ task :pylint => LMS_REPORT_DIR do end end -[:lms].each do |system| +[:lms, :cms].each do |system| task_name = "test_#{system}" report_dir = File.join(REPORT_DIR, task_name) directory report_dir @@ -69,6 +69,15 @@ end sh(django_admin(:lms, :test, 'test', *Dir['lms/djangoapps'].each)) end task :test => task_name + + desc <<-desc + Start the #{system} locally with the specified environment (defaults to dev). + Other useful environments are devplus (for dev testing with a real local database) + desc + task system, [:env, :options] => [] do |t, args| + args.with_defaults(:env => 'dev', :options => '') + sh(django_admin(system, args.env, 'runserver', args.options)) + end end Dir["common/lib/*"].each do |lib| @@ -85,15 +94,6 @@ Dir["common/lib/*"].each do |lib| task :test => task_name end -desc <<-desc - Start the lms locally with the specified environment (defaults to dev). - Other useful environments are devplus (for dev testing with a real local database) - desc -task :lms, [:env] => [] do |t, args| - args.with_defaults(:env => 'dev') - sh(django_admin(:lms, args.env, 'runserver')) -end - task :runserver => :lms desc "Run django-admin against the specified system and environment" From dc85e46315dfb30ed9a3ead1df2c4e38411b7e6f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 8 Jun 2012 16:55:38 -0400 Subject: [PATCH 003/252] Allow login in the cms, and read a particular course from mongo --- .../djangoapps/instructor}/__init__.py | 0 cms/djangoapps/instructor/models.py | 61 ++++++++++++++ cms/djangoapps/instructor/tests.py | 16 ++++ cms/djangoapps/instructor/views.py | 49 ++++++++++++ cms/envs/dev.py | 6 ++ cms/lib/keystore/__init__.py | 80 +++++++++++++++++++ cms/lib/keystore/django.py | 12 +++ cms/lib/keystore/exceptions.py | 7 ++ cms/lib/keystore/mongo.py | 26 ++++++ cms/templates/login.html | 11 +++ cms/urls.py | 5 +- cms/views.py | 11 ++- common/lib/django_future/__init__.py | 0 {lms => common}/lib/django_future/csrf.py | 0 requirements.txt | 1 + 15 files changed, 281 insertions(+), 4 deletions(-) rename {lms/lib/django_future => cms/djangoapps/instructor}/__init__.py (100%) create mode 100644 cms/djangoapps/instructor/models.py create mode 100644 cms/djangoapps/instructor/tests.py create mode 100644 cms/djangoapps/instructor/views.py create mode 100644 cms/lib/keystore/__init__.py create mode 100644 cms/lib/keystore/django.py create mode 100644 cms/lib/keystore/exceptions.py create mode 100644 cms/lib/keystore/mongo.py create mode 100644 cms/templates/login.html create mode 100644 common/lib/django_future/__init__.py rename {lms => common}/lib/django_future/csrf.py (100%) diff --git a/lms/lib/django_future/__init__.py b/cms/djangoapps/instructor/__init__.py similarity index 100% rename from lms/lib/django_future/__init__.py rename to cms/djangoapps/instructor/__init__.py diff --git a/cms/djangoapps/instructor/models.py b/cms/djangoapps/instructor/models.py new file mode 100644 index 0000000000..906aeee2f1 --- /dev/null +++ b/cms/djangoapps/instructor/models.py @@ -0,0 +1,61 @@ +""" +WE'RE USING MIGRATIONS! + +If you make changes to this model, be sure to create an appropriate migration +file and check it in at the same time as your model changes. To do that, + +1. Go to the mitx dir +2. ./manage.py schemamigration user --auto description_of_your_change +3. Add the migration file created in mitx/courseware/migrations/ +""" +import uuid + +from django.db import models +from django.contrib.auth.models import User + + +class UserProfile(models.Model): + class Meta: + db_table = "auth_userprofile" + + ## CRITICAL TODO/SECURITY + # Sanitize all fields. + # This is not visible to other users, but could introduce holes later + user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile') + name = models.CharField(blank=True, max_length=255, db_index=True) + org = models.CharField(blank=True, max_length=255, db_index=True) + + +class Registration(models.Model): + ''' Allows us to wait for e-mail before user is registered. A + registration profile is created when the user creates an + account, but that account is inactive. Once the user clicks + on the activation key, it becomes active. ''' + class Meta: + db_table = "auth_registration" + + user = models.ForeignKey(User, unique=True) + activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) + + def register(self, user): + # MINOR TODO: Switch to crypto-secure key + self.activation_key = uuid.uuid4().hex + self.user = user + self.save() + + def activate(self): + self.user.is_active = True + self.user.save() + #self.delete() + + +class PendingNameChange(models.Model): + user = models.OneToOneField(User, unique=True, db_index=True) + new_name = models.CharField(blank=True, max_length=255) + rationale = models.CharField(blank=True, max_length=1024) + + +class PendingEmailChange(models.Model): + user = models.OneToOneField(User, unique=True, db_index=True) + new_email = models.CharField(blank=True, max_length=255, db_index=True) + activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) diff --git a/cms/djangoapps/instructor/tests.py b/cms/djangoapps/instructor/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/cms/djangoapps/instructor/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/cms/djangoapps/instructor/views.py b/cms/djangoapps/instructor/views.py new file mode 100644 index 0000000000..fbb341b468 --- /dev/null +++ b/cms/djangoapps/instructor/views.py @@ -0,0 +1,49 @@ +import logging + +from django.views.decorators.http import require_http_methods, require_POST, require_GET +from django.contrib.auth import logout, authenticate, login +from django.shortcuts import redirect +from mitxmako.shortcuts import render_to_response + +from django_future.csrf import ensure_csrf_cookie + +log = logging.getLogger("mitx.student") + + +@require_http_methods(['GET', 'POST']) +def do_login(request): + if request.method == 'POST': + return post_login(request) + elif request.method == 'GET': + return get_login(request) + + +@require_POST +@ensure_csrf_cookie +def post_login(request): + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(username=username, password=password) + if user is not None: + if user.is_active: + login(request, user) + return redirect(request.POST.get('next', '/')) + else: + raise Exception("Can't log in, account disabled") + else: + raise Exception("Can't log in, invalid authentication") + + +@require_GET +@ensure_csrf_cookie +def get_login(request): + return render_to_response('login.html', { + 'next': request.GET.get('next') + }) + + +@ensure_csrf_cookie +def logout_user(request): + ''' HTTP request to log in the user. Redirects to marketing page''' + logout(request) + return redirect('/') diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 72a5e512c4..f7277b3d3f 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -6,6 +6,12 @@ from .common import * DEBUG = True TEMPLATE_DEBUG = DEBUG +KEYSTORE = { + 'host': 'localhost', + 'db': 'mongo_base', + 'collection': 'key_store', +} + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', diff --git a/cms/lib/keystore/__init__.py b/cms/lib/keystore/__init__.py new file mode 100644 index 0000000000..5e6374cf4a --- /dev/null +++ b/cms/lib/keystore/__init__.py @@ -0,0 +1,80 @@ +""" +This module provides an abstraction for working objects that conceptually have +the following attributes: + + location: An identifier for an item, of which there might be many revisions + children: A list of urls for other items required to fully define this object + data: A set of nested data needed to define this object + editor: The editor/owner of the object + parents: Url pointers for objects that this object was derived from + revision: What revision of the item this is +""" + + +class Location(object): + ''' Encodes a location. + Can be: + * String (url) + * Tuple + * Dictionary + ''' + def __init__(self, location): + self.update(location) + + def update(self, location): + if isinstance(location, basestring): + self.tag = location.split('/')[0][:-1] + (self.org, self.course, self.category, self.name) = location.split('/')[2:] + elif isinstance(location, list): + (self.tag, self.org, self.course, self.category, self.name) = location + elif isinstance(location, dict): + self.tag = location['tag'] + self.org = location['org'] + self.course = location['course'] + self.category = location['category'] + self.name = location['name'] + elif isinstance(location, Location): + self.update(location.list()) + + def url(self): + return "i4x://{org}/{course}/{category}/{name}".format(**self.dict()) + + def list(self): + return [self.tag, self.org, self.course, self.category, self.name] + + def dict(self): + return {'tag': self.tag, + 'org': self.org, + 'course': self.course, + 'category': self.category, + 'name': self.name} + + def to_json(self): + return self.dict() + + +class KeyStore(object): + def get_children_for_item(self, location): + """ + Returns the children for the most recent revision of the object + with the specified location. + + If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + """ + raise NotImplementedError + + +class KeyStoreItem(object): + """ + An object from a KeyStore, which can be saved back to that keystore + """ + def __init__(self, location, children, data, editor, parents, revision): + self.location = location + self.children = children + self.data = data + self.editor = editor + self.parents = parents + self.revision = revision + + def save(self): + raise NotImplementedError diff --git a/cms/lib/keystore/django.py b/cms/lib/keystore/django.py new file mode 100644 index 0000000000..b6ffb83b5c --- /dev/null +++ b/cms/lib/keystore/django.py @@ -0,0 +1,12 @@ +""" +Module that provides a connection to the keystore specified in the django settings. + +Passes settings.KEYSTORE as kwargs to MongoKeyStore +""" + +from __future__ import absolute_import + +from django.conf import settings +from .mongo import MongoKeyStore + +keystore = MongoKeyStore(**settings.KEYSTORE) diff --git a/cms/lib/keystore/exceptions.py b/cms/lib/keystore/exceptions.py new file mode 100644 index 0000000000..b66470859f --- /dev/null +++ b/cms/lib/keystore/exceptions.py @@ -0,0 +1,7 @@ +""" +Exceptions thrown by KeyStore objects +""" + + +class ItemNotFoundError(Exception): + pass diff --git a/cms/lib/keystore/mongo.py b/cms/lib/keystore/mongo.py new file mode 100644 index 0000000000..fc190ee098 --- /dev/null +++ b/cms/lib/keystore/mongo.py @@ -0,0 +1,26 @@ +import pymongo +from . import KeyStore +from .exceptions import ItemNotFoundError + + +class MongoKeyStore(KeyStore): + """ + A Mongodb backed KeyStore + """ + def __init__(self, host, db, collection, port=27017): + self.collection = pymongo.connection.Connection( + host=host, + port=port + )[db][collection] + + def get_children_for_item(self, location): + item = self.collection.find_one( + {'location': location.dict()}, + fields={'children': True}, + sort=[('revision', pymongo.ASCENDING)], + ) + + if item is None: + raise ItemNotFoundError() + + return item['children'] diff --git a/cms/templates/login.html b/cms/templates/login.html new file mode 100644 index 0000000000..03ea5f967c --- /dev/null +++ b/cms/templates/login.html @@ -0,0 +1,11 @@ +
+ + + % if next is not None: + + % endif + + Username: + Possword: + +
diff --git a/cms/urls.py b/cms/urls.py index 80e2b19e9d..55d7a1086e 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -4,6 +4,7 @@ from django.conf.urls.defaults import patterns, url # from django.contrib import admin # admin.autodiscover() -urlpatterns = patterns('cms.views', - url(r'^(?P[^/]+)/calendar/', 'calendar', name='calendar'), +urlpatterns = patterns('', + url(r'^(?P[^/]+)/(?P[^/]+)/calendar/', 'cms.views.calendar', name='calendar'), + url(r'^accounts/login/', 'instructor.views.do_login', name='login'), ) diff --git a/cms/views.py b/cms/views.py index d0d4f4871c..c6786b03c4 100644 --- a/cms/views.py +++ b/cms/views.py @@ -1,5 +1,12 @@ from mitxmako.shortcuts import render_to_response +from keystore import Location +from keystore.django import keystore +from django.contrib.auth.decorators import login_required -def calendar(request, course): - return render_to_response('calendar.html', {}) +@login_required +def calendar(request, org, course): + weeks = keystore.get_children_for_item( + Location(['i4x', org, course, 'Course', 'course']) + ) + return render_to_response('calendar.html', {'weeks': weeks}) diff --git a/common/lib/django_future/__init__.py b/common/lib/django_future/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/lib/django_future/csrf.py b/common/lib/django_future/csrf.py similarity index 100% rename from lms/lib/django_future/csrf.py rename to common/lib/django_future/csrf.py diff --git a/requirements.txt b/requirements.txt index 82b28b09cf..5e95e1bf9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,3 +23,4 @@ requests sympy newrelic glob2 +pymongo From ae44d86e2780c6e0cae137a318cde5bb23630d99 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 9 Jun 2012 18:26:49 -0400 Subject: [PATCH 004/252] add msg to textinput_dynamath --- common/lib/capa/templates/textinput_dynamath.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/capa/templates/textinput_dynamath.html b/common/lib/capa/templates/textinput_dynamath.html index e8b26c5fcc..41b9c5d172 100644 --- a/common/lib/capa/templates/textinput_dynamath.html +++ b/common/lib/capa/templates/textinput_dynamath.html @@ -30,4 +30,7 @@ + % if msg: + ${msg|n} + % endif From 46b45969d04680b8e1cd74f389d006b8dd064b47 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 9 Jun 2012 18:27:09 -0400 Subject: [PATCH 005/252] first pass in capa cleanup: - responsetype used to be instantiated multiple times(!) in capa_problem now it is instantiated once, and stored in self.responders - responsetypes.GenericResponse restructured; each superclass show now provide setup_response (and not __init__), and may provide get_max_score(); general __init__ provided to clean up superclasses. --- common/lib/capa/capa_problem.py | 191 ++++++------- common/lib/capa/responsetypes.py | 275 ++++++++++++------- common/lib/capa/util.py | 18 ++ lms/djangoapps/courseware/module_render.py | 2 +- lms/static/coffee/src/modules/problem.coffee | 2 +- 5 files changed, 287 insertions(+), 201 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index b655270a9a..f790190215 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -1,6 +1,11 @@ # # File: capa/capa_problem.py # +# Nomenclature: +# +# A capa Problem is a collection of text and capa Response questions. Each Response may have one or more +# Input entry fields. The capa Problem may include a solution. +# ''' Main module which shows problems (of "capa" type). @@ -83,17 +88,32 @@ html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formul class LoncapaProblem(object): + ''' + Main class for capa Problems. + ''' + def __init__(self, fileobject, id, state=None, seed=None, system=None): + ''' + Initializes capa Problem. The problem itself is defined by the XML file + pointed to by fileobject. + + Arguments: + + - filesobject : an OSFS instance: see fs.osfs + - id : string used as the identifier for this problem; often a filename (no spaces) + - state : student state (represented as a dict) + - seed : random number generator seed (int) + - system : I4xSystem instance which provides OS, rendering, and user context + + ''' + ## Initialize class variables from state - self.seed = None self.student_answers = dict() self.correct_map = dict() self.done = False self.problem_id = id self.system = system - - if seed is not None: - self.seed = seed + self.seed = seed if state: if 'seed' in state: @@ -109,22 +129,21 @@ class LoncapaProblem(object): if not self.seed: self.seed = struct.unpack('i', os.urandom(4))[0] - ## Parse XML file - if getattr(system, 'DEBUG', False): + self.fileobject = fileobject # save problem file object, so we can use for debugging information later + if getattr(system, 'DEBUG', False): # get the problem XML string from the problem file log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject) file_text = fileobject.read() - self.fileobject = fileobject # save it, so we can use for debugging information later - # Convert startouttext and endouttext to proper - # TODO: Do with XML operations - file_text = re.sub("startouttext\s*/", "text", file_text) + file_text = re.sub("startouttext\s*/", "text", file_text) # Convert startouttext and endouttext to proper file_text = re.sub("endouttext\s*/", "/text", file_text) - self.tree = etree.XML(file_text) - self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers) + self.tree = etree.XML(file_text) # parse problem XML file into an element tree + + # construct script processor context (eg for customresponse problems) self.context = self.extract_context(self.tree, seed=self.seed) - for response in self.tree.xpath('//' + "|//".join(response_types)): - responder = response_types[response.tag](response, self.context, self.system) - responder.preprocess_response() + + # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations + # this also creates the list (self.responders) of Response instances for each question in the problem + self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers) def __unicode__(self): return u"LoncapaProblem ({0})".format(self.fileobject) @@ -140,12 +159,27 @@ class LoncapaProblem(object): def get_max_score(self): ''' - TODO: multiple points for programming problems. + Return maximum score for this problem. + We do this by counting the number of answers available for each question + in the problem. If the Response for a question has a get_max_score() method + then we call that and add its return value to the count. That can be + used to give complex problems (eg programming questions) multiple points. ''' - sum = 0 - for et in entry_types: - sum = sum + self.tree.xpath('count(//' + et + ')') - return int(sum) + maxscore = 0 + for responder in self.responders: + if hasattr(responder,'get_max_score'): + try: + maxscore += responder.get_max_score() + except Exception, err: + log.error('responder %s failed to properly return from get_max_score()' % responder) + raise + else: + try: + maxscore += len(responder.get_answers()) + except: + log.error('responder %s failed to properly return get_answers()' % responder) + raise + return maxscore def get_score(self): correct = 0 @@ -166,34 +200,35 @@ class LoncapaProblem(object): of each key removed (the string before the first "_"). Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123 + + Calles the Response for each question in this problem, to do the actual grading. ''' self.student_answers = answers self.correct_map = dict() - problems_simple = self.extract_problems(self.tree) - for response in problems_simple: - grader = response_types[response.tag](response, self.context, self.system) - results = grader.get_score(answers) # call the responsetype instance to do the actual grading + log.info('%s: in grade_answers, answers=%s' % (self,answers)) + for responder in self.responders: + results = responder.get_score(answers) # call the responsetype instance to do the actual grading self.correct_map.update(results) return self.correct_map def get_question_answers(self): - """Returns a dict of answer_ids to answer values. If we can't generate + """Returns a dict of answer_ids to answer values. If we cannot generate an answer (this sometimes happens in customresponses), that answer_id is not included. Called by "show answers" button JSON request (see capa_module) """ answer_map = dict() - problems_simple = self.extract_problems(self.tree) # purified (flat) XML tree of just response queries - for response in problems_simple: - responder = response_types[response.tag](response, self.context, self.system) # instance of numericalresponse, customresponse,... + for responder in self.responders: results = responder.get_answers() answer_map.update(results) # dict of (id,correct_answer) + # This should be handled in each responsetype, not here. # example for the following: - for entry in problems_simple.xpath("//" + "|//".join(response_properties + entry_types)): - answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline - if answer: - answer_map[entry.get('id')] = contextualize_text(answer, self.context) + for responder in self.responders: + for entry in responder.inputfields: + answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline + if answer: + answer_map[entry.get('id')] = contextualize_text(answer, self.context) # include solutions from ... stanzas # Tentative merge; we should figure out how we want to handle hints and solutions @@ -209,17 +244,16 @@ class LoncapaProblem(object): the dicts returned by grade_answers and get_question_answers. (Though get_question_answers may only return a subset of these.""" answer_ids = [] - problems_simple = self.extract_problems(self.tree) - for response in problems_simple: - responder = response_types[response.tag](response, self.context) - if hasattr(responder, "answer_id"): - answer_ids.append(responder.answer_id) - # customresponse types can have multiple answer_ids - elif hasattr(responder, "answer_ids"): - answer_ids.extend(responder.answer_ids) - + for responder in self.responders: + answer_ids.append(responder.get_answers().keys()) return answer_ids + def get_html(self): + ''' + Main method called externally to get the HTML to be rendered for this capa Problem. + ''' + return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context) + # ======= Private ======== def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private ''' @@ -253,9 +287,6 @@ class LoncapaProblem(object): log.exception("Error while execing code: " + code) return context - def get_html(self): - return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context) - def extract_html(self, problemtree): # private ''' Helper function for get_html. Recursively converts XML tree to HTML ''' @@ -335,76 +366,34 @@ class LoncapaProblem(object): Assign sub-IDs to all entries (textline, schematic, etc.) Annoted correctness and value In-place transformation + + Also create capa Response instances for each responsetype and save as self.responders ''' response_id = 1 + self.responders = [] for response in tree.xpath('//' + "|//".join(response_types)): response_id_str = self.problem_id + "_" + str(response_id) - response.attrib['id'] = response_id_str - if response_id not in correct_map: - correct = 'unsubmitted' - response.attrib['state'] = correct - response_id = response_id + 1 + response.attrib['id'] = response_id_str # create and save ID for this response + + # if response_id not in correct_map: correct = 'unsubmitted' # unused - to be removed + # response.attrib['state'] = correct + response_id += response_id + answer_id = 1 - for entry in tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), - id=response_id_str): - # assign one answer_id for each entry_type or solution_type + inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), + id=response_id_str) + for entry in inputfields: # assign one answer_id for each entry_type or solution_type entry.attrib['response_id'] = str(response_id) entry.attrib['answer_id'] = str(answer_id) entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) answer_id = answer_id + 1 + responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response + self.responders.append(responder) # save in list in self + # ... may not be associated with any specific response; give IDs for those separately # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). solution_id = 1 for solution in tree.findall('.//solution'): solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id) solution_id += 1 - - def extract_problems(self, problem_tree): - ''' Remove layout from the problem, and give a purified XML tree of just the problems ''' - problem_tree = copy.deepcopy(problem_tree) - tree = Element('problem') - for response in problem_tree.xpath("//" + "|//".join(response_types)): - newresponse = copy.copy(response) - for e in newresponse: - newresponse.remove(e) - # copy.copy is needed to make xpath work right. Otherwise, it starts at the root - # of the tree. We should figure out if there's some work-around - for e in copy.copy(response).xpath("//" + "|//".join(response_properties + entry_types)): - newresponse.append(e) - - tree.append(newresponse) - return tree - -if __name__ == '__main__': - problem_id = 'simpleFormula' - filename = 'simpleFormula.xml' - - problem_id = 'resistor' - filename = 'resistor.xml' - - lcp = LoncapaProblem(filename, problem_id) - - context = lcp.extract_context(lcp.tree) - problem = lcp.extract_problems(lcp.tree) - print lcp.grade_problems({'resistor_2_1': '1.0', 'resistor_3_1': '2.0'}) - #print lcp.grade_problems({'simpleFormula_2_1':'3*x^3'}) -#numericalresponse(problem, context) - -#print etree.tostring((lcp.tree)) - print '============' - print -#print etree.tostring(lcp.extract_problems(lcp.tree)) - print lcp.get_html() -#print extract_context(tree) - - - - # def handle_fr(self, element): - # problem={"answer":self.contextualize_text(answer), - # "type":"formularesponse", - # "tolerance":evaluator({},{},self.contextualize_text(tolerance)), - # "sample_range":dict(zip(variables, sranges)), - # "samples_count": numsamples, - # "id":id, - # self.questions[self.lid]=problem diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index c5683bb0bf..c0ad98baa2 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -21,40 +21,123 @@ import abc # specific library imports from calc import evaluator, UndefinedVariable -from util import contextualize_text +from util import * from lxml import etree from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? -log = logging.getLogger(__name__) +#log = logging.getLogger(__name__) +log = logging.getLogger('mitx.common.lib.capa.responsetypes') -def compare_with_tolerance(v1, v2, tol): - ''' Compare v1 to v2 with maximum tolerance tol - tol is relative if it ends in %; otherwise, it is absolute +#----------------------------------------------------------------------------- +# Exceptions + +class LoncapaProblemError(Exception): ''' - relative = "%" in tol - if relative: - tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01 - tolerance = tolerance_rel * max(abs(v1), abs(v2)) - else: - tolerance = evaluator(dict(),dict(),tol) - return abs(v1-v2) <= tolerance + Error in specification of a problem + ''' + pass + +class ResponseError(Exception): + ''' + Error for failure in processing a response + ''' + pass + +class StudentInputError(Exception): + pass + +#----------------------------------------------------------------------------- +# +# Main base class for CAPA responsetypes class GenericResponse(object): + ''' + Base class for CAPA responsetypes. Each response type (ie a capa question, + which is part of a capa problem) is represented as a superclass, + which should provide the following methods: + + - get_score : evaluate the given student answers, and return a CorrectMap + - get_answers : provide a dict of the expected answers for this problem + + In addition, these methods are optional: + + - get_max_score : if defined, this is called to obtain the maximum score possible for this question + - setup_response : find and note the answer input field IDs for the response; called by __init__ + + Each response type may also specify the following attributes: + + - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) + - allowed_inputfields : list of allowed input fields (each a string) for this Response + - required_attributes : list of required attributes (each a string) on the main response XML stanza + + ''' __metaclass__=abc.ABCMeta # abc = Abstract Base Class + max_inputfields = None + allowed_inputfields = [] + required_attributes = [] + + def __init__(self, xml, inputfields, context, system=None): + ''' + Init is passed the following arguments: + + - xml : ElementTree of this Response + - inputfields : list of ElementTrees for each input entry field in this Response + - context : script processor context + - system : I4xSystem instance which provides OS, rendering, and user context + - __unicode__ : unicode representation of this Response + + ''' + self.xml = xml + self.inputfields = inputfields + self.context = context + self.system = system + + for abox in inputfields: + if not abox.tag in self.allowed_inputfields: + msg = "%s: cannot have input field %s" % (unicode(self),abox.tag) + msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') + raise LoncapaProblemError(msg) + + if self.max_inputfields and len(inputfields)>self.max_inputfields: + msg = "%s: cannot have more than %s input fields" % (unicode(self),self.max_inputfields) + msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') + raise LoncapaProblemError(msg) + + for prop in self.required_attributes: + if not xml.get(prop): + msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self),prop) + msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') + raise LoncapaProblemError(msg) + + self.answer_ids = [x.get('id') for x in self.inputfields] + if self.max_inputfields==1: + self.answer_id = self.answer_ids[0] # for convenience + + if hasattr(self,'setup_response'): + self.setup_response() + @abc.abstractmethod def get_score(self, student_answers): + ''' + Return a CorrectMap for the answers expected vs given. This includes + (correctness, npoints, msg) for each answer_id. + ''' pass @abc.abstractmethod def get_answers(self): + ''' + Return a dict of (answer_id,answer_text) for each answer for this question. + ''' pass #not an abstract method because plenty of responses will not want to preprocess anything, and we should not require that they override this method. - def preprocess_response(self): + def setup_response(self): pass -#Every response type needs methods "get_score" and "get_answers" + def __unicode__(self): + return 'LoncapaProblem Response %s' % self.xml.tag #----------------------------------------------------------------------------- @@ -69,30 +152,19 @@ class MultipleChoiceResponse(GenericResponse): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.correct_choices = xml.xpath('//*[@id=$id]//choice[@correct="true"]', - id=xml.get('id')) - self.correct_choices = [choice.get('name') for choice in self.correct_choices] - self.context = context - self.answer_field = xml.find('choicegroup') # assumes only ONE choicegroup within this response - self.answer_id = xml.xpath('//*[@id=$id]//choicegroup/@id', - id=xml.get('id')) - if not len(self.answer_id) == 1: - raise Exception("should have exactly one choice group per multiplechoicceresponse") - self.answer_id=self.answer_id[0] + max_inputfields = 1 + allowed_inputfields = ['choicegroup'] - def get_score(self, student_answers): - if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: - return {self.answer_id:'correct'} - else: - return {self.answer_id:'incorrect'} + def setup_response(self): + self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes - def get_answers(self): - return {self.answer_id:self.correct_choices} + # define correct choices (after calling secondary setup) + xml = self.xml + cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]',id=xml.get('id')) + self.correct_choices = [choice.get('name') for choice in cxml] - def preprocess_response(self): + def mc_setup_response(self): ''' Initialize name attributes in stanzas in the in this response. ''' @@ -107,9 +179,22 @@ class MultipleChoiceResponse(GenericResponse): i+=1 else: choice.set("name", "choice_"+choice.get("name")) - + + def get_score(self, student_answers): + ''' + grade student response. + ''' + # log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices)) + if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: + return {self.answer_id:'correct'} + else: + return {self.answer_id:'incorrect'} + + def get_answers(self): + return {self.answer_id:self.correct_choices} + class TrueFalseResponse(MultipleChoiceResponse): - def preprocess_response(self): + def mc_setup_response(self): i=0 for response in self.xml.xpath("choicegroup"): response.set("type", "TrueFalse") @@ -140,12 +225,13 @@ class OptionResponse(GenericResponse): The location of the earth '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.answer_fields = xml.findall('optioninput') - self.context = context + allowed_inputfields = ['optioninput'] + + def setup_response(self): + self.answer_fields = self.inputfields def get_score(self, student_answers): + # log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) cmap = {} amap = self.get_answers() for aid in amap: @@ -157,17 +243,20 @@ class OptionResponse(GenericResponse): def get_answers(self): amap = dict([(af.get('id'),af.get('correct')) for af in self.answer_fields]) + # log.debug('%s: expected answers=%s' % (unicode(self),amap)) return amap #----------------------------------------------------------------------------- class NumericalResponse(GenericResponse): - def __init__(self, xml, context, system=None): - self.xml = xml - if not xml.get('answer'): - msg = "Error in problem specification: numericalresponse missing required answer attribute\n" - msg += "See XML source line %s" % getattr(xml,'sourceline','') - raise Exception,msg + + allowed_inputfields = ['textline'] + required_attributes = ['answer'] + max_inputfields = 1 + + def setup_response(self): + xml = self.xml + context = self.context self.correct_answer = contextualize_text(xml.get('answer'), context) try: self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default', @@ -182,7 +271,7 @@ class NumericalResponse(GenericResponse): self.answer_id = None def get_score(self, student_answers): - ''' Display HTML for a numeric response ''' + '''Grade a numeric response ''' student_answer = student_answers[self.answer_id] try: correct = compare_with_tolerance (evaluator(dict(),dict(),student_answer), complex(self.correct_answer), self.tolerance) @@ -241,16 +330,11 @@ def sympy_check2(): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.system = system - ## CRITICAL TODO: Should cover all entrytypes - ## NOTE: xpath will look at root of XML tree, not just - ## what's in xml. @id=id keeps us in the right customresponse. - self.answer_ids = xml.xpath('//*[@id=$id]//textline/@id', - id=xml.get('id')) - self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs - self.context = context + allowed_inputfields = ['textline','textbox'] + + def setup_response(self): + xml = self.xml + context = self.context # if has an "expect" (or "answer") attribute then save that self.expect = xml.get('expect') or xml.get('answer') @@ -271,15 +355,17 @@ def sympy_check2(): cfn = xml.get('cfn') if cfn: log.debug("cfn = %s" % cfn) - if cfn in context: - self.code = context[cfn] + if cfn in self.context: + self.code = self.context[cfn] else: - print "can't find cfn in context = ",context + msg = "%s: can't find cfn in context = %s" % (unicode(self),self.context) + msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','') + raise LoncapaProblemError(msg) if not self.code: if answer is None: # raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid - print "[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid + log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid) self.code = '' else: answer_src = answer.get('src') @@ -294,6 +380,8 @@ def sympy_check2(): of each key removed (the string before the first "_"). ''' + log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) + idset = sorted(self.answer_ids) # ordered list of answer id's try: submission = [student_answers[k] for k in idset] # ordered list of answers @@ -425,12 +513,12 @@ class SymbolicResponse(CustomResponse): Your input should be typed in as a list of lists, eg [[1,2],[3,4]]. '''}] - def __init__(self, xml, context, system=None): - xml.set('cfn','symmath_check') + + def setup_response(self): + self.xml.set('cfn','symmath_check') code = "from symmath import *" - exec code in context,context - CustomResponse.__init__(self,xml,context,system) - + exec code in self.context,self.context + CustomResponse.setup_response(self) #----------------------------------------------------------------------------- @@ -480,15 +568,13 @@ main() '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL - self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id', - id=xml.get('id')) - self.context = context - answer = xml.xpath('//*[@id=$id]//answer', - id=xml.get('id'))[0] + allowed_inputfields = ['textline','textbox'] + def setup_response(self): + xml = self.xml + self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + + answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer_src = answer.get('src') if answer_src is not None: self.code = self.system.filesystem.open('src/'+answer_src).read() @@ -590,8 +676,6 @@ main() raise Exception,'Short response from external server' return dict(zip(self.answer_ids,exans)) -class StudentInputError(Exception): - pass #----------------------------------------------------------------------------- @@ -617,8 +701,13 @@ class FormulaResponse(GenericResponse): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml + allowed_inputfields = ['textline'] + required_attributes = ['answer'] + max_inputfields = 1 + + def setup_response(self): + xml = self.xml + context = self.context self.correct_answer = contextualize_text(xml.get('answer'), context) self.samples = contextualize_text(xml.get('samples'), context) try: @@ -628,14 +717,6 @@ class FormulaResponse(GenericResponse): except Exception: self.tolerance = 0 - try: - self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', - id=xml.get('id'))[0] - except Exception: - self.answer_id = None - raise Exception, "[courseware.capa.responsetypes.FormulaResponse] Error: missing answer_id!!" - - self.context = context ts = xml.get('type') if ts is None: typeslist = [] @@ -648,7 +729,6 @@ class FormulaResponse(GenericResponse): else: # Default self.case_sensitive = False - def get_score(self, student_answers): variables=self.samples.split('@')[0].split(',') numsamples=int(self.samples.split('@')[1].split('#')[1]) @@ -697,13 +777,12 @@ class FormulaResponse(GenericResponse): #----------------------------------------------------------------------------- class SchematicResponse(GenericResponse): - def __init__(self, xml, context, system=None): - self.xml = xml - self.answer_ids = xml.xpath('//*[@id=$id]//schematic/@id', - id=xml.get('id')) - self.context = context - answer = xml.xpath('//*[@id=$id]//answer', - id=xml.get('id'))[0] + + allowed_inputfields = ['schematic'] + + def setup_response(self): + xml = self.xml + answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0] answer_src = answer.get('src') if answer_src is not None: self.code = self.system.filestore.open('src/'+answer_src).read() # Untested; never used @@ -740,10 +819,10 @@ class ImageResponse(GenericResponse): '''}] - def __init__(self, xml, context, system=None): - self.xml = xml - self.context = context - self.ielements = xml.findall('imageinput') + allowed_inputfields = ['imageinput'] + + def setup_response(self): + self.ielements = self.inputfields self.answer_ids = [ie.get('id') for ie in self.ielements] def get_score(self, student_answers): diff --git a/common/lib/capa/util.py b/common/lib/capa/util.py index d042aa21d3..996f6c8dac 100644 --- a/common/lib/capa/util.py +++ b/common/lib/capa/util.py @@ -1,3 +1,21 @@ +from calc import evaluator, UndefinedVariable + +#----------------------------------------------------------------------------- +# +# Utility functions used in CAPA responsetypes + +def compare_with_tolerance(v1, v2, tol): + ''' Compare v1 to v2 with maximum tolerance tol + tol is relative if it ends in %; otherwise, it is absolute + ''' + relative = "%" in tol + if relative: + tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01 + tolerance = tolerance_rel * max(abs(v1), abs(v2)) + else: + tolerance = evaluator(dict(),dict(),tol) + return abs(v1-v2) <= tolerance + def contextualize_text(text, context): # private ''' Takes a string with variables. E.g. $a+$b. Does a substitution of those variables from the context ''' diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 97fd1b948c..0f82d9ba94 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N content = instance.get_html() # special extra information about each problem, only for users who are staff - if user.is_staff: + if False and user.is_staff: module_id = xml_module.get('id') histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index c1a801d2b2..e1e062e949 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -48,7 +48,7 @@ class @Problem @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' else - @$("#answer_#{key}").text(value) + @$("#answer_#{key}").html(value) // needs to be html, not text, for complex solutions (eg coding) @$('.show').val 'Hide Answer' @element.addClass 'showed' else From 7b3ad553074dc323d079d0d564a0c5eeede160ae Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 9 Jun 2012 18:35:42 -0400 Subject: [PATCH 006/252] responsetypes - fix comment --- common/lib/capa/responsetypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index c0ad98baa2..1c09493b03 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -63,6 +63,7 @@ class GenericResponse(object): - get_max_score : if defined, this is called to obtain the maximum score possible for this question - setup_response : find and note the answer input field IDs for the response; called by __init__ + - __unicode__ : unicode representation of this Response Each response type may also specify the following attributes: @@ -85,7 +86,6 @@ class GenericResponse(object): - inputfields : list of ElementTrees for each input entry field in this Response - context : script processor context - system : I4xSystem instance which provides OS, rendering, and user context - - __unicode__ : unicode representation of this Response ''' self.xml = xml @@ -137,7 +137,7 @@ class GenericResponse(object): pass def __unicode__(self): - return 'LoncapaProblem Response %s' % self.xml.tag + return u'LoncapaProblem Response %s' % self.xml.tag #----------------------------------------------------------------------------- From 7b3c79698fb3e537da9c06a7b3233ec4fe05303e Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 9 Jun 2012 21:29:11 -0400 Subject: [PATCH 007/252] second pass in capa cleanup: - each response can now render its own xhtml - cleaned up LoncapaProblem.extract_html --- common/lib/capa/capa_problem.py | 119 ++++++++++++------------------- common/lib/capa/inputtypes.py | 71 ++++-------------- common/lib/capa/responsetypes.py | 40 ++++++++--- 3 files changed, 89 insertions(+), 141 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index f790190215..c63c13d420 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -23,7 +23,6 @@ import scipy import struct from lxml import etree -from lxml.etree import Element from xml.sax.saxutils import unescape from util import contextualize_text @@ -36,6 +35,7 @@ import eia log = logging.getLogger(__name__) +# dict of tagname, Response Class -- this should come from auto-registering response_types = {'numericalresponse': NumericalResponse, 'formularesponse': FormulaResponse, 'customresponse': CustomResponse, @@ -47,20 +47,13 @@ response_types = {'numericalresponse': NumericalResponse, 'optionresponse': OptionResponse, 'symbolicresponse': SymbolicResponse, } -entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput'] -solution_types = ['solution'] # extra things displayed after "show answers" is pressed -response_properties = ["responseparam", "answer"] # these get captured as student responses -# How to convert from original XML to HTML -# We should do this with xlst later +entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput'] +solution_types = ['solution'] # extra things displayed after "show answers" is pressed +response_properties = ["responseparam", "answer"] # these get captured as student responses + +# special problem tags which should be turned into innocuous HTML html_transforms = {'problem': {'tag': 'div'}, - "numericalresponse": {'tag': 'span'}, - "customresponse": {'tag': 'span'}, - "externalresponse": {'tag': 'span'}, - "schematicresponse": {'tag': 'span'}, - "formularesponse": {'tag': 'span'}, - "symbolicresponse": {'tag': 'span'}, - "multiplechoiceresponse": {'tag': 'span'}, "text": {'tag': 'span'}, "math": {'tag': 'span'}, } @@ -74,18 +67,6 @@ global_context = {'random': random, # These should be removed from HTML output, including all subelements html_problem_semantics = ["responseparam", "answer", "script"] -# These should be removed from HTML output, but keeping subelements -html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text", "externalresponse", 'symbolicresponse'] - -# removed in MC -## These should be transformed -#html_special_response = {"textline":inputtypes.textline.render, -# "schematic":inputtypes.schematic.render, -# "textbox":inputtypes.textbox.render, -# "formulainput":inputtypes.jstextline.render, -# "solution":inputtypes.solution.render, -# } - class LoncapaProblem(object): ''' @@ -142,7 +123,8 @@ class LoncapaProblem(object): self.context = self.extract_context(self.tree, seed=self.seed) # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations - # this also creates the list (self.responders) of Response instances for each question in the problem + # this also creates the dict (self.responders) of Response instances for each question in the problem. + # the dict has keys = xml subtree of Response, values = Response instance self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers) def __unicode__(self): @@ -166,7 +148,7 @@ class LoncapaProblem(object): used to give complex problems (eg programming questions) multiple points. ''' maxscore = 0 - for responder in self.responders: + for responder in self.responders.values(): if hasattr(responder,'get_max_score'): try: maxscore += responder.get_max_score() @@ -182,6 +164,10 @@ class LoncapaProblem(object): return maxscore def get_score(self): + ''' + Compute score for this problem. The score is the number of points awarded. + Returns an integer, from 0 to get_max_score(). + ''' correct = 0 for key in self.correct_map: if self.correct_map[key] == u'correct': @@ -206,7 +192,7 @@ class LoncapaProblem(object): self.student_answers = answers self.correct_map = dict() log.info('%s: in grade_answers, answers=%s' % (self,answers)) - for responder in self.responders: + for responder in self.responders.values(): results = responder.get_score(answers) # call the responsetype instance to do the actual grading self.correct_map.update(results) return self.correct_map @@ -218,24 +204,14 @@ class LoncapaProblem(object): (see capa_module) """ answer_map = dict() - for responder in self.responders: + for responder in self.responders.values(): results = responder.get_answers() answer_map.update(results) # dict of (id,correct_answer) - # This should be handled in each responsetype, not here. - # example for the following: - for responder in self.responders: - for entry in responder.inputfields: - answer = entry.get('correct_answer') # correct answer, when specified elsewhere, eg in a textline - if answer: - answer_map[entry.get('id')] = contextualize_text(answer, self.context) - # include solutions from ... stanzas - # Tentative merge; we should figure out how we want to handle hints and solutions for entry in self.tree.xpath("//" + "|//".join(solution_types)): answer = etree.tostring(entry) - if answer: - answer_map[entry.get('id')] = answer + if answer: answer_map[entry.get('id')] = answer return answer_map @@ -244,7 +220,7 @@ class LoncapaProblem(object): the dicts returned by grade_answers and get_question_answers. (Though get_question_answers may only return a subset of these.""" answer_ids = [] - for responder in self.responders: + for responder in self.responders.values(): answer_ids.append(responder.get_answers().keys()) return answer_ids @@ -252,7 +228,7 @@ class LoncapaProblem(object): ''' Main method called externally to get the HTML to be rendered for this capa Problem. ''' - return contextualize_text(etree.tostring(self.extract_html(self.tree)[0]), self.context) + return contextualize_text(etree.tostring(self.extract_html(self.tree)), self.context) # ======= Private ======== def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private @@ -264,12 +240,11 @@ class LoncapaProblem(object): Problem XML goes to Python execution context. Runs everything in script tags ''' random.seed(self.seed) - context = {'global_context': global_context} # save global context in here also - context.update(global_context) # initialize context to have stuff in global_context - context['__builtins__'] = globals()['__builtins__'] # put globals there also - context['the_lcp'] = self # pass instance of LoncapaProblem in + context = {'global_context': global_context} # save global context in here also + context.update(global_context) # initialize context to have stuff in global_context + context['__builtins__'] = globals()['__builtins__'] # put globals there also + context['the_lcp'] = self # pass instance of LoncapaProblem in - #for script in tree.xpath('/problem/script'): for script in tree.findall('.//script'): stype = script.get('type') if stype: @@ -288,16 +263,20 @@ class LoncapaProblem(object): return context def extract_html(self, problemtree): # private - ''' Helper function for get_html. Recursively converts XML tree to HTML + ''' + Main (private) function which converts Problem XML tree to HTML. + Calls itself recursively. + + Returns Element tree of XHTML representation of problemtree. + Calls render_html of Response instances to render responses into XHTML. + + Used by get_html. ''' if problemtree.tag in html_problem_semantics: return problemid = problemtree.get('id') # my ID - # used to be - # if problemtree.tag in html_special_response: - if problemtree.tag in inputtypes.get_input_xml_tags(): # status is currently the answer for the problem ID for the input element, # but it will turn into a dict containing both the answer and any associated message @@ -334,31 +313,25 @@ class LoncapaProblem(object): use='capa_input') return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...) - tree = Element(problemtree.tag) + if problemtree in self.responders: # let each Response render itself + return self.responders[problemtree].render_html(self.extract_html) + + tree = etree.Element(problemtree.tag) for item in problemtree: - subitems = self.extract_html(item) - if subitems is not None: - for subitem in subitems: - tree.append(subitem) - for (key, value) in problemtree.items(): - tree.set(key, value) + item_xhtml = self.extract_html(item) # nothing special: recurse + if item_xhtml is not None: + tree.append(item_xhtml) + + if tree.tag in html_transforms: + tree.tag = html_transforms[problemtree.tag]['tag'] + else: + for (key, value) in problemtree.items(): # copy attributes over if not innocufying + tree.set(key, value) tree.text = problemtree.text tree.tail = problemtree.tail - if problemtree.tag in html_transforms: - tree.tag = html_transforms[problemtree.tag]['tag'] - # Reset attributes. Otherwise, we get metadata in HTML - # (e.g. answers) - # TODO: We should remove and not zero them. - # I'm not sure how to do that quickly with lxml - for k in tree.keys(): - tree.set(k, "") - - # TODO: Fix. This loses Element().tail - #if problemtree.tag in html_skip: - # return tree - return [tree] + return tree def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private ''' @@ -370,7 +343,7 @@ class LoncapaProblem(object): Also create capa Response instances for each responsetype and save as self.responders ''' response_id = 1 - self.responders = [] + self.responders = {} for response in tree.xpath('//' + "|//".join(response_types)): response_id_str = self.problem_id + "_" + str(response_id) response.attrib['id'] = response_id_str # create and save ID for this response @@ -389,7 +362,7 @@ class LoncapaProblem(object): answer_id = answer_id + 1 responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response - self.responders.append(responder) # save in list in self + self.responders[response] = responder # save in list in self # ... may not be associated with any specific response; give IDs for those separately # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). diff --git a/common/lib/capa/inputtypes.py b/common/lib/capa/inputtypes.py index 3b25be3db7..10fbdb7f98 100644 --- a/common/lib/capa/inputtypes.py +++ b/common/lib/capa/inputtypes.py @@ -33,26 +33,17 @@ def get_input_xml_tags(): class SimpleInput():# XModule ''' Type for simple inputs -- plain HTML with a form element + State is a dictionary with optional keys: * Value * ID * Status (answered, unanswered, unsubmitted) * Feedback (dictionary containing keys for hints, errors, or other feedback from previous attempt) + ''' xml_tags = {} ## Maps tags to functions - - @classmethod - def get_xml_tags(c): - return c.xml_tags.keys() - - @classmethod - def get_uses(c): - return ['capa_input', 'capa_transform'] - - def get_html(self): - return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg) def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): self.xml = xml @@ -83,49 +74,16 @@ class SimpleInput():# XModule if 'status' in state: self.status = state['status'] -## TODO -# class SimpleTransform(): -# ''' Type for simple XML to HTML transforms. Examples: -# * Math tags, which go from LON-CAPA-style m-tags to MathJAX -# ''' -# xml_tags = {} ## Maps tags to functions - -# @classmethod -# def get_xml_tags(c): -# return c.xml_tags.keys() + @classmethod + def get_xml_tags(c): + return c.xml_tags.keys() -# @classmethod -# def get_uses(c): -# return ['capa_transform'] - -# def get_html(self): -# return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg) - -# def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): -# self.xml = xml -# self.tag = xml.tag -# if not state: -# state = {} -# if item_id: -# self.id = item_id -# if xml.get('id'): -# self.id = xml.get('id') -# if 'id' in state: -# self.id = state['id'] -# self.system = system - -# self.value = '' -# if 'value' in state: -# self.value = state['value'] - -# self.msg = '' -# if 'feedback' in state and 'message' in state['feedback']: -# self.msg = state['feedback']['message'] - -# self.status = 'unanswered' -# if 'status' in state: -# self.status = state['status'] + @classmethod + def get_uses(c): + return ['capa_input', 'capa_transform'] + def get_html(self): + return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg) def register_render_function(fn, names=None, cls=SimpleInput): if names is None: @@ -136,9 +94,6 @@ def register_render_function(fn, names=None, cls=SimpleInput): return fn return wrapped - - - #----------------------------------------------------------------------------- @register_render_function @@ -201,16 +156,16 @@ def choicegroup(element, value, status, render_template, msg=''): return etree.XML(html) @register_render_function -def textline(element, value, state, render_template, msg=""): +def textline(element, value, status, render_template, msg=""): ''' Simple text line input, with optional size specification. ''' if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x - return SimpleInput.xml_tags['textline_dynamath'](element,value,state,render_template,msg) + return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg) eid=element.get('id') count = int(eid.split('_')[-2])-1 # HACK size = element.get('size') - context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size, 'msg': msg} + context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg} html = render_template("textinput.html", context) return etree.XML(html) diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index 1c09493b03..bfd42814f7 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -63,7 +63,8 @@ class GenericResponse(object): - get_max_score : if defined, this is called to obtain the maximum score possible for this question - setup_response : find and note the answer input field IDs for the response; called by __init__ - - __unicode__ : unicode representation of this Response + - render_html : render this Response as HTML (must return XHTML compliant string) + - __unicode__ : unicode representation of this Response Each response type may also specify the following attributes: @@ -114,9 +115,30 @@ class GenericResponse(object): if self.max_inputfields==1: self.answer_id = self.answer_ids[0] # for convenience + self.default_answer_map = {} # dict for default answer map (provided in input elements) + for entry in self.inputfields: + answer = entry.get('correct_answer') + if answer: + self.default_answer_map[entry.get('id')] = contextualize_text(answer, self.context) + if hasattr(self,'setup_response'): self.setup_response() + def render_html(self,renderer): + ''' + Return XHTML Element tree representation of this Response. + + Arguments: + + - renderer : procedure which produces HTML given an ElementTree + ''' + tree = etree.Element('span') # render ourself as a + our content + for item in self.xml: + item_xhtml = renderer(item) # call provided procedure to do the rendering + if item_xhtml is not None: tree.append(item_xhtml) + tree.tail = self.xml.tail + return tree + @abc.abstractmethod def get_score(self, student_answers): ''' @@ -132,7 +154,6 @@ class GenericResponse(object): ''' pass - #not an abstract method because plenty of responses will not want to preprocess anything, and we should not require that they override this method. def setup_response(self): pass @@ -485,17 +506,17 @@ def sympy_check2(): ''' Give correct answer expected for this response. - capa_problem handles correct_answers from entry objects like textline, and that - is what should be used when this response has multiple entry objects. + use default_answer_map from entry elements (eg textline), + when this response has multiple entry objects. but for simplicity, if an "expect" attribute was given by the content author - ie then return it now. + ie then that. ''' if len(self.answer_ids)>1: - return {} + return self.default_answer_map if self.expect: return {self.answer_ids[0] : self.expect} - return {} + return self.default_answer_map #----------------------------------------------------------------------------- @@ -797,9 +818,8 @@ class SchematicResponse(GenericResponse): return zip(sorted(self.answer_ids), self.context['correct']) def get_answers(self): - # Since this is explicitly specified in the problem, this will - # be handled by capa_problem - return {} + # use answers provided in input elements + return self.default_answer_map #----------------------------------------------------------------------------- From c724affe316654e031fc66f8f1ebccc6a7f2df90 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sat, 9 Jun 2012 23:29:08 -0400 Subject: [PATCH 008/252] third pass in capa cleanup: correct_map -> CorrectMap - added correctmap.py with CorrectMap class - messages subsumed into CorrectMap - response get_score called with old CorrectMap so hints based on history are possible --- common/lib/capa/capa_problem.py | 83 +++++++++++++++------------- common/lib/capa/correctmap.py | 80 +++++++++++++++++++++++++++ common/lib/capa/responsetypes.py | 92 +++++++++++++++++-------------- common/lib/xmodule/capa_module.py | 25 +++------ 4 files changed, 183 insertions(+), 97 deletions(-) create mode 100644 common/lib/capa/correctmap.py diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index c63c13d420..93d5620aae 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -25,15 +25,14 @@ import struct from lxml import etree from xml.sax.saxutils import unescape -from util import contextualize_text -import inputtypes - -from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse - import calc +from correctmap import CorrectMap import eia +import inputtypes +from util import contextualize_text -log = logging.getLogger(__name__) +# to be replaced with auto-registering +from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse # dict of tagname, Response Class -- this should come from auto-registering response_types = {'numericalresponse': NumericalResponse, @@ -68,6 +67,12 @@ global_context = {'random': random, # These should be removed from HTML output, including all subelements html_problem_semantics = ["responseparam", "answer", "script"] +#log = logging.getLogger(__name__) +log = logging.getLogger('mitx.common.lib.capa.capa_problem') + +#----------------------------------------------------------------------------- +# main class for this module + class LoncapaProblem(object): ''' Main class for capa Problems. @@ -89,9 +94,7 @@ class LoncapaProblem(object): ''' ## Initialize class variables from state - self.student_answers = dict() - self.correct_map = dict() - self.done = False + self.do_reset() self.problem_id = id self.system = system self.seed = seed @@ -102,7 +105,7 @@ class LoncapaProblem(object): if 'student_answers' in state: self.student_answers = state['student_answers'] if 'correct_map' in state: - self.correct_map = state['correct_map'] + self.correct_map.set_dict(state['correct_map']) if 'done' in state: self.done = state['done'] @@ -125,7 +128,15 @@ class LoncapaProblem(object): # pre-parse the XML tree: modifies it to add ID's and perform some in-place transformations # this also creates the dict (self.responders) of Response instances for each question in the problem. # the dict has keys = xml subtree of Response, values = Response instance - self.preprocess_problem(self.tree, correct_map=self.correct_map, answer_map=self.student_answers) + self.preprocess_problem(self.tree, answer_map=self.student_answers) + + def do_reset(self): + ''' + Reset internal state to unfinished, with no answers + ''' + self.student_answers = dict() + self.correct_map = CorrectMap() + self.done = False def __unicode__(self): return u"LoncapaProblem ({0})".format(self.fileobject) @@ -134,9 +145,10 @@ class LoncapaProblem(object): ''' Stored per-user session data neeeded to: 1) Recreate the problem 2) Populate any student answers. ''' + return {'seed': self.seed, 'student_answers': self.student_answers, - 'correct_map': self.correct_map, + 'correct_map': self.correct_map.get_dict(), 'done': self.done} def get_max_score(self): @@ -170,8 +182,12 @@ class LoncapaProblem(object): ''' correct = 0 for key in self.correct_map: - if self.correct_map[key] == u'correct': - correct += 1 + try: + correct += self.correct_map.get_npoints(key) + except Exception,err: + log.error('key=%s, correct_map = %s' % (key,self.correct_map)) + raise + if (not self.student_answers) or len(self.student_answers) == 0: return {'score': 0, 'total': self.get_max_score()} @@ -190,12 +206,14 @@ class LoncapaProblem(object): Calles the Response for each question in this problem, to do the actual grading. ''' self.student_answers = answers - self.correct_map = dict() - log.info('%s: in grade_answers, answers=%s' % (self,answers)) + oldcmap = self.correct_map # old CorrectMap + newcmap = CorrectMap() # start new with empty CorrectMap for responder in self.responders.values(): - results = responder.get_score(answers) # call the responsetype instance to do the actual grading - self.correct_map.update(results) - return self.correct_map + results = responder.get_score(answers,oldcmap) # call the responsetype instance to do the actual grading + newcmap.update(results) + self.correct_map = newcmap + log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap)) + return newcmap def get_question_answers(self): """Returns a dict of answer_ids to answer values. If we cannot generate @@ -282,27 +300,17 @@ class LoncapaProblem(object): # but it will turn into a dict containing both the answer and any associated message # for the problem ID for the input element. status = "unsubmitted" + msg = '' if problemid in self.correct_map: - status = self.correct_map[problemtree.get('id')] + pid = problemtree.get('id') + status = self.correct_map.get_correctness(pid) + msg = self.correct_map.get_msg(pid) value = "" if self.student_answers and problemid in self.student_answers: value = self.student_answers[problemid] - #### This code is a hack. It was merged to help bring two branches - #### in sync, but should be replaced. msg should be passed in a - #### response_type - # prepare the response message, if it exists in correct_map - if 'msg' in self.correct_map: - msg = self.correct_map['msg'] - elif ('msg_%s' % problemid) in self.correct_map: - msg = self.correct_map['msg_%s' % problemid] - else: - msg = '' - # do the rendering - # This should be broken out into a helper function - # that handles all input objects render_object = inputtypes.SimpleInput(system=self.system, xml=problemtree, state={'value': value, @@ -333,7 +341,7 @@ class LoncapaProblem(object): return tree - def preprocess_problem(self, tree, correct_map=dict(), answer_map=dict()): # private + def preprocess_problem(self, tree, answer_map=dict()): # private ''' Assign IDs to all the responses Assign sub-IDs to all entries (textline, schematic, etc.) @@ -346,11 +354,8 @@ class LoncapaProblem(object): self.responders = {} for response in tree.xpath('//' + "|//".join(response_types)): response_id_str = self.problem_id + "_" + str(response_id) - response.attrib['id'] = response_id_str # create and save ID for this response - - # if response_id not in correct_map: correct = 'unsubmitted' # unused - to be removed - # response.attrib['state'] = correct - response_id += response_id + response.set('id',response_id_str) # create and save ID for this response + response_id += 1 answer_id = 1 inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x for x in (entry_types + solution_types)]), diff --git a/common/lib/capa/correctmap.py b/common/lib/capa/correctmap.py new file mode 100644 index 0000000000..3eac98cc3a --- /dev/null +++ b/common/lib/capa/correctmap.py @@ -0,0 +1,80 @@ +#----------------------------------------------------------------------------- +# class used to store graded responses to CAPA questions +# +# Used by responsetypes and capa_problem + +class CorrectMap(object): + ''' + Stores (correctness, npoints, msg) for each answer_id. + Behaves as a dict. + ''' + cmap = {} + + def __init__(self,*args,**kwargs): + self.set(*args,**kwargs) + + def set(self,answer_id=None,correctness=None,npoints=None,msg=''): + if answer_id is not None: + self.cmap[answer_id] = {'correctness': correctness, + 'npoints': npoints, + 'msg': msg } + + def __repr__(self): + return repr(self.cmap) + + def get_dict(self): + ''' + return dict version of self + ''' + return self.cmap + + def set_dict(self,correct_map): + ''' + set internal dict to provided correct_map dict + for graceful migration, if correct_map is a one-level dict, then convert it to the new + dict of dicts format. + ''' + if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict): + for k in self.cmap.keys(): self.cmap.pop(k) # empty current dict + for k in correct_map: self.set(k,correct_map[k]) # create new dict entries + else: + self.cmap = correct_map + + def is_correct(self,answer_id): + if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct' + return None + + def get_npoints(self,answer_id): + if self.is_correct(answer_id): + npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct + return npoints or 1 + return 0 # if not correct, return 0 + + def set_property(self,answer_id,property,value): + if answer_id in self.cmap: self.cmap[answer_id][property] = value + else: self.cmap[answer_id] = {property:value} + + def get_property(self,answer_id,property,default=None): + if answer_id in self.cmap: return self.cmap[answer_id].get(property,default) + return default + + def get_correctness(self,answer_id): + return self.get_property(answer_id,'correctness') + + def get_msg(self,answer_id): + return self.get_property(answer_id,'msg','') + + def update(self,other_cmap): + ''' + Update this CorrectMap with the contents of another CorrectMap + ''' + if not isinstance(other_cmap,CorrectMap): + raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap) + self.cmap.update(other_cmap.get_dict()) + + __getitem__ = cmap.__getitem__ + __iter__ = cmap.__iter__ + items = cmap.items + keys = cmap.keys + + diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index bfd42814f7..2de9e27893 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -21,6 +21,7 @@ import abc # specific library imports from calc import evaluator, UndefinedVariable +from correctmap import CorrectMap from util import * from lxml import etree from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? @@ -53,7 +54,7 @@ class StudentInputError(Exception): class GenericResponse(object): ''' Base class for CAPA responsetypes. Each response type (ie a capa question, - which is part of a capa problem) is represented as a superclass, + which is part of a capa problem) is represented as a subclass, which should provide the following methods: - get_score : evaluate the given student answers, and return a CorrectMap @@ -140,10 +141,16 @@ class GenericResponse(object): return tree @abc.abstractmethod - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): ''' Return a CorrectMap for the answers expected vs given. This includes (correctness, npoints, msg) for each answer_id. + + Arguments: + + - student_answers : dict of (answer_id,answer) where answer = student input (string) + - old_cmap : previous CorrectMap (may be empty); useful for analyzing or recording history of responses + ''' pass @@ -201,15 +208,15 @@ class MultipleChoiceResponse(GenericResponse): else: choice.set("name", "choice_"+choice.get("name")) - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): ''' grade student response. ''' # log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices)) if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices: - return {self.answer_id:'correct'} + return CorrectMap(self.answer_id,'correct') else: - return {self.answer_id:'incorrect'} + return CorrectMap(self.answer_id,'incorrect') def get_answers(self): return {self.answer_id:self.correct_choices} @@ -226,14 +233,14 @@ class TrueFalseResponse(MultipleChoiceResponse): else: choice.set("name", "choice_"+choice.get("name")) - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): correct = set(self.correct_choices) answers = set(student_answers.get(self.answer_id, [])) if correct == answers: - return { self.answer_id : 'correct'} + return CorrectMap( self.answer_id , 'correct') - return {self.answer_id : 'incorrect'} + return CorrectMap(self.answer_id ,'incorrect') #----------------------------------------------------------------------------- @@ -251,15 +258,15 @@ class OptionResponse(GenericResponse): def setup_response(self): self.answer_fields = self.inputfields - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): # log.debug('%s: student_answers=%s' % (unicode(self),student_answers)) - cmap = {} + cmap = CorrectMap() amap = self.get_answers() for aid in amap: if aid in student_answers and student_answers[aid]==amap[aid]: - cmap[aid] = 'correct' + cmap.set(aid,'correct') else: - cmap[aid] = 'incorrect' + cmap.set(aid,'incorrect') return cmap def get_answers(self): @@ -291,7 +298,7 @@ class NumericalResponse(GenericResponse): except Exception: self.answer_id = None - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): '''Grade a numeric response ''' student_answer = student_answers[self.answer_id] try: @@ -303,9 +310,9 @@ class NumericalResponse(GenericResponse): raise StudentInputError('Invalid input -- please use a number only') if correct: - return {self.answer_id:'correct'} + return CorrectMap(self.answer_id,'correct') else: - return {self.answer_id:'incorrect'} + return CorrectMap(self.answer_id,'incorrect') def get_answers(self): return {self.answer_id:self.correct_answer} @@ -395,7 +402,7 @@ def sympy_check2(): else: self.code = answer.text - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): ''' student_answers is a dict with everything from request.POST, but with the first part of each key removed (the string before the first "_"). @@ -495,12 +502,10 @@ def sympy_check2(): correct = ['correct']*len(idset) if ret else ['incorrect']*len(idset) # build map giving "correct"ness of the answer(s) - #correct_map = dict(zip(idset, self.context['correct'])) - correct_map = {} + correct_map = CorrectMap() for k in range(len(idset)): - correct_map[idset[k]] = correct[k] - correct_map['msg_%s' % idset[k]] = messages[k] - return correct_map + correct_map.set(idset[k], correct[k], msg=messages[k]) + return correct_map def get_answers(self): ''' @@ -642,9 +647,11 @@ main() return rxml - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): + idset = sorted(self.answer_ids) + cmap = CorrectMap() try: - submission = [student_answers[k] for k in sorted(self.answer_ids)] + submission = [student_answers[k] for k in idset] except Exception,err: log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers)) raise Exception,err @@ -658,9 +665,9 @@ main() except Exception, err: log.error('Error %s' % err) if self.system.DEBUG: - correct_map = dict(zip(sorted(self.answer_ids), ['incorrect'] * len(self.answer_ids) )) - correct_map['msg_%s' % self.answer_ids[0]] = '%s' % str(err).replace('<','<') - return correct_map + cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset) ))) + cmap.set_property(self.answer_ids[0],'msg','%s' % str(err).replace('<','<')) + return cmap ad = rxml.find('awarddetail').text admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses @@ -670,13 +677,13 @@ main() if ad in admap: self.context['correct'][0] = admap[ad] - # self.context['correct'] = ['correct','correct'] - correct_map = dict(zip(sorted(self.answer_ids), self.context['correct'])) - - # store message in correct_map - correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace(' ',' ') + # create CorrectMap + for key in idset: + idx = idset.index(key) + msg = rxml.find('message').text.replace(' ',' ') if idx==0 else None + cmap.set(key, self.context['correct'][idx], msg=msg) - return correct_map + return cmap def get_answers(self): ''' @@ -750,7 +757,7 @@ class FormulaResponse(GenericResponse): else: # Default self.case_sensitive = False - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): variables=self.samples.split('@')[0].split(',') numsamples=int(self.samples.split('@')[1].split('#')[1]) sranges=zip(*map(lambda x:map(float, x.split(",")), @@ -776,11 +783,11 @@ class FormulaResponse(GenericResponse): #traceback.print_exc() raise StudentInputError("Error in formula") if numpy.isnan(student_result) or numpy.isinf(student_result): - return {self.answer_id:"incorrect"} + return CorrectMap(self.answer_id, "incorrect") if not compare_with_tolerance(student_result, instructor_result, self.tolerance): - return {self.answer_id:"incorrect"} + return CorrectMap(self.answer_id, "incorrect") - return {self.answer_id:"correct"} + return CorrectMap(self.answer_id, "correct") def strip_dict(self, d): ''' Takes a dict. Returns an identical dict, with all non-word @@ -810,12 +817,13 @@ class SchematicResponse(GenericResponse): else: self.code = answer.text - def get_score(self, student_answers): + def get_score(self, student_answers, old_cmap): from capa_problem import global_context submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] self.context.update({'submission':submission}) exec self.code in global_context, self.context - return zip(sorted(self.answer_ids), self.context['correct']) + cmap = CorrectMap() + return cmap.set_dict(zip(sorted(self.answer_ids), self.context['correct'])) def get_answers(self): # use answers provided in input elements @@ -845,8 +853,8 @@ class ImageResponse(GenericResponse): self.ielements = self.inputfields self.answer_ids = [ie.get('id') for ie in self.ielements] - def get_score(self, student_answers): - correct_map = {} + def get_score(self, student_answers, old_cmap): + correct_map = CorrectMap() expectedset = self.get_answers() for aid in self.answer_ids: # loop through IDs of fields in our stanza @@ -869,9 +877,9 @@ class ImageResponse(GenericResponse): # answer is correct if (x,y) is within the specified rectangle if (llx <= gx <= urx) and (lly <= gy <= ury): - correct_map[aid] = 'correct' + correct_map.set(aid, 'correct') else: - correct_map[aid] = 'incorrect' + correct_map.set(aid, 'incorrect') return correct_map def get_answers(self): diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 6bd7cbebdc..439982a2c1 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -13,6 +13,7 @@ from lxml import etree from x_module import XModule, XModuleDescriptor from capa.capa_problem import LoncapaProblem from capa.responsetypes import StudentInputError + log = logging.getLogger("mitx.courseware") #----------------------------------------------------------------------------- @@ -365,18 +366,17 @@ class Module(XModule): self.attempts = self.attempts + 1 self.lcp.done=True - success = 'correct' - for i in correct_map: - if correct_map[i]!='correct': + success = 'correct' # success = correct if ALL questions in this problem are correct + for answer_id in correct_map: + if not correct_map.is_correct(answer_id): success = 'incorrect' - event_info['correct_map']=correct_map + event_info['correct_map']=correct_map.get_dict() # log this in the tracker event_info['success']=success - self.tracker('save_problem_check', event_info) try: - html = self.get_problem_html(encapsulate=False) + html = self.get_problem_html(encapsulate=False) # render problem into HTML except Exception,err: log.error('failed to generate html') raise Exception,err @@ -430,17 +430,10 @@ class Module(XModule): self.tracker('reset_problem_fail', event_info) return "Refresh the page and make an attempt before resetting." - self.lcp.done=False - self.lcp.answers=dict() - self.lcp.correct_map=dict() - self.lcp.student_answers = dict() - - + self.lcp.do_reset() # call method in LoncapaProblem to reset itself if self.rerandomize == "always": - self.lcp.context=dict() - self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid. - self.lcp.seed=None - + self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line) + self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system) event_info['new_state']=self.lcp.get_state() From 5ac13e03aa2bcfb6b798b2730f0e9b7d7c27ad8d Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 17:17:57 -0400 Subject: [PATCH 009/252] fourth pass in capa cleanup: - Added hints + hintmethod - hintgroup compatible with loncapa spec - also does hintfn for custom hints (can do answer history) - GenericResponse -> LoncapaResponse - moved response type tags into responsetype classes - capa_problem should use __future__ division - hints stored in CorrectMap, copied to 'feedback' in SimpleInput for display --- common/lib/capa/capa_problem.py | 40 +++--- common/lib/capa/correctmap.py | 32 ++++- common/lib/capa/inputtypes.py | 55 +++++--- common/lib/capa/responsetypes.py | 226 +++++++++++++++++++++++++------ common/lib/capa/util.py | 5 + 5 files changed, 272 insertions(+), 86 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index 93d5620aae..b14001ef03 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -12,6 +12,8 @@ Main module which shows problems (of "capa" type). This is used by capa_module. ''' +from __future__ import division + import copy import logging import math @@ -32,20 +34,10 @@ import inputtypes from util import contextualize_text # to be replaced with auto-registering -from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse +import responsetypes # dict of tagname, Response Class -- this should come from auto-registering -response_types = {'numericalresponse': NumericalResponse, - 'formularesponse': FormulaResponse, - 'customresponse': CustomResponse, - 'schematicresponse': SchematicResponse, - 'externalresponse': ExternalResponse, - 'multiplechoiceresponse': MultipleChoiceResponse, - 'truefalseresponse': TrueFalseResponse, - 'imageresponse': ImageResponse, - 'optionresponse': OptionResponse, - 'symbolicresponse': SymbolicResponse, - } +response_tag_dict = dict([(x.response_tag,x) for x in responsetypes.__all__]) entry_types = ['textline', 'schematic', 'choicegroup', 'textbox', 'imageinput', 'optioninput'] solution_types = ['solution'] # extra things displayed after "show answers" is pressed @@ -65,7 +57,7 @@ global_context = {'random': random, 'eia': eia} # These should be removed from HTML output, including all subelements -html_problem_semantics = ["responseparam", "answer", "script"] +html_problem_semantics = ["responseparam", "answer", "script","hintgroup"] #log = logging.getLogger(__name__) log = logging.getLogger('mitx.common.lib.capa.capa_problem') @@ -209,7 +201,7 @@ class LoncapaProblem(object): oldcmap = self.correct_map # old CorrectMap newcmap = CorrectMap() # start new with empty CorrectMap for responder in self.responders.values(): - results = responder.get_score(answers,oldcmap) # call the responsetype instance to do the actual grading + results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading newcmap.update(results) self.correct_map = newcmap log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap)) @@ -248,7 +240,8 @@ class LoncapaProblem(object): ''' return contextualize_text(etree.tostring(self.extract_html(self.tree)), self.context) - # ======= Private ======== + # ======= Private Methods Below ======== + def extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private ''' Extract content of from the problem.xml file, and exec it in the @@ -296,15 +289,17 @@ class LoncapaProblem(object): problemid = problemtree.get('id') # my ID if problemtree.tag in inputtypes.get_input_xml_tags(): - # status is currently the answer for the problem ID for the input element, - # but it will turn into a dict containing both the answer and any associated message - # for the problem ID for the input element. + status = "unsubmitted" msg = '' + hint = '' + hintmode = None if problemid in self.correct_map: pid = problemtree.get('id') status = self.correct_map.get_correctness(pid) msg = self.correct_map.get_msg(pid) + hint = self.correct_map.get_hint(pid) + hintmode = self.correct_map.get_hintmode(pid) value = "" if self.student_answers and problemid in self.student_answers: @@ -316,7 +311,10 @@ class LoncapaProblem(object): state={'value': value, 'status': status, 'id': problemtree.get('id'), - 'feedback': {'message': msg} + 'feedback': {'message': msg, + 'hint' : hint, + 'hintmode' : hintmode, + } }, use='capa_input') return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...) @@ -352,7 +350,7 @@ class LoncapaProblem(object): ''' response_id = 1 self.responders = {} - for response in tree.xpath('//' + "|//".join(response_types)): + for response in tree.xpath('//' + "|//".join(response_tag_dict)): response_id_str = self.problem_id + "_" + str(response_id) response.set('id',response_id_str) # create and save ID for this response response_id += 1 @@ -366,7 +364,7 @@ class LoncapaProblem(object): entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) answer_id = answer_id + 1 - responder = response_types[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response + responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response self.responders[response] = responder # save in list in self # ... may not be associated with any specific response; give IDs for those separately diff --git a/common/lib/capa/correctmap.py b/common/lib/capa/correctmap.py index 3eac98cc3a..f694391cc6 100644 --- a/common/lib/capa/correctmap.py +++ b/common/lib/capa/correctmap.py @@ -5,7 +5,16 @@ class CorrectMap(object): ''' - Stores (correctness, npoints, msg) for each answer_id. + Stores map between answer_id and response evaluation result for each question + in a capa problem. The response evaluation result for each answer_id includes + (correctness, npoints, msg, hint, hintmode). + + - correctness : either 'correct' or 'incorrect' + - npoints : None, or integer specifying number of points awarded for this answer_id + - msg : string (may have HTML) giving extra message response (displayed below textline or textbox) + - hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg) + - hintmode : one of (None,'on_request','always') criteria for displaying hint + Behaves as a dict. ''' cmap = {} @@ -13,11 +22,14 @@ class CorrectMap(object): def __init__(self,*args,**kwargs): self.set(*args,**kwargs) - def set(self,answer_id=None,correctness=None,npoints=None,msg=''): + def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None): if answer_id is not None: self.cmap[answer_id] = {'correctness': correctness, 'npoints': npoints, - 'msg': msg } + 'msg': msg, + 'hint' : hint, + 'hintmode' : hintmode, + } def __repr__(self): return repr(self.cmap) @@ -64,6 +76,20 @@ class CorrectMap(object): def get_msg(self,answer_id): return self.get_property(answer_id,'msg','') + def get_hint(self,answer_id): + return self.get_property(answer_id,'hint','') + + def get_hintmode(self,answer_id): + return self.get_property(answer_id,'hintmode',None) + + def set_hint_and_mode(self,answer_id,hint,hintmode): + ''' + - hint : (string) HTML text for hint + - hintmode : (string) mode for hint display ('always' or 'on_request') + ''' + self.set_property(answer_id,'hint',hint) + self.set_property(answer_id,'hintmode',hintmode) + def update(self,other_cmap): ''' Update this CorrectMap with the contents of another CorrectMap diff --git a/common/lib/capa/inputtypes.py b/common/lib/capa/inputtypes.py index 10fbdb7f98..1fa51f2f84 100644 --- a/common/lib/capa/inputtypes.py +++ b/common/lib/capa/inputtypes.py @@ -32,44 +32,57 @@ def get_input_xml_tags(): return SimpleInput.get_xml_tags() class SimpleInput():# XModule - ''' Type for simple inputs -- plain HTML with a form element - - State is a dictionary with optional keys: - * Value - * ID - * Status (answered, unanswered, unsubmitted) - * Feedback (dictionary containing keys for hints, errors, or other - feedback from previous attempt) - + ''' + Type for simple inputs -- plain HTML with a form element ''' xml_tags = {} ## Maps tags to functions def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'): + ''' + Instantiate a SimpleInput class. Arguments: + + - system : I4xSystem instance which provides OS, rendering, and user context + - xml : Element tree of this Input element + - item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string + - track_url : URL used for tracking - string + - state : a dictionary with optional keys: + * Value + * ID + * Status (answered, unanswered, unsubmitted) + * Feedback (dictionary containing keys for hints, errors, or other + feedback from previous attempt) + - use : + ''' + self.xml = xml self.tag = xml.tag - if not state: - state = {} + self.system = system + if not state: state = {} + ## ID should only come from one place. ## If it comes from multiple, we use state first, XML second, and parameter ## third. Since we don't make this guarantee, we can swap this around in ## the future if there's a more logical order. - if item_id: - self.id = item_id - if xml.get('id'): - self.id = xml.get('id') - if 'id' in state: - self.id = state['id'] - self.system = system + if item_id: self.id = item_id + if xml.get('id'): self.id = xml.get('id') + if 'id' in state: self.id = state['id'] self.value = '' if 'value' in state: self.value = state['value'] self.msg = '' - if 'feedback' in state and 'message' in state['feedback']: - self.msg = state['feedback']['message'] - + feedback = state.get('feedback') + if feedback is not None: + self.msg = feedback.get('message','') + self.hint = feedback.get('hint','') + self.hintmode = feedback.get('hintmode',None) + + # put hint above msg if to be displayed + if self.hintmode == 'always': + self.msg = self.hint + ('
' if self.msg else '') + self.msg + self.status = 'unanswered' if 'status' in state: self.status = state['status'] diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index 2de9e27893..5a5296d805 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -51,7 +51,7 @@ class StudentInputError(Exception): # # Main base class for CAPA responsetypes -class GenericResponse(object): +class LoncapaResponse(object): ''' Base class for CAPA responsetypes. Each response type (ie a capa question, which is part of a capa problem) is represented as a subclass, @@ -60,22 +60,31 @@ class GenericResponse(object): - get_score : evaluate the given student answers, and return a CorrectMap - get_answers : provide a dict of the expected answers for this problem + Each subclass must also define the following attributes: + + - response_tag : xhtml tag identifying this response (used in auto-registering) + In addition, these methods are optional: - - get_max_score : if defined, this is called to obtain the maximum score possible for this question - - setup_response : find and note the answer input field IDs for the response; called by __init__ - - render_html : render this Response as HTML (must return XHTML compliant string) - - __unicode__ : unicode representation of this Response + - get_max_score : if defined, this is called to obtain the maximum score possible for this question + - setup_response : find and note the answer input field IDs for the response; called by __init__ + - check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed + - render_html : render this Response as HTML (must return XHTML compliant string) + - __unicode__ : unicode representation of this Response Each response type may also specify the following attributes: - - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) - - allowed_inputfields : list of allowed input fields (each a string) for this Response - - required_attributes : list of required attributes (each a string) on the main response XML stanza + - max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None) + - allowed_inputfields : list of allowed input fields (each a string) for this Response + - required_attributes : list of required attributes (each a string) on the main response XML stanza + - hint_tag : xhtml tag identifying hint associated with this response inside hintgroup ''' __metaclass__=abc.ABCMeta # abc = Abstract Base Class + response_tag = None + hint_tag = None + max_inputfields = None allowed_inputfields = [] required_attributes = [] @@ -85,7 +94,7 @@ class GenericResponse(object): Init is passed the following arguments: - xml : ElementTree of this Response - - inputfields : list of ElementTrees for each input entry field in this Response + - inputfields : ordered list of ElementTrees for each input entry field in this Response - context : script processor context - system : I4xSystem instance which provides OS, rendering, and user context @@ -112,7 +121,7 @@ class GenericResponse(object): msg += "\nSee XML source line %s" % getattr(xml,'sourceline','') raise LoncapaProblemError(msg) - self.answer_ids = [x.get('id') for x in self.inputfields] + self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response if self.max_inputfields==1: self.answer_id = self.answer_ids[0] # for convenience @@ -140,8 +149,85 @@ class GenericResponse(object): tree.tail = self.xml.tail return tree + def evaluate_answers(self,student_answers,old_cmap): + ''' + Called by capa_problem.LoncapaProblem to evaluate student answers, and to + generate hints (if any). + + Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id. + ''' + new_cmap = self.get_score(student_answers) + self.get_hints(student_answers, new_cmap, old_cmap) + return new_cmap + + def get_hints(self, student_answers, new_cmap, old_cmap): + ''' + Generate adaptive hints for this problem based on student answers, the old CorrectMap, + and the new CorrectMap produced by get_score. + + Does not return anything. + + Modifies new_cmap, by adding hints to answer_id entries as appropriate. + ''' + hintgroup = self.xml.find('hintgroup') + if hintgroup is None: return + + # hint specified by function? + hintfn = hintgroup.get('hintfn') + if hintfn: + ''' + Hint is determined by a function defined in the @@ -358,6 +463,7 @@ def sympy_check2():
'''}] + response_tag = 'customresponse' allowed_inputfields = ['textline','textbox'] def setup_response(self): @@ -402,7 +508,7 @@ def sympy_check2(): else: self.code = answer.text - def get_score(self, student_answers, old_cmap): + def get_score(self, student_answers): ''' student_answers is a dict with everything from request.POST, but with the first part of each key removed (the string before the first "_"). @@ -540,6 +646,8 @@ class SymbolicResponse(CustomResponse): '''}] + response_tag = 'symbolicresponse' + def setup_response(self): self.xml.set('cfn','symmath_check') code = "from symmath import *" @@ -548,7 +656,7 @@ class SymbolicResponse(CustomResponse): #----------------------------------------------------------------------------- -class ExternalResponse(GenericResponse): +class ExternalResponse(LoncapaResponse): ''' Grade the students input using an external server. @@ -594,6 +702,7 @@ main() '''}] + response_tag = 'externalresponse' allowed_inputfields = ['textline','textbox'] def setup_response(self): @@ -647,7 +756,7 @@ main() return rxml - def get_score(self, student_answers, old_cmap): + def get_score(self, student_answers): idset = sorted(self.answer_ids) cmap = CorrectMap() try: @@ -707,7 +816,7 @@ main() #----------------------------------------------------------------------------- -class FormulaResponse(GenericResponse): +class FormulaResponse(LoncapaResponse): ''' Checking of symbolic math response using numerical sampling. ''' @@ -729,6 +838,8 @@ class FormulaResponse(GenericResponse): '''}] + response_tag = 'formularesponse' + hint_tag = 'formulahint' allowed_inputfields = ['textline'] required_attributes = ['answer'] max_inputfields = 1 @@ -743,7 +854,7 @@ class FormulaResponse(GenericResponse): id=xml.get('id'))[0] self.tolerance = contextualize_text(self.tolerance_xml, context) except Exception: - self.tolerance = 0 + self.tolerance = '0.00001' ts = xml.get('type') if ts is None: @@ -757,11 +868,16 @@ class FormulaResponse(GenericResponse): else: # Default self.case_sensitive = False - def get_score(self, student_answers, old_cmap): - variables=self.samples.split('@')[0].split(',') - numsamples=int(self.samples.split('@')[1].split('#')[1]) + def get_score(self, student_answers): + given = student_answers[self.answer_id] + correctness = self.check_formula(self.correct_answer, given, self.samples) + return CorrectMap(self.answer_id, correctness) + + def check_formula(self,expected, given, samples): + variables=samples.split('@')[0].split(',') + numsamples=int(samples.split('@')[1].split('#')[1]) sranges=zip(*map(lambda x:map(float, x.split(",")), - self.samples.split('@')[1].split('#')[0].split(':'))) + samples.split('@')[1].split('#')[0].split(':'))) ranges=dict(zip(variables, sranges)) for i in range(numsamples): @@ -771,23 +887,26 @@ class FormulaResponse(GenericResponse): value = random.uniform(*ranges[var]) instructor_variables[str(var)] = value student_variables[str(var)] = value - instructor_result = evaluator(instructor_variables,dict(),self.correct_answer, cs = self.case_sensitive) + #log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected)) + instructor_result = evaluator(instructor_variables,dict(),expected, cs = self.case_sensitive) try: - #print student_variables,dict(),student_answers[self.answer_id] - student_result = evaluator(student_variables,dict(), - student_answers[self.answer_id], + #log.debug('formula: student_vars=%s, given=%s' % (student_variables,given)) + student_result = evaluator(student_variables, + dict(), + given, cs = self.case_sensitive) except UndefinedVariable as uv: + log.debbug('formularesponse: undefined variable in given=%s' % given) raise StudentInputError(uv.message+" not permitted in answer") - except: + except Exception, err: #traceback.print_exc() + log.debug('formularesponse: error %s in formula' % err) raise StudentInputError("Error in formula") if numpy.isnan(student_result) or numpy.isinf(student_result): - return CorrectMap(self.answer_id, "incorrect") + return "incorrect" if not compare_with_tolerance(student_result, instructor_result, self.tolerance): - return CorrectMap(self.answer_id, "incorrect") - - return CorrectMap(self.answer_id, "correct") + return "incorrect" + return "correct" def strip_dict(self, d): ''' Takes a dict. Returns an identical dict, with all non-word @@ -799,13 +918,30 @@ class FormulaResponse(GenericResponse): isinstance(d[k], numbers.Number)]) return d + def check_hint_condition(self,hxml_set,student_answers): + given = student_answers[self.answer_id] + hints_to_show = [] + for hxml in hxml_set: + samples = hxml.get('samples') + name = hxml.get('name') + correct_answer = contextualize_text(hxml.get('answer'),self.context) + try: + correctness = self.check_formula(correct_answer, given, samples) + except Exception,err: + correctness = 'incorrect' + if correctness=='correct': + hints_to_show.append(name) + log.debug('hints_to_show = %s' % hints_to_show) + return hints_to_show + def get_answers(self): return {self.answer_id:self.correct_answer} #----------------------------------------------------------------------------- -class SchematicResponse(GenericResponse): +class SchematicResponse(LoncapaResponse): + response_tag = 'schematicresponse' allowed_inputfields = ['schematic'] def setup_response(self): @@ -817,7 +953,7 @@ class SchematicResponse(GenericResponse): else: self.code = answer.text - def get_score(self, student_answers, old_cmap): + def get_score(self, student_answers): from capa_problem import global_context submission = [json.loads(student_answers[k]) for k in sorted(self.answer_ids)] self.context.update({'submission':submission}) @@ -831,7 +967,7 @@ class SchematicResponse(GenericResponse): #----------------------------------------------------------------------------- -class ImageResponse(GenericResponse): +class ImageResponse(LoncapaResponse): """ Handle student response for image input: the input is a click on an image, which produces an [x,y] coordinate pair. The click is correct if it falls @@ -847,13 +983,14 @@ class ImageResponse(GenericResponse): '''}] + response_tag = 'imageresponse' allowed_inputfields = ['imageinput'] def setup_response(self): self.ielements = self.inputfields self.answer_ids = [ie.get('id') for ie in self.ielements] - def get_score(self, student_answers, old_cmap): + def get_score(self, student_answers): correct_map = CorrectMap() expectedset = self.get_answers() @@ -884,3 +1021,10 @@ class ImageResponse(GenericResponse): def get_answers(self): return dict([(ie.get('id'),ie.get('rectangle')) for ie in self.ielements]) + +#----------------------------------------------------------------------------- +# TEMPORARY: List of all response subclasses +# FIXME: To be replaced by auto-registration + +__all__ = [ NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse ] + diff --git a/common/lib/capa/util.py b/common/lib/capa/util.py index 996f6c8dac..f1cc8f859e 100644 --- a/common/lib/capa/util.py +++ b/common/lib/capa/util.py @@ -7,6 +7,11 @@ from calc import evaluator, UndefinedVariable def compare_with_tolerance(v1, v2, tol): ''' Compare v1 to v2 with maximum tolerance tol tol is relative if it ends in %; otherwise, it is absolute + + - v1 : student result (number) + - v2 : instructor result (number) + - tol : tolerance (string or number) + ''' relative = "%" in tol if relative: From 5eda2f3a63bd6fe5edbb2772af94ecd435ed6cbb Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 18:41:54 -0400 Subject: [PATCH 010/252] bugfixes - correctmap should reinit self.cmap on init --- common/lib/capa/capa_problem.py | 1 + common/lib/capa/correctmap.py | 16 +++++++++------- common/lib/capa/responsetypes.py | 5 +++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index b14001ef03..a341f954bc 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -200,6 +200,7 @@ class LoncapaProblem(object): self.student_answers = answers oldcmap = self.correct_map # old CorrectMap newcmap = CorrectMap() # start new with empty CorrectMap + log.debug('Responders: %s' % self.responders) for responder in self.responders.values(): results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading newcmap.update(results) diff --git a/common/lib/capa/correctmap.py b/common/lib/capa/correctmap.py index f694391cc6..ec3fed54c2 100644 --- a/common/lib/capa/correctmap.py +++ b/common/lib/capa/correctmap.py @@ -17,11 +17,17 @@ class CorrectMap(object): Behaves as a dict. ''' - cmap = {} - def __init__(self,*args,**kwargs): + self.cmap = dict() # start with empty dict + self.__getitem__ = self.cmap.__getitem__ + self.__iter__ = self.cmap.__iter__ + self.items = self.cmap.items + self.keys = self.cmap.keys self.set(*args,**kwargs) + def __iter__(self): + return self.cmap.__iter__() + def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None): if answer_id is not None: self.cmap[answer_id] = {'correctness': correctness, @@ -47,7 +53,7 @@ class CorrectMap(object): dict of dicts format. ''' if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict): - for k in self.cmap.keys(): self.cmap.pop(k) # empty current dict + self.__init__() # empty current dict for k in correct_map: self.set(k,correct_map[k]) # create new dict entries else: self.cmap = correct_map @@ -98,9 +104,5 @@ class CorrectMap(object): raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap) self.cmap.update(other_cmap.get_dict()) - __getitem__ = cmap.__getitem__ - __iter__ = cmap.__iter__ - items = cmap.items - keys = cmap.keys diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index 5a5296d805..a63cb991cf 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -158,6 +158,7 @@ class LoncapaResponse(object): ''' new_cmap = self.get_score(student_answers) self.get_hints(student_answers, new_cmap, old_cmap) + # log.debug('new_cmap = %s' % new_cmap) return new_cmap def get_hints(self, student_answers, new_cmap, old_cmap): @@ -492,7 +493,7 @@ def sympy_check2(): if cfn in self.context: self.code = self.context[cfn] else: - msg = "%s: can't find cfn in context = %s" % (unicode(self),self.context) + msg = "%s: can't find cfn %s in context" % (unicode(self),cfn) msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','') raise LoncapaProblemError(msg) @@ -896,7 +897,7 @@ class FormulaResponse(LoncapaResponse): given, cs = self.case_sensitive) except UndefinedVariable as uv: - log.debbug('formularesponse: undefined variable in given=%s' % given) + log.debug('formularesponse: undefined variable in given=%s' % given) raise StudentInputError(uv.message+" not permitted in answer") except Exception, err: #traceback.print_exc() From 58e359e7ec80e0e8e4609070ddc2393725a1d5d3 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 20:04:30 -0400 Subject: [PATCH 011/252] problem.coffee change: show answer -> also show solution_* & do mathjax typeset --- lms/static/coffee/src/modules/problem.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index e1e062e949..1f17d01405 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -48,11 +48,15 @@ class @Problem @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' else - @$("#answer_#{key}").html(value) // needs to be html, not text, for complex solutions (eg coding) + @$("#answer_#{key}").html(value) # needs to be html, not text, for complex solutions (eg coding) + @$("#solution_#{key}").html(value) # needs to be html, not text, for complex solutions (eg coding) + MathJax.Hub.Queue ["Typeset", MathJax.Hub] + MathJax.Hub.Queue ["Typeset", MathJax.Hub] @$('.show').val 'Hide Answer' @element.addClass 'showed' else @$('[id^=answer_]').text '' + @$('[id^=solution_]').text '' @$('[correct_answer]').attr correct_answer: null @element.removeClass 'showed' @$('.show').val 'Show Answer' From 989a74ba3f67bcb8b81bf93b99f4e0a71bffd633 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 20:05:33 -0400 Subject: [PATCH 012/252] django pipeline working now (with new pip -e git+git...) --- common/lib/capa/capa_problem.py | 1 + lms/envs/dev_ike.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index a341f954bc..16ecb9186a 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -224,6 +224,7 @@ class LoncapaProblem(object): answer = etree.tostring(entry) if answer: answer_map[entry.get('id')] = answer + log.debug('answer_map = %s' % answer_map) return answer_map def get_answer_ids(self): diff --git a/lms/envs/dev_ike.py b/lms/envs/dev_ike.py index ee5b6e831b..0930ef4651 100644 --- a/lms/envs/dev_ike.py +++ b/lms/envs/dev_ike.py @@ -27,7 +27,7 @@ DEBUG = True ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome) QUICKEDIT = True -MITX_FEATURES['USE_DJANGO_PIPELINE'] = False +# MITX_FEATURES['USE_DJANGO_PIPELINE'] = False COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x', 'title' : 'Circuits and Electronics', From 2af525f19c3489905d1f57e8fc77724aee1a423c Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 20:52:10 -0400 Subject: [PATCH 013/252] fixes to schematicresponse to work with new CorrectMap --- common/lib/capa/responsetypes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index a63cb991cf..947a66b13c 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -960,7 +960,8 @@ class SchematicResponse(LoncapaResponse): self.context.update({'submission':submission}) exec self.code in global_context, self.context cmap = CorrectMap() - return cmap.set_dict(zip(sorted(self.answer_ids), self.context['correct'])) + cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct']))) + return cmap def get_answers(self): # use answers provided in input elements From f4a3c54481c0d6ba7cca09d72dd09b66c6c6d6ec Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 21:05:21 -0400 Subject: [PATCH 014/252] fix xmodule/capa tests to use new CorrectMap --- common/lib/capa/capa_problem.py | 4 ++-- common/lib/xmodule/tests.py | 34 ++++++++++++++++----------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index 16ecb9186a..80ec8806f3 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -200,12 +200,12 @@ class LoncapaProblem(object): self.student_answers = answers oldcmap = self.correct_map # old CorrectMap newcmap = CorrectMap() # start new with empty CorrectMap - log.debug('Responders: %s' % self.responders) + # log.debug('Responders: %s' % self.responders) for responder in self.responders.values(): results = responder.evaluate_answers(answers,oldcmap) # call the responsetype instance to do the actual grading newcmap.update(results) self.correct_map = newcmap - log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap)) + # log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap)) return newcmap def get_question_answers(self): diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests.py index 69e69aa1d9..f8187269e9 100644 --- a/common/lib/xmodule/tests.py +++ b/common/lib/xmodule/tests.py @@ -1,9 +1,9 @@ # -# unittests for courseware +# unittests for xmodule (and capa) # # Note: run this using a like like this: # -# django-admin.py test --settings=envs.test_ike --pythonpath=. courseware +# django-admin.py test --settings=lms.envs.test_ike --pythonpath=. common/lib/xmodule import unittest import os @@ -96,31 +96,31 @@ class MultiChoiceTest(unittest.TestCase): multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml" test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) correct_answers = {'1_2_1':'choice_foil3'} - self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') + self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') false_answers = {'1_2_1':'choice_foil2'} - self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') def test_MC_bare_grades(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml" test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) correct_answers = {'1_2_1':'choice_2'} - self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') + self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') false_answers = {'1_2_1':'choice_1'} - self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') def test_TF_grade(self): truefalse_file = os.path.dirname(__file__)+"/test_files/truefalse.xml" test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1', system=i4xs) correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']} - self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') + self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') false_answers = {'1_2_1':['choice_foil1']} - self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') false_answers = {'1_2_1':['choice_foil1', 'choice_foil3']} - self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') false_answers = {'1_2_1':['choice_foil3']} - self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']} - self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') class ImageResponseTest(unittest.TestCase): def test_ir_grade(self): @@ -131,8 +131,8 @@ class ImageResponseTest(unittest.TestCase): test_answers = {'1_2_1':'[500,20]', '1_2_2':'[250,300]', } - self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') class SymbolicResponseTest(unittest.TestCase): def test_sr_grade(self): @@ -220,8 +220,8 @@ class SymbolicResponseTest(unittest.TestCase): ''', } - self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct') - self.assertEquals(test_lcp.grade_answers(wrong_answers)['1_2_1'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') + self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect') class OptionResponseTest(unittest.TestCase): ''' @@ -237,8 +237,8 @@ class OptionResponseTest(unittest.TestCase): test_answers = {'1_2_1':'True', '1_2_2':'True', } - self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') + self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') #----------------------------------------------------------------------------- # Grading tests From fdc4a14cf0a26a557e42342d0966d9feac604e08 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 21:11:04 -0400 Subject: [PATCH 015/252] fix i4xs in tests.py; move symbolicresponse.xml test back to where it should be --- .../xmodule/test_files/{test_files => }/symbolicresponse.xml | 0 common/lib/xmodule/tests.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename common/lib/xmodule/test_files/{test_files => }/symbolicresponse.xml (100%) diff --git a/common/lib/xmodule/test_files/test_files/symbolicresponse.xml b/common/lib/xmodule/test_files/symbolicresponse.xml similarity index 100% rename from common/lib/xmodule/test_files/test_files/symbolicresponse.xml rename to common/lib/xmodule/test_files/symbolicresponse.xml diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests.py index f8187269e9..00711ce0da 100644 --- a/common/lib/xmodule/tests.py +++ b/common/lib/xmodule/tests.py @@ -28,12 +28,13 @@ class I4xSystem(object): self.track_function = lambda x: None self.render_function = lambda x: {} # Probably incorrect self.exception404 = Exception + self.DEBUG = True def __repr__(self): return repr(self.__dict__) def __str__(self): return str(self.__dict__) -i4xs = I4xSystem +i4xs = I4xSystem() class ModelsTest(unittest.TestCase): def setUp(self): @@ -136,7 +137,7 @@ class ImageResponseTest(unittest.TestCase): class SymbolicResponseTest(unittest.TestCase): def test_sr_grade(self): - raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test + # raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml" test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file), '1', system=i4xs) correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]', From a7d0f8322d5727b3085b95f96e61db46e6428f20 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 21:11:43 -0400 Subject: [PATCH 016/252] SymbolicResponseTest works if snuggletex war running ; back to skipping it for now --- common/lib/xmodule/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests.py index 00711ce0da..1ba423983c 100644 --- a/common/lib/xmodule/tests.py +++ b/common/lib/xmodule/tests.py @@ -137,7 +137,7 @@ class ImageResponseTest(unittest.TestCase): class SymbolicResponseTest(unittest.TestCase): def test_sr_grade(self): - # raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test + raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml" test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file), '1', system=i4xs) correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]', From b48b33e65ecc22a22dfa03e211d0163943cb7562 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 21:27:11 -0400 Subject: [PATCH 017/252] add FormulaResponseWithHintTest test --- common/lib/xmodule/tests.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests.py index 1ba423983c..6f054b4bfb 100644 --- a/common/lib/xmodule/tests.py +++ b/common/lib/xmodule/tests.py @@ -241,6 +241,21 @@ class OptionResponseTest(unittest.TestCase): self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') +class FormulaResponseWithHintTest(unittest.TestCase): + ''' + Test Formula response problem with a hint + This problem also uses calc. + ''' + def test_or_grade(self): + problem_file = os.path.dirname(__file__)+"/test_files/formularesponse_with_hint.xml" + test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs) + correct_answers = {'1_2_1':'2.5*x-5.0'} + test_answers = {'1_2_1':'0.4*x-5.0'} + self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') + cmap = test_lcp.grade_answers(test_answers) + self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect') + self.assertTrue('You have inverted' in cmap.get_hint('1_2_1')) + #----------------------------------------------------------------------------- # Grading tests From 855112f8e744520f7b3470112ea50541ed0c83ea Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 21:59:29 -0400 Subject: [PATCH 018/252] added StringResponse (with hints) for hints, still to be done: numericalhint, optionhint no default hint processing done yet (ie hintmode = on_request) --- common/lib/capa/inputtypes.py | 4 ++++ common/lib/capa/responsetypes.py | 38 +++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/inputtypes.py b/common/lib/capa/inputtypes.py index 1fa51f2f84..75588e8aea 100644 --- a/common/lib/capa/inputtypes.py +++ b/common/lib/capa/inputtypes.py @@ -176,6 +176,10 @@ def textline(element, value, status, render_template, msg=""): if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x return SimpleInput.xml_tags['textline_dynamath'](element,value,status,render_template,msg) eid=element.get('id') + if eid is None: + msg = 'textline has no id: it probably appears outside of a known response type' + msg += "\nSee problem XML source line %s" % getattr(element,'sourceline','') + raise Exception(msg) count = int(eid.split('_')[-2])-1 # HACK size = element.get('size') context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg} diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index 947a66b13c..cf7310f92e 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -425,6 +425,42 @@ class NumericalResponse(LoncapaResponse): #----------------------------------------------------------------------------- +class StringResponse(LoncapaResponse): + + response_tag = 'stringresponse' + hint_tag = 'stringhint' + allowed_inputfields = ['textline'] + required_attributes = ['answer'] + max_inputfields = 1 + + def setup_response(self): + self.correct_answer = contextualize_text(self.xml.get('answer'), self.context).strip() + + def get_score(self, student_answers): + '''Grade a string response ''' + student_answer = student_answers[self.answer_id].strip() + correct = self.check_string(self.correct_answer,student_answer) + return CorrectMap(self.answer_id,'correct' if correct else 'incorrect') + + def check_string(self,expected,given): + if self.xml.get('type')=='ci': return given.lower() == expected.lower() + return given == expected + + def check_hint_condition(self,hxml_set,student_answers): + given = student_answers[self.answer_id].strip() + hints_to_show = [] + for hxml in hxml_set: + name = hxml.get('name') + correct_answer = contextualize_text(hxml.get('answer'),self.context).strip() + if self.check_string(correct_answer,given): hints_to_show.append(name) + log.debug('hints_to_show = %s' % hints_to_show) + return hints_to_show + + def get_answers(self): + return {self.answer_id:self.correct_answer} + +#----------------------------------------------------------------------------- + class CustomResponse(LoncapaResponse): ''' Custom response. The python code to be run should be in ... @@ -1028,5 +1064,5 @@ class ImageResponse(LoncapaResponse): # TEMPORARY: List of all response subclasses # FIXME: To be replaced by auto-registration -__all__ = [ NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse ] +__all__ = [ NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse ] From 4f6d9143c0fa30b70975b3de6dfcc364e404346f Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 22:06:13 -0400 Subject: [PATCH 019/252] added stringresponse_with_hint test --- .../test_files/formularesponse_with_hint.xml | 45 +++++++++++++++++++ .../test_files/stringresponse_with_hint.xml | 25 +++++++++++ common/lib/xmodule/tests.py | 14 ++++++ 3 files changed, 84 insertions(+) create mode 100644 common/lib/xmodule/test_files/formularesponse_with_hint.xml create mode 100644 common/lib/xmodule/test_files/stringresponse_with_hint.xml diff --git a/common/lib/xmodule/test_files/formularesponse_with_hint.xml b/common/lib/xmodule/test_files/formularesponse_with_hint.xml new file mode 100644 index 0000000000..e5b3e28708 --- /dev/null +++ b/common/lib/xmodule/test_files/formularesponse_with_hint.xml @@ -0,0 +1,45 @@ + + + + +

Hints can be provided to students, based on the last response given, as well as the history of responses given. Here is an example of a hint produced by a Formula Response problem.

+ +

+What is the equation of the line which passess through ($x1,$y1) and +($x2,$y2)?

+ +

The correct answer is $answer. A common error is to invert the equation for the slope. Enter +$wrongans to see a hint.

+ +
+ + + + y = + + + + + You have inverted the slope in the question. + + + +
+ diff --git a/common/lib/xmodule/test_files/stringresponse_with_hint.xml b/common/lib/xmodule/test_files/stringresponse_with_hint.xml new file mode 100644 index 0000000000..86efdf0f18 --- /dev/null +++ b/common/lib/xmodule/test_files/stringresponse_with_hint.xml @@ -0,0 +1,25 @@ + +

Example: String Response Problem

+
+
+ + Which US state has Lansing as its capital? + + + + + + + + + The state capital of Wisconsin is Madison. + + + The state capital of Minnesota is St. Paul. + + + The state you are looking for is also known as the 'Great Lakes State' + + + +
diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests.py index 6f054b4bfb..370b3befe5 100644 --- a/common/lib/xmodule/tests.py +++ b/common/lib/xmodule/tests.py @@ -256,6 +256,20 @@ class FormulaResponseWithHintTest(unittest.TestCase): self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect') self.assertTrue('You have inverted' in cmap.get_hint('1_2_1')) +class StringResponseWithHintTest(unittest.TestCase): + ''' + Test String response problem with a hint + ''' + def test_or_grade(self): + problem_file = os.path.dirname(__file__)+"/test_files/stringresponse_with_hint.xml" + test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs) + correct_answers = {'1_2_1':'Michigan'} + test_answers = {'1_2_1':'Minnesota'} + self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') + cmap = test_lcp.grade_answers(test_answers) + self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect') + self.assertTrue('St. Paul' in cmap.get_hint('1_2_1')) + #----------------------------------------------------------------------------- # Grading tests From 6d444de05bf767f11f0662d6a64614b9a83c4aeb Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 22:20:56 -0400 Subject: [PATCH 020/252] fix capa_problems pep8 --- common/lib/capa/capa_problem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index 80ec8806f3..eed6953521 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -146,7 +146,7 @@ class LoncapaProblem(object): def get_max_score(self): ''' Return maximum score for this problem. - We do this by counting the number of answers available for each question + We do this by counting the number of answers available for each question in the problem. If the Response for a question has a get_max_score() method then we call that and add its return value to the count. That can be used to give complex problems (eg programming questions) multiple points. @@ -351,7 +351,7 @@ class LoncapaProblem(object): Also create capa Response instances for each responsetype and save as self.responders ''' response_id = 1 - self.responders = {} + self.responders = {} for response in tree.xpath('//' + "|//".join(response_tag_dict)): response_id_str = self.problem_id + "_" + str(response_id) response.set('id',response_id_str) # create and save ID for this response @@ -367,7 +367,7 @@ class LoncapaProblem(object): answer_id = answer_id + 1 responder = response_tag_dict[response.tag](response, inputfields, self.context, self.system) # instantiate capa Response - self.responders[response] = responder # save in list in self + self.responders[response] = responder # save in list in self # ... may not be associated with any specific response; give IDs for those separately # TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i). From 6bcb40b52f79bb5f34091c66726d53f5c1793394 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 22:27:40 -0400 Subject: [PATCH 021/252] capa_problem and responsetypes pep8 and pyflakes --- common/lib/capa/capa_problem.py | 5 ++--- common/lib/capa/responsetypes.py | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index eed6953521..516c72494b 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -14,7 +14,6 @@ This is used by capa_module. from __future__ import division -import copy import logging import math import numpy @@ -156,7 +155,7 @@ class LoncapaProblem(object): if hasattr(responder,'get_max_score'): try: maxscore += responder.get_max_score() - except Exception, err: + except Exception: log.error('responder %s failed to properly return from get_max_score()' % responder) raise else: @@ -176,7 +175,7 @@ class LoncapaProblem(object): for key in self.correct_map: try: correct += self.correct_map.get_npoints(key) - except Exception,err: + except Exception: log.error('key=%s, correct_map = %s' % (key,self.correct_map)) raise diff --git a/common/lib/capa/responsetypes.py b/common/lib/capa/responsetypes.py index cf7310f92e..32c1c6783c 100644 --- a/common/lib/capa/responsetypes.py +++ b/common/lib/capa/responsetypes.py @@ -505,7 +505,6 @@ def sympy_check2(): def setup_response(self): xml = self.xml - context = self.context # if has an "expect" (or "answer") attribute then save that self.expect = xml.get('expect') or xml.get('answer') @@ -560,7 +559,7 @@ def sympy_check2(): msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers msg += '\n idset = %s, error = %s' % (idset,err) log.error(msg) - raise Exception,msg + raise Exception(msg) # global variable in context which holds the Presentation MathML from dynamic math input dynamath = [ student_answers.get(k+'_dynamath',None) for k in idset ] # ordered list of dynamath responses @@ -623,7 +622,7 @@ def sympy_check2(): log.error("oops in customresponse (cfn) error %s" % err) # print "context = ",self.context log.error(traceback.format_exc()) - raise Exception,"oops in customresponse (cfn) error %s" % err + raise Exception("oops in customresponse (cfn) error %s" % err) log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret) if type(ret)==dict: correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset) @@ -777,19 +776,19 @@ main() except Exception,err: msg = 'Error %s - cannot connect to external server url=%s' % (err,self.url) log.error(msg) - raise Exception, msg + raise Exception(msg) if self.system.DEBUG: log.info('response = %s' % r.text) if (not r.text ) or (not r.text.strip()): - raise Exception,'Error: no response from external server url=%s' % self.url + raise Exception('Error: no response from external server url=%s' % self.url) try: rxml = etree.fromstring(r.text) # response is XML; prase it except Exception,err: msg = 'Error %s - cannot parse response from external server r.text=%s' % (err,r.text) log.error(msg) - raise Exception, msg + raise Exception(msg) return rxml @@ -800,7 +799,7 @@ main() submission = [student_answers[k] for k in idset] except Exception,err: log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err,self.answer_ids,student_answers)) - raise Exception,err + raise Exception(err) self.context.update({'submission':submission}) @@ -817,7 +816,7 @@ main() ad = rxml.find('awarddetail').text admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses - 'WRONG_FORMAT': 'incorrect', + 'WRONG_FORMAT': 'incorrect', } self.context['correct'] = ['correct'] if ad in admap: @@ -847,7 +846,7 @@ main() if not (len(exans)==len(self.answer_ids)): log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids),len(exans))) - raise Exception,'Short response from external server' + raise Exception('Short response from external server') return dict(zip(self.answer_ids,exans)) @@ -964,7 +963,7 @@ class FormulaResponse(LoncapaResponse): correct_answer = contextualize_text(hxml.get('answer'),self.context) try: correctness = self.check_formula(correct_answer, given, samples) - except Exception,err: + except Exception: correctness = 'incorrect' if correctness=='correct': hints_to_show.append(name) @@ -1041,13 +1040,13 @@ class ImageResponse(LoncapaResponse): if not m: msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid], pretty_print=True)) - raise Exception,'[capamodule.capa.responsetypes.imageinput] '+msg + raise Exception('[capamodule.capa.responsetypes.imageinput] '+msg) (llx,lly,urx,ury) = [int(x) for x in m.groups()] # parse given answer m = re.match('\[([0-9]+),([0-9]+)]',given.strip().replace(' ','')) if not m: - raise Exception,'[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given) + raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid,given)) (gx,gy) = [int(x) for x in m.groups()] # answer is correct if (x,y) is within the specified rectangle From 6f14acee9e216399210e039a21c3730f90f58155 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 22:34:30 -0400 Subject: [PATCH 022/252] add MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] flag to settings --- lms/djangoapps/courseware/module_render.py | 2 +- lms/envs/common.py | 1 + lms/envs/dev_ike.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0f82d9ba94..fd419a68dc 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -169,7 +169,7 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N content = instance.get_html() # special extra information about each problem, only for users who are staff - if False and user.is_staff: + if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff: module_id = xml_module.get('id') histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 diff --git a/lms/envs/common.py b/lms/envs/common.py index d583217dc5..430ffc25ab 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -37,6 +37,7 @@ PERFSTATS = False MITX_FEATURES = { 'SAMPLE' : False, 'USE_DJANGO_PIPELINE' : True, + 'DISPLAY_HISTOGRAMS_TO_STAFF' : True, } # Used for A/B testing diff --git a/lms/envs/dev_ike.py b/lms/envs/dev_ike.py index 0930ef4651..af51274433 100644 --- a/lms/envs/dev_ike.py +++ b/lms/envs/dev_ike.py @@ -28,6 +28,7 @@ ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see QUICKEDIT = True # MITX_FEATURES['USE_DJANGO_PIPELINE'] = False +MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x', 'title' : 'Circuits and Electronics', From 17ca0e793ca77946b0a4212cc5a3550c04613ef3 Mon Sep 17 00:00:00 2001 From: ichuang Date: Sun, 10 Jun 2012 22:39:16 -0400 Subject: [PATCH 023/252] remove loncapa import in formularesponse_with_hint test (jenkins import path not consistent with dev machines?) --- common/lib/xmodule/test_files/formularesponse_with_hint.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/test_files/formularesponse_with_hint.xml b/common/lib/xmodule/test_files/formularesponse_with_hint.xml index e5b3e28708..90248dcf04 100644 --- a/common/lib/xmodule/test_files/formularesponse_with_hint.xml +++ b/common/lib/xmodule/test_files/formularesponse_with_hint.xml @@ -1,6 +1,6 @@ from the problem.xml file, and exec it in the context of this problem. Provides ability to randomize problems, and also set @@ -273,7 +273,7 @@ class LoncapaProblem(object): log.exception("Error while execing code: " + code) return context - def extract_html(self, problemtree): # private + def _extract_html(self, problemtree): # private ''' Main (private) function which converts Problem XML tree to HTML. Calls itself recursively. @@ -320,11 +320,11 @@ class LoncapaProblem(object): return render_object.get_html() # function(problemtree, value, status, msg) # render the special response (textline, schematic,...) if problemtree in self.responders: # let each Response render itself - return self.responders[problemtree].render_html(self.extract_html) + return self.responders[problemtree].render_html(self._extract_html) tree = etree.Element(problemtree.tag) for item in problemtree: - item_xhtml = self.extract_html(item) # nothing special: recurse + item_xhtml = self._extract_html(item) # nothing special: recurse if item_xhtml is not None: tree.append(item_xhtml) @@ -339,7 +339,7 @@ class LoncapaProblem(object): return tree - def preprocess_problem(self, tree): # private + def _preprocess_problem(self, tree): # private ''' Assign IDs to all the responses Assign sub-IDs to all entries (textline, schematic, etc.) From 5031c838ccdbefb27198e16f3dae9266006c558f Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 12 Jun 2012 13:52:34 -0400 Subject: [PATCH 038/252] typo in correctmap --- common/lib/capa/correctmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/capa/correctmap.py b/common/lib/capa/correctmap.py index 63d4fb33b2..786b2f5e2d 100644 --- a/common/lib/capa/correctmap.py +++ b/common/lib/capa/correctmap.py @@ -24,7 +24,7 @@ class CorrectMap(object): self.set(*args,**kwargs) def __getitem__(self, *args, **kwargs): - return self.cmap.__getitem(*args, **kwargs) + return self.cmap.__getitem__(*args, **kwargs) def __iter__(self): return self.cmap.__iter__() From 35202817d5c102dc9786aea7c6942a05d214db0f Mon Sep 17 00:00:00 2001 From: ichuang Date: Tue, 12 Jun 2012 14:07:48 -0400 Subject: [PATCH 039/252] problem.coffee : queue mathjax typesetting just once --- lms/static/coffee/src/modules/problem.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index 1f17d01405..17587b9ade 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -51,7 +51,6 @@ class @Problem @$("#answer_#{key}").html(value) # needs to be html, not text, for complex solutions (eg coding) @$("#solution_#{key}").html(value) # needs to be html, not text, for complex solutions (eg coding) MathJax.Hub.Queue ["Typeset", MathJax.Hub] - MathJax.Hub.Queue ["Typeset", MathJax.Hub] @$('.show').val 'Hide Answer' @element.addClass 'showed' else From 8e98e59cfa044364ed0c163339318f87269a6721 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 12 Jun 2012 14:08:43 -0400 Subject: [PATCH 040/252] Cleanup show answer code --- lms/static/coffee/src/modules/problem.coffee | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index 17587b9ade..aad41f23d4 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -45,17 +45,14 @@ class @Problem $.each response, (key, value) => if $.isArray(value) for choice in value - @$("label[for='input_#{key}_#{choice}']").attr - correct_answer: 'true' + @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' else - @$("#answer_#{key}").html(value) # needs to be html, not text, for complex solutions (eg coding) - @$("#solution_#{key}").html(value) # needs to be html, not text, for complex solutions (eg coding) + @$("#answer_#{key}, #solution_#{key}").html(value) MathJax.Hub.Queue ["Typeset", MathJax.Hub] @$('.show').val 'Hide Answer' @element.addClass 'showed' else - @$('[id^=answer_]').text '' - @$('[id^=solution_]').text '' + @$('[id^=answer_], [id^=solution_]').text '' @$('[correct_answer]').attr correct_answer: null @element.removeClass 'showed' @$('.show').val 'Show Answer' From a3561c96f79853751afce497b37a5f8c9b23fc86 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 11 Jun 2012 15:48:33 -0400 Subject: [PATCH 041/252] added TODO comment --- common/lib/xmodule/capa_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 439982a2c1..c7b52ae8ba 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -281,6 +281,7 @@ class Module(XModule): def answer_available(self): ''' Is the user allowed to see an answer? + TODO: simplify. ''' if self.show_answer == '': return False From c9cbc52ffada2abe6fcbfa10704ea2406cb7b2eb Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 11 Jun 2012 15:49:39 -0400 Subject: [PATCH 042/252] Add rough overview docs --- doc/README | 3 ++ doc/overview.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/testing.md | 12 ++++++ 3 files changed, 119 insertions(+) create mode 100644 doc/README create mode 100644 doc/overview.md create mode 100644 doc/testing.md diff --git a/doc/README b/doc/README new file mode 100644 index 0000000000..65f3cd2cb5 --- /dev/null +++ b/doc/README @@ -0,0 +1,3 @@ +This directory contains some high level documentation for the code. We should strive to keep it up-to-date, but don't take it as the absolute truth. + +A good place to start is 'overview' diff --git a/doc/overview.md b/doc/overview.md new file mode 100644 index 0000000000..a53e7371c2 --- /dev/null +++ b/doc/overview.md @@ -0,0 +1,104 @@ +# Documentation for edX code (mitx repo) + +This document explains the general structure of the edX platform, and defines some of the acronyms and terms you'll see flying around in the code. + +## Assumptions: + +You should be familiar with the following. If you're not, go read some docs... + + - python + - django + - javascript + - html, xml -- xpath, xslt + - css + - git + - mako templates -- we use these instead of django templates, because they support embedding real python. + +## Other relevant terms + + - CAPA -- lon-capa.org -- content management system that has defined a standard for online learning and assessment materials. Many of our materials follow this standard. + - TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive. + - lcp = loncapa problem + + +## Parts of the system + + - LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc. + + - CMS -- Course Management System. The instructor-facing parts of the system. Allows instructors to see and modify their course, add lectures, problems, reorder things, etc. + + - Askbot -- the discussion forums. We have a custom fork of this project. We're also hoping to replace it with something better later. (e.g. need support for multiple classes, etc) + + - Data. In the data/ dir. There is currently a single `course.xml` file that describes an entire course. Speaking of which... + + - Courses. A course is broken up into Chapters ("week 1", "week 2", etc). A chapter is broken up into Sections ("Lecture 1", "Simple Circuits Exercises", "HW1", etc). A section can contain modules: Problems, Html, Videos, Verticals, or Sequences. + - Problems: specified in problem files. May have python scripts embedded to both generate random parameters and check answers. Also allows specifying things like tolerance or precision in answers + - Html: any html - often description, or links to outside resources + - Videos: links to youtube or elsewhere + - Verticals: a nesting tag: collect several videos, problems, html modules and display them vertically. + - Sequences: a sequence of modules, displayed with a horizontal navigation bar, displaying one component at a time. + - see `data/course.xml` for more examples + + +## High Level Entities in the code + +### Common libraries + +- x_modules -- generic learning modules. *x* can be sequence, video, template, html, vertical, capa, etc. These are the things that one puts inside sections in the course structure. Modules know how to render themselves to html, how to score themselves, and handle ajax calls from the front end. + - x_modules take a 'system' context parameter, which is a reference to an object that knows how to render things, track events, complain about 404s, etc. (TODO: figure out, document the necessary interface--different in `x_module.XModule.__init__` and in `x_module tests.py`) + - in `common/lib/xmodule` + +- capa modules -- defines `LoncapaProblem` and many related things. + - in `common/lib/capa` + +### LMS + +The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`. + +- We use the Django Auth system, including the is_staff and is_superuser flags. User profiles and related code lives in `lms/djangoapps/student/`. There is support for groups of students (e.g. 'want emails about future courses', 'have unenrolled', etc) in `lms/djangoapps/student/models.py`. + +- `StudentModule` -- keeps track of where a particular student is in a module (problem, video, html)--what's their grade, have they started, are they done, etc. [This is only partly implemented so far.] + - `lms/djangoapps/courseware/models.py` + +- Core rendering path: + - `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits). + + - Calls `render_accordion` to render the "accordion"--the display of the course structure. + + - To render the current module, calls `module_render.py:render_x_module()`, which gets the `StudentModule` instance, and passes the `StudentModule` state and other system context to the module constructor the get an instance of the appropriate module class for this user. + + - calls the module's `.get_html()` method. If the module has nested submodules, render_x_module() will be called again for each. + + - ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed. + +- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`. + +### Other modules + +- Wiki -- in `lms/djangoapps/simplewiki`. Has some markdown extentions for embedding circuits, videos, etc. + +## Testing + +See `testing.md`. + + +## QUESTIONS: + +`common/lib/capa` : what is eia, `eia.py`? Random lists of numbers? + +what is `lms/lib/dogfood`? Looks like a way to test capa problems... Is it being used? + +is lms/envs/README.txt out of date? + +## TODO: + +- Only describes backend code so far. How does the front-end work? + +- What big pieces are missing? + +- Where should reader go next? + +--- +Note: this file uses markdown. To convert to html, run: + + markdown2 overview.md > overview.html diff --git a/doc/testing.md b/doc/testing.md new file mode 100644 index 0000000000..fa134ade66 --- /dev/null +++ b/doc/testing.md @@ -0,0 +1,12 @@ +# Testing + +Testing is good. Here is some useful info about how we set up tests-- + +### Backend code: + +- TODO + +### Frontend code: + +- TODO + From 17af925a8a0016f66c1b169fe463c60c4bc3966a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 11 Jun 2012 15:34:52 -0400 Subject: [PATCH 043/252] add function docs to module_render.py --- lms/djangoapps/courseware/module_render.py | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index fd419a68dc..ec13dc11a5 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -89,6 +89,19 @@ def grade_histogram(module_id): return grades def get_module(user, request, xml_module, module_object_preload, position=None): + ''' Get the appropriate xmodule and StudentModule. + + Arguments: + - user : current django User + - request : current django HTTPrequest + - xml_module : lxml etree of xml subtree for the current module + - module_object_preload : list of StudentModule objects, one of which may match this module type and id + - position : extra information from URL for user-specified position within module + + Returns: + - a tuple (xmodule instance, student module, module type). + + ''' module_type=xml_module.tag module_class=xmodule.get_module_class(module_type) module_id=xml_module.get('id') #module_class.id_attribute) or "" @@ -184,7 +197,19 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N return content def modx_dispatch(request, module=None, dispatch=None, id=None): - ''' Generic view for extensions. This is where AJAX calls go.''' + ''' Generic view for extensions. This is where AJAX calls go. + + Arguments: + + - request -- the django request. + - module -- the name of the module, as used in the course configuration xml. + - dispatch -- the command string to pass through to the module's handle_ajax call + (e.g. 'problem_reset'). If this string contains '?', only pass + through the part before the first '?'. + - id -- the module id. Used to look up the student module. + + TODO: why are id and module not the same? + ''' if not request.user.is_authenticated(): return redirect('/') @@ -200,6 +225,8 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): oldgrade = s.grade oldstate = s.state + # TODO: if dispatch is left at default value None, this will go boom. What's the correct + # behavior? dispatch=dispatch.split('?')[0] ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/' From 9f38ccb65df418b2a3972932a5b19d49f7f7482c Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 12 Jun 2012 09:31:22 -0400 Subject: [PATCH 044/252] add README.md for dogfood --- lms/lib/dogfood/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 lms/lib/dogfood/README.md diff --git a/lms/lib/dogfood/README.md b/lms/lib/dogfood/README.md new file mode 100644 index 0000000000..c6a7113049 --- /dev/null +++ b/lms/lib/dogfood/README.md @@ -0,0 +1 @@ +This is a library for edx4edx, allowing users to practice writing problems. From 600899c16cb357f7594b6ba3e395fe88ccb7ad97 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 12 Jun 2012 09:32:04 -0400 Subject: [PATCH 045/252] add comment/explanation to eia.py --- common/lib/capa/eia.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/capa/eia.py b/common/lib/capa/eia.py index bc962312ec..362dc33a2d 100644 --- a/common/lib/capa/eia.py +++ b/common/lib/capa/eia.py @@ -1,3 +1,6 @@ +""" Standard resistor codes. +http://en.wikipedia.org/wiki/Electronic_color_code +""" E6=[10,15,22,33,47,68] E12=[10,12,15,18,22,27,33,39,47,56,68,82] E24=[10,12,15,18,22,27,33,39,47,56,68,82,11,13,16,20,24,30,36,43,51,62,75,91] From c3a1d7788ccc36a744d251c97d375844d48e3053 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 12 Jun 2012 09:32:41 -0400 Subject: [PATCH 046/252] clarify docstring for modx_dispatch --- lms/djangoapps/courseware/module_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index ec13dc11a5..fe6ebdd585 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -202,13 +202,13 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): Arguments: - request -- the django request. - - module -- the name of the module, as used in the course configuration xml. + - module -- the type of the module, as used in the course configuration xml. + e.g. 'problem', 'video', etc - dispatch -- the command string to pass through to the module's handle_ajax call (e.g. 'problem_reset'). If this string contains '?', only pass through the part before the first '?'. - id -- the module id. Used to look up the student module. - - TODO: why are id and module not the same? + e.g. filenamexformularesponse ''' if not request.user.is_authenticated(): return redirect('/') From 7608003c15121026a4609925f88fc7dcc33a1757 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 12 Jun 2012 09:33:41 -0400 Subject: [PATCH 047/252] Update overview to reflect suggestions --- doc/README | 2 +- doc/overview.md | 22 +++++++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/doc/README b/doc/README index 65f3cd2cb5..d40f5d988d 100644 --- a/doc/README +++ b/doc/README @@ -1,3 +1,3 @@ This directory contains some high level documentation for the code. We should strive to keep it up-to-date, but don't take it as the absolute truth. -A good place to start is 'overview' +A good place to start is 'overview.md' diff --git a/doc/overview.md b/doc/overview.md index a53e7371c2..304d5161b0 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -45,7 +45,8 @@ You should be familiar with the following. If you're not, go read some docs... ### Common libraries - x_modules -- generic learning modules. *x* can be sequence, video, template, html, vertical, capa, etc. These are the things that one puts inside sections in the course structure. Modules know how to render themselves to html, how to score themselves, and handle ajax calls from the front end. - - x_modules take a 'system' context parameter, which is a reference to an object that knows how to render things, track events, complain about 404s, etc. (TODO: figure out, document the necessary interface--different in `x_module.XModule.__init__` and in `x_module tests.py`) + - x_modules take a 'system context' parameter, which helps isolate xmodules from any particular application, so they can be used in many places. The modules should make no references to Django (though there are still a few left). The system context knows how to render things, track events, complain about 404s, etc. + - TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here) - in `common/lib/xmodule` - capa modules -- defines `LoncapaProblem` and many related things. @@ -71,6 +72,10 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro - ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed. + - [This diagram](https://github.com/MITx/mitx/wiki/MITx-Architecture) visually shows how the clients communicate with problems + modules. + +- See `lms/urls.py` for the wirings of urls to views. + - Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`. ### Other modules @@ -81,22 +86,13 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro See `testing.md`. - -## QUESTIONS: - -`common/lib/capa` : what is eia, `eia.py`? Random lists of numbers? - -what is `lms/lib/dogfood`? Looks like a way to test capa problems... Is it being used? - -is lms/envs/README.txt out of date? - ## TODO: -- Only describes backend code so far. How does the front-end work? +- update lms/envs/README.txt -- What big pieces are missing? +- describe our production environment -- Where should reader go next? +- describe the front-end architecture, tools, etc. Starting point: `lms/static` --- Note: this file uses markdown. To convert to html, run: From b9befbafcd562a037c09140aa2b48cf1f1f18e71 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 13 Jun 2012 11:42:50 -0400 Subject: [PATCH 048/252] Add contentstore from cms proto as a baseline for importing and views for the cms --- cms/djangoapps/contentstore/__init__.py | 0 .../contentstore/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/ftpserve.py | 61 +++++++ .../management/commands/import.py | 158 ++++++++++++++++++ cms/djangoapps/contentstore/tests.py | 16 ++ cms/{ => djangoapps/contentstore}/views.py | 0 cms/envs/common.py | 6 +- cms/urls.py | 2 +- 9 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 cms/djangoapps/contentstore/__init__.py create mode 100644 cms/djangoapps/contentstore/management/__init__.py create mode 100644 cms/djangoapps/contentstore/management/commands/__init__.py create mode 100644 cms/djangoapps/contentstore/management/commands/ftpserve.py create mode 100644 cms/djangoapps/contentstore/management/commands/import.py create mode 100644 cms/djangoapps/contentstore/tests.py rename cms/{ => djangoapps/contentstore}/views.py (100%) diff --git a/cms/djangoapps/contentstore/__init__.py b/cms/djangoapps/contentstore/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/contentstore/management/__init__.py b/cms/djangoapps/contentstore/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/contentstore/management/commands/__init__.py b/cms/djangoapps/contentstore/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/contentstore/management/commands/ftpserve.py b/cms/djangoapps/contentstore/management/commands/ftpserve.py new file mode 100644 index 0000000000..f0a1c19dbf --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/ftpserve.py @@ -0,0 +1,61 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User +import contentstore.tasks + +from pyftpdlib import ftpserver +import os + +class DjangoAuthorizer(object): + def validate_authentication(self, username, password): + try: + u=User.objects.get(username=username) + except User.DoesNotExist: + return False + # TODO: Check security groups + return u.check_password(password) + def has_user(self, username): + print "????",username + return True + def has_perm(self, username, perm, path=None): + print "!!!!!",username, perm, path + return True + def get_home_dir(self, username): + d = "/tmp/ftp/"+username + try: + os.mkdir(d) + except OSError: + pass + return "/tmp/ftp/"+username + def get_perms(self, username): + return 'elradfmw' + def get_msg_login(self, username): + return 'Hello' + def get_msg_quit(self, username): + return 'Goodbye' + def __init__(self): + pass + def impersonate_user(self, username, password): + pass + def terminate_impersonation(self, username): + pass + +def on_upload(ftp_handler, filename): + source = ftp_handler.remote_ip + author = ftp_handler.username + print filename, author, source + # We pass on this for now: + # contentstore.tasks.on_upload + # It is a changing API, and it makes testing the FTP server slow. + +class Command(BaseCommand): + help = \ +''' Run FTP server.''' + def handle(self, *args, **options): + authorizer = DjangoAuthorizer() #ftpserver.DummyAuthorizer() + handler = ftpserver.FTPHandler + handler.on_file_received = on_upload + + handler.authorizer = authorizer + address = ("127.0.0.1", 2121) + ftpd = ftpserver.FTPServer(address, handler) + ftpd.serve_forever() diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py new file mode 100644 index 0000000000..78984f4119 --- /dev/null +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -0,0 +1,158 @@ +### +### One-off script for importing courseware form XML format +### + + +#import mitxmako.middleware +#from courseware import content_parser +#from django.contrib.auth.models import User +from mako.template import Template +from mako.lookup import TemplateLookup + +from django.core.management.base import BaseCommand +from contentstore.models import create_item, update_item, update_children + +from lxml import etree + +class Command(BaseCommand): + help = \ +''' Run FTP server.''' + def handle(self, *args, **options): + print args + data_dir = args[0] + course_file = 'course.xml' + + parser = etree.XMLParser(remove_comments = True) + + lookup = TemplateLookup(directories=[data_dir]) + template = lookup.get_template("course.xml") + course_string = template.render(groups=[]) + course = etree.XML(course_string, parser=parser) + + elements = course.xpath("//*") + + tag_to_category = {# Inside HTML ==> Skip these + # Custom tags + 'videodev': 'Custom', + 'slides': 'Custom', + 'book': 'Custom', + 'image': 'Custom', + 'discuss': 'Custom', + # Simple lists + 'chapter': 'Sequence', + 'course': 'Sequence', + 'sequential': 'Sequence', + 'vertical': 'Sequence', + 'section': 'Sequence', + # True types + 'video': 'VideoSegment', + 'html': 'HTML', + 'problem': 'Problem', + } + + + name_index=0 + for e in elements: + name = e.attrib.get('name', None) + for f in elements: + if f != e and f.attrib.get('name', None) == name: + name = None + if not name: + name = "{tag}_{index}".format(tag = e.tag,index = name_index) + name_index = name_index + 1 + if e.tag in tag_to_category: + category = tag_to_category[e.tag] + category = category.replace('/', '-') + name = name.replace('/', '-') + e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format(category = category, + name = name)) + + + def handle_skip(e): + print "Skipping ", e + + results = {} + + def handle_custom(e): + data = {'type':'i4x://mit.edu/6002xs12/tag/{tag}'.format(tag=e.tag), + 'attrib':dict(e.attrib)} + results[e.attrib['url']] = {'data':data} + + def handle_list(e): + if e.attrib.get("class", None) == "tutorials": + return + children = [{'url':le.attrib['url']} for le in e.getchildren()] + results[e.attrib['url']] = {'children':children} + + def handle_video(e): + url = e.attrib['url'] + clip_url = url.replace('VideoSegment', 'VideoClip') + # Take: 0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8 + # Make: [(0.75, 'izygArpw-Qo'), (1.0, 'p2Q6BrNhdh8'), (1.25, '1EeWXzPdhSA'), (1.5, 'rABDYkeK0x8')] + youtube_str = e.attrib['youtube'] + youtube_list = [(float(x), y) for x,y in map(lambda x:x.split(':'), youtube_str.split(','))] + clip_infos = [{ "status": "ready", + "format": "youtube", + "sane": True, + "location": "youtube", + "speed": speed, + "id": youtube_id, + "size": None} \ + for (speed, youtube_id) \ + in youtube_list] + results[clip_url] = {'data':{'clip_infos':clip_infos}} + results[url] = {'children' : [{'url':clip_url}]} + + def handle_html(e): + if 'src' in e.attrib: + text = open(data_dir+'html/'+e.attrib['src']).read() + else: + textlist=[e.text]+[etree.tostring(elem) for elem in e]+[e.tail] + textlist=[i for i in textlist if type(i)==str] + text = "".join(textlist) + + results[e.attrib['url']] = {'data':{'text':text}} + + def handle_problem(e): + data = open(data_dir+'problems/'+e.attrib['filename']+'.xml').read() + results[e.attrib['url']] = {'data':{'statement':data}} + + element_actions = {# Inside HTML ==> Skip these + 'a': handle_skip, + 'h1': handle_skip, + 'h2': handle_skip, + 'hr': handle_skip, + 'strong': handle_skip, + 'ul': handle_skip, + 'li': handle_skip, + 'p': handle_skip, + # Custom tags + 'videodev': handle_custom, + 'slides': handle_custom, + 'book': handle_custom, + 'image': handle_custom, + 'discuss': handle_custom, + # Simple lists + 'chapter': handle_list, + 'course': handle_list, + 'sequential': handle_list, + 'vertical': handle_list, + 'section': handle_list, + # True types + 'video': handle_video, + 'html': handle_html, + 'problem': handle_problem, + } + + for e in elements: + element_actions[e.tag](e) + + for k in results: + print k + create_item(k, 'Piotr Mitros') + if 'data' in results[k]: + update_item(k, results[k]['data']) + if 'children' in results[k]: + update_children(k, results[k]['children']) + + diff --git a/cms/djangoapps/contentstore/tests.py b/cms/djangoapps/contentstore/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/cms/djangoapps/contentstore/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/cms/views.py b/cms/djangoapps/contentstore/views.py similarity index 100% rename from cms/views.py rename to cms/djangoapps/contentstore/views.py diff --git a/cms/envs/common.py b/cms/envs/common.py index 9b349a06d0..8d402b6fa9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -144,8 +144,6 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - # 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', + 'contentstore', + 'instructor', ) diff --git a/cms/urls.py b/cms/urls.py index 55d7a1086e..a7266066cc 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -5,6 +5,6 @@ from django.conf.urls.defaults import patterns, url # admin.autodiscover() urlpatterns = patterns('', - url(r'^(?P[^/]+)/(?P[^/]+)/calendar/', 'cms.views.calendar', name='calendar'), + url(r'^(?P[^/]+)/(?P[^/]+)/calendar/', 'contentstore.views.calendar', name='calendar'), url(r'^accounts/login/', 'instructor.views.do_login', name='login'), ) From a333612cce7b9e285dfdc1fc38abf552bb320781 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 13 Jun 2012 11:54:17 -0400 Subject: [PATCH 049/252] Add templates directly from cms_proto/ui_prototype --- cms/djangoapps/contentstore/views.py | 4 + cms/static/css/base-style.css | 589 +++++++++++++++++ cms/static/css/ie.css | 196 ++++++ cms/static/css/style.css | 0 cms/static/img/indicator.gif | Bin 0 -> 1553 bytes cms/static/img/video.jpg | Bin 0 -> 7217 bytes cms/static/js/jquery.inlineedit.js | 251 ++++++++ cms/static/js/jquery.leanModal.min.js | 5 + cms/static/js/jquery.tablednd.js | 257 ++++++++ cms/static/js/main.js | 159 +++++ cms/static/js/markitup/jquery.markitup.js | 593 ++++++++++++++++++ .../js/markitup/sets/wiki/images/bold.png | Bin 0 -> 304 bytes .../js/markitup/sets/wiki/images/clean.png | Bin 0 -> 667 bytes .../js/markitup/sets/wiki/images/code.png | Bin 0 -> 859 bytes .../js/markitup/sets/wiki/images/h1.png | Bin 0 -> 276 bytes .../js/markitup/sets/wiki/images/h2.png | Bin 0 -> 304 bytes .../js/markitup/sets/wiki/images/h3.png | Bin 0 -> 306 bytes .../js/markitup/sets/wiki/images/h4.png | Bin 0 -> 293 bytes .../js/markitup/sets/wiki/images/h5.png | Bin 0 -> 304 bytes .../js/markitup/sets/wiki/images/image.png | Bin 0 -> 516 bytes .../js/markitup/sets/wiki/images/italic.png | Bin 0 -> 223 bytes .../js/markitup/sets/wiki/images/link.png | Bin 0 -> 343 bytes .../markitup/sets/wiki/images/list-bullet.png | Bin 0 -> 344 bytes .../sets/wiki/images/list-numeric.png | Bin 0 -> 357 bytes .../js/markitup/sets/wiki/images/picture.png | Bin 0 -> 606 bytes .../js/markitup/sets/wiki/images/preview.png | Bin 0 -> 537 bytes .../js/markitup/sets/wiki/images/quotes.png | Bin 0 -> 743 bytes .../js/markitup/sets/wiki/images/stroke.png | Bin 0 -> 269 bytes .../js/markitup/sets/wiki/images/url.png | Bin 0 -> 957 bytes cms/static/js/markitup/sets/wiki/set.js | 34 + cms/static/js/markitup/sets/wiki/style.css | 57 ++ .../markitup/skins/simple/images/handle.png | Bin 0 -> 258 bytes .../js/markitup/skins/simple/images/menu.png | Bin 0 -> 27151 bytes .../markitup/skins/simple/images/submenu.png | Bin 0 -> 240 bytes cms/static/js/markitup/skins/simple/style.css | 118 ++++ cms/static/sass/_base.scss | 51 ++ cms/static/sass/_calendar.scss | 206 ++++++ cms/static/sass/_problem.scss | 41 ++ cms/static/sass/_reset.scss | 229 +++++++ cms/static/sass/_video.scss | 40 ++ cms/static/sass/_week.scss | 152 +++++ cms/static/sass/base-style.scss | 18 + cms/static/sass/bourbon/_bourbon.scss | 35 ++ cms/static/sass/bourbon/addons/_button.scss | 267 ++++++++ cms/static/sass/bourbon/addons/_clearfix.scss | 29 + .../sass/bourbon/addons/_font-family.scss | 5 + .../bourbon/addons/_html5-input-types.scss | 36 ++ cms/static/sass/bourbon/addons/_position.scss | 30 + .../bourbon/addons/_timing-functions.scss | 32 + cms/static/sass/bourbon/css3/_animation.scss | 171 +++++ cms/static/sass/bourbon/css3/_appearance.scss | 7 + .../sass/bourbon/css3/_background-image.scss | 57 ++ .../sass/bourbon/css3/_background-size.scss | 15 + .../sass/bourbon/css3/_border-image.scss | 56 ++ .../sass/bourbon/css3/_border-radius.scss | 63 ++ cms/static/sass/bourbon/css3/_box-shadow.scss | 14 + cms/static/sass/bourbon/css3/_box-sizing.scss | 6 + cms/static/sass/bourbon/css3/_columns.scss | 67 ++ cms/static/sass/bourbon/css3/_flex-box.scss | 67 ++ .../sass/bourbon/css3/_inline-block.scss | 10 + .../sass/bourbon/css3/_linear-gradient.scss | 41 ++ .../sass/bourbon/css3/_radial-gradient.scss | 31 + cms/static/sass/bourbon/css3/_transform.scss | 19 + cms/static/sass/bourbon/css3/_transition.scss | 104 +++ .../sass/bourbon/css3/_user-select.scss | 6 + .../_deprecated-webkit-gradient.scss | 36 ++ .../sass/bourbon/functions/_flex-grid.scss | 35 ++ .../sass/bourbon/functions/_grid-width.scss | 13 + .../bourbon/functions/_linear-gradient.scss | 23 + .../bourbon/functions/_modular-scale.scss | 40 ++ .../bourbon/functions/_radial-gradient.scss | 15 + .../bourbon/functions/_render-gradients.scss | 14 + .../sass/bourbon/functions/_tint-shade.scss | 9 + cms/static/sass/bourbon/lib/bourbon.rb | 19 + .../bourbon/lib/bourbon/sass_extensions.rb | 6 + .../lib/bourbon/sass_extensions/functions.rb | 13 + .../sass_extensions/functions/compact.rb | 13 + cms/templates/base.html | 30 + cms/templates/index.html | 19 + cms/templates/widgets/captions.html | 242 +++++++ cms/templates/widgets/header.html | 5 + cms/templates/widgets/navigation.html | 120 ++++ cms/templates/widgets/new-module.html | 9 + cms/templates/widgets/problem-edit.html | 73 +++ cms/templates/widgets/problem-new.html | 52 ++ cms/templates/widgets/raw-videos.html | 3 + cms/templates/widgets/save-captions.html | 4 + cms/templates/widgets/speed-tooltip.html | 7 + cms/templates/widgets/video-box-unused.html | 38 ++ cms/templates/widgets/video-box.html | 35 ++ cms/templates/widgets/video-edit.html | 151 +++++ cms/templates/widgets/video-new.html | 48 ++ cms/templates/widgets/week-edit.html | 83 +++ cms/templates/widgets/week-new.html | 62 ++ cms/urls.py | 1 + 95 files changed, 5386 insertions(+) create mode 100644 cms/static/css/base-style.css create mode 100644 cms/static/css/ie.css create mode 100644 cms/static/css/style.css create mode 100755 cms/static/img/indicator.gif create mode 100644 cms/static/img/video.jpg create mode 100755 cms/static/js/jquery.inlineedit.js create mode 100644 cms/static/js/jquery.leanModal.min.js create mode 100644 cms/static/js/jquery.tablednd.js create mode 100644 cms/static/js/main.js create mode 100644 cms/static/js/markitup/jquery.markitup.js create mode 100644 cms/static/js/markitup/sets/wiki/images/bold.png create mode 100644 cms/static/js/markitup/sets/wiki/images/clean.png create mode 100644 cms/static/js/markitup/sets/wiki/images/code.png create mode 100644 cms/static/js/markitup/sets/wiki/images/h1.png create mode 100644 cms/static/js/markitup/sets/wiki/images/h2.png create mode 100644 cms/static/js/markitup/sets/wiki/images/h3.png create mode 100644 cms/static/js/markitup/sets/wiki/images/h4.png create mode 100644 cms/static/js/markitup/sets/wiki/images/h5.png create mode 100644 cms/static/js/markitup/sets/wiki/images/image.png create mode 100644 cms/static/js/markitup/sets/wiki/images/italic.png create mode 100644 cms/static/js/markitup/sets/wiki/images/link.png create mode 100644 cms/static/js/markitup/sets/wiki/images/list-bullet.png create mode 100644 cms/static/js/markitup/sets/wiki/images/list-numeric.png create mode 100644 cms/static/js/markitup/sets/wiki/images/picture.png create mode 100644 cms/static/js/markitup/sets/wiki/images/preview.png create mode 100644 cms/static/js/markitup/sets/wiki/images/quotes.png create mode 100644 cms/static/js/markitup/sets/wiki/images/stroke.png create mode 100644 cms/static/js/markitup/sets/wiki/images/url.png create mode 100644 cms/static/js/markitup/sets/wiki/set.js create mode 100644 cms/static/js/markitup/sets/wiki/style.css create mode 100644 cms/static/js/markitup/skins/simple/images/handle.png create mode 100644 cms/static/js/markitup/skins/simple/images/menu.png create mode 100644 cms/static/js/markitup/skins/simple/images/submenu.png create mode 100644 cms/static/js/markitup/skins/simple/style.css create mode 100644 cms/static/sass/_base.scss create mode 100644 cms/static/sass/_calendar.scss create mode 100644 cms/static/sass/_problem.scss create mode 100644 cms/static/sass/_reset.scss create mode 100644 cms/static/sass/_video.scss create mode 100644 cms/static/sass/_week.scss create mode 100644 cms/static/sass/base-style.scss create mode 100644 cms/static/sass/bourbon/_bourbon.scss create mode 100644 cms/static/sass/bourbon/addons/_button.scss create mode 100644 cms/static/sass/bourbon/addons/_clearfix.scss create mode 100644 cms/static/sass/bourbon/addons/_font-family.scss create mode 100644 cms/static/sass/bourbon/addons/_html5-input-types.scss create mode 100644 cms/static/sass/bourbon/addons/_position.scss create mode 100644 cms/static/sass/bourbon/addons/_timing-functions.scss create mode 100644 cms/static/sass/bourbon/css3/_animation.scss create mode 100644 cms/static/sass/bourbon/css3/_appearance.scss create mode 100644 cms/static/sass/bourbon/css3/_background-image.scss create mode 100644 cms/static/sass/bourbon/css3/_background-size.scss create mode 100644 cms/static/sass/bourbon/css3/_border-image.scss create mode 100644 cms/static/sass/bourbon/css3/_border-radius.scss create mode 100644 cms/static/sass/bourbon/css3/_box-shadow.scss create mode 100644 cms/static/sass/bourbon/css3/_box-sizing.scss create mode 100644 cms/static/sass/bourbon/css3/_columns.scss create mode 100644 cms/static/sass/bourbon/css3/_flex-box.scss create mode 100644 cms/static/sass/bourbon/css3/_inline-block.scss create mode 100644 cms/static/sass/bourbon/css3/_linear-gradient.scss create mode 100644 cms/static/sass/bourbon/css3/_radial-gradient.scss create mode 100644 cms/static/sass/bourbon/css3/_transform.scss create mode 100644 cms/static/sass/bourbon/css3/_transition.scss create mode 100644 cms/static/sass/bourbon/css3/_user-select.scss create mode 100644 cms/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss create mode 100644 cms/static/sass/bourbon/functions/_flex-grid.scss create mode 100644 cms/static/sass/bourbon/functions/_grid-width.scss create mode 100644 cms/static/sass/bourbon/functions/_linear-gradient.scss create mode 100644 cms/static/sass/bourbon/functions/_modular-scale.scss create mode 100644 cms/static/sass/bourbon/functions/_radial-gradient.scss create mode 100644 cms/static/sass/bourbon/functions/_render-gradients.scss create mode 100644 cms/static/sass/bourbon/functions/_tint-shade.scss create mode 100644 cms/static/sass/bourbon/lib/bourbon.rb create mode 100644 cms/static/sass/bourbon/lib/bourbon/sass_extensions.rb create mode 100644 cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb create mode 100644 cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb create mode 100644 cms/templates/base.html create mode 100644 cms/templates/index.html create mode 100644 cms/templates/widgets/captions.html create mode 100644 cms/templates/widgets/header.html create mode 100644 cms/templates/widgets/navigation.html create mode 100644 cms/templates/widgets/new-module.html create mode 100644 cms/templates/widgets/problem-edit.html create mode 100644 cms/templates/widgets/problem-new.html create mode 100644 cms/templates/widgets/raw-videos.html create mode 100644 cms/templates/widgets/save-captions.html create mode 100644 cms/templates/widgets/speed-tooltip.html create mode 100644 cms/templates/widgets/video-box-unused.html create mode 100644 cms/templates/widgets/video-box.html create mode 100644 cms/templates/widgets/video-edit.html create mode 100644 cms/templates/widgets/video-new.html create mode 100644 cms/templates/widgets/week-edit.html create mode 100644 cms/templates/widgets/week-new.html diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index c6786b03c4..38e9e8ad35 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -10,3 +10,7 @@ def calendar(request, org, course): Location(['i4x', org, course, 'Course', 'course']) ) return render_to_response('calendar.html', {'weeks': weeks}) + + +def index(request): + return render_to_response('index.html', {}) diff --git a/cms/static/css/base-style.css b/cms/static/css/base-style.css new file mode 100644 index 0000000000..2cdeafe9e5 --- /dev/null +++ b/cms/static/css/base-style.css @@ -0,0 +1,589 @@ +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, +del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, +b, i, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + outline: 0; + vertical-align: baseline; + background: transparent; } + +html, body { + font-size: 100%; } + +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { + display: block; } + +audio, canvas, video { + display: inline-block; } + +audio:not([controls]) { + display: none; } + +[hidden] { + display: none; } + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; } + +html, button, input, select, textarea { + font-family: sans-serif; } + +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } +a:hover, a:active { + outline: 0; } + +abbr[title] { + border-bottom: 1px dotted; } + +b, strong { + font-weight: bold; } + +blockquote { + margin: 1em 40px; } + +dfn { + font-style: italic; } + +mark { + background: #ff0; + color: #000; } + +pre, code, kbd, samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; } + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; } + +blockquote, q { + quotes: none; } + blockquote:before, blockquote:after, q:before, q:after { + content: ''; + content: none; } + +small { + font-size: 75%; } + +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sup { + top: -0.5em; } + +sub { + bottom: -0.25em; } + +nav ul, nav ol { + list-style: none; + list-style-image: none; } + +img { + border: 0; + height: auto; + max-width: 100%; + -ms-interpolation-mode: bicubic; } + +svg:not(:root) { + overflow: hidden; } + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } + +legend { + border: 0; + padding: 0; + white-space: normal; } + +button, input, select, textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; } + +button, input { + line-height: normal; } + +button, input[type="button"], input[type="reset"], input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; } + +button[disabled], input[disabled] { + cursor: default; } + +input[type="checkbox"], input[type="radio"] { + box-sizing: border-box; + padding: 0; } + +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; } + +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; } + +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; } + +textarea { + overflow: auto; + vertical-align: top; } + +table { + border-collapse: collapse; + border-spacing: 0; } + +html { + height: 100%; } + +body { + zoom: 1; + height: 100%; + font: 14px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; } + body:before, body:after { + content: ""; + display: table; } + body:after { + clear: both; } + body > section { + display: table; + width: 100%; } + body > header { + background: #000; + color: #fff; + display: block; + float: none; + padding: 6px 20px; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + body > header nav { + zoom: 1; } + body > header nav:before, body > header nav:after { + content: ""; + display: table; } + body > header nav:after { + clear: both; } + body > header nav h2 { + font-size: 14px; + text-transform: uppercase; } + +a { + text-decoration: none; + color: #888; } + +input[type="submit"], .button, section.week-edit > header a, section.week-new > header a { + border: 1px solid #ccc; + background: #efefef; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + padding: 6px; } + +section.cal { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 25px; + zoom: 1; + overflow: scroll; } + section.cal:before, section.cal:after { + content: ""; + display: table; } + section.cal:after { + clear: both; } + section.cal > header { + zoom: 1; + margin-bottom: 10px; } + section.cal > header:before, section.cal > header:after { + content: ""; + display: table; } + section.cal > header:after { + clear: both; } + section.cal > header h1 { + float: left; + font-size: 18px; } + section.cal > header ul { + float: right; } + section.cal > header ul li { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.cal > header ul li a { + padding: 6px; + border: 1px solid #ddd; + display: block; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + background: #efefef; } + section.cal > header ul li.dropdown { + position: relative; } + section.cal > header ul li.dropdown ul { + display: none; + position: absolute; + background: #fff; + border: 1px solid #ddd; } + section.cal > header ul li.dropdown ul li { + padding: 6px; + display: block; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; } + section.cal > header ul li.dropdown ul li:hover { + background-color: #efefef; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; } + section.cal > header ul li.dropdown:hover ul { + display: block; } + section.cal > header ul li.dropdown:hover a { + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + -ms-border-radius: 3px 3px 0 0; + -o-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; + border-bottom: 0; } + section.cal ol { + list-style: none; + zoom: 1; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + border-left: 1px solid #333; + border-top: 1px solid #333; + width: 100%; } + section.cal ol:before, section.cal ol:after { + content: ""; + display: table; } + section.cal ol:after { + clear: both; } + section.cal ol > li { + border-right: 1px solid #333; + border-bottom: 1px solid; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + float: left; + width: 25.0%; } + section.cal ol > li:last-child { + text-align: center; + background: #eee; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + section.cal ol > li:last-child p { + width: 100%; + height: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + section.cal ol > li:last-child p a { + display: block; + width: 100%; + height: 100%; } + section.cal ol > li:last-child section.new-week header { + background: #fff; + text-align: left; } + section.cal ol > li:last-child section.new-week form { + background: #fff; + width: 50%; + padding: 6px; + border: 1px solid #000; + margin: 0 auto; + -webkit-box-shadow: 0 0 2px #333333; + -moz-box-shadow: 0 0 2px #333333; + box-shadow: 0 0 2px #333333; + position: relative; } + section.cal ol > li:last-child section.new-week form:before { + background: #fff; + border-left: 1px solid #000; + border-top: 1px solid #000; + content: " "; + display: block; + height: 10px; + left: 50%; + position: absolute; + top: -6px; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + width: 10px; + z-index: 0; } + section.cal ol > li:last-child section.new-week form select { + margin-bottom: 6px; + width: 100%; } + section.cal ol > li:last-child section.new-week form select option { + padding: 10px 0 !important; } + section.cal ol > li:last-child section.new-week form input[type="submit"] { + display: block; + margin-bottom: 6px; + width: 100%; } + section.cal ol > li:last-child section.new-week form a:first-child { + float: left; } + section.cal ol > li:last-child section.new-week form a:last-child { + float: right; } + section.cal ol > li header { + border-bottom: 1px solid #000; + -webkit-box-shadow: 0 1px 2px #cccccc; + -moz-box-shadow: 0 1px 2px #cccccc; + box-shadow: 0 1px 2px #cccccc; + display: block; + margin-bottom: 2px; + padding: 6px; } + section.cal ol > li header h1 { + font-size: 14px; } + section.cal ol > li ul { + list-style: none; + margin-bottom: 1px; } + section.cal ol > li ul li { + background: #efefef; + border-bottom: 1px solid #666; + padding: 6px; } + section.cal ol > li ul li.goal { + background: #fff; } + +body.content +section.cal { + width: 25.577%; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + body.content + section.cal > header ul { + display: none; } + body.content + section.cal ol li { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; } + +section.week-edit > header, section.week-new > header { + border-bottom: 1px solid #ccc; + zoom: 1; + padding: 6px; } + section.week-edit > header:before, section.week-edit > header:after, section.week-new > header:before, section.week-new > header:after { + content: ""; + display: table; } + section.week-edit > header:after, section.week-new > header:after { + clear: both; } + section.week-edit > header h1, section.week-new > header h1 { + font-size: 18px; + float: left; + margin-top: 8px 6px; } + section.week-edit > header a, section.week-new > header a { + float: right; + display: block; } +section.week-edit section header, section.week-new section header { + background: #666; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; } + section.week-edit section header h2, section.week-new section header h2 { + font-size: 14px; } +section.week-edit section.sidebar, section.week-new section.sidebar { + width: 34.368%; + float: left; + background: #ccc; + border-right: 1px solid #333; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + section.week-edit section.sidebar section, section.week-new section.sidebar section { + height: 50%; } + section.week-edit section.sidebar section > ul, section.week-new section.sidebar section > ul { + list-style: none; } + section.week-edit section.sidebar section > ul > li, section.week-new section.sidebar section > ul > li { + padding: 6px; + border-bottom: 1px solid #666; + background: #eee; } + section.week-edit section.sidebar section > ul > li.new-module, section.week-new section.sidebar section > ul > li.new-module { + position: relative; } + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown { + list-style: none; } + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown li, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown li { + display: none; } + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child { + display: block; } + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown:hover li { + display: block; + padding: 6px 0; } + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child { + padding-top: 0; } + section.week-edit section.sidebar section p, section.week-new section.sidebar section p { + padding: 6px; + border-bottom: 1px solid #666; } +section.week-edit section.weeks-content, section.week-new section.weeks-content { + width: 65.632%; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + section.week-edit section.weeks-content header, section.week-new section.weeks-content header { + zoom: 1; } + section.week-edit section.weeks-content header:before, section.week-edit section.weeks-content header:after, section.week-new section.weeks-content header:before, section.week-new section.weeks-content header:after { + content: ""; + display: table; } + section.week-edit section.weeks-content header:after, section.week-new section.weeks-content header:after { + clear: both; } + section.week-edit section.weeks-content header h2, section.week-new section.weeks-content header h2 { + float: left; } + section.week-edit section.weeks-content header form, section.week-new section.weeks-content header form { + float: right; + margin: -2px 0; } + section.week-edit section.weeks-content header form input, section.week-new section.weeks-content header form input { + border: 1px solid #000; + background: #ddd; + padding: 2px 4px; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -ms-border-radius: 2px; + -o-border-radius: 2px; + border-radius: 2px; } + section.week-edit section.weeks-content section.filters, section.week-new section.weeks-content section.filters { + border-bottom: 1px solid #999; } + section.week-edit section.weeks-content section.filters ul, section.week-new section.weeks-content section.filters ul { + zoom: 1; + list-style: none; + padding: 6px; } + section.week-edit section.weeks-content section.filters ul:before, section.week-edit section.weeks-content section.filters ul:after, section.week-new section.weeks-content section.filters ul:before, section.week-new section.weeks-content section.filters ul:after { + content: ""; + display: table; } + section.week-edit section.weeks-content section.filters ul:after, section.week-new section.weeks-content section.filters ul:after { + clear: both; } + section.week-edit section.weeks-content section.filters ul li, section.week-new section.weeks-content section.filters ul li { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.week-edit section.weeks-content section.filters ul li.advanced, section.week-new section.weeks-content section.filters ul li.advanced { + float: right; } + section.week-edit section.weeks-content section.modules ul, section.week-new section.weeks-content section.modules ul { + list-style: none; } + section.week-edit section.weeks-content section.modules ul li, section.week-new section.weeks-content section.modules ul li { + padding: 6px; + font-weight: bold; + font-size: 16px; + border-bottom: 1px solid #333; } + section.week-edit section.weeks-content section.modules ul li a, section.week-new section.weeks-content section.modules ul li a { + color: #000; } + +section.video-new, section.video-edit { + position: absolute; + top: 80px; + right: 0; + background: #fff; + width: 40.32%; + -webkit-box-shadow: 0 0 6px #666666; + -moz-box-shadow: 0 0 6px #666666; + box-shadow: 0 0 6px #666666; + border: 1px solid #333; + border-right: 0; + z-index: 4; } + section.video-new > header, section.video-edit > header { + background: #666; + zoom: 1; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; } + section.video-new > header:before, section.video-new > header:after, section.video-edit > header:before, section.video-edit > header:after { + content: ""; + display: table; } + section.video-new > header:after, section.video-edit > header:after { + clear: both; } + section.video-new > header h2, section.video-edit > header h2 { + float: left; + font-size: 14px; } + section.video-new > header a, section.video-edit > header a { + float: right; } + section.video-new section ul, section.video-edit section ul { + list-style: none; } + section.video-new section ul li, section.video-edit section ul li { + border-bottom: 1px solid #333; + padding: 10px 25px; } + +section.problem-new, section.problem-edit { + position: absolute; + top: 80px; + right: 0; + background: #fff; + width: 40.32%; + -webkit-box-shadow: 0 0 6px #666666; + -moz-box-shadow: 0 0 6px #666666; + box-shadow: 0 0 6px #666666; + border: 1px solid #333; + border-right: 0; + z-index: 4; } + section.problem-new > header, section.problem-edit > header { + background: #666; + zoom: 1; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; } + section.problem-new > header:before, section.problem-new > header:after, section.problem-edit > header:before, section.problem-edit > header:after { + content: ""; + display: table; } + section.problem-new > header:after, section.problem-edit > header:after { + clear: both; } + section.problem-new > header h2, section.problem-edit > header h2 { + float: left; + font-size: 14px; } + section.problem-new > header a, section.problem-edit > header a { + float: right; } + section.problem-new section ul, section.problem-edit section ul { + list-style: none; } + section.problem-new section ul li, section.problem-edit section ul li { + border-bottom: 1px solid #333; + padding: 10px 25px; } + +body.content section.main-content { + border-left: 2px solid #000; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 74.423%; + float: left; + -webkit-box-shadow: -2px 0 3px #dddddd; + -moz-box-shadow: -2px 0 3px #dddddd; + box-shadow: -2px 0 3px #dddddd; } diff --git a/cms/static/css/ie.css b/cms/static/css/ie.css new file mode 100644 index 0000000000..f497a329cf --- /dev/null +++ b/cms/static/css/ie.css @@ -0,0 +1,196 @@ +/* line 12, ../sass/_reset.scss */ +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, +del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, +b, i, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + outline: 0; + vertical-align: baseline; + background: transparent; } + +/* line 21, ../sass/_reset.scss */ +html, body { + font-size: 100%; } + +/* line 26, ../sass/_reset.scss */ +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { + display: block; } + +/* line 31, ../sass/_reset.scss */ +audio, canvas, video { + display: inline-block; } + +/* line 36, ../sass/_reset.scss */ +audio:not([controls]) { + display: none; } + +/* line 41, ../sass/_reset.scss */ +[hidden] { + display: none; } + +/* line 47, ../sass/_reset.scss */ +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; } + +/* line 54, ../sass/_reset.scss */ +html, button, input, select, textarea { + font-family: sans-serif; } + +/* line 60, ../sass/_reset.scss */ +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; } +/* line 69, ../sass/_reset.scss */ +a:hover, a:active { + outline: 0; } + +/* line 75, ../sass/_reset.scss */ +abbr[title] { + border-bottom: 1px dotted; } + +/* line 80, ../sass/_reset.scss */ +b, strong { + font-weight: bold; } + +/* line 84, ../sass/_reset.scss */ +blockquote { + margin: 1em 40px; } + +/* line 89, ../sass/_reset.scss */ +dfn { + font-style: italic; } + +/* line 94, ../sass/_reset.scss */ +mark { + background: #ff0; + color: #000; } + +/* line 101, ../sass/_reset.scss */ +pre, code, kbd, samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; } + +/* line 108, ../sass/_reset.scss */ +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; } + +/* line 115, ../sass/_reset.scss */ +blockquote, q { + quotes: none; } + /* line 117, ../sass/_reset.scss */ + blockquote:before, blockquote:after, q:before, q:after { + content: ''; + content: none; } + +/* line 123, ../sass/_reset.scss */ +small { + font-size: 75%; } + +/* line 127, ../sass/_reset.scss */ +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +/* line 134, ../sass/_reset.scss */ +sup { + top: -0.5em; } + +/* line 138, ../sass/_reset.scss */ +sub { + bottom: -0.25em; } + +/* line 143, ../sass/_reset.scss */ +nav ul, nav ol { + list-style: none; + list-style-image: none; } + +/* line 150, ../sass/_reset.scss */ +img { + border: 0; + height: auto; + max-width: 100%; + -ms-interpolation-mode: bicubic; } + +/* line 158, ../sass/_reset.scss */ +svg:not(:root) { + overflow: hidden; } + +/* line 163, ../sass/_reset.scss */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; } + +/* line 169, ../sass/_reset.scss */ +legend { + border: 0; + padding: 0; + white-space: normal; } + +/* line 175, ../sass/_reset.scss */ +button, input, select, textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; } + +/* line 182, ../sass/_reset.scss */ +button, input { + line-height: normal; } + +/* line 186, ../sass/_reset.scss */ +button, input[type="button"], input[type="reset"], input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; } + +/* line 192, ../sass/_reset.scss */ +button[disabled], input[disabled] { + cursor: default; } + +/* line 196, ../sass/_reset.scss */ +input[type="checkbox"], input[type="radio"] { + box-sizing: border-box; + padding: 0; } + +/* line 201, ../sass/_reset.scss */ +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; } + +/* line 209, ../sass/_reset.scss */ +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; } + +/* line 215, ../sass/_reset.scss */ +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; } + +/* line 220, ../sass/_reset.scss */ +textarea { + overflow: auto; + vertical-align: top; } + +/* line 226, ../sass/_reset.scss */ +table { + border-collapse: collapse; + border-spacing: 0; } diff --git a/cms/static/css/style.css b/cms/static/css/style.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/static/img/indicator.gif b/cms/static/img/indicator.gif new file mode 100755 index 0000000000000000000000000000000000000000..085ccaecaf5fa5c34bc14cd2c2ed5cbbd8e25dcb GIT binary patch literal 1553 zcma)+TTl~c6vwlh>nb99Af5rT)t{mCEg5urg=A(g z{C|6SPb~9Xage|wB`SrZk2FOMYM!buln2sX?5Y+T78iB(Zu9cS7|LZyZ++}u$^oi1 z_j@S}bW9OzU2R+RMy&~OT>X-oZ98$jq#ogNfJ!BM-42wHGZk*6s2KD}U*IA%epmxb zm}|6BK9YoIF;*xSL!+z@<64lB7->LTW2Vi4ostCA(z&2XniwNIv}fFo-`MbG;)u4G z^p@F!)|9HhZprHd_vXjDoxs6WkK-6P0@lfxnGT>*p(QHoUV=u1FAqb@b%*W=a3{`LsH5k^AvQNL>6fPpy#oU(&MuH(*aEX4b35*} zn4n7)`I2U%=+Z=?BVZQ?vjQFW4gD@~XSOO6b{qu81`4&LFuU2(ilxW+1|ZkNMnWe79C$gs zWT?Ele|HR{JGPe)5BTW>0Ey?-Ls6S#GoV0tbt6ku7B&*0 z;i9QM$W1Rj*rRIdceL)rAOSl+sDe3LkB87<%){;ZdHp6|SNlopDXRx< zxBDF9-lTo&v`8$humFygUij@qgT=Qzhj8{ym2-{Xciwqq_Xwk%=O3B-MNAL_6e`3U zyxwmXex4`g0^1RYw~Dth3av3Dl^AAlpO3mG!nLr#&ZZ7c_wUboI+deC+&%TFjK2Lm z!Y&f1h|T_On%RCV&=4bx`!>(YezqGVhl&QpED?N6GV)HmzJ9&rh$x*i?*@o9#6QI< z5ZI_MRX;0+pY8$`j)eF#TlUyG(eE%E7S!rj;mj^M5vhUicPm zVWQ2z+imFyg}SRABmOBY_@osR!>7Ov!ioK`NB6_Rv}7Ud?35ed5Sb@?yND?kv~RCa wqs^a3Sh>&&L4)!LKI?D2&k@))k(LESaga|C278ChSzn3NWVkcuNoY&{0f?~U_5c6? literal 0 HcmV?d00001 diff --git a/cms/static/img/video.jpg b/cms/static/img/video.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e92271c3ad5e23185f1fac4bbf5954df735d5949 GIT binary patch literal 7217 zcmeHLYgm$Lw?@q@%^Y<;bF|daZH@;(Jd2o(nl(0BqlJo!DH0(83Mw8-YDUNHsF|Z= zO%l^ivL=yCNeRi)l+v^rJd0vx#GVn;fdxue-sfCT~OHaqgyH zu2(e)_B(9iMI@n396`1~l&urQ#K{2!ajgWu$g_!(d zriyB$=;P1;Z=XMEDNgRDe>6p*P(T!W5RrrdLtI^5!M1i_J3F8v0!U6K#78Cp31qYP z8NAVC6bTy}k0laJ)-y&%5fkFwO%+VvwSbTPkoKQy^_M_DzkeNy$FB?UHZ3_m0R1Q5 zf0~$lBsmrh4nUKM2_zI+5pK2)j)i)W(2?;((h(x@DD+p}?xuax$y)xpKd(aFmh0)gyrdinfyc5n%l-UnimR=LN7;V~hG`H5FAQR1~GPRkQ&f03qyu*7sV;srxVda88U zu!6%T@ZhXt_yMZ`rg^(taK}hqMpT6HST0x0d=fxg^f@}N`3vmWiUZd$q+U2TXj^-? zJrOA4Til52dME@*lOgH%;O8$?vRc$Tc@?GD0R5nA-Lo{T_^Mqoaj|$Qzh`j}6OUip z`^v2|&-WDpC{5Klof0qg#oL7Ic#Pd}D+N#G%(C;(W@lj@_4jYfq2`G@fg}ra_^KR` zgELqer6Rd8beyFmV;0OJCNA9OtU8AY%btyf z)-27_^9x8G&91TRnN3>B_{Hy%s~^=QFK0I*5h7&JiJFSy;i_qTJ4$x5orTs9sk|vc zW4lxKbI>-+M_JSZlyL*@jPqu)#W~&r{DqA2VwLir$y#3Q)fS9wmp%AkTe>wu=!_zQ zJ}H|xR2m$f8SGCVMzq&JZ)|w7(u!d){(kU|L+w)p3}z%Oqu9pUmL}?Ehtoda;`s>^ zZRvZIY3UotiqRZeg{4`O2Mh>3)AqgM%S9#Fkk=KAO4tFxV_pV5rDOEqT^S*#4C^U# zH#eAD&B*eZfVNMDrQdWj5f_|W9L}rBMo+A{yl5?8#nS7#K>G#Z0pCR4;VF{Jqg(b6UJYl&usRoI0rMn2)LyS&8Z5TYK?BQJ{)IHlDZ05qm!tY1zaKd(^9FvwEAIK*dPbFq z-{SF4u8oN(aafnWwZZNR_7LtTJ$!aZaB@vDoEbHjJx^>p%#C?z9^9tG#9G0tSWx9V zgO9U+8<7JTh6{2))fLCzyGbxtNp1j~Jl}aQjK2rg?@FWnJ~~6gBD%kR!ZKgBzQYcN z(<-2I%$wnw5gTmP&|=Mgjr|%nF|%C`a3ZyxB?eD5JmTF;tK?Rl;(1VX<78!7Vcct2 za&9wxhfOFg3oY@fvx4gl5#kobV;Vkt-xX8~m=UN=F@CIdegfP2G&QD6HVg_9l4kKk zKZ{GKj~bgZn~H$GRV@5qCDdo)a>d#$irHsPRfX8Yfz)xs`qgFWOiLSjD)2lwDB$UE z?tVESXuu=pE(>=#bx#MmitjUE-N&FBj9Zf+EUZQ${XVD6Y8zx@&FU>IfnHatJ!Bw| zu(iq(~#{96`1&NFAk4%f01dhvG9&)M>XV5;1fS;K@qoAW{O@(NwzorCfy!N=_7J8s` z?VCnKVWNdK22tH5S>=}d&LhULC6>>mLOKJCT*1Z8$7J%-UsgDY27jAyKuwTzOL@`(*sQl2d*etuoHQ%Z3{vT306rQ?p$ z{7S;3QsitUUBlfP;i=05J@1WaTy}Lj`EnR}&S#G|%5BGxmA`KK=ycAtFXot7L=ek* zZ?Zz}{aV#+nvK)WUEpF^AXVp5@1>PwHqMZuS0$b4x?AqCcJ#UrqoMpbJ%*lk=6c75 z)YFYy9R6KjR5W0bZjcy$FPxKmV^Y&on`MazkQE3JYqtn|IUs#%6rn6y&l~ugY}s$E zbE5KDZA7lwLb7h~lTm-hh$YdUO&nhM@*fX=ia>~``QI+hbtbme4{J(NFE1jJ^&oL8 zxB(AdmqW8~qB%4-S5LwX0{ioX6&q$&#;T3pfj+JdQ>V`Pgj7KgdD zN5rWOhLbVH_B`-0Wan+SfC@S;(}Qam1f5GKl?@M;9+FmWAMo0J2Cy@gazbCk#>!5! zFB6JxFYKmYK3f!Xgo(8dx!5kCS^LhjtiS5`G;BlSD5E`$fs(8W_s2;<)~j&lWmi#N z&~I0vv!kKBJZy|8r6Y9}4eJpyMpf^fQDnYRI<4ML<)r4#lR%J-R91B*{x;KUSmp33a6Ni@Hdl^DC&e=hTE z2(psd$40bE2!VCMT^00E0rLyplK_B9IrqEdsJLz&Qjy8X05fM-R<$D^iOo0si`4?PAsu5 zMo2{sE4~b{NZPnYjnjR}uhrN4L_7g*49;Oy@@(TA-Famp?B+J)_B5DX#bhYE%Nf{a z$E0R_Ik^`Az-2#?1I|&;tsPo+oYs2A09Nl4`}c%>$GCf^W<;=4s~_7J&-||mO*1ck zYQ)oO7xCkq>D%RP`ZaUu*2cMnFuJ+h5zX{@WOHwS9LGU3OTY{C>k(PR42RZ>480E2 zH?_yNy6Ln+J=>f>JAr*1g2SrW7Ga^9g+hr<(A4>9Gu@`a0CAKoW<;PB{_-@}{W}h1 zEkc9qn)+%yQL@;}CQ3OO1^LwK6rTZ{|E#LFg7gMVSvc6Vqix(6xJsHpn)E|Cc3f&h z2ZsL)c{fAdJ(n zr!LWl^)(BeaZOX&dYeP~vD&Y->{wS*ZU(7v6%>W-mJJq3eFY%n;0^=$d;Gu0c5Be= z(0CCkrD5h^^vua}^jMhdy=#8Fm!Q@rIY0+-uDg}L)qnMJ0L#p#Orzp^xJL^7Xa;`e zrbfY--s`D(IiNR;RdF|ScU;q}N8(f!Dz+x&e(S)+9ayT?H|^x{{T9ZiPluQ4W9in1 z*i*9Y$KJ8p2AD^U>;AABvv|2~CI4mie1axsbAOfiKYDa{2cl)CyNiw|JZzr`pq1Pt zmRVDFOFV{p(no|GrrF`ryq{b%&#!zdxw7xo8lpdlf-xsuub`I`{T`+=&GQ*IB52FB z9J5uY34vs)n&40psU84fn)x2y75(oi@*%44*3{I(Q2uj5MQKvLWzQo@ye7X$8aNE|-Fv8=o)61`j0e>pX2VnQo%Vi!d# z9*=L$%41YJ@1_@EoK-y3Kpn)M$AZU#!ifP6NvGthi*J?ULKE_pWM|eH=(k7)FYZ)& z-o05?_c<(o5boq*j6ZP-7{CXOLGNEB>CkfBzHThJ2`g@y%f%A&TX=xMJw3V1(qDKmNFG#W?JpIXKNRk(0ip|q5O19_x#$`VDSaj zCvZ+-S(mPWj3u$fje z9(7vULEEF7TV7|SnkYVq9AHrXrd5hW2W}|qn73e%}s3&pKvqi8KfPzXKdQwvR)qH zNU_D%`l>Rtfsn?;BCBrf4sFtUY}h9U{BSV^{#{k=^^RmP8tYJU@+4gj$jz;-xD~U@ zQsbM}&TH3kqvn=%N7aQ?4ck6Fw9%bO|7vp~ zowxS$BXRgj^{qyfl4%|E{43fk+hf5{K;ImEc=~T`0E<5_4pI#Y + * Licensed under the MIT (MIT-LICENSE.txt) license. + * + * http://github.com/caphun/jquery.inlineedit/ + * + * Inline (in-place) editing. + */ + +(function($) { + +// cached values +var namespace = '.inlineedit', + placeholderClass = 'inlineEdit-placeholder'; + +// define inlineEdit method +$.fn.inlineEdit = function( options ) { + var self = this; + + return this + + .each( function() { + $.inlineEdit.getInstance( this, options ).initValue(); + }) + + .live( ['click', 'mouseenter','mouseleave'].join(namespace+' '), function( event ) { + + var widget = $.inlineEdit.getInstance( this, options ), + editableElement = widget.element.find( widget.options.control ), + mutated = !!editableElement.length; + + widget.element.removeClass( widget.options.hover ); + + if ( event.target !== editableElement[0] ) { + switch ( event.type ) { + case 'click': + widget[ mutated ? 'mutate' : 'init' ](); + break; + + case 'mouseover': // jquery 1.4.x + case 'mouseout': // jquery 1.4.x + case 'mouseenter': + case 'mouseleave': + if ( !mutated ) { + widget.hoverClassChange( event ); + } + break; + } + } + + }); +} + +// plugin constructor +$.inlineEdit = function( elem, options ) { + + // deep extend + this.options = $.extend( true, {}, $.inlineEdit.defaults, options ); + + // the original element + this.element = $( elem ); + +} + +// plugin instance +$.inlineEdit.getInstance = function( elem, options ) { + return ( $.inlineEdit.initialised( elem ) ) + ? $( elem ).data( 'widget' + namespace ) + : new $.inlineEdit( elem, options ); +} + +// check if plugin initialised +$.inlineEdit.initialised = function( elem ) { + var init = $( elem ).data( 'init' + namespace ); + return init !== undefined && init !== null ? true : false; +} + +// plugin defaults +$.inlineEdit.defaults = { + hover: 'ui-state-hover', + value: '', + save: '', + buttons: ' ', + placeholder: 'Click to edit', + control: 'input', + cancelOnBlur: false, + saveOnBlur: false +}; + +// plugin prototypes +$.inlineEdit.prototype = { + + // initialisation + init: function() { + + // set initialise flag + this.element.data( 'init' + namespace, true ); + + // initialise value + this.initValue(); + + // mutate + this.mutate(); + + // save widget data + this.element.data( 'widget' + namespace, this ); + + }, + + initValue: function() { + this.value( $.trim( this.element.text() ) || this.options.value ); + + if ( !this.value() ) { + this.element.html( $( this.placeholderHtml() ) ); + } else if ( this.options.value ) { + this.element.html( this.options.value ); + } + }, + + mutate: function() { + var self = this; + + return self + .element + .html( self.mutatedHtml( self.value() ) ) + .find( 'button.save' ) + .bind( 'click', function( event ) { + self.save( self.element, event ); + self.change( self.element, event ); + return false; + }) + .end() + .find( 'button.cancel' ) + .bind( 'click', function( event ) { + self.change( self.element, event ); + return false; + }) + .end() + .find( self.options.control ) + .bind( 'blur', function( event ) { + if (self.options.cancelOnBlur === true) + self.change( self.element, event ); + else if (self.options.saveOnBlur == true){ + self.save( self.element, event ); + self.change( self.element, event ); + } + }) + .bind( 'keyup', function( event ) { + switch ( event.keyCode ) { + case 13: // save on ENTER + if (self.options.control !== 'textarea') { + self.save( self.element, event ); + self.change( self.element, event ); + } + break; + case 27: // cancel on ESC + self.change( self.element, event ); + break; + } + }) + .focus() + .end(); + }, + + value: function( newValue ) { + if ( arguments.length ) { + var value = newValue === this.options.placeholder ? '' : newValue; + this.element.data( 'value' + namespace, $( '.' + placeholderClass, this ).length ? '' : value && this.encodeHtml( value.replace( /\n/g,"
" ) ) ); + } + return this.element.data( 'value' + namespace ); + }, + + mutatedHtml: function( value ) { + return this.controls[ this.options.control ].call( this, value ); + }, + + placeholderHtml: function() { + return ''+ this.options.placeholder +''; + }, + + buttonHtml: function( options ) { + var o = $.extend({}, { + before: ' ', + buttons: this.options.buttons, + after: '' + }, options); + + return o.before + o.buttons + o.after; + }, + + save: function( elem, event ) { + var $control = this.element.find( this.options.control ), + hash = { + value: this.encodeHtml( $control.val() ) + }; + + // save value back to control to avoid XSS + $control.val(hash.value); + + if ( ( $.isFunction( this.options.save ) && this.options.save.call( this.element[0], event, hash ) ) !== false || !this.options.save ) { + this.value( hash.value ); + } + }, + + change: function( elem, event ) { + var self = this; + + if ( this.timer ) { + window.clearTimeout( this.timer ); + } + + this.timer = window.setTimeout( function() { + self.element.html( self.value() || self.placeholderHtml() ); + self.element.removeClass( self.options.hover ); + }, 200 ); + + }, + + controls: { + textarea: function( value ) { + return '' + this.buttonHtml( { before: '
' } ); + }, + input: function( value ) { + return '' + this.buttonHtml(); + } + }, + + hoverClassChange: function( event ) { + $( event.target )[ /mouseover|mouseenter/.test( event.type ) ? 'addClass':'removeClass']( this.options.hover ); + }, + + encodeHtml: function( s ) { + var encoding = [ + {key: //g, value: '>'}, + {key: /"/g, value: '"'} + ], + value = s; + + $.each(encoding, function(i,n) { + value = value.replace(n.key, n.value); + }); + + return value; + } + +}; + +})(jQuery); diff --git a/cms/static/js/jquery.leanModal.min.js b/cms/static/js/jquery.leanModal.min.js new file mode 100644 index 0000000000..a5772dd8e2 --- /dev/null +++ b/cms/static/js/jquery.leanModal.min.js @@ -0,0 +1,5 @@ +// leanModal v1.1 by Ray Stone - http://finelysliced.com.au +// Dual licensed under the MIT and GPL + +(function($){$.fn.extend({leanModal:function(options){var defaults={top:100,overlay:0.5,closeButton:null};var overlay=$("
");$("body").append(overlay);options=$.extend(defaults,options);return this.each(function(){var o=options;$(this).click(function(e){var modal_id=$(this).attr("href");$("#lean_overlay").click(function(){close_modal(modal_id)});$(o.closeButton).click(function(){close_modal(modal_id)});var modal_height=$(modal_id).outerHeight();var modal_width=$(modal_id).outerWidth(); +$("#lean_overlay").css({"display":"block",opacity:0});$("#lean_overlay").fadeTo(200,o.overlay);$(modal_id).css({"display":"block","position":"fixed","opacity":0,"z-index":11000,"left":50+"%","margin-left":-(modal_width/2)+"px","top":o.top+"px"});$(modal_id).fadeTo(200,1);e.preventDefault()})});function close_modal(modal_id){$("#lean_overlay").fadeOut(200);$(modal_id).css({"display":"none"})}}})})(jQuery); diff --git a/cms/static/js/jquery.tablednd.js b/cms/static/js/jquery.tablednd.js new file mode 100644 index 0000000000..56413ccb08 --- /dev/null +++ b/cms/static/js/jquery.tablednd.js @@ -0,0 +1,257 @@ + +jQuery.tableDnD = { + /** Keep hold of the current table being dragged */ + currentTable : null, + /** Keep hold of the current drag object if any */ + dragObject: null, + /** The current mouse offset */ + mouseOffset: null, + /** Remember the old value of Y so that we don't do too much processing */ + oldY: 0, + + /** Actually build the structure */ + build: function(options) { + // Make sure options exists + options = options || {}; + // Set up the defaults if any + + this.each(function() { + // Remember the options + this.tableDnDConfig = { + onDragStyle: options.onDragStyle, + onDropStyle: options.onDropStyle, + // Add in the default class for whileDragging + onDragClass: options.onDragClass ? options.onDragClass : "dragged", + onDrop: options.onDrop, + onDragStart: options.onDragStart, + scrollAmount: options.scrollAmount ? options.scrollAmount : 5 + }; + // Now make the rows draggable + jQuery.tableDnD.makeDraggable(this); + }); + + // Now we need to capture the mouse up and mouse move event + // We can use bind so that we don't interfere with other event handlers + jQuery(document) + .bind('mousemove', jQuery.tableDnD.mousemove) + .bind('mouseup', jQuery.tableDnD.mouseup); + + // Don't break the chain + return this; + }, + + /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */ + makeDraggable: function(table) { + // Now initialise the rows + var rows = table.rows; //getElementsByTagName("tr") + var config = table.tableDnDConfig; + for (var i=0; i jQuery.tableDnD.oldY; + // update the old value + jQuery.tableDnD.oldY = y; + // update the style to show we're dragging + if (config.onDragClass) { + dragObj.addClass(config.onDragClass); + } else { + dragObj.css(config.onDragStyle); + } + // If we're over a row then move the dragged row to there so that the user sees the + // effect dynamically + var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y); + if (currentRow) { + // TODO worry about what happens when there are multiple TBODIES + if (movingDown && jQuery.tableDnD.dragObject != currentRow) { + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling); + } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) { + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow); + } + } + } + + return false; + }, + + /** We're only worried about the y position really, because we can only move rows up and down */ + findDropTargetRow: function(draggedRow, y) { + var rows = jQuery.tableDnD.currentTable.rows; + for (var i=0; i rowY - rowHeight) && (y < (rowY + rowHeight))) { + // that's the row we're over + // If it's the same as the current row, ignore it + if (row == draggedRow) {return null;} + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + if (config.onAllowDrop) { + if (config.onAllowDrop(draggedRow, row)) { + return row; + } else { + return null; + } + } else { + // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic) + var nodrop = $(row).hasClass("nodrop"); + if (! nodrop) { + return row; + } else { + return null; + } + } + return row; + } + } + return null; + }, + + mouseup: function(e) { + if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) { + var droppedRow = jQuery.tableDnD.dragObject; + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + // If we have a dragObject, then we need to release it, + // The row will already have been moved to the right place so we just reset stuff + if (config.onDragClass) { + jQuery(droppedRow).removeClass(config.onDragClass); + } else { + jQuery(droppedRow).css(config.onDropStyle); + } + jQuery.tableDnD.dragObject = null; + if (config.onDrop) { + // Call the onDrop method if there is one + config.onDrop(jQuery.tableDnD.currentTable, droppedRow); + } + jQuery.tableDnD.currentTable = null; // let go of the table too + } + }, + + serialize: function() { + if (jQuery.tableDnD.currentTable) { + var result = ""; + var tableId = jQuery.tableDnD.currentTable.id; + var rows = jQuery.tableDnD.currentTable.rows; + for (var i=0; i 0) result += "&"; + result += tableId + '[]=' + rows[i].id; + } + return result; + } else { + return "Error: No Table id set, you need to set an id on your table and every row"; + } + } +} + +jQuery.fn.extend( + { + tableDnD : jQuery.tableDnD.build + } + ); diff --git a/cms/static/js/main.js b/cms/static/js/main.js new file mode 100644 index 0000000000..45f94849f8 --- /dev/null +++ b/cms/static/js/main.js @@ -0,0 +1,159 @@ +$(document).ready(function(){ + $('section.main-content').children().hide(); + + $(function(){ + $('.editable').inlineEdit(); + $('.editable-textarea').inlineEdit({control: 'textarea'}); + }); + + // $("a[rel*=leanModal]").leanModal(); + + // $(".remove").click(function(){ + // $(this).parents('li').hide(); + // }); + + // $("#show-sidebar").click(function(){ + // $("#video-selector").toggleClass('hidden'); + // return false; + // }); + + // $('.use-video').click(function() { + // var used = $('#used'); + // if (used.is(':visible')) { + // used.hide().show('slow'); + // } + // used.show(); + // $('.no-video').hide(); + // }); + + // $('.remove-video').click(function() { + // $('#used').hide(); + // $('.no-video').show(); + // }); + + // $('#new-upload').click(function() { + // $('.selected-files').toggle(); + // return false; + // }); + + // /* $('.block').append('✕<\/a>'); */ + + // $('a.delete').click(function() { + // $(this).parents('.block').hide(); + // }); + + // $('.speed-list > li').hover(function(){ + // $(this).children('.tooltip').toggle(); + // }); + + // $('.delete-speed').click(function(){ + // $(this).parents('li.speed').hide(); + // return false; + // }); + + // $('.edit-captions').click(function(){ + // var parentVid = $(this).parents('div'); + // parentVid.siblings('div.caption-box').toggle(); + // return false; + // }); + + // $('.close-box').click(function(){ + // $(this).parents('.caption-box').hide(); + // return false; + // }); + + // $('ul.dropdown').hide(); + // $('li.questions').click(function() { + // $('ul.dropdown').toggle(); + // return false; + // }); + + // $('#mchoice').click(function(){ + // $('div.used').append($('
').load("/widgets/multi-choice.html")); + // return false; + // }); + + // $('#text').click(function(){ + // $('div.used').append($('
').load("/widgets/text.html")); + // return false; + // }); + + // $('#numerical').click(function(){ + // $('div.used').append($('
').load("/widgets/text-question.html")); + // return false; + // }); + + // $('#equation').click(function(){ + // $('div.used').append($('
').load("/widgets/latex-equation.html")); + // return false; + // }); + + // $('#script').click(function(){ + // $('div.used').append($('
').load("/widgets/script-widget.html")); + // return false; + // }); + + // $("#mark").markItUp(myWikiSettings); + + + var heighest = 0; + $('.cal ol > li').each(function(){ + heighest = ($(this).height() > heighest) ? $(this).height() : heighest; + + }); + + $('.cal ol > li').css('height',heighest + 'px'); + + $('.new-week').hide(); + $('.add-new-week').click(function() { + $(this).hide(); + $('.new-week').show(); + return false; + }); + + $('.new-week .close').click( function(){ + $(this).parents('.new-week').hide(); + $('p.add-new-week').show(); + return false; + }); + + var windowHeight = $(window).resize().height(); + + $('.sidebar').css('height', windowHeight); + + $('.edit-week').click( function() { + $('body').addClass('content'); + $('body.content .cal').css('height', windowHeight); + $('section.week-new').show(); + return false; + }); + + $('.cal ol li header h1 a').click( function() { + $('body').addClass('content'); + $('body.content .cal').css('height', windowHeight); + $('section.week-edit').show(); + return false; + }); + + + $('.video-new a').click(function(){ + $('section.video-new').show(); + return false; + }); + + $('.video-edit a').click(function(){ + $('section.video-edit').show(); + return false; + }); + + $('.problem-new a').click(function(){ + $('section.problem-new').show(); + return false; + }); + + $('.problem-edit a').click(function(){ + $('section.problem-edit').show(); + return false; + }); +}); + diff --git a/cms/static/js/markitup/jquery.markitup.js b/cms/static/js/markitup/jquery.markitup.js new file mode 100644 index 0000000000..10add9d27c --- /dev/null +++ b/cms/static/js/markitup/jquery.markitup.js @@ -0,0 +1,593 @@ +// ---------------------------------------------------------------------------- +// markItUp! Universal MarkUp Engine, JQuery plugin +// v 1.1.x +// Dual licensed under the MIT and GPL licenses. +// ---------------------------------------------------------------------------- +// Copyright (C) 2007-2011 Jay Salvat +// http://markitup.jaysalvat.com/ +// ---------------------------------------------------------------------------- +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// ---------------------------------------------------------------------------- +(function($) { + $.fn.markItUp = function(settings, extraSettings) { + var options, ctrlKey, shiftKey, altKey; + ctrlKey = shiftKey = altKey = false; + + options = { id: '', + nameSpace: '', + root: '', + previewInWindow: '', // 'width=800, height=600, resizable=yes, scrollbars=yes' + previewAutoRefresh: true, + previewPosition: 'after', + previewTemplatePath: '~/templates/preview.html', + previewParser: false, + previewParserPath: '', + previewParserVar: 'data', + resizeHandle: true, + beforeInsert: '', + afterInsert: '', + onEnter: {}, + onShiftEnter: {}, + onCtrlEnter: {}, + onTab: {}, + markupSet: [ { /* set */ } ] + }; + $.extend(options, settings, extraSettings); + + // compute markItUp! path + if (!options.root) { + $('script').each(function(a, tag) { + miuScript = $(tag).get(0).src.match(/(.*)jquery\.markitup(\.pack)?\.js$/); + if (miuScript !== null) { + options.root = miuScript[1]; + } + }); + } + + return this.each(function() { + var $$, textarea, levels, scrollPosition, caretPosition, caretOffset, + clicked, hash, header, footer, previewWindow, template, iFrame, abort; + $$ = $(this); + textarea = this; + levels = []; + abort = false; + scrollPosition = caretPosition = 0; + caretOffset = -1; + + options.previewParserPath = localize(options.previewParserPath); + options.previewTemplatePath = localize(options.previewTemplatePath); + + // apply the computed path to ~/ + function localize(data, inText) { + if (inText) { + return data.replace(/("|')~\//g, "$1"+options.root); + } + return data.replace(/^~\//, options.root); + } + + // init and build editor + function init() { + id = ''; nameSpace = ''; + if (options.id) { + id = 'id="'+options.id+'"'; + } else if ($$.attr("id")) { + id = 'id="markItUp'+($$.attr("id").substr(0, 1).toUpperCase())+($$.attr("id").substr(1))+'"'; + + } + if (options.nameSpace) { + nameSpace = 'class="'+options.nameSpace+'"'; + } + $$.wrap('
'); + $$.wrap('
'); + $$.wrap('
'); + $$.addClass("markItUpEditor"); + + // add the header before the textarea + header = $('
').insertBefore($$); + $(dropMenus(options.markupSet)).appendTo(header); + + // add the footer after the textarea + footer = $('
').insertAfter($$); + + // add the resize handle after textarea + if (options.resizeHandle === true && $.browser.safari !== true) { + resizeHandle = $('
') + .insertAfter($$) + .bind("mousedown", function(e) { + var h = $$.height(), y = e.clientY, mouseMove, mouseUp; + mouseMove = function(e) { + $$.css("height", Math.max(20, e.clientY+h-y)+"px"); + return false; + }; + mouseUp = function(e) { + $("html").unbind("mousemove", mouseMove).unbind("mouseup", mouseUp); + return false; + }; + $("html").bind("mousemove", mouseMove).bind("mouseup", mouseUp); + }); + footer.append(resizeHandle); + } + + // listen key events + $$.keydown(keyPressed).keyup(keyPressed); + + // bind an event to catch external calls + $$.bind("insertion", function(e, settings) { + if (settings.target !== false) { + get(); + } + if (textarea === $.markItUp.focused) { + markup(settings); + } + }); + + // remember the last focus + $$.focus(function() { + $.markItUp.focused = this; + }); + } + + // recursively build header with dropMenus from markupset + function dropMenus(markupSet) { + var ul = $('
    '), i = 0; + $('li:hover > ul', ul).css('display', 'block'); + $.each(markupSet, function() { + var button = this, t = '', title, li, j; + title = (button.key) ? (button.name||'')+' [Ctrl+'+button.key+']' : (button.name||''); + key = (button.key) ? 'accesskey="'+button.key+'"' : ''; + if (button.separator) { + li = $('
  • '+(button.separator||'')+'
  • ').appendTo(ul); + } else { + i++; + for (j = levels.length -1; j >= 0; j--) { + t += levels[j]+"-"; + } + li = $('
  • '+(button.name||'')+'
  • ') + .bind("contextmenu", function() { // prevent contextmenu on mac and allow ctrl+click + return false; + }).click(function() { + return false; + }).bind("focusin", function(){ + $$.focus(); + }).mouseup(function() { + if (button.call) { + eval(button.call)(); + } + setTimeout(function() { markup(button) },1); + return false; + }).hover(function() { + $('> ul', this).show(); + $(document).one('click', function() { // close dropmenu if click outside + $('ul ul', header).hide(); + } + ); + }, function() { + $('> ul', this).hide(); + } + ).appendTo(ul); + if (button.dropMenu) { + levels.push(i); + $(li).addClass('markItUpDropMenu').append(dropMenus(button.dropMenu)); + } + } + }); + levels.pop(); + return ul; + } + + // markItUp! markups + function magicMarkups(string) { + if (string) { + string = string.toString(); + string = string.replace(/\(\!\(([\s\S]*?)\)\!\)/g, + function(x, a) { + var b = a.split('|!|'); + if (altKey === true) { + return (b[1] !== undefined) ? b[1] : b[0]; + } else { + return (b[1] === undefined) ? "" : b[0]; + } + } + ); + // [![prompt]!], [![prompt:!:value]!] + string = string.replace(/\[\!\[([\s\S]*?)\]\!\]/g, + function(x, a) { + var b = a.split(':!:'); + if (abort === true) { + return false; + } + value = prompt(b[0], (b[1]) ? b[1] : ''); + if (value === null) { + abort = true; + } + return value; + } + ); + return string; + } + return ""; + } + + // prepare action + function prepare(action) { + if ($.isFunction(action)) { + action = action(hash); + } + return magicMarkups(action); + } + + // build block to insert + function build(string) { + var openWith = prepare(clicked.openWith); + var placeHolder = prepare(clicked.placeHolder); + var replaceWith = prepare(clicked.replaceWith); + var closeWith = prepare(clicked.closeWith); + var openBlockWith = prepare(clicked.openBlockWith); + var closeBlockWith = prepare(clicked.closeBlockWith); + var multiline = clicked.multiline; + + if (replaceWith !== "") { + block = openWith + replaceWith + closeWith; + } else if (selection === '' && placeHolder !== '') { + block = openWith + placeHolder + closeWith; + } else { + string = string || selection; + + var lines = selection.split(/\r?\n/), blocks = []; + + for (var l=0; l < lines.length; l++) { + line = lines[l]; + var trailingSpaces; + if (trailingSpaces = line.match(/ *$/)) { + blocks.push(openWith + line.replace(/ *$/g, '') + closeWith + trailingSpaces); + } else { + blocks.push(openWith + line + closeWith); + } + } + + block = blocks.join("\n"); + } + + block = openBlockWith + block + closeBlockWith; + + return { block:block, + openWith:openWith, + replaceWith:replaceWith, + placeHolder:placeHolder, + closeWith:closeWith + }; + } + + // define markup to insert + function markup(button) { + var len, j, n, i; + hash = clicked = button; + get(); + $.extend(hash, { line:"", + root:options.root, + textarea:textarea, + selection:(selection||''), + caretPosition:caretPosition, + ctrlKey:ctrlKey, + shiftKey:shiftKey, + altKey:altKey + } + ); + // callbacks before insertion + prepare(options.beforeInsert); + prepare(clicked.beforeInsert); + if ((ctrlKey === true && shiftKey === true) || button.multiline === true) { + prepare(clicked.beforeMultiInsert); + } + $.extend(hash, { line:1 }); + + if ((ctrlKey === true && shiftKey === true)) { + lines = selection.split(/\r?\n/); + for (j = 0, n = lines.length, i = 0; i < n; i++) { + if ($.trim(lines[i]) !== '') { + $.extend(hash, { line:++j, selection:lines[i] } ); + lines[i] = build(lines[i]).block; + } else { + lines[i] = ""; + } + } + string = { block:lines.join('\n')}; + start = caretPosition; + len = string.block.length + (($.browser.opera) ? n-1 : 0); + } else if (ctrlKey === true) { + string = build(selection); + start = caretPosition + string.openWith.length; + len = string.block.length - string.openWith.length - string.closeWith.length; + len = len - (string.block.match(/ $/) ? 1 : 0); + len -= fixIeBug(string.block); + } else if (shiftKey === true) { + string = build(selection); + start = caretPosition; + len = string.block.length; + len -= fixIeBug(string.block); + } else { + string = build(selection); + start = caretPosition + string.block.length ; + len = 0; + start -= fixIeBug(string.block); + } + if ((selection === '' && string.replaceWith === '')) { + caretOffset += fixOperaBug(string.block); + + start = caretPosition + string.openWith.length; + len = string.block.length - string.openWith.length - string.closeWith.length; + + caretOffset = $$.val().substring(caretPosition, $$.val().length).length; + caretOffset -= fixOperaBug($$.val().substring(0, caretPosition)); + } + $.extend(hash, { caretPosition:caretPosition, scrollPosition:scrollPosition } ); + + if (string.block !== selection && abort === false) { + insert(string.block); + set(start, len); + } else { + caretOffset = -1; + } + get(); + + $.extend(hash, { line:'', selection:selection }); + + // callbacks after insertion + if ((ctrlKey === true && shiftKey === true) || button.multiline === true) { + prepare(clicked.afterMultiInsert); + } + prepare(clicked.afterInsert); + prepare(options.afterInsert); + + // refresh preview if opened + if (previewWindow && options.previewAutoRefresh) { + refreshPreview(); + } + + // reinit keyevent + shiftKey = altKey = ctrlKey = abort = false; + } + + // Substract linefeed in Opera + function fixOperaBug(string) { + if ($.browser.opera) { + return string.length - string.replace(/\n*/g, '').length; + } + return 0; + } + // Substract linefeed in IE + function fixIeBug(string) { + if ($.browser.msie) { + return string.length - string.replace(/\r*/g, '').length; + } + return 0; + } + + // add markup + function insert(block) { + if (document.selection) { + var newSelection = document.selection.createRange(); + newSelection.text = block; + } else { + textarea.value = textarea.value.substring(0, caretPosition) + block + textarea.value.substring(caretPosition + selection.length, textarea.value.length); + } + } + + // set a selection + function set(start, len) { + if (textarea.createTextRange){ + // quick fix to make it work on Opera 9.5 + if ($.browser.opera && $.browser.version >= 9.5 && len == 0) { + return false; + } + range = textarea.createTextRange(); + range.collapse(true); + range.moveStart('character', start); + range.moveEnd('character', len); + range.select(); + } else if (textarea.setSelectionRange ){ + textarea.setSelectionRange(start, start + len); + } + textarea.scrollTop = scrollPosition; + textarea.focus(); + } + + // get the selection + function get() { + textarea.focus(); + + scrollPosition = textarea.scrollTop; + if (document.selection) { + selection = document.selection.createRange().text; + if ($.browser.msie) { // ie + var range = document.selection.createRange(), rangeCopy = range.duplicate(); + rangeCopy.moveToElementText(textarea); + caretPosition = -1; + while(rangeCopy.inRange(range)) { + rangeCopy.moveStart('character'); + caretPosition ++; + } + } else { // opera + caretPosition = textarea.selectionStart; + } + } else { // gecko & webkit + caretPosition = textarea.selectionStart; + + selection = textarea.value.substring(caretPosition, textarea.selectionEnd); + } + return selection; + } + + // open preview window + function preview() { + if (!previewWindow || previewWindow.closed) { + if (options.previewInWindow) { + previewWindow = window.open('', 'preview', options.previewInWindow); + $(window).unload(function() { + previewWindow.close(); + }); + } else { + iFrame = $(''); + if (options.previewPosition == 'after') { + iFrame.insertAfter(footer); + } else { + iFrame.insertBefore(header); + } + previewWindow = iFrame[iFrame.length - 1].contentWindow || frame[iFrame.length - 1]; + } + } else if (altKey === true) { + if (iFrame) { + iFrame.remove(); + } else { + previewWindow.close(); + } + previewWindow = iFrame = false; + } + if (!options.previewAutoRefresh) { + refreshPreview(); + } + if (options.previewInWindow) { + previewWindow.focus(); + } + } + + // refresh Preview window + function refreshPreview() { + renderPreview(); + } + + function renderPreview() { + var phtml; + if (options.previewParser && typeof options.previewParser === 'function') { + var data = options.previewParser( $$.val() ); + writeInPreview( localize(data, 1) ); + } else if (options.previewParserPath !== '') { + $.ajax({ + type: 'POST', + dataType: 'text', + global: false, + url: options.previewParserPath, + data: options.previewParserVar+'='+encodeURIComponent($$.val()), + success: function(data) { + writeInPreview( localize(data, 1) ); + } + }); + } else { + if (!template) { + $.ajax({ + url: options.previewTemplatePath, + dataType: 'text', + global: false, + success: function(data) { + writeInPreview( localize(data, 1).replace(//g, $$.val()) ); + } + }); + } + } + return false; + } + + function writeInPreview(data) { + if (previewWindow.document) { + try { + sp = previewWindow.document.documentElement.scrollTop + } catch(e) { + sp = 0; + } + previewWindow.document.open(); + previewWindow.document.write(data); + previewWindow.document.close(); + previewWindow.document.documentElement.scrollTop = sp; + } + } + + // set keys pressed + function keyPressed(e) { + shiftKey = e.shiftKey; + altKey = e.altKey; + ctrlKey = (!(e.altKey && e.ctrlKey)) ? (e.ctrlKey || e.metaKey) : false; + + if (e.type === 'keydown') { + if (ctrlKey === true) { + li = $('a[accesskey="'+String.fromCharCode(e.keyCode)+'"]', header).parent('li'); + if (li.length !== 0) { + ctrlKey = false; + setTimeout(function() { + li.triggerHandler('mouseup'); + },1); + return false; + } + } + if (e.keyCode === 13 || e.keyCode === 10) { // Enter key + if (ctrlKey === true) { // Enter + Ctrl + ctrlKey = false; + markup(options.onCtrlEnter); + return options.onCtrlEnter.keepDefault; + } else if (shiftKey === true) { // Enter + Shift + shiftKey = false; + markup(options.onShiftEnter); + return options.onShiftEnter.keepDefault; + } else { // only Enter + markup(options.onEnter); + return options.onEnter.keepDefault; + } + } + if (e.keyCode === 9) { // Tab key + if (shiftKey == true || ctrlKey == true || altKey == true) { + return false; + } + if (caretOffset !== -1) { + get(); + caretOffset = $$.val().length - caretOffset; + set(caretOffset, 0); + caretOffset = -1; + return false; + } else { + markup(options.onTab); + return options.onTab.keepDefault; + } + } + } + } + + init(); + }); + }; + + $.fn.markItUpRemove = function() { + return this.each(function() { + var $$ = $(this).unbind().removeClass('markItUpEditor'); + $$.parent('div').parent('div.markItUp').parent('div').replaceWith($$); + } + ); + }; + + $.markItUp = function(settings) { + var options = { target:false }; + $.extend(options, settings); + if (options.target) { + return $(options.target).each(function() { + $(this).focus(); + $(this).trigger('insertion', [options]); + }); + } else { + $('textarea').trigger('insertion', [options]); + } + }; +})(jQuery); diff --git a/cms/static/js/markitup/sets/wiki/images/bold.png b/cms/static/js/markitup/sets/wiki/images/bold.png new file mode 100644 index 0000000000000000000000000000000000000000..889ae80e37b6167cc15f2a89e05a183815ec18b2 GIT binary patch literal 304 zcmV-00nh%4P)b^}|6b=Y6y(;Y{!a!g z@UQp#@Aw}>L3(}s|7f5BUjeuKZvQRjV<2U7yvu*H{aAbvQ6K!@3oKzW z-{Qa8d3gae1^)HE{~f^!v<1}u>;4xnKvUpW540I-w9J3a{{r=B3T*2g{_BH1CtaZO zpZ`6V0*V5g1e5i;`_=Z#_e=H*@8|93RG@lX;D!K7TKswwko8{x0000LlS^n5Q5c2KWM*m}YV3eAws|yDY&wl?DsIf8f*>k% z<*Hc~7ec_5rHi-`yO3f)SBkFeMi8|t(GpA*#H3ciSWBp+c{r^&PVFO&3C&}i$+#MA zZCZ-`mgm3cyWAfxp=la+gJD~$f3lF*v;Jqx`Q8T$Z@WYD zo5ULg-eG}Z89L0f4Zd=*mecc6xt8;=w2x)zu;*%)sVnHZF8Gfgv50&V?aO?Vg=C`G zqt8;QwVsqD3w#sp>uk4(?#r`&eMV@ShR&fc4IOUt64yxY7gl5?KAAs}zQf1Y-^2&g z$Gu&9-`V$(R7R2uQ}2HsoE!1T1^`G5aq_1Rg+NBCRv6kqwQ}ZBXMdtuFS`d*&78wH z%FqTXD^)8WCsqSp-l}5wzGHH~3TMv4``ZJRQO3=(w6oCI`;E z`gMDg;9p*xrm`moLYyi48W1M{s};+X6Y)q)IQvrJsBPN-$Qt1?9(Dn}gMTvW8Vj;U zv~1YHR;Z*VmZrvRmZz6cE&o6XK(RnVCGj2D!Cx>lhwjfzzEPx#2?dhIYK}l!BvcK! z3)ER+Jz{5>jf6w4x#gTU%_MMNlkNp$oSbvBp&uHw9M;u0-4@=t5BI zP6Hx#-C_{5RMJ z0_P+Xkumexn8%)S+Y)#l(gR;YJP<6#1-=jjK0LONWPdJQIR8uK1HpvVIxBIQ2ztt+ zqoEx_X9S%QGMe=~(k#sebCL-an)%CR%a7YtUOQUgv+G>~?N~XSWhx=? z@$fx}0MB;$`JWcQ-Re{XV~5|{DvU(#*+NF*g)j^qk#b~G9_O!i*y&mZVZ=a3;Go(K z`DkskYn56Nhu+k@1Ke*uY|x zI&k6j$JfNe_a{GH%=n2rZOz$Z8R9V?Pe36hIk}jo+A-`;dt9vyvBu#Xm@veu&@v`| zzt%mwc_$nd0-sMVx2d)b0!MqGxmfCumx7yB#nIUWvA{!HOMfslMyW1iV&nY>zxwyj z8^JfLN|kT z4m^Q1mhO(_r4w@`V?H=YNkOf(i&bHT3Auc3bryK1_{hDSetLoLN{VLB^78ULiNFy^ zkUqqG$fjVkJj5tfWkOn|P5`HVEp5@-mGnc0wvJGHC=+39MC2TWT#i?t*~fNch*he_ zgtS^8dH$(KlW)EF1b4Fzv~?&0IQaNdg;W5&{t&Bmg9&N1-rBBr_;Rg8ekw^mn;@T# zlS{|Rq+-Nlg18i%UY;i|q1NnSwf>I@85#4U4002ovPDHLkV1mEDi4_0< literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/h1.png b/cms/static/js/markitup/sets/wiki/images/h1.png new file mode 100644 index 0000000000000000000000000000000000000000..9c122e91e358860733eaf08fd543e5fc585d4cfd GIT binary patch literal 276 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$x=IP=XqH%ujg^j!|20W|*-XbO@ zAtBSd^khRzd{=pNv@vN)9uZDqGTXuVPf6#I=?;x2B`Y;1YQMI>>GxvE??vtn{c>{A z7MYxUVrui2JTF|YR&ldntL8M3{q7no52$4`vIX;r@S8@YTFOM5*~nV4Gk-UYI5L$} zDw(h9UDksmVjphbEsSQ?UdGUxU4Htk{EZoY&3@-Y5*_;Wwk`ZAkg@!FaC~ii{N>;; VD%(>GD}XL$@O1TaS?83{1OVXtVO9VD literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/h2.png b/cms/static/js/markitup/sets/wiki/images/h2.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd87657fbe001c0a78fb095284fffc32e739497 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$ROXGNhtQ`{C(%$wdeB3 zGTnLdz~IMJtNg?T>Z(s;oVU0)KW5x*9xvq)rPF;` zY|Fc4&#rLa>Txf#@y+aKf+ac0%`STzAI(*qdYo^XFH557y+_x*JpKO5^1S9?c^6}{ zP=+&OVHtDUrGNmdKI;Vst0KHFj AM*si- literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/h3.png b/cms/static/js/markitup/sets/wiki/images/h3.png new file mode 100644 index 0000000000000000000000000000000000000000..c7836cf09e4565cc76c13bd14c13971c9e093c40 GIT binary patch literal 306 zcmV-20nPr2P)wEzGB literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/h4.png b/cms/static/js/markitup/sets/wiki/images/h4.png new file mode 100644 index 0000000000000000000000000000000000000000..4e929eaf583f10cf50eb1666ff6530b9d4cc7915 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-&H?&;zfqH%ujg^j$;1_G=B-XbO( zon2btF4r#xDw?mmvtwaL3R_~+Yz66*rrQmiHySwec#9a-mn?F*9`VcnzTNDrbNY9( zxOA;CUb9S|sk-b7=Pc<<>lMA8+|AaBj(NoLhh2`-;+F zenR#-z6;(Syt?;Ub#DVE_OC literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/h5.png b/cms/static/js/markitup/sets/wiki/images/h5.png new file mode 100644 index 0000000000000000000000000000000000000000..30cabebf7445e168a0f31b0ed68c43d54eaf017d GIT binary patch literal 304 zcmV-00nh%4P)ZVg9Hj*!Zw zxkAM~zCH&l><=6QeDdgV4l9hop+%GWq_IPV?Z641X8iiHrWJUN^2}hSiGjhsfbOLp z?d`9_MC0P3jVAVsE0oSgT$J*kO*Aq9I~CW*s{G*(t$KS{OS+#aO%?udUme<*TTEO`Fr@r_QT zk=#}u-n~>Vm!+9S1PE{@3<)G~CPb<$Za;W?3+O}|+q)?*Pn355=}S(XIZmEANjZci zf5 zj<%@MX^bD1^BwlS^+AD|$dm-1wial0hwPI;CDM?Y9SXW#@w-UF0SQ8OgplRTleOB2 zUjkDS|0U9pI|lSN*EvXUa~*UIclJdZ#)Npbwh9>YT?Z;=B8|l&^t~P~om?<5Lre$+ z;%`P>SL7`djY#8Y9$wv9dv|3p)C#5QQ<|d}62BjvZR2H60wE-$B^mK6y(Kw&{<9vg>Q9!g~ne(gm zmj4swoA@7?D86%i^8WzK9JM17E&sp&Z#dpHfz$E-U9ks&4?Z9Gyg!%0k2Q{M-Tz#> z2OnD>vrPZ*#{EHKLq)>Jcx{H|Ovdb&|4aQZWSipI{El%e^Cxx{^9vSw28s;a3IDB= TS1%U=TF&6<>gTe~DWM4fm>N^1 literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/link.png b/cms/static/js/markitup/sets/wiki/images/link.png new file mode 100644 index 0000000000000000000000000000000000000000..25eacb7c2524142262d68bf729c5e2b61adfd6d4 GIT binary patch literal 343 zcmV-d0jU0oP)$`dXYaZs9=SbAto%g@>T~?_bH&lTUn@`uo|1bXE{eSR(AO)ESb=V4`uk}mK|39Px&03WLbv~pzk+s7D@lK^ zn+aB+sp)&Y_x-B3>;6ywU--WQNUr<8>TU0P-|L#1U&;A)67w(+> pDf@fM7q9#F25QXo3rUI;002ro52U44e~JJA002ovPDHLkV1l;_q@Mr) literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/list-bullet.png b/cms/static/js/markitup/sets/wiki/images/list-bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8672bde48f806d3d4d37db192588a9aa3eac10 GIT binary patch literal 344 zcmV-e0jK_nP)PbXFR5;6H z`2YVu10|S&DhA}te_Swi*Xsu$nk)lAnzx?+_#Z@r_~qs0*+Bfiq@?73K|#U)?Ck9S zsi~>|6A}{sM@B~e4-O9gPhA%bd?2RGd{of6>E(lvp1b6Ep>6&12TPB<{a?EDDL4@0 z;^ML+A|n0=1_u83^78uc?CkvC#>VEqiHXU7U0vP(YHDhzff&$ntDt1@;|H#l*M@2! z+U8#>h@W=vfpy*`^1J}j+`sMRe-I7g8yOj8Yin!&S5Z;VicFNp}SURRVGD{CSNFe~ni^^#wyl5uzj4je z|23%2?k#{x(*%mqe9M%mih+W%ElRQ}7!$^91> z7ymCLB=nz$hvz>#JNtiTW@gkt1Zf0eyP_){bPq%T_kY#2Z7&xs00000NkvXXu0mjf DNYA0= literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/picture.png b/cms/static/js/markitup/sets/wiki/images/picture.png new file mode 100644 index 0000000000000000000000000000000000000000..4a158fef7e0da8fd19525f574f2c4966443866cf GIT binary patch literal 606 zcmV-k0-^nhP)Q2rnAt>LM%-F zK|rtwgcU)}7x~z1Hrcs5bH*ZO$!>xO8K#?==bZPQ_ecnV>#P`H`QzGaRhd62G_&rC zTLU$c7_x*nFP_dW#Q+*);mMHE?j)HexK784D4x9l_tfpz2$@1y}9rkF+ zI+J5NMWeZyObc!d+rUc=>D+uOdAOg#%+Ej6h+wn5^xPmVVH*Eu446Y0A_@ zo$rlds-+sL10DbHs{AQG2a)rMyf zFQK~pm1x3+7!nu%-M`k}``c>^00{o_1pjWJUTfl8mg=3qGEl8H@}^@w`VUx0_$uy4 z2FhRqKX}xI*?Tv1DJd8z#F#0c%*~rM30HE1@2o5m~}ZyoWhqv>ql{V z1ZGE0lgcoK^lx+eqc*rAX1Ky;Xx3U%u#zG!m-;eD1Qsn@kf3|F9qz~|95=&g3(7!X zB}JAT>RU;a%vaNOGnJ%e1=K6eAh43c(QN8RQ6~GP%O}Jju$~Ld*%`mO1p?P)vCi#|P&Xm-dkucwL z3)87{8iWe96huvPHfK`KOdC2Z({T6vJ9pwDx$D4>d(Pqff6w7Lmj{5i6;ZyPPpPN; zroaW=6d#@oL2Fa53F~$Su10(RG%K0p3VTuP3?Z=nBA8z$uq+XLUL^QrC74`bU|!e| zr>hK{)%Q!vdmIO5Z3JIvaOyjOX`X@c8-ua03`Q&)f&%p*{(A$q`ZTTjk%q_T7>v^J zu!R-a9fFLScYlKkNBP_Cob=9m9JLVoC-?c{)eOtMnh7qNN{ejy2sM{pS^mgFHJm@(buuM4>=<5Vr$&Kzw{B?uPr; z(1Yf=#g)zADkWnx=MR%ykl| z3Ui42k+O2{bCn)01-s5Sxp|z{G2di&KT(_M6;$EI zDL57JFf}cw4bP1P$pgTRKH$0@h|~aA>j`qZ2*kU5t2EVD5#~@VNhqx{vz8ethDD-=+1vnemftUBA zF;N!Q%PBB5B=KLB#QO(CHe?;R+-C8M?ppDW>R$5`cCPq@YpusFRTaH1i9Kv;l<>I( Ze*oTy+;kdDB`N>_002ovPDHLkV1l3CM+g7_ literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/images/stroke.png b/cms/static/js/markitup/sets/wiki/images/stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..612058a78eba4e3ca259aa13417fd60cd6cf2fbd GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-%c@9E+gqH#X?$N2_%qkv$?&V++! z-fX$@sb%KG-5a$|R8J4(Tcmu7`G0cYgxS-++3#dN^zA^J(=P8r|6lx9G_LLaZ+v5S z!d9iC@)!RP{FnQmdw#}3Ee(C$|NPA#|L6YmR@fBO9w|C7oMMQ$vc`Tu(RETvC<`(>OPJ!ieM-fH~m>7>j(^b;|pVbz=yzjpaJj$ zhrpuriKefui_0DvN;1Ymq&%nwWg*IrK!Xz^eJWuq3u2H~0ra?EC@ge%+`A>6mV z9{TYo{=G6 zt@5m|4G+Q2zKv;Ch@O;`PfWArmB5n3gvMsxV&Iu>97{a!2kL74wd@!f_AP^O%_&ND zm}1c*+F;TcH^{p$P_|akvD5o7vmT>HCkP;z;;&+8tDBI;koi9eX`W!oH4`pYaHlFZwV;$>vvfQTw zM-`m&R_SPIBa^FUasC0GCCh%{h`$~db`z&-lFX#%(f>H6JD6Z(sIW`RKE+xOL+?+uQ%q z){?+F%=6pqEH{6=NzusC-*<`PZYiLCGyKD}Z8^V8ul-K=AV@SE1t4~D2*b1(9UUc= zN-;Dv#Ngl{rd7e$ZUPXC##BFmV>$26ZQi?6Po#@{4gllsPbku3Vq${Y+FAf~T}OJb zGWEz9{(zcvI&CUaN&p7GcqMG4&7ULx##68M4k(F4l7Q+Xm&>uSv4N&(w6?a=)YOC{ zoYLN-J?7@-9xGBx007$C+kK7w_2Z$(k&l}jo2#`dO;J#Ipsbc$pS#^Dy3Q&nSeE5x fGMT)t>sS8=`naU3reLNz00000NkvXXu0mjf)bGN+ literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/sets/wiki/set.js b/cms/static/js/markitup/sets/wiki/set.js new file mode 100644 index 0000000000..895625621d --- /dev/null +++ b/cms/static/js/markitup/sets/wiki/set.js @@ -0,0 +1,34 @@ +// ---------------------------------------------------------------------------- +// markItUp! +// ---------------------------------------------------------------------------- +// Copyright (C) 2008 Jay Salvat +// http://markitup.jaysalvat.com/ +// ---------------------------------------------------------------------------- +myWikiSettings = { + nameSpace: "wiki", // Useful to prevent multi-instances CSS conflict + previewParserPath: "~/sets/wiki/preview.php", + onShiftEnter: {keepDefault:false, replaceWith:'\n\n'}, + markupSet: [ + {name:'Heading 1', key:'1', openWith:'== ', closeWith:' ==', placeHolder:'Your title here...' }, + {name:'Heading 2', key:'2', openWith:'=== ', closeWith:' ===', placeHolder:'Your title here...' }, + {name:'Heading 3', key:'3', openWith:'==== ', closeWith:' ====', placeHolder:'Your title here...' }, + {name:'Heading 4', key:'4', openWith:'===== ', closeWith:' =====', placeHolder:'Your title here...' }, + {name:'Heading 5', key:'5', openWith:'====== ', closeWith:' ======', placeHolder:'Your title here...' }, + {separator:'---------------' }, + {name:'Bold', key:'B', openWith:"'''", closeWith:"'''"}, + {name:'Italic', key:'I', openWith:"''", closeWith:"''"}, + {name:'Stroke through', key:'S', openWith:'', closeWith:''}, + {separator:'---------------' }, + {name:'Bulleted list', openWith:'(!(* |!|*)!)'}, + {name:'Numeric list', openWith:'(!(# |!|#)!)'}, + {separator:'---------------' }, + {name:'Picture', key:'P', replaceWith:'[[Image:[![Url:!:http://]!]|[![name]!]]]'}, + {name:'Link', key:'L', openWith:'[[![Link]!] ', closeWith:']', placeHolder:'Your text to link here...' }, + {name:'Url', openWith:'[[![Url:!:http://]!] ', closeWith:']', placeHolder:'Your text to link here...' }, + {separator:'---------------' }, + {name:'Quotes', openWith:'(!(> |!|>)!)'}, + {name:'Code', openWith:'(!(|!|
    )!)', closeWith:'(!(|!|
    )!)'}, + {separator:'---------------' }, + {name:'Preview', call:'preview', className:'preview'} + ] +} diff --git a/cms/static/js/markitup/sets/wiki/style.css b/cms/static/js/markitup/sets/wiki/style.css new file mode 100644 index 0000000000..0a26c698f5 --- /dev/null +++ b/cms/static/js/markitup/sets/wiki/style.css @@ -0,0 +1,57 @@ +/* ------------------------------------------------------------------- +// markItUp! +// By Jay Salvat - http://markitup.jaysalvat.com/ +// ------------------------------------------------------------------*/ +.wiki .markItUpButton1 a { + background-image:url(images/h1.png); +} +.wiki .markItUpButton2 a { + background-image:url(images/h2.png); +} +.wiki .markItUpButton3 a { + background-image:url(images/h3.png); +} +.wiki .markItUpButton4 a { + background-image:url(images/h4.png); +} +.wiki .markItUpButton5 a { + background-image:url(images/h5.png); +} + +.wiki .markItUpButton6 a { + background-image:url(images/bold.png); +} +.wiki .markItUpButton7 a { + background-image:url(images/italic.png); +} +.wiki .markItUpButton8 a { + background-image:url(images/stroke.png); +} + +.wiki .markItUpButton9 a { + background-image:url(images/list-bullet.png); +} +.wiki .markItUpButton10 a { + background-image:url(images/list-numeric.png); +} + +.wiki .markItUpButton11 a { + background-image:url(images/picture.png); +} +.wiki .markItUpButton12 a { + background-image:url(images/link.png); +} +.wiki .markItUpButton13 a { + background-image:url(images/url.png); +} + +.wiki .markItUpButton14 a { + background-image:url(images/quotes.png); +} +.wiki .markItUpButton15 a { + background-image:url(images/code.png); +} + +.wiki .preview a { + background-image:url(images/preview.png); +} diff --git a/cms/static/js/markitup/skins/simple/images/handle.png b/cms/static/js/markitup/skins/simple/images/handle.png new file mode 100644 index 0000000000000000000000000000000000000000..3993b20337e33a36c9125d139f1f53a279a4c128 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^qCm{X#0(?@t!)i}6mzkYX9x!e$L)vy4}e^r0G|-o z4LkP#|NsB{_wSo_?oZ!X{TC?CQWE4B{GZ|f|H~VSrU3bz1s;*b3=G^tAk28_ZrvZC zpje4(M2T}zYGO%dex5=|W^O8jfw{hsp}v86ds2l5P=!25MR0yvNqJ&XDuZuga#4P6 zYD#9Jf?H-$YI%N9cCmuR){ILPK&1wrE{-7_Gm{H=1WS1m6Eb=Pa(faIBBVrjnVf1= soMNglKFuA6ghUcX`bbD&-ZPgg&ebxsLQ0Hz~TmH+?% literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/skins/simple/images/menu.png b/cms/static/js/markitup/skins/simple/images/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..44a07afd30f499cdba30847094a1e92f13e1320e GIT binary patch literal 27151 zcmb@uby!=^voH>&K#@Wz#kHjZ#i7NeKyfI=U0STTJ0Vbtw8dQ$C`DS_-AZu@9z1yP z;DID0FQ0qw_ul(^|9Ic`xzBH(J!dvMJ9~C!&(6-AIVWG=zf-U&MM|6AsRp$5n7ToocSJk_Bn}YAzMBn}yzS-~r2X-X~Pg_4XPbV*SvHDMw{Bi(&-4O^6FCut;Y6b0D#;nkADCyhU-M0NFxfj4cv3?psgw0C*dj%d|)lI1h@ztu& zMbd4Jii?xH5^7}#dt1KyuNMDP$n{e=9}ka((#1(n_jZitrhbb`Nqsi!b4j-I-@Jr^ z#yXC3q+8EY$X)LaDr-M_)|>J-A%z#-OMI6ko7#A2C(Ymg3h0OHHP9D*lQ)5fAKf^Y z;QTteue|d&_IFDQqg04~C!45bP(iiGE+$xr(bTosz++@EzzM=qn%r3C?=|h-pCuwL zwWmElk7(KWCbR1eQF?DK8%ox6F6_`~g4_sX!X6x6y7mo4P8u?A@P5C}J zF(^2Vx$nrAucR9ix{!EE%D|z$Y){E+7RJ0uNgOgVdPP_7$yAf_}8@Rh8|OF>H8i3%)Vc8iqyV)?{~@s zsxp55an_BT7fRG-i;ijGv;leAP^37GXTVY@-}{x4Q&$Tzi1gfb*5eY1qyD3$InG!- zR3&&m5?GK)&YInAE$siB9P~20SYo#U7}k3JVwsPBu{hLZK9VJvgC$gwxl4MBv}Kv5 z>s~HJi}mpUdxrZ5d0qY&`3}WzbjRzSz9Q7Dg_k4<(Dv^iRD7NiVGA9>-aLh@)2X2p`N|irxPxg2^#m_a?>_EPu)0B7b%fktFm5jpgyng&B z`TZm`C9JNw;9UNjPB_YhP2)q2NICaA)RM=nLbsGcFRcGfIsMPcq0$C=A%#%OroCeE;fwSw0n;NO&Z76$e@5N>elO-UTt| zJPXs+GY+o9@WD5ZuPF*wv>2tnzqezQffMOg77}=6hc^?G`pI?wLWVT0C67C4bIJa> zO!Z7<&RbhH5Z000&vLXwG}@*% zde0v5W*WyN(|q{TR_}x@!X5jaVh*X7cN2{5@Eibgjcq&NApy@MiI=~7otK};ldsx% z6ueQZ$IJWCvgJ43#!^R657^5~|LRyYvt4R`#=^&^1nF3#Z^t)v=Tj-dabB3|PEtkYAI}!h)?46{X9+Tm9_)iJGG*iL zSwhd05|%Pd*In3aM)*%eBr+d|FjHjMhNV0Ajhx*5Sr;d ze-wlnpKQh$74HrNrUr4n;#B>bmOngRLvKQ>F#ORxlZxH-+&rJVAa4Q9d_R zDJ;*SANeshUxm*EPw^t3GmBiY=%Wt3$GZm})70)fX99Rl`aExSHdH)&Yn!%>?U*yL zBRRxH-0@Pq+_v#WQ7l_fo_4WRgQ7FgmZNGe!+js%5k{FP5>-WP^Wu|3#^F~(t@y{; zDL=xfd8K1!RPXda^L{?t%5b}%y8ntao6xFUN`pEnu~pvdl(l@|QSkEjfxoTsgL5oN zOxUp24Yq-s=CrZKwR!8t-p#bYmeUMh%QbUzff?pU+R zRhul|dnFnco@880?Yr{TZxdrI!0;LWk~~tB{c%KGh9z&rR8){dYNHwoHOkq6j)dC?rME_pTeglmw`TOAM-vNp)VTPA-{@w^W`*e zJo1&#Y<#OG@&4vu5L50{s7@)l(_?~H(tW!`pg?^_nICHt%v6_sxsh~|FN=wdjyxD@ zJYE!z_C1@m3m5!8yyxla)3daEz5JKq2@&z9o9g~1^kWIoW=0){_k)3z zy{&G6k8*6RT?Zt}1!jJ$RqISGvaT<38UVa}S4SH+K4 zL9Hq+_EhbQE3w3L%qOt36s;_Mq`u!qKGDJ8DJ^|CydLzNZ=9Xn7GJa3@2}u%n+ys4 zX7S3CYWrB?NGDn*<;020b?1v;RLFH#`2$YYzrQN4`9mk3YjGJ%4+q#+3mZM!rXo}- zVzBe<3QGF@;>hjvMzaAHl~m>v!L(00^`25kXIx=h%y$34jRaz||FB{F*EXx}u3b|j zvhbJhXE0YipNDIZS&FpO_hJ1+%wR@vjd18*68QzoeP5{;*dU5^C(k*Sp&uRj`T+Wc zzlCp~Wv%r}-E5MmIE=)NrWfZ@3)vCt<_Lh3_#fRb%*gD^U5aLsf>Epe$%ujle2UP1 z3ICkXsG7xcd|vuNS+?K}t4A2U8b^PQ?K4eJo2+}ks4m5O81Dl_MHd3~AGS!?ywj!4 z5h)f6`^K68yABfisPi4n2c8bs$kx0QH@)tpkm)m-MmnE{&t3y3hd{^)d^X_#55U#K>3@gz5oOl@nJktS8^la$#7rz+*o!YhQ%NBiF)mI zfGAMg{wEUjnXKt1p9&1__ivm3nn}>|yJyT4mvhIKqNOPzkTDhRYAbNiH%-H6#3d{{ zI8;;)!MyLIiRG2WJC;K58r#DgvCjtI85RaW=rc&!E9^<=`yF__#9Ak`uX*k^65PYe zY1coDh!e6_X?A2V5V#V{sIyQWp}-ry~k}x|`EG>z?_FfnBrcn`Y7bQNW$@ zd57`GxfG?6HofAs$#(9K`dOp-uX?Gnk5#&}YUi?ov z5+HP&PFg#9?B6CjuxuH=H%0i+jD|z;FoUeES5(eybtm_EG%vr=q=q~XdpTOVj?xeF6eI6yyxB0iHH&Y< z{E4?ubA@bj@zVCGOItHG)tFP5NQS#^W55T~^r(UiAY-B?q#ZNc%_0c9NYGwXgw)YYi$ zGo;VTSf-5(@HAX+@!t^0=9a6_$psQP!7&7O;ko?DrAFy2q?m}J)u&NF6n3xw)2Rb!o% z03A`&{y*u4BWd5%&5tX9W5!Pc!(5wl}b&-!fI+`R=1TEM%KWzZf%Wiht>A zs8dUXuK9&x^Y&7DQuRA$J}7HA^$mLdQl;{jZ!@j^$HIzrf{NOg32o!qZ0^}aYj#Y|6M(NW`V!%}+Y+TjJL=Jz zGM_ki6niu5siR{x?XF&d44+f#^#*wza~wu|ZyA*O+=(9TyYcU2$bd3G98pdgjN#b5 zbpeV+a{|vkDgec!%a`6=E-s@ztlijfZBNn`4b4pIV(pIZ5m`6`mZO0Rs@90t%ondk z!>p|(~oNw+>erc4p9Q-j{HJ_#v$U61H-Gg`2}FYxXYyUiuw1` zvc*4ABD^lwep}t&>BXQq@XLh^C3kxl7&a+x@k9-6oj*}|-Ibw?SVdglk zY_EN_#?!Fy{O6P|YSmua084i4*S!4&(W>dR4}^zv91w;NTr;_MFkZcWWY~3@&?Iq9 z>bh#!_D8qn520vW4V*HEH)pMJSd2jcceTC?wr1bsm{xUcoCdc13}~#*s@~v@+*G#ccP#VYjszCXRrYe=C{!?{ z5ZF^L@{opE}Xbc~JUM>gf@;%dn>H7B^ zYfN$b1B{Ej6wGd zp&^}F?)TDGce)PRj(@8jn|ZQNYi925(>*h2cH@##dVTVWtg?##7w=rF6+*GL(W1() zdxbE_{v$$jY^C!(KZdXZ&5F|Qo+UKp&nh4NTu#=&Hz#3e_OxsEpWfTuD?~^u_?lW) z9Z3w;y;0vsU{jTD9puKgcmBn+e=diHm4E*S zk~NIc(Ov&_r>l6$-k+=n!aT))Pmuz_`FjF7PB88W~BBrOQp5H@mN4;hF{!v!BRd*ep zPfH=}1Wr%=%R6$XzC6o#aa?CVqT|_K_OlVDN&{F!a|dC0!cMl#8e>$V<>mxL+Is#0 zN~O*IrP6a;Tgb1uIDK@XV#g_Xf`8^qhxt|E33)rg#Q~RWl12JX8*KaU3`-MjKs#96 zaQ`0x-wK9G!AQukz&9ltKf|K+j-f22YVF%YYgnWo75%Uap@Db$H+uPZ*}B_lf1fdZ zPAj_8`J5Rs&rHy8EU9pkDScR(lale?>SD#YzA zTY>i_REtS%63NmI7k&< zQyG5pD!C&}qWGQ63k-jW`C|*ckiW2K-?7=M4jqC6maFgBtptotaxjTjj{xl#)K=0; zwMDFQ!Sg4zRpp&TUoTWqhHda=4T|HSMaqSC^htmdWoOb3FZc5)Kw$I8<$B*!Pfg69q5umo zGSDRY<8Xill4~9;DIXkSm}`w2`U#U12<&RW5QtGq?@CTmw^7y{1fK&|_gg!BQCODT zwfXp`kN%)gVNRi%nL`7aje-VV#0l{h!lRk|1FY3l!bnn`+(trsv+-&fHqwPBY^LRE z#GO&C7Rn|l%896~WD$uD5l5%uSfH-S6;1xnc*8IJs739kkq>j19+QETEkVhkyOrSW zGJS_((e4X8@wyLqNxb1opUnQqj?=s1tz^>km!T{e90}r%h>k6vnVUq4NRiZ?M?Nv$ z55*Dfl!zV;AgI=^2LEyrJswpWXJj`@Q<1a$^+;sxEPCPlJTp)wa7ARbEBJZT(NLcX zA_M$r5&j1ryecgkJZq_H*h{q$in??el9VQ624ne{-p6wWf9YJms81

    +35?Hj$O) z2JCMj9zju?tEyh`;6@iN+FC>%gS>oOR0Ayr^JovjDPut@bP+CYawfiDCK-o) zx4QE%HM(2*$P4!!bCLbIk!*#}zjgKS_2%K|E^CP`#=m;gI1xYASMwV{@)g}jUC>+2 z44UqVT9%>0ya0zGcV^NU9H;L)6A{T1`m!#!SKt_twPgOOPcq#~R%6Wlzr&5nji;B4 z;^Y1rM?c)P{n|I-=NzcFjTL7jRt?@JXz+fb4D!hkQk z&OggEP90d;px;c1P7KzlB;uj*gagko~;y(+L z&Aj)FMPj{KI5o3wNCLHOvrIM8!FBYz!Q6ErZ)-jT5bNGedz@K$!+hs?`Yv3mT_Ic| z{UCWasCtEGL?WQtatfUpCTH;ZglsFcZJVj(v!)JQd4y}I0L5|7`mhSP_JOiIgstlX zXDkj#C(&2r9|E?%h&zh*O7p|=U>nDOuzk(n0G6k!0J1l?N7bav)@>7vQvHYTyydA; zI%#6QI>Zs+#H-lUM@Aey+E)AUuUSnUeWI-pWRnCwTA<%rZ3#d45@ z=&PB+9gM%`v1`Y7Y0;1svmC;BN5!apfiddQaN>mU`$JPggG77YH!mFs$Nr|Ma#uCB z{-h(Q$$BmruCB4lqsZMrVB*2jWo4Xo?6OnwLGyvVO~;)te_D=(E}dh5b3=*)rC3sN zzsTp#T%DYTkCX$NXs53jIc*4r!Cq5?4>wRHG9CmqQHs_HGAEza3vM5+vhGmxD%>uj zrZpIIgzauO87|J)$4WWS&f?nbN{2sDqw?AHjWIv~5DTbcIre4=98=G#1;=TreQSM- zI1q%$wH6(~)MeEVV+{TVynZGdB;W=nSMM{k9>fs4aev?Z*ol$sB`K;=8=5oH;0LGz zJYL58J^Ij+e=&b{d?&erf8%pa7a{CL4luh8^fZF+TTtyjNP#gTk#wZb)`P4;?FD0F zNtqr}JfFg>7T_~ZsQ9Bz?HZlRVLfY-m29Q8fq2>s0V zmGo!(R-HLVQLX2#o5d+bubagKRCd<)?wo&R{zm_6lLhzYHGnnZ^&30J*hZsmi9ltA z)9Uf>7HJw-RUYsC;f=U;e|i9`s`7h&m1F_v*`)GImhAQvP(Z)BAkuH%*fpM}!9Xa- z@TgsKEdjK(Cv3^YU_mH=6Xh{Z^s=jroBkqfS04AaQf!~`zK2Dr5!;~M{T4p zV$EL!OXO~ssFyQ?6uJsWpZ+Z~%i{jHp&g|h?-bVdwxLFcaNF!~Xvkfijx9cr6tcF= zFY)A$%zAGo`5Fs5)8Ajj0A%p3#PQ6$$Xc$9LJ7=3QX2iE4(ANE5o&^|CW*C+w z9kS{`?5O^}9F-#Gh^F0|rGM2L1Dv<#A+B>1r~3OTD4i;G;7M{GG^!RbVo{+cQeSuh z{pnko9Rnh}X5=BJLNv9==vgGa&Yiim9oTzj`=`2%Jy`2+CIX$)~cO;+0QjnpwF-ER(xcEeZxX z)`GjaP(=ShUwoH?J!vq>Xn>Dw$W)~D@4d~0^!6iucWD}txICKng1%2?9>hiJXI|}R z+rfOV=4IVr@!9>-&VMXQ!xdW0+t;JJW$U8_MAo*YB>-5CzeG;)tOQYO%GepK^ZLbu024rmb#rXiF9=Q-}mPF;HkP zOX3`-(;Bk6`DIToiUpxPA@)S&(qvBc^5k1kr8_QUpWb`_ste{3vhY*Xr0R3~@aOiy zKRE)n)-85I`lXSt%iY9xtS)`!EUwN__p(m2mYK31LchJRA?L@C0C~^PO-`pE9kG3G zwOmWv_c4dHrVhVCzb!d>jyo*HQ@(>xm4#0=>Upmve~`OQON}YwL4^r3TG3K4de?^c zahK`nV<^EC+M`UBCrZWUKXRvz__!u;1YA_>##d|7}M^2iD>xDoHMLp=PSZ7JiXfSkpneNS~OAM`hyJe zifAp3!pz~kAoO?J>`s%bQcwbIL!o`b&KJ2vZ=$Ihdgmp*KZ#;Pbbmw{;v3dX&!AlhnwDyVfUlqjX zxnkp}iE*4an{&f=ij4$L-LG<4C-{PoESsY5KQhF_Bdx#XMGrN08#)FWw){^+#{Zq} zHe{R|o46=OuB5BNsQQ@eY3$F0M@dRLDy|9HerHEyDuc$AEQ|iapqag>pZ6aAz)L_~ zd(lUJ_|NRG+5yC27ZLsOY&e$I7y5BQ>1*@X$7?-qot2iI*x$&74*MHJ@!#Y<=&&}( z`sHyNwz;(-ZpQ5eutM4JevnKEW^Q&y4SR%h2BOY)p)faC#|`FmYZ!?7aY&L%`~Qyy4Pp6FFigEZdkf4wTxArUEq zH)Jg>crZVRE5mAC`yt480yZsxrM&FWg$ACR5u-s^Al?o-739-1?xlIn0Zr03-2H$%5h;UwzNlc85^%~QKDS7&7lE2 z={0383+#cBf`fZm{~=3%Mf!eNvT_x)7 z4&XWGWOMdL>>LNagm7(m9cA*ReIv^PIZ_^!Nqc&9FrR5L1oJsB*$At^5%_#+U+r*_ zjRORk$BgrZ3;@P`Es;1i$$fvg+lal|<^H*`dD2SlKJZP$E&<;`DOcv2n+H%K4bzz{ z3xLVI=|>xZyT)J{r!ROhc$-s$IL>y%L|MosD!Zn-?fZG}A{`vHvP*RSb&;f{w}svp z+n3S_+27R48Ol7@!RA1(d$7I-rs}e$a)3fv-W$Km!>#&hmx-Cg8-SGZfc2L8m()%{ zKy+KN&(?|mwnpGe5i6@cwnu>cpZwn1^dK7}CDUndCVd&oVBN{q063}D?D$UBP6O=0 z^w`_mPwin=XGwM_`oF-Jy3Tp!E%wixEbV1SPV*|2WOE8?OWAR$%TQl|;NZEqai%$- zbbql*=li~+MSHl{LE{F0|0lMqx~{oXfAA#~87#8WuR)uI>>)_^eCzM^aicnjK3 zKZ6`Jp1t0Q-o?R<27!e*L*L``!eT8JCa%LL=xS?$G3H@kpY z>JFG`ajB2)DsYprbnxnqWq2;8pY94{ri!x<)t55ab zk7WJ;d?@Su7ipoekqV>obt*5LYv-s~y%mNnI^l>5S0^kbYyl-+WNK*s<8)kR<9uxm z?0(+psLq|8v*iYSVta~`;@cPkS!H+cS#Symj+{a+cYH{}-9^re+#;^OQA{snw0v!sYe2E1(?;Kh645iXcl=9)j)1RTE`F{|0&|P&HZj z!ZwAtfU=T?E`6RCuX8ahZkyJZY_(vlYT$t^sv{#lQR@g9=#PvEYJNV~{}>%%+Z@P; z{e@}0$;C|3P4_;TsGKK{ef(43;?IJg<;UG|z#eGw`GZjNtxuJf5S2mk77Bm_(KM1oOK#-xclv9C; zf@uLNVs!sBry>OO;ox60R+VMyQ`eczeJpyV z>+4b0gE-ipEx!yHi-41|w9Br1P6WmS=o|94au|)S;eLm`4T%fBBeJlqD%h`M6D*x1P!7jk+n+S2Xog>U_Uguy_GEjz60_@r?yh{TowKT<9z69>Ocvo4w z9P}v2Y0;|*HIlC$^xu=Jk%d2$a{UoMQR(^^_eVw=315~vZ4>DSN?vdDm!5lJ zkUQ9=|J<&ER{e)#GR=B{E8mvUd+sd$@CI1{==U}{4%}-HZ1pN|yMH1+Gu*n{IvGqBwsUt<>sI|l-=%U1~f|6KC_p%edIGYYiv{e4fH9zuDS4f}F-Oz@4e z**ds$b4h*Zc;x}iQ{4!cQST)LT(9PY6`sOd;?(vJ8fja^QsQD>$gC$By;)CdqZrdJ zLu=nKek@pB@HmglJ@xbdN_OxO$(7u>f7E0-q#m3apXfVA$G);82#=f5Y!i9lbk7!S z%PZ8MjX=G=28*YkvB;`h#3{D?4EZX43aQpcR96^RK$V)s()NK=l<#D>LL?=m)uhr9 zi!f|H?}0boNz31)fOm2$O#Qp|tyl$Y9EsT)`rXaC(UAIHx);}rT?=)by;;aQTnLaY zatN5N?QC2_a5b|oxteRWqCHnwESN@+wT@9Ckxv?rg4o=*o7b;}#Qj1=QkZA>$N8AS zfxNzl5@_z5eq6y>jb_k_VLIzgJX+1TxzYq#KD1Z=!|GXM%>Bd3pU&d(5p56QsfVD3 zOy-$N2!|F#KahK;TOTJYd2~a4?G*=KvdIcz^*vdA*O(pkHWx1XlFt}k%Eav}F%@7d zd444}3@8fO#VrR%G}dtUB(6lf!-@k7R+qZ~sX_96AP#S$%H0g8|53umOu1LQ(O{=j zWsGhao55i=hJOa$>Z2y@wnwgf(|IiQeWq381+JjxOrz{nf^ze%d5y@DW0_rU=Bd3# z#{S*FT>eV#xpvc_xJ(#Ji|6sO!ntOJG1Q|dWNpW^l{qLr;Cov^#p!4OBZ)QPK%iV_ z@ydDYdE$!X)R7N^^pkZeUTBRcA?BL8G>Vv;94lnOBihZ^7Spgm% z1@ylR*rc%kZixO*%l~e`GINnfutZaPuv7f~D)xYQ{Vnr=#qORAwAEhq4FrNUs&sU; zZ*_R^@ZNYhC_TN~N`Ri6DEwbwRThJ~*x!Ofz`)>j3FL41x>sQtp6wN~8>hW{{S7M| zjKkh+O%-eX8(Jv%`g98h;p+gPFC`IGqPBO>(aK8G=sHj;Nb92Wj>rVI;?nX$8|9l? zlngzDUFVwXotAChHD_$MC>*Y^KWqz zD>CMuiPZrVWqvBt4gA{GJG#mvTc{agf$FjSw&w+(0H9dZ2u&FVXka&s+Xd#fg^ghM z+)8u}{0^7{4FD5->*4(4G9@8lQU;#BEpKFo9?K$W^mA{n6ClGd1{^|=uBjKhBZcC) zINB`4rrrQp*1H2cihOweVEv6VGFdGhPPuh+_+86 zdU-F4S)5uCoA+%l4S`XJQ4}X&`6hl(p{zY1ymFcQB#7ENdI-d+rsj%+eAVPnZ67hJ$37rZu-J4#(VY((7Oow zW4-ma&zlVCXskKwag#`7oo_449 zSlke67B$*1C*5mysNH~h-t~I=;w9K%p6$r!nq%vcG;Ty`VCe?$xves$_*Dk$+R;^@ zm%69GDL`)U>@HVr zg;vIF35K`~Y=vL=RiwZZ?Un5_uhD6ezVV)Q5TtL2H^_r+qYM*-jy9|+xkM;NaD?j zODg*sTP`c@tyL;f!yq`rkwbef@>|eR`uT`lkuYfYTorgG7TN!)_SElm$Yx~H2kJLG zaNZQI&#a2vG=zJkgddBQAI(7Fss%E{x!mamCvUb6QYhUs&zZG*!E0v&%GOwu^xv4= zQ?tI`BYUX0w!cjJebi+Ku-wMG<|C8W{8ti(xSb)0unk;tuvEfcUVK0&H+JA05v*(& zlA3-5^yJ-_eX~C}u-pa0ovE%-#lv&~<2P}TNi3*$z4lbPGPm}N ze&);tOMtL6h@A24yrr=(y|nJIm-IS2k{xy zA=XhGda?y4+!gzvfJ;PBoA&h|o_HRMTn{A{exqKruUHG;>@R!jHK;J)aG+`Zj@J8Y z;6Xfji0iYBPx{0w%C9Jj;a8lN=m;;@U_*eO{d0c{>;3a&9;L&Zh=%@anyQ|O@+OMY z((`)mjc;U9-9tDIZUFy=zMq)1&ITUEA?(9(!kL*oVuljidQ} z(doHjtP#4u;3gUO;JOMtCRl4Y&HVsam}S*eKKQ= zU$5AE>u7mo)>RAz=qTUt=5zayb5g3CQKCPUy`Q3bQMmNdVlKa?)1vaA^2B37+U22i zK(PAR>6LG9sV(N~qS;M`=fJnzSNQedem3d2?2{nFy(ZMM;3OJ-H&;@6xpC{%pLt=Q z+8r(F_C^MkoidQIOQdHu9e2acs!*LwB>fVRIanH;(W!i_#-i1vFtkNrXnvhW;~yu; zSDT5b&!ZH#R0NM#|60G_eq(Us(T~Ny01j<%`A%}eOE&U`5&@8dPE+NryC>HyekD2J zqyCmVEQ{wGLA>kXZs+aHOS&vJ&{WG|5Mm4(K7_pz+L~sObkKVa_In8%^Fu{7W+qDz zO%t^g_xIaF%Nvq)v0C{X#9ku@D~|EYdo74(Cb6a=mcx}cyR#LIKTm47b8}`-&YMU4 z%;#~U|1rr|t$F+O=8z>P*7-x#7@vd=ZrfrF^U{ZivDwcEd^b6ZczD&3q_nb5eywZ{ z-mdRKWi{=rq!g0&Jm7O9yBlr%(|1RLOz^sBX$e^2`y+!58hnrsxIn6|pIz#CDW-Ez zSiLlWTH#j{JwM&`tF!`U&wefk>3Zx-{@}4Viv;sw?dEsmxq=>X%FXv;3;kqznuE4d z*T|%leqWrO#g26wV8Na>LhY{eE6#4Xx!Ael{@`kt2Dj{nJ1^EfJ|2`G{+RkceB_yH zWeYsmv$dNbvR8W^;Iu|YBB#jH;5LL4q?MrA#wG$9S6z~heVcCdtbR!OV8=${M*;Qfj8wIj@ zjVl1T$FvY{(W0H2kD}b_WhH(x&kR@G3@Y?f)Hfgb%?}a-D=^^UeHqy)bzjJqgj@sm z44^2{I51{e>dMR=G=cTO^++F7o^|dmdFIt-BRxiwJ4R~FWn^%}HWL7P?)EDTWQ#Fa zqb=fMef2`%64xq-!Tw{S9z{6cG=8%UZ9K2IjtHolv9VUH0& zD&+(M&#~WAgX*L*SH|QC-?dZD8qb=RKe(of*Kr7;U+vMll7>y+hj30YmJsy` zNXl8cm^?r6a(C{h>m5bNOdebMi|^ulezHJy$w_~SQ-7x87DB1CgWxv}l#AB2ex&!8*bMV z<4edk@Jjw7y(Rt*SkL22g)RmN#Sye(DPSqPQrSI-D`2_lbBHtsXorh44UWViDKC&- zRe?wE;am*LJ6k*R-M;}rCeVm6;nLbnSJ7TaES)>r@T_yp&u+{6649jC=?A%nnfA9s zCBu;s)i^6>*?(!xe{FLA_!Gy-HL9SAdGvW=cL+7E?6SSKe-h?EW#y)E8Sq8M$L&`S z%)!j#>w)@NFaUQCfj3Es76lFJ`ud2EDkBe1ejQYHTL04oel{3SpK|>EyIb;RiNBPr$ zZ2X1n4yUhx5LC=j^-OTRW>O^Lg&2c^9v!wKzs!xh#MBUk`aE(O(|#4HpSk%9fzZyp zxbb{(iEKYIg9FQ7z>~al^o*Mze&^pt0{Pya6-t*Z83s=chImL@=6B!){O-_fT{?l! z&*P8wBhxB3`{SLk_};~ckGj%A+vwoJ$!bVI-Ma-#fL7&^E%Z8NJF$H+{&Mx^*jr;Y zM^^;wVWogO9C2i}yQvdS;~3sus=9RoW(3J_yN4==l~{9RRxb74xmL zvLP%+OR_`g#hZ?UjW;3Q%1mPziI!5#Z+OtwvQO9$c25-FMesY#PNudw?7d+wn*p3j z2(4QYw-sm(DGUTpda>o+Bn~b@g~xl%ec`l;N57Yr{rx6E=gSfc=`sre-)?6;|AaF7 zspF4*zjoT&FZu%h%f!gmqNB$3&*=t=L?W^K`%Y>Ru<#nzbPe>wT}xek@UpAu*4ddC4uwK_ zg$(-?v+B?C>URJG0>tNTt)0s9a~v9t=2!D63zRLuIiMk{xJ(%tnUDoMFMOAR3ho%+ zZl8#m#4DPycH=Cc5wY4jRISfDhDq6w-Vgmg`ye>Zyk2&}vZTsG>3JskQUa8INfZSTDzzNu^b<0@cGWwLH$`xDOf)1D1WmLeX;|0u`A^Pjj})`Q*w`FA!{JT(=}i~*TsS(99va@m$ zSp)uqbZZM-<74FG2SE%zBlSad`bG0+jPs2+YPp<=n?#(lt&B1NE8B}>0A6AfeAjc@ z8RD*laf3$GA&%{+ob;eOSAlxP=W>l&7Le1SstFltWUZ`MZb1A{a7Dv05T?upeqSWT zFE}e0B9t}(f<}UM`2Wz0Ud7`vO?5o*azO&wygcJiGjP|yCuf`&=w)235V+JD3u$JN zJP%3o;k`KF0)%%)4@))ZUkSF_(>HA1NWeezxvzhO*S!wPy%I)p#;1k@olYLRyl@!; z%@5!TgurKf10klfTmWU*Sj)%w2_wZNjY>H5W**(0<^CsaFZIK8c=J!RDExFnlpcbF zD+OE#EWCkz*I=TSJp-jSBkR>aIkdl#?l|xKj|KT}tI}LklIs8Wlqk8$7L^|7I4>EO zgNKK&`(H|^Fyg-}lmBV?Rte3nIfPq1iqt0dpd$8oK&OJ#a>}6B~PE*{~mEAifXY3d{wd`{p}aET(RTy*dz< zI4C?IWI+~sfCJbr1|C5Xiw!rZ5hT3xrn3^o3PqM8-&D-_p>q^xz${Rw{~Q_$`OjD5 z|8ME)jV$SPS(DDf%YQ!ep7}5FHuU_zb?|>~bt~S(Jyk+eR` zw-1yB443XAGz(LBE8;oQ=pD;3MvRc^#-rC`M)ipPef&eo)NO0Dl|Ksof4;*14cp>P z5L3p8%hP@Blx2UW6) zD(Jc6Zr)4iga?ZV={ftzvKmJE0Kb8hX&m&l+iy8}zWPh|$!M?_=0w2_FEx8vj(f&K zxS_*Yi{{a)f9{(#&Rv1y56zX_vPhDLc}sdcKP_VkDn-MZyv%YJS0EV{^3H->7zL`c z)JaN8?rEn*&LgFOcTdDF(rn%gu61mCPF0{K#t0<=(f#oWC41_H&=v&d^Y9cD|l@k{#Ar$ z7S+_-)&1Fp0>pCHM=S3NQ6lbcb~SY)#m_{^FWZJ`YK;~iI_gF(8&?;$yVz#w#x{@=PtYgRdd0=>?eHntF5_Z` zWTMF^Qf6p~x#Z!n;&AWj1{Y1*v=k-JuE?d=tKPLP;Oy({z}1h(vOj;Qu^q2onlVW+ zXfbO()8gP1-4t1sD5jMr$^E@|GxeR=Sgrv1CtT%zL+QN)jXW=r>@fn(aHG9}Yx&zh z19;w_q1I0Aeb=^?Z|L$NrUXJF%t!BN%b5a^otBc#Yl5GFO7Et^nb+oFoq1b za6`goE~V!EC%@6aK0C`QSd~X=vnds@s&q!7?Pxyk_RRGChlv~fXptLwqldh zzhxhk7`uJK3snNqVFW}(*TTx#MAVc{(v?k*BMAL`xw0UlvOLV2Z~Wp&V~JcfSNn^W zsa-}0hn!?&FLnks790_jwKUS9%ajYx{K8G59Rii~{E5OIoPT*r6;|o={!%Pku`$8r z^oqn$PAl_d8j4y8Ed?>lgkX~*x0Y7tA`@GPcICxU_3I>&WV1i-i0PM?sxwn4^!U~} ze%z)4E6D6i?LCT}L2w+mBoTo4u2(s|(t`~aZBABo z)$$``kd*XyXdy1JDm@ zBP$0eEBiso{=|bf&LiSWe6xwTbicN)$YNj)0ekpwu*j(rHHC;@q}sXOPz`qNGpyq2i`yu`)J5%-%{_lzg3@n)ZLfy?_^*)jGZ&dD|Jzph}IVC8x5I#PL^+T&OB z`QcGyMgor2dKoz`d~VM4m@+LQ+CZP;jhK`KL|yv7BGF5g2BZ?V3i)rFPFI(O z3`!@h)5qyi9=m4p8>t z5J&IWfvrzR#d#xSxb7nO9H+&*4>`+6?Lz)K9_7~ zaM39xMw;b$cuX7^^l|MfWDx8iWs#{8a`0^be09O9foB?FF;J+)-^v^p(ZN)~4&3j6 zy?ce~Mn}E&8r?)tlB6E4M-7n$F>%^4x+rOJPFNB@GwxYc^#b}fm}h@+!czEYg%2mT zp+h3m>Y`b1LSWzDeCMF5+}}2N?evJAP8Phc}vI0FLK& zGtIMZQ6ZyH0PyW__@YnoE>gIMP>ChmsSWC=qr1?p^UTbAa5f0k*dBxhzb#@itFtrfa=~7tvBoWUlKoHqOGQY-T z`$YRZ)Cp^sRt2;qTTL+j*(WCsTI(cK&quCkq>;`rrJFcrMJk;ZJEJ?}aOj8A5+gr* z{16&SLG*tq{qe81Wrs&WHA(orfAbTiQH@{Dqp+Z&;lXrM`@OIBJXyXk!$v3s>$L}& za_~_qgwGrOCAm@a!$YAjeI8QV9j~p6q zUAFCtS(37i-ygr?l*2O1eK>lcc*P252(Gv8Ixbk^j=BfPH?qEV)pdb(d82~y1& z{%?2bbIae~bb zAnlWOr7LBeP=m7;y71Qy)2xV=17pcBtgxqH&|Z@XMZ|Sh1G_^}7(}W^o}Y=fuL!rp zgcWa3<++b$xXOcIEJm((gcB6!NNBlL6V^}h<|asElNJ?(JSjonF=`uD>tF)tQ)8G!;`{Wss9iE1#}y$StHp#h-U}{u@7u44eO5 znJ1wY?_YG1l36?r)!?XXwVttxw~JhPNB@q1Q?3g}{VuW}^NpFD$Sy#bY*v6vn9k zK#X4CaQJIk;eNTE&A+h=O=A6_(RO79P310lr=XG6`WssrU1sjv>Vr0pWT*!x9rveQ zZ5esdl%nz}>zQL6_}MzMp8jdIVQ}7}>2yi)mSCc0m91+z!RX}quUijB(XCn8d0%HR zv~t$dzNE@t5(i9}Kub2?SFRo4|G zRj$@;2a$|%b;T8kL%%ydEPEv7wtgbWR_Jjhe(nj>il8iYlwC8(uQs$cAoDAzh)lMA zTePLOgzgE-(_y8Gh{X0&w5)aDJzYhO^}|71@#?}tS3}N-C(;O)v%E{%d-lFJU%c&~ z;K{=YiO+Rhxrx4nvk=`cdi<3Zbd%dBm7A$^_<2qzqD%?iva~usZ5?#SO&*uf`cEHu z6<;e2PB=}x8(K#0(-gFm3*@N!zmTD?_!I#Yrg?TmAdO9AK5Isx%4dc?y9*uE;1IRg zANq6G@|hSKs%gC?uMp6J&s&(dML%H!h^H0tKiSq$;#0LiQ#PZWnpFl_ThD#ANvjLU zHC<76>y=^irR>hDfMgjDWdx|8IG+e}91Ug0`UsJE@>ZSh1gBg=I1*PgrdezVK4O4y z5b}U~-o&p!#QEcQ8zqMxPrPUj%DiG7Y4;y$khk{<85qUI{LZS+ zhrQe2GSpXMBYb#^M3y~VzbaAZR`Pf>B&>6ew!ybqtWl*SCu2fI2a(LZ{Yp^Y+1X}? zfRh4a+iFMnROi2sPdvDCzNj$}xSDPR|I^Q*&)}Odb#dcYo6p8%fx70ko$Ob`+N{O_Mzm{;hsG45~qJcLa-L1^4ntnW=|dsETclL#_x zom%TC)hNR^hvMwAjkk%)yJRzv0_CysKM^MLF6Ize7fE%qxuT!NWd_yC8aho|rxOh# zO4{o$Pqen1R?Su`akdE=G-R9}2;ldTdkr>>59i4TdB0ZYVobK0{{ zvHaiuv!Pn%fFOhKx9+3gsKM2gmn#M;n6NJ+o1#jP?J6ub3LLp^nfF>1Syvq$eC9Pw08SXRj8$ zDzJ%PN8}v)VDcb7q9m#@4Ef-XX`^cqKywMrEuqfpU`c_ddB#W^}Nin9+`ZZ@HPG@w{tSCYQwr`9LrA{2haOz4>N# zLsJ+AFSI#LnaTVde`yRTv%|Q;YtJs(Dr4L3%I7GF%HyMqe4Jy4$zdVH*cKASGpM!s zr8=JlHrVf(qwc27gia`AMNpp%^GWF%P@EgDvxko>EF})x;2#fK2@^x-j~74uvvoTg zaE=XY0jx1gu)J~i;|InT0V@49Fxj1jiOjO5(SlC44uI39#oxB&Ng8$R0YS;+s0E#8 zQMwsl_7H}ww5At$0Jt4#qb4eM3%rUnFa0c9>)~?J>vJ+ z<4KyuyfWObG2IwlS+B6E28m#x_kukykT!b!ZJEWE2}6&TPps{oK7h@%%3@b~O^VeQ zJC_tQY)*}^vZkSO360_?vguc+kbdH1kBI+QszIR@Mzoz+T~yvRlVAehSFD71CyJcE z5bcu*55U@cU1PKh92qenFQ5=hd>(4UtxB2uMV9p%3-{4e(~+3&4Gcr^<3(u4Fzz+x zVDVw?DRfoaPtwjtIID+3eW#|!OIQ2O`O(nxbS!0DlN;H@_4j7+ycPQ@PJ_RBOj#9l zHf;Z0HJ$N?3k|_VDs8m!%MVtAO-Ki?m-`*lB@IgppzXeVRL$KdMQ9cEfsEcmf*%yBlOs3D4o zsgG3RNr2xcVOfarH(CA^=;?6sY7HzG5~_`{)6F8)Vvx|mge{ArJO=4p^YbjQTz@rZ zr`bREY1rO{E#b2=`q@~cO#%UK`%0<^VH=0Edu`**Q2$J*+LE6$;Ln@1N4jY4`S+u0 zjB%tECO)`yr$Kpk90DgeO3?JDNi%sVj$Rm-lN~YRle~C@zi_$%7a4V<{Hz?DLgr^4 z(Kb``H^}aRJ@T<88kSd}GFv%8egW9LT&0umcIh8zN3?zUgnW3La;-ds zAtFm|XLGn$@n3H|lYTMh*ZVIaPGwo@qo(wtu9QR+?@&liX4**6^*K9iF70eC=&LRYzOcz@(3}`{b86*xXe2eJ+!bFV@=0>;8kYql~C4bLSr$)84!fsBnUN$LKw(Z z;Jo6DiNmzFCwks8usK!E44>;D2Y;I&x@=qvYP>>5qchkXa_OiQ@VS-C)ULwX%*=~V zpfaxXDHGgd%h^h=jsF|khoWhb&N+)Q+bgE=o%FSZQd!1miTc8G$@$JLqCn;Kms=&t z2A$g=22@v}gCAa#%KVpglQa6u79Z1))Vu)4q0{FD=` zC3F(YZvgORS#s^QIL*Co0`)H#M0eqSsy5eo#u8A)AhKv3JQ|7ZMzx*@$dj1YJO5fI zz2Kjm+C~y2BNbzd3>yqPxvdXpiZEIiX~F=0s{+t1GfjJDoD`^JL)$r+&JOa^mLCroN5ojd={?9H84Xbt& zXUC`u_fWHHH!)PJHdgM5$Iu>pnyrooX@+Mb=2q~sFVmdlz<v-pOJ@8L33Nv=sV><)^ZL^vc9~@4jHDT2eGpLSPttdSPhxs`TNd zQV~QM@<1aib-yn{??XRsOGN%yShCEDsF=r-`7=7-)vHM1SbF!@B`1PIjcZ@P?TQ}nCp6e6u_~zn18JH=1?B#xfKBm82AtcdjOIQm^)|aK3 zuo>`A_pWbNzLdrjTGI3VrbA~R=A}*2i%Uek5fJL*LO|iRUUl5S@U^0WF3^XmkW-U# z1!pGd_f2zwN}+BVBGF-Xl!!E_;~DF`9O?r*51G8q^FMuj&*LOr7(!*VloD;s_En33 zXv76x(FgWysOep0H+X%1FuvKhVa4S3;e)PvuhnW5VtzC&Zo2%}ppnx}@z2 z*5QFB`%fs^f{#MHzOcZ<*>R_TffpR5ot;kO_UesRpnm+7o=svn_xO~M`^JdXe!Zmq zF{}5cI;T@6ku0p(|Jm_4mP8lU$hUa!Cp-kpX2Z`K(o@}co7$X$ z3GwToCPl5wrll1ZpAA&ZzfnEm7oM=oCQXNWNC2E~8ZAcRclM(`$dw1Z$u54`?ivT$ zAV@-}`?d}y>~#xMu{)wn1p|KVV%$vA+bf{sE74H-jM;ZaiV;JTkScMj@MI^tI@x0h z8N>M4Y(t?0i?jVF#nN>bx^-B=aJY7~!Db_`gkAZ`iblWGDl+3Ev;R@(_(a;N8|o*I zL3_*I>twTRnE;uVqg^<8?%1Da-mNDpo*o%Lx?$D3l#xs-(tm9o?zWRy7mx`Hay1o6 z*WA}c1ch24>&pkorHPtZ3#g&!0Pk<^5urV>Vly!{mz|+@)}SI3!A&N^P>*mKSVwLrQgfS=@8`%C6Hr1##nT$B@u?4*}eCx>FC;!-v>fUy$!z* zG)a|l+TpVne8Ps%`3j4lACsttB@NpZ(aNmeckmn}(V#CCp)BKTYN03ILj>7e!}Qm( zNW8U^2=&RO040G+`7)NY8l2Z#cSu{kt7YMgsV+zHdq?2ERz|Jn8tmPX?H}QjS=V*v z%VbZFPZSFF4oZDxrjiLb6gGeh^1gCeC6ir`@P;`(NB!f9Dn&gv`m4_ejf`Zz>QpSF zVxuccjxDG#4>;Ggyl(Vc@UWn+BPn%uZ@~@P5AoH2rs)c1T}@zTARo)!$~^T+3znj* zt!*I@Wg$(tNYoD;ADNNCZHo7GI)tx@3K~eFgMUpCsw7SkWg;+AM7dkbJVMRbuS4`3 z>?y^LaNRaK@LTCc>H&g1J^x*2O9djo{R#OQk~ndhsUneRmjlP|Yr>0KDBdzI{5a>G zZ4k4MRX^*7A6%s*PtmeL-m}FPf$||bgHfTVwL#f&HbTba$A+O|Esp+O=fWH2DJWG; z^1u{u6)Etw5u>uwD`$?@#BV{kfSQ`Pm5}jjg{MLfj(J`!onjS`S<*C&-21DgU|Zub zwCE{zsy#$Y6V;7zI<@Mm(N$yM1-;7gU4MU5_g#=05MvH3w57{K?og&yrds6)H#TMf zRZS+}YUNC_a?rc!vpIdj^G`bLPch_VKHN`Dey|BkKK23k-u}HDF??;V%1!Zlv`kNF zx_vS6IUfY9r^Ty%0rVitV33MOT!u(4X)LU7VSruA=#r%|G0UNbPqiXne8xphl?DCwU?#8kwl%@N_i=2~X zoR(Y<-WATzR#KIvB$`y|XFeAKovnnt;KA;Scu`^10k!Aq*?q8M2xG=L02OZ5NYl!W z(J29*>l#}=kF+#7<830NdHm_TzmM1pKveMZUacrHC9Ub{fi8qKzvV($won~ zaxTXJ6{KkqGeTsnbdW6*H~PzJEl`i)X>?*+Vds$Y9N;wOvPK&#m`#6l+98o#_(|;e2h)=usQB>d;+j%EYQ;9T9n_nOt&+5qB zyrD_U$r=4qu%0P6U5BkmiVlL5u5#bf6^QzJrlwT7kn5ZVh#3ysGCkeO{#$ELYglX` z8fyN)Cv>^_89xYDUC4@~?fXT?C<^NjiejR0%v`yN*>i#0ZxZqdM~wQ@0;XH<$Bc;v zJY`?Oy)FWS-`MpgzhJ%1`&56l-GBBvM0;wC%9%jVzvf4J?@vB%2I8rk0r1>V$mdA4 z6ujFbAoG#pn>`3~onDIA_l|~WnX|wwWWrpxCs<4qa9zhQZPi5w?wbO^SOzVZF0L+uhI=17JkZMg+(3;T2Zh);;PSX&n{?uL+_5))1C01C?_e|osldP!1ftXrqoQ5 z*v{qg&raA2q9){_RO6YH5wIv#0@Mj~rmJy-uN(Truad*`Rtl{7vdOli9lsH{1Dm7cZlCijhSD z)W|}zcApBoxw!`pGfLK({aTI4@n^|tr`DU;jl3x|ZDx98;_)-b z>mBMjV{Y`2*O8M&B`MJlmN^*H7i#C2PuP3#e!^kXXO66EoZs^bp6uE9u55;B^T~|w z?pfj;V(`6rPsc96(GJm&YJ!+cOgpQXKy)+t$$yX~l*nS(ggk!a?yyFp_a}V46-}+; zGR88>OX$zb8kB=WKS*UA^Phr0q`i6)96T~A#&pSv3>$DRwPnS2tr2bPP?R64WDgX> z-P$K<%gYc|a#%i2@#ae|x#I-7VWzv9Dlz(aer^bHHWddE5TI4;z36@e;GY!qP?S&$V8!wP~CE`wN~ar66U|m1XvYhyyKfl!iltR zbYRMO`W!tug5Q4pWcfH=JpLtOe8`MKJ&_b#JeK(iVJibQ7sl+;r^DVhlEM+_v^KlJ z9w2)C2^e%~C z*0FtB#A8ZjaMbwVtCc*km`bwwx>uFZdb7TF^^-y@2UZ_y?;Yd_*ihnkUmtQH7 z{DT21@i6x7uQsnqE^B~dm9WU;cVI-beiIhB{YWgiT)0hP?YMZ&E_|O>$Pg z3`cTglRC6lAPN4Rr64|ebt4E> ze;`Z>4Pp8WHV;Hu(&rF(&j9Gd}8IL`hrmYo+{c!PG!T$pDLRUk~`j;W+4{qp0c$ zNazdGO(hmc2m4Dr#L0fZ$ltH-6N&mJnZPOj2hgDVT9DdalH1$<@G{;jEqw<;d;juKpS|rimIJgCy?Z~m9*|)EPhdRLVFlRh&@yck!>>YCP*H6s7?yL2-* z5i2WU4|zYS>hfxs3vHt_SZj94iwfD%|4&BA@G4uU4;B+LzbSgRg%9KVNZXLHBV)Lq zqG$MX%|{Yaex_uL=z<9`Ru zQxATnL`&I^LJ$J*DbtD5&Wq={HDKYyh!yx+)ugh??;-Fdk_+?B=(Ett`c{f#J|ni6^h+9saOK z3Me!bX#Qh3B9wk^&d+Wg4grT7BK8ehe_Hhz1sr%56u2D&0-YC020Q`|Jxg4;92g7@ eConKD@i1hIny&nuDpC)04TGnvpUXO@geCwEnM@7< literal 0 HcmV?d00001 diff --git a/cms/static/js/markitup/skins/simple/style.css b/cms/static/js/markitup/skins/simple/style.css new file mode 100644 index 0000000000..af8dc9cdb5 --- /dev/null +++ b/cms/static/js/markitup/skins/simple/style.css @@ -0,0 +1,118 @@ +/* ------------------------------------------------------------------- +// markItUp! Universal MarkUp Engine, JQuery plugin +// By Jay Salvat - http://markitup.jaysalvat.com/ +// ------------------------------------------------------------------*/ +.markItUp * { + margin:0px; padding:0px; + outline:none; +} +.markItUp a:link, +.markItUp a:visited { + color:#000; + text-decoration:none; +} +.markItUp { + /* width:700px; */ + margin:5px 0 10px 0; +} +.markItUpContainer { + font:11px Verdana, Arial, Helvetica, sans-serif; +} +.markItUpEditor { + font:12px 'Courier New', Courier, monospace; + padding:5px; + /* width:690px; */ + height:320px; + clear:both; + line-height:18px; + overflow:auto; +} +.markItUpPreviewFrame { + overflow:auto; + background-color:#FFF; + width:99.9%; + height:300px; + margin:5px 0; +} +.markItUpFooter { + width:100%; +} +.markItUpResizeHandle { + overflow:hidden; + width:22px; height:5px; + margin-left:auto; + margin-right:auto; + background-image:url(images/handle.png); + cursor:n-resize; +} +/***************************************************************************************/ +/* first row of buttons */ +.markItUpHeader ul li { + list-style:none; + float:left; + position:relative; +} +.markItUpHeader ul li:hover > ul{ + display:block; +} +.markItUpHeader ul .markItUpDropMenu { + background:transparent url(images/menu.png) no-repeat 115% 50%; + margin-right:5px; +} +.markItUpHeader ul .markItUpDropMenu li { + margin-right:0px; +} +/* next rows of buttons */ +.markItUpHeader ul ul { + display:none; + position:absolute; + top:18px; left:0px; + background:#FFF; + border:1px solid #000; +} +.markItUpHeader ul ul li { + float:none; + border-bottom:1px solid #000; +} +.markItUpHeader ul ul .markItUpDropMenu { + background:#FFF url(images/submenu.png) no-repeat 100% 50%; +} +.markItUpHeader ul .markItUpSeparator { + margin:0 10px; + width:1px; + height:16px; + overflow:hidden; + background-color:#CCC; +} +.markItUpHeader ul ul .markItUpSeparator { + width:auto; height:1px; + margin:0px; +} +/* next rows of buttons */ +.markItUpHeader ul ul ul { + position:absolute; + top:-1px; left:150px; +} +.markItUpHeader ul ul ul li { + float:none; +} +.markItUpHeader ul a { + display:block; + width:16px; height:16px; + text-indent:-10000px; + background-repeat:no-repeat; + padding:3px; + margin:0px; +} +.markItUpHeader ul ul a { + display:block; + padding-left:0px; + text-indent:0; + width:120px; + padding:5px 5px 5px 25px; + background-position:2px 50%; +} +.markItUpHeader ul ul a:hover { + color:#FFF; + background-color:#000; +} diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss new file mode 100644 index 0000000000..d9f3971d9e --- /dev/null +++ b/cms/static/sass/_base.scss @@ -0,0 +1,51 @@ + +$fg-column: 70px; +$fg-gutter: 26px; +$fg-max-columns: 12; + +html { + height: 100%; +} + +body { + @include clearfix(); + height: 100%; + font: 14px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; + + > section { + display: table; + width: 100%; + } + + > header { + background: #000; + color: #fff; + display: block; + float: none; + padding: 6px 20px; + width: 100%; + @include box-sizing(border-box); + + nav { + @include clearfix; + + h2 { + font-size: 14px; + text-transform: uppercase; + } + } + } +} + +a { + text-decoration: none; + color: #888; +} + +input[type="submit"], .button { + border: 1px solid #ccc; + background: #efefef; + @include border-radius(3px); + padding: 6px; +} + diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss new file mode 100644 index 0000000000..b149346ad6 --- /dev/null +++ b/cms/static/sass/_calendar.scss @@ -0,0 +1,206 @@ +section.cal { + @include box-sizing(border-box); + padding: 25px; + @include clearfix; + overflow: scroll; + + > header { + @include clearfix; + margin-bottom: 10px; + + h1 { + float: left; + font-size: 18px; + } + + ul { + float: right; + + li { + @include inline-block; + + a { + padding: 6px; + border: 1px solid #ddd; + display: block; + @include border-radius(3px); + background: #efefef; + } + + &.dropdown { + position: relative; + + ul { + display: none; + position: absolute; + background: #fff; + border: 1px solid #ddd; + + li { + padding: 6px; + display: block; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + + &:hover { + background-color: #efefef; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + } + } + } + + &:hover { + + ul { + display: block; + } + + a { + @include border-radius(3px 3px 0 0); + border-bottom: 0; + } + } + } + } + } + } + + ol { + list-style: none; + @include clearfix; + @include box-sizing(border-box); + border-left: 1px solid #333; + border-top: 1px solid #333; + width: 100%; + + > li { + border-right: 1px solid #333; + border-bottom: 1px solid; + @include box-sizing(border-box); + float: left; + width: flex-grid(3) + ((flex-gutter() * 3) / 4); + + &:last-child { + text-align: center; + background: #eee; + @include box-sizing(border-box); + + p { + width: 100%; + height: 100%; + @include box-sizing(border-box); + + a { + display: block; + width: 100%; + height: 100%; + } + } + + section.new-week { + header { + background: #fff; + text-align: left; + } + + form { + background: #fff; + width: 50%; + padding: 6px; + border: 1px solid #000; + margin: 0 auto; + @include box-shadow(0 0 2px #333); + position: relative; + + &:before { + background: #fff; + border-left: 1px solid #000; + border-top: 1px solid #000; + content: " "; + display: block; + height: 10px; + left: 50%; + position: absolute; + top: -6px; + @include transform(rotate(45deg)); + width: 10px; + z-index: 0; + } + + select { + margin-bottom: 6px; + width: 100%; + + option { + padding: 10px 0 !important; + } + } + + input[type="submit"] { + display: block; + margin-bottom: 6px; + width: 100%; + } + + a { + + &:first-child { + float: left; + } + + &:last-child { + float: right; + } + } + } + } + } + + header { + border-bottom: 1px solid #000; + @include box-shadow(0 1px 2px #ccc); + display: block; + margin-bottom: 2px; + padding: 6px; + + h1 { + font-size: 14px; + } + } + + ul { + list-style: none; + margin-bottom: 1px; + + li { + background: #efefef; + border-bottom: 1px solid #666; + padding: 6px; + + &.goal { + background: #fff; + } + } + } + } + } +} + +body.content + section.cal { + width: flex-grid(3) + flex-gutter(); + float: left; + @include box-sizing(border-box); + + > header ul { + display: none; + } + + ol { + li { + @include box-sizing(border-box); + width: 100%; + } + } + } diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_problem.scss new file mode 100644 index 0000000000..c513ce38d9 --- /dev/null +++ b/cms/static/sass/_problem.scss @@ -0,0 +1,41 @@ +section.problem-new, section.problem-edit { + position: absolute; + top: 80px; + right: 0; + background: #fff; + width: flex-grid(5); + @include box-shadow(0 0 6px #666); + border: 1px solid #333; + border-right: 0; + z-index: 4; + + > header { + background: #666; + @include clearfix; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; + + h2 { + float: left; + font-size: 14px; + } + + a { + float: right; + } + } + + section { + ul { + list-style: none; + + li { + border-bottom: 1px solid #333; + padding: 10px 25px; + } + } + } +} + diff --git a/cms/static/sass/_reset.scss b/cms/static/sass/_reset.scss new file mode 100644 index 0000000000..bfe619c1b0 --- /dev/null +++ b/cms/static/sass/_reset.scss @@ -0,0 +1,229 @@ +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, +del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, +b, i, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin:0; + padding:0; + border:0; + outline:0; + vertical-align:baseline; + background:transparent; +} + +html,body { + font-size: 100%; +} + +// Corrects block display not defined in IE8/9 & FF3 +article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { + display: block; +} + +// Corrects inline-block display not defined in IE8/9 & FF3 +audio, canvas, video { + display: inline-block; +} + +// Prevents modern browsers from displaying 'audio' without controls +audio:not([controls]) { + display: none; +} + +// Addresses styling for 'hidden' attribute not present in IE8/9, FF3, S4 +[hidden] { + display: none; +} + +// Prevents iOS text size adjust after orientation change, without disabling user zoom +// www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +// Addresses font-family inconsistency between 'textarea' and other form elements. +html, button, input, select, textarea { + font-family: sans-serif; +} + +a { + // Addresses outline displayed oddly in Chrome + &:focus { + outline: thin dotted; + // Webkit + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + } + + // Improves readability when focused and also mouse hovered in all browsers + // people.opera.com/patrickl/experiments/keyboard/test + &:hover, &:active { + outline: 0; + } +} + +// Addresses styling not present in IE8/9, S5, Chrome +abbr[title] { + border-bottom: 1px dotted; +} + +// Addresses style set to 'bolder' in FF3+, S4/5, Chrome +b, strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +// Addresses styling not present in S5, Chrome +dfn { + font-style: italic; +} + +// Addresses styling not present in IE8/9 +mark { + background: #ff0; + color: #000; +} + +// Corrects font family set oddly in S4/5, Chrome +// en.wikipedia.org/wiki/User:Davidgothberg/Test59 +pre, code, kbd, samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +// Improves readability of pre-formatted text in all browsers +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +// Addresses quote property not supported in S4 +blockquote, q { + quotes: none; + &:before, &:after { + content: ''; + content: none; + } +} + +small { + font-size: 75%; +} + +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +nav { + ul, ol { + list-style: none; + list-style-image: none; + } +} + +// Removes border when inside 'a' element in IE8/9, FF3 +img { + border: 0; + height: auto; + max-width: 100%; + -ms-interpolation-mode: bicubic; +} + +// Corrects overflow displayed oddly in IE9 +svg:not(:root) { + overflow: hidden; +} + +// Define consistent border, margin, and padding +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +legend { + border: 0; // Corrects color not being inherited in IE8/9 + padding: 0; + white-space: normal; // Corrects text not wrapping in FF3 +} + +button, input, select, textarea { + font-size: 100%; // Corrects font size not being inherited in all browsers + margin: 0; // Addresses margins set differently in FF3+, S5, Chrome + vertical-align: baseline; // Improves appearance and consistency in all browsers +} + +// Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet +button, input { + line-height: normal; +} + +button, input[type="button"], input[type="reset"], input[type="submit"] { + cursor: pointer; // Improves usability and consistency of cursor style between image-type 'input' and others + -webkit-appearance: button; // Corrects inability to style clickable 'input' types in iOS +} + +// Re-set default cursor for disabled elements +button[disabled], input[disabled] { + cursor: default; +} + +input[type="checkbox"], input[type="radio"] { + box-sizing: border-box; // Addresses box sizing set to content-box in IE8/9 + padding: 0; //Removes excess padding in IE8/9 +} + +input[type="search"] { + -webkit-appearance: textfield; // Addresses appearance set to searchfield in S5, Chrome + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; // Addresses box-sizing set to border-box in S5, Chrome (-moz to future-proof) + box-sizing: content-box; +} + +// Removes inner padding and search cancel button in S5, Chrome on OS X +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +// Removes inner padding and border in FF3+ +// www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ +button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; +} + +textarea { + overflow: auto; // Removes default vertical scrollbar in IE8/9 + vertical-align: top; // Improves readability and alignment in all browsers +} + +// Remove most spacing between table cells +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/cms/static/sass/_video.scss b/cms/static/sass/_video.scss new file mode 100644 index 0000000000..cb234108f4 --- /dev/null +++ b/cms/static/sass/_video.scss @@ -0,0 +1,40 @@ +section.video-new, section.video-edit { + position: absolute; + top: 80px; + right: 0; + background: #fff; + width: flex-grid(5); + @include box-shadow(0 0 6px #666); + border: 1px solid #333; + border-right: 0; + z-index: 4; + + > header { + background: #666; + @include clearfix; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; + + h2 { + float: left; + font-size: 14px; + } + + a { + float: right; + } + } + + section { + ul { + list-style: none; + + li { + border-bottom: 1px solid #333; + padding: 10px 25px; + } + } + } +} diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss new file mode 100644 index 0000000000..a628b0f164 --- /dev/null +++ b/cms/static/sass/_week.scss @@ -0,0 +1,152 @@ +section.week-edit, section.week-new { + + > header { + border-bottom: 1px solid #ccc; + @include clearfix(); + padding: 6px; + + h1 { + font-size: 18px; + float: left; + margin-top: 8px 6px; + } + + a { + float: right; + @extend .button; + display: block; + } + } + + section { + header { + background: #666; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; + + h2 { + font-size: 14px; + } + } + + &.sidebar { + width: flex-grid(3, 9) + flex-gutter(9); + float: left; + background: #ccc; + border-right: 1px solid #333; + @include box-sizing(border-box); + + section { + height: 50%; + + > ul { + list-style: none; + + > li { + padding: 6px; + border-bottom: 1px solid #666; + background: #eee; + + &.new-module { + position: relative; + + ul.new-dropdown { + list-style: none; + + li { + display: none; + + &:first-child { + display: block; + } + } + + &:hover { + li { + display: block; + padding: 6px 0; + + &:first-child { + padding-top: 0; + } + } + } + } + } + } + } + + p { + padding: 6px; + border-bottom: 1px solid #666; + } + } + } + + &.weeks-content { + width: flex-grid(6, 9); + float: left; + @include box-sizing(border-box); + + header { + @include clearfix; + + h2 { + float: left; + } + + form { + float: right; + margin: -2px 0; + + input { + border: 1px solid #000; + background: #ddd; + padding: 2px 4px; + @include border-radius(2px); + } + } + } + + + section { + &.filters { + border-bottom: 1px solid #999; + + ul { + @include clearfix(); + list-style: none; + padding: 6px; + + li { + @include inline-block(); + + &.advanced { + float: right; + } + } + } + } + + &.modules { + ul { + list-style: none; + + li { + padding: 6px; + font-weight: bold; + font-size: 16px; + border-bottom: 1px solid #333; + + a { + color: #000; + } + } + } + } + } + } + } +} diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss new file mode 100644 index 0000000000..6a2dfbd0d2 --- /dev/null +++ b/cms/static/sass/base-style.scss @@ -0,0 +1,18 @@ +@import 'bourbon/bourbon'; +@import 'reset'; + +@import 'base'; +@import 'calendar'; +@import 'week', 'video', 'problem'; + +body { + &.content { + section.main-content { + border-left: 2px solid #000; + @include box-sizing(border-box); + width: flex-grid(9); + float: left; + @include box-shadow( -2px 0 3px #ddd ); + } + } +} diff --git a/cms/static/sass/bourbon/_bourbon.scss b/cms/static/sass/bourbon/_bourbon.scss new file mode 100644 index 0000000000..27b056e303 --- /dev/null +++ b/cms/static/sass/bourbon/_bourbon.scss @@ -0,0 +1,35 @@ +// 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"; diff --git a/cms/static/sass/bourbon/addons/_button.scss b/cms/static/sass/bourbon/addons/_button.scss new file mode 100644 index 0000000000..1d32125140 --- /dev/null +++ b/cms/static/sass/bourbon/addons/_button.scss @@ -0,0 +1,267 @@ +@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; + } +} + diff --git a/cms/static/sass/bourbon/addons/_clearfix.scss b/cms/static/sass/bourbon/addons/_clearfix.scss new file mode 100644 index 0000000000..a9f6a795c5 --- /dev/null +++ b/cms/static/sass/bourbon/addons/_clearfix.scss @@ -0,0 +1,29 @@ +// 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/) diff --git a/cms/static/sass/bourbon/addons/_font-family.scss b/cms/static/sass/bourbon/addons/_font-family.scss new file mode 100644 index 0000000000..df8a80ddfc --- /dev/null +++ b/cms/static/sass/bourbon/addons/_font-family.scss @@ -0,0 +1,5 @@ +$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; diff --git a/cms/static/sass/bourbon/addons/_html5-input-types.scss b/cms/static/sass/bourbon/addons/_html5-input-types.scss new file mode 100644 index 0000000000..9d86fbb4d4 --- /dev/null +++ b/cms/static/sass/bourbon/addons/_html5-input-types.scss @@ -0,0 +1,36 @@ +//************************************************************************// +// 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; +// } diff --git a/cms/static/sass/bourbon/addons/_position.scss b/cms/static/sass/bourbon/addons/_position.scss new file mode 100644 index 0000000000..6ad330f1df --- /dev/null +++ b/cms/static/sass/bourbon/addons/_position.scss @@ -0,0 +1,30 @@ +@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; + } +} diff --git a/cms/static/sass/bourbon/addons/_timing-functions.scss b/cms/static/sass/bourbon/addons/_timing-functions.scss new file mode 100644 index 0000000000..51b2410914 --- /dev/null +++ b/cms/static/sass/bourbon/addons/_timing-functions.scss @@ -0,0 +1,32 @@ +// 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); diff --git a/cms/static/sass/bourbon/css3/_animation.scss b/cms/static/sass/bourbon/css3/_animation.scss new file mode 100644 index 0000000000..f99e06eb6f --- /dev/null +++ b/cms/static/sass/bourbon/css3/_animation.scss @@ -0,0 +1,171 @@ +// 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 | + $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."; +} + diff --git a/cms/static/sass/bourbon/css3/_appearance.scss b/cms/static/sass/bourbon/css3/_appearance.scss new file mode 100644 index 0000000000..548767e166 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_appearance.scss @@ -0,0 +1,7 @@ +@mixin appearance ($value) { + -webkit-appearance: $value; + -moz-appearance: $value; + -ms-appearance: $value; + -o-appearance: $value; + appearance: $value; +} diff --git a/cms/static/sass/bourbon/css3/_background-image.scss b/cms/static/sass/bourbon/css3/_background-image.scss new file mode 100644 index 0000000000..c23cef7c31 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_background-image.scss @@ -0,0 +1,57 @@ +//************************************************************************// +// 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); diff --git a/cms/static/sass/bourbon/css3/_background-size.scss b/cms/static/sass/bourbon/css3/_background-size.scss new file mode 100644 index 0000000000..4bba11027d --- /dev/null +++ b/cms/static/sass/bourbon/css3/_background-size.scss @@ -0,0 +1,15 @@ +@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; +} diff --git a/cms/static/sass/bourbon/css3/_border-image.scss b/cms/static/sass/bourbon/css3/_border-image.scss new file mode 100644 index 0000000000..da4f20ba49 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_border-image.scss @@ -0,0 +1,56 @@ +@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)); diff --git a/cms/static/sass/bourbon/css3/_border-radius.scss b/cms/static/sass/bourbon/css3/_border-radius.scss new file mode 100644 index 0000000000..f24389ebbe --- /dev/null +++ b/cms/static/sass/bourbon/css3/_border-radius.scss @@ -0,0 +1,63 @@ +@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); +} diff --git a/cms/static/sass/bourbon/css3/_box-shadow.scss b/cms/static/sass/bourbon/css3/_box-shadow.scss new file mode 100644 index 0000000000..327b66d251 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_box-shadow.scss @@ -0,0 +1,14 @@ +// 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; +} diff --git a/cms/static/sass/bourbon/css3/_box-sizing.scss b/cms/static/sass/bourbon/css3/_box-sizing.scss new file mode 100644 index 0000000000..3f3f7cca9a --- /dev/null +++ b/cms/static/sass/bourbon/css3/_box-sizing.scss @@ -0,0 +1,6 @@ +@mixin box-sizing ($box) { +// content-box | border-box | inherit + -webkit-box-sizing: $box; + -moz-box-sizing: $box; + box-sizing: $box; +} diff --git a/cms/static/sass/bourbon/css3/_columns.scss b/cms/static/sass/bourbon/css3/_columns.scss new file mode 100644 index 0000000000..2896c91d7f --- /dev/null +++ b/cms/static/sass/bourbon/css3/_columns.scss @@ -0,0 +1,67 @@ +@mixin columns($arg: auto) { +// || + -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) { +// || || + -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; +} diff --git a/cms/static/sass/bourbon/css3/_flex-box.scss b/cms/static/sass/bourbon/css3/_flex-box.scss new file mode 100644 index 0000000000..44c1dfd789 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_flex-box.scss @@ -0,0 +1,67 @@ +// 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; +} diff --git a/cms/static/sass/bourbon/css3/_inline-block.scss b/cms/static/sass/bourbon/css3/_inline-block.scss new file mode 100644 index 0000000000..d79a13c851 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_inline-block.scss @@ -0,0 +1,10 @@ +// 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; +} diff --git a/cms/static/sass/bourbon/css3/_linear-gradient.scss b/cms/static/sass/bourbon/css3/_linear-gradient.scss new file mode 100644 index 0000000000..e366a299a9 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_linear-gradient.scss @@ -0,0 +1,41 @@ +@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%); diff --git a/cms/static/sass/bourbon/css3/_radial-gradient.scss b/cms/static/sass/bourbon/css3/_radial-gradient.scss new file mode 100644 index 0000000000..e83cab5234 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_radial-gradient.scss @@ -0,0 +1,31 @@ +// 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); diff --git a/cms/static/sass/bourbon/css3/_transform.scss b/cms/static/sass/bourbon/css3/_transform.scss new file mode 100644 index 0000000000..8d19e8b88d --- /dev/null +++ b/cms/static/sass/bourbon/css3/_transform.scss @@ -0,0 +1,19 @@ +@mixin transform($property: none) { +// none | + -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; +} diff --git a/cms/static/sass/bourbon/css3/_transition.scss b/cms/static/sass/bourbon/css3/_transition.scss new file mode 100644 index 0000000000..058dbe0e33 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_transition.scss @@ -0,0 +1,104 @@ +// 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; +} + diff --git a/cms/static/sass/bourbon/css3/_user-select.scss b/cms/static/sass/bourbon/css3/_user-select.scss new file mode 100644 index 0000000000..d5f5749431 --- /dev/null +++ b/cms/static/sass/bourbon/css3/_user-select.scss @@ -0,0 +1,6 @@ +@mixin user-select($arg: none) { + -webkit-user-select: $arg; + -moz-user-select: $arg; + -ms-user-select: $arg; + user-select: $arg; +} diff --git a/cms/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss b/cms/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss new file mode 100644 index 0000000000..1322f6f60e --- /dev/null +++ b/cms/static/sass/bourbon/functions/_deprecated-webkit-gradient.scss @@ -0,0 +1,36 @@ +// 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; +} diff --git a/cms/static/sass/bourbon/functions/_flex-grid.scss b/cms/static/sass/bourbon/functions/_flex-grid.scss new file mode 100644 index 0000000000..707f994e15 --- /dev/null +++ b/cms/static/sass/bourbon/functions/_flex-grid.scss @@ -0,0 +1,35 @@ +// 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%; +// } +// } diff --git a/cms/static/sass/bourbon/functions/_grid-width.scss b/cms/static/sass/bourbon/functions/_grid-width.scss new file mode 100644 index 0000000000..8e63d83d60 --- /dev/null +++ b/cms/static/sass/bourbon/functions/_grid-width.scss @@ -0,0 +1,13 @@ +@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; +// } diff --git a/cms/static/sass/bourbon/functions/_linear-gradient.scss b/cms/static/sass/bourbon/functions/_linear-gradient.scss new file mode 100644 index 0000000000..3b10ca82a6 --- /dev/null +++ b/cms/static/sass/bourbon/functions/_linear-gradient.scss @@ -0,0 +1,23 @@ +@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; +} + diff --git a/cms/static/sass/bourbon/functions/_modular-scale.scss b/cms/static/sass/bourbon/functions/_modular-scale.scss new file mode 100644 index 0000000000..dddccb5224 --- /dev/null +++ b/cms/static/sass/bourbon/functions/_modular-scale.scss @@ -0,0 +1,40 @@ +@function modular-scale($value, $increment, $ratio) { + @if $increment > 0 { + @for $i from 1 through $increment { + $value: ($value * $ratio); + } + } + + @if $increment < 0 { + $increment: abs($increment); + @for $i from 1 through $increment { + $value: ($value / $ratio); + } + } + + @return $value; +} + +// div { +// Increment Up GR with positive value +// font-size: modular-scale(14px, 1, 1.618); // returns: 22.652px +// +// Increment Down GR with negative value +// font-size: modular-scale(14px, -1, 1.618); // returns: 8.653px +// +// Can be used with ceil(round up) or floor(round down) +// font-size: floor( modular-scale(14px, 1, 1.618) ); // returns: 22px +// font-size: ceil( modular-scale(14px, 1, 1.618) ); // returns: 23px +// } +// +// modularscale.com + +@function golden-ratio($value, $increment) { + @return modular-scale($value, $increment, 1.618) +} + +// div { +// font-size: golden-ratio(14px, 1); // returns: 22.652px +// } +// +// goldenratiocalculator.com diff --git a/cms/static/sass/bourbon/functions/_radial-gradient.scss b/cms/static/sass/bourbon/functions/_radial-gradient.scss new file mode 100644 index 0000000000..3d5461ad6e --- /dev/null +++ b/cms/static/sass/bourbon/functions/_radial-gradient.scss @@ -0,0 +1,15 @@ +// This function is required and used by the background-image mixin. +@function radial-gradient($pos, $shape-size, + $G1, $G2, + $G3: false, $G4: false, + $G5: false, $G6: false, + $G7: false, $G8: false, + $G9: false, $G10: false) { + + $type: radial; + $gradient: compact($pos, $shape-size, $G1, $G2, $G3, $G4, $G5, $G6, $G7, $G8, $G9, $G10); + $type-gradient: append($type, $gradient, comma); + + @return $type-gradient; +} + diff --git a/cms/static/sass/bourbon/functions/_render-gradients.scss b/cms/static/sass/bourbon/functions/_render-gradients.scss new file mode 100644 index 0000000000..fe7c799ebe --- /dev/null +++ b/cms/static/sass/bourbon/functions/_render-gradients.scss @@ -0,0 +1,14 @@ +// User for linear and radial gradients within background-image or border-image properties + +@function render-gradients($gradients, $gradient-type, $vendor: false) { + $vendor-gradients: false; + @if $vendor { + $vendor-gradients: -#{$vendor}-#{$gradient-type}-gradient($gradients); + } + + @else if $vendor == false { + $vendor-gradients: "#{$gradient-type}-gradient(#{$gradients})"; + $vendor-gradients: unquote($vendor-gradients); + } + @return $vendor-gradients; +} diff --git a/cms/static/sass/bourbon/functions/_tint-shade.scss b/cms/static/sass/bourbon/functions/_tint-shade.scss new file mode 100644 index 0000000000..f7172004ac --- /dev/null +++ b/cms/static/sass/bourbon/functions/_tint-shade.scss @@ -0,0 +1,9 @@ +// Add percentage of white to a color +@function tint($color, $percent){ + @return mix(white, $color, $percent); +} + +// Add percentage of black to a color +@function shade($color, $percent){ + @return mix(black, $color, $percent); +} diff --git a/cms/static/sass/bourbon/lib/bourbon.rb b/cms/static/sass/bourbon/lib/bourbon.rb new file mode 100644 index 0000000000..1635be836d --- /dev/null +++ b/cms/static/sass/bourbon/lib/bourbon.rb @@ -0,0 +1,19 @@ +require "bourbon/generator" + +module Bourbon + if defined?(Rails) + class Engine < ::Rails::Engine + require 'bourbon/engine' + end + + module Rails + class Railtie < ::Rails::Railtie + rake_tasks do + load "tasks/install.rake" + end + end + end + end +end + +require File.join(File.dirname(__FILE__), "/bourbon/sass_extensions") diff --git a/cms/static/sass/bourbon/lib/bourbon/sass_extensions.rb b/cms/static/sass/bourbon/lib/bourbon/sass_extensions.rb new file mode 100644 index 0000000000..ad567200e3 --- /dev/null +++ b/cms/static/sass/bourbon/lib/bourbon/sass_extensions.rb @@ -0,0 +1,6 @@ +module Bourbon::SassExtensions +end + +require "sass" + +require File.join(File.dirname(__FILE__), "/sass_extensions/functions") diff --git a/cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb b/cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb new file mode 100644 index 0000000000..daa877650e --- /dev/null +++ b/cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions.rb @@ -0,0 +1,13 @@ +module Bourbon::SassExtensions::Functions +end + +require File.join(File.dirname(__FILE__), "/functions/compact") + +module Sass::Script::Functions + include Bourbon::SassExtensions::Functions::Compact +end + +# Wierd that this has to be re-included to pick up sub-modules. Ruby bug? +class Sass::Script::Functions::EvaluationContext + include Sass::Script::Functions +end diff --git a/cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb b/cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb new file mode 100644 index 0000000000..5192e921e7 --- /dev/null +++ b/cms/static/sass/bourbon/lib/bourbon/sass_extensions/functions/compact.rb @@ -0,0 +1,13 @@ +# Compact function pulled from compass +module Bourbon::SassExtensions::Functions::Compact + + def compact(*args) + sep = :comma + if args.size == 1 && args.first.is_a?(Sass::Script::List) + args = args.first.value + sep = args.first.separator + end + Sass::Script::List.new(args.reject{|a| !a.to_bool}, sep) + end + +end diff --git a/cms/templates/base.html b/cms/templates/base.html new file mode 100644 index 0000000000..497296ee66 --- /dev/null +++ b/cms/templates/base.html @@ -0,0 +1,30 @@ + + + + + + + + + + {% block title %}{% endblock %} + + + + + + {% include "widgets/header.html"%} + + {% block content %}{% endblock %} + + + + + + + + + + + + diff --git a/cms/templates/index.html b/cms/templates/index.html new file mode 100644 index 0000000000..82c3133a37 --- /dev/null +++ b/cms/templates/index.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block title %}Course Manager{% endblock %} + +{% block content %} +

    + + {% include "widgets/navigation.html"%} + +
    + {% include "widgets/week-edit.html"%} + {% include "widgets/week-new.html"%} + {% include "widgets/video-edit.html"%} + {% include "widgets/video-new.html"%} + {% include "widgets/problem-edit.html"%} + {% include "widgets/problem-new.html"%} +
    + +
    +{% endblock %} diff --git a/cms/templates/widgets/captions.html b/cms/templates/widgets/captions.html new file mode 100644 index 0000000000..088beb7a33 --- /dev/null +++ b/cms/templates/widgets/captions.html @@ -0,0 +1,242 @@ +
      +
    • English (main)
    • +
    • French
    • +
    • English v2
    • +
    • +
    • +
    + + diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html new file mode 100644 index 0000000000..86a2afb0f9 --- /dev/null +++ b/cms/templates/widgets/header.html @@ -0,0 +1,5 @@ +
    + +
    diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html new file mode 100644 index 0000000000..049e5da537 --- /dev/null +++ b/cms/templates/widgets/navigation.html @@ -0,0 +1,120 @@ +
    +
    +

    Circuts & Electronics

    + + +
    + +
      +
    1. +
      +

      Week 1

      +
      + +
        +
      • Goal 1
      • +
      • Goal 2
      • +
      • Lecture Sequence
      • +
      • Lecture Sequence
      • +
      • Lab
      • +
      • Homework
      • +
      • + Add new sequence
      • +
      +
    2. +
    3. +
      +

      Week 2

      +
      + +
        +
      • Goal 1
      • +
      • Lecture Sequence
      • +
      • Lecture Sequence
      • +
      • Lab
      • +
      • Homework
      • +
      • + Add new sequence
      • +
      +
    4. +
    5. +
      +

      Week 3

      +
      + +
        +
      • Goal 1
      • +
      • Lab
      • +
      • Lab
      • +
      • Homework
      • +
      • + Add new sequence
      • +
      +
    6. +
    7. +
      +

      Week 4

      +
      + +
        +
      • Goal 1
      • +
      • Lecture Sequence
      • +
      • Lab
      • +
      • Homework
      • +
      • Homework
      • +
      • + Add new sequence
      • +
      +
    8. + +
    9. +
      +

      Week 5

      +
      + +
        +
      • + Add new sequence
      • +
      +
    10. +
    11. +

      + + Add New +

      + +
      +
      +

      Week 6

      +
      + +
      + + +
      +
      +
    12. +
    +
    + diff --git a/cms/templates/widgets/new-module.html b/cms/templates/widgets/new-module.html new file mode 100644 index 0000000000..070b6462bb --- /dev/null +++ b/cms/templates/widgets/new-module.html @@ -0,0 +1,9 @@ +
  • + +
  • diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html new file mode 100644 index 0000000000..b1d5796a9f --- /dev/null +++ b/cms/templates/widgets/problem-edit.html @@ -0,0 +1,73 @@ +
    +
    + cancel + Save & Update +
    + +
    +
    +

    Old Problem

    +
    + + +
    + Settings +
    + + +
    +
    +
    +

    Tags:

    +

    Click to edit

    +
    + +
    +

    Last modified:

    +

    mm/dd/yy

    +
    + +
    +

    By

    +

    Anant Agarwal

    +
    +
    +
    + +
    + +
    + Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. +
    +
    + +
    +
      +
    • +

      Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

      +

      Anant Agarwal

      +
    • +
    • +

      Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

      +

      Anant Agarwal

      +
    • +
    + +
    +

    Add notes

    + + +
    +
    + + Save & Update +
    +
    + diff --git a/cms/templates/widgets/problem-new.html b/cms/templates/widgets/problem-new.html new file mode 100644 index 0000000000..9f91dd1177 --- /dev/null +++ b/cms/templates/widgets/problem-new.html @@ -0,0 +1,52 @@ +
    +
    + cancel + Save & Update +
    + +
    +
    +

    New Problem

    +
    + + Settings + +
    +
    +
    +

    Tags:

    +

    Click to edit

    +
    + + + + + + + + + + +
    +
    + +
    + +
    +
    +
    + +
    +

    Add notes

    +
    + + Save & Update +
    +
    diff --git a/cms/templates/widgets/raw-videos.html b/cms/templates/widgets/raw-videos.html new file mode 100644 index 0000000000..f466fd59bc --- /dev/null +++ b/cms/templates/widgets/raw-videos.html @@ -0,0 +1,3 @@ +
  • +
    Video-file-name
    +
  • diff --git a/cms/templates/widgets/save-captions.html b/cms/templates/widgets/save-captions.html new file mode 100644 index 0000000000..87342f0cd0 --- /dev/null +++ b/cms/templates/widgets/save-captions.html @@ -0,0 +1,4 @@ +
    + Cancel + +
    diff --git a/cms/templates/widgets/speed-tooltip.html b/cms/templates/widgets/speed-tooltip.html new file mode 100644 index 0000000000..2a82e237e7 --- /dev/null +++ b/cms/templates/widgets/speed-tooltip.html @@ -0,0 +1,7 @@ +
    + +
    diff --git a/cms/templates/widgets/video-box-unused.html b/cms/templates/widgets/video-box-unused.html new file mode 100644 index 0000000000..8cde10f151 --- /dev/null +++ b/cms/templates/widgets/video-box-unused.html @@ -0,0 +1,38 @@ +
  • + +
    + +
    + video-name 236mb Uploaded 6 hours ago by Anant Agrawal +

    +

      + Speed +
    • + 0.75x + {% include "widgets/speed-tooltip.html" %} +
    • +
    • Normal + {% include "widgets/speed-tooltip.html" %} +
    • +
    • 1.25x + {% include "widgets/speed-tooltip.html" %} +
    • +
    • 1.5x + {% include "widgets/speed-tooltip.html" %} +
    • +
    • +
    • +
    +

    +

    + Download All — + Delete All — + Edit Captions — + Use clip ⬆ +

    +
    +
    + {% include "widgets/captions.html" %} + {% include "widgets/save-captions.html" %} +
    +
  • + diff --git a/cms/templates/widgets/video-box.html b/cms/templates/widgets/video-box.html new file mode 100644 index 0000000000..9ab17030bd --- /dev/null +++ b/cms/templates/widgets/video-box.html @@ -0,0 +1,35 @@ +
  • +
    + +
    + video-name 236mb +

    Uploaded 6 hours ago by Anant Agrawal

    +

    +

      + Speed +
    • + 0.75x + {% include "widgets/speed-tooltip.html" %} +
    • +
    • Normal + {% include "widgets/speed-tooltip.html" %} +
    • +
    • 1.25x + {% include "widgets/speed-tooltip.html" %} +
    • +
    • 1.5x + {% include "widgets/speed-tooltip.html" %} +
    • +
    • +
    • +
    +

    +

    + Download all — +Remove ⬇ + +

    +
    +
    + {% include "widgets/captions.html" %} +
    +
  • diff --git a/cms/templates/widgets/video-edit.html b/cms/templates/widgets/video-edit.html new file mode 100644 index 0000000000..6b49dc27d7 --- /dev/null +++ b/cms/templates/widgets/video-edit.html @@ -0,0 +1,151 @@ +
    + +
    +
    + +
    +
    + Created 22/03/12 + by Piotr Mitros +
    + +
    + Last edited 22/03/12 + by David Ormsbee +
    +
    +
    +
    +

    Video title

    + +
    + Tag mitx, s4v1, circuits, anant +
    + +
    +
    + Keyword + S4V1 + + + Due Date + 21/03/12 + + Status + + + +
    + +
    + +
    +
    +
    + or + use an already uploaded one +
    +
    + +
    +
    +
      +
    • video-name-@0-75x.extension
    • +
    • video-name-@1x.extension
    • +
    • video-name-@1-25x.extension
    • +
    • video-name-@1-5x.extension
    • +
    +
    + + + Cancel +
    + +
    + +
    +
    +
      +

      Video clip in use

      + {% include "widgets/video-box.html" %} +
    +

    No video clip used. Select one from the list below, upload a new one or import an already uploaded video.

    +
    +
    + +
    +
    + + +
    +
    +
      + {% include "widgets/video-box-unused.html" %} +
    +
    +
    + +
    + +
    +

    Annotations

    + + +
    + +
    + +
    + +
    +

    Notes

    + + + +
    + +
    +
    +
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. +

    By Piotr Mitros 10 hours ago

    +
    +
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    By Piotr Mitros 10 hours ago

    +
    +
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    By Piotr Mitros 10 hours ago

    +
    +
    +
    + + + +
    + +
    + Select the source video or directly enter its ID +
    + +
    + +
    + +
    + + + Cancel +
    +
    diff --git a/cms/templates/widgets/video-new.html b/cms/templates/widgets/video-new.html new file mode 100644 index 0000000000..2555169c05 --- /dev/null +++ b/cms/templates/widgets/video-new.html @@ -0,0 +1,48 @@ +
    +
    + cancel + Save & Update +
    + +
    +
    +

    Untitled Video

    +
    + + Settings + +
    + +
    +
    +

    Tags:

    +

    Click to edit

    +
    + + + + + + + + + + +
    +
    + +
    + +
    +
    + Save & Update +
    +
    +
    diff --git a/cms/templates/widgets/week-edit.html b/cms/templates/widgets/week-edit.html new file mode 100644 index 0000000000..5e6ac199e9 --- /dev/null +++ b/cms/templates/widgets/week-edit.html @@ -0,0 +1,83 @@ +{% block content %} +
    +
    +

    Week 3

    + Done +
    + + + +
    +
    +

    Weeks Content

    +
    + +
    +
    + +
    +
      +
    • + + +
    • + +
    • + + +
    • +
    • + +
    • + +
    • + Advanced filters +
    • +
    +
    + +
    + +
    +
    +
    +{% endblock %} diff --git a/cms/templates/widgets/week-new.html b/cms/templates/widgets/week-new.html new file mode 100644 index 0000000000..42f4f78e1e --- /dev/null +++ b/cms/templates/widgets/week-new.html @@ -0,0 +1,62 @@ +{% block content %} +
    +
    +

    Week 6

    + Done +
    + + + +
    +
    +

    Weeks Content

    +
    + +
    +
    + +
    +
      +
    • + + +
    • + +
    • + + +
    • +
    • + +
    • + +
    • + Advanced filters +
    • +
    +
    +
    +
    +{% endblock %} diff --git a/cms/urls.py b/cms/urls.py index a7266066cc..781c2c261f 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -7,4 +7,5 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('', url(r'^(?P[^/]+)/(?P[^/]+)/calendar/', 'contentstore.views.calendar', name='calendar'), url(r'^accounts/login/', 'instructor.views.do_login', name='login'), + url(r'^$', 'contentstore.views.index', name='index'), ) From 8338031f3e90c76ea740d1c000456231511b3822 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 13 Jun 2012 13:22:38 -0400 Subject: [PATCH 050/252] Switch to mako templates --- cms/templates/base.html | 24 ++++++++++----------- cms/templates/index.html | 22 +++++++++---------- cms/templates/widgets/video-box-unused.html | 12 +++++------ cms/templates/widgets/video-box.html | 10 ++++----- cms/templates/widgets/video-edit.html | 8 +++---- cms/templates/widgets/week-edit.html | 6 +++--- cms/templates/widgets/week-new.html | 6 +++--- 7 files changed, 44 insertions(+), 44 deletions(-) diff --git a/cms/templates/base.html b/cms/templates/base.html index 497296ee66..a23a31d9a5 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -4,26 +4,26 @@ - - - - {% block title %}{% endblock %} + + + + <%block name="title"></%block> - {% include "widgets/header.html"%} + <%include file="widgets/header.html"/> - {% block content %}{% endblock %} + <%block name="content"> - - - - - - + + + + + + diff --git a/cms/templates/index.html b/cms/templates/index.html index 82c3133a37..efd9f9a242 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -1,19 +1,19 @@ -{% extends "base.html" %} -{% block title %}Course Manager{% endblock %} +<%inherit file="base.html" /> +<%block name="title">Course Manager -{% block content %} +<%block name="content">
    - {% include "widgets/navigation.html"%} + <%include file="widgets/navigation.html"/>
    - {% include "widgets/week-edit.html"%} - {% include "widgets/week-new.html"%} - {% include "widgets/video-edit.html"%} - {% include "widgets/video-new.html"%} - {% include "widgets/problem-edit.html"%} - {% include "widgets/problem-new.html"%} + <%include file="widgets/week-edit.html"/> + <%include file="widgets/week-new.html"/> + <%include file="widgets/video-edit.html"/> + <%include file="widgets/video-new.html"/> + <%include file="widgets/problem-edit.html"/> + <%include file="widgets/problem-new.html"/>
    -{% endblock %} + diff --git a/cms/templates/widgets/video-box-unused.html b/cms/templates/widgets/video-box-unused.html index 8cde10f151..3d643ff3c9 100644 --- a/cms/templates/widgets/video-box-unused.html +++ b/cms/templates/widgets/video-box-unused.html @@ -9,16 +9,16 @@ Speed
  • 0.75x - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • Normal - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • 1.25x - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • 1.5x - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • +
  • @@ -31,8 +31,8 @@

    - {% include "widgets/captions.html" %} - {% include "widgets/save-captions.html" %} + <%include file="captions.html"/> + <%include file="save-captions.html"/>
    diff --git a/cms/templates/widgets/video-box.html b/cms/templates/widgets/video-box.html index 9ab17030bd..1f17e33511 100644 --- a/cms/templates/widgets/video-box.html +++ b/cms/templates/widgets/video-box.html @@ -9,16 +9,16 @@ Speed
  • 0.75x - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • Normal - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • 1.25x - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • 1.5x - {% include "widgets/speed-tooltip.html" %} + <%include file="speed-tooltip.html"/>
  • +
  • @@ -30,6 +30,6 @@

    - {% include "widgets/captions.html" %} + <%include file="captions.html"/>
    diff --git a/cms/templates/widgets/video-edit.html b/cms/templates/widgets/video-edit.html index 6b49dc27d7..ac4c921918 100644 --- a/cms/templates/widgets/video-edit.html +++ b/cms/templates/widgets/video-edit.html @@ -71,7 +71,7 @@

      Video clip in use

      - {% include "widgets/video-box.html" %} + <%include file="video-box.html"/>

    No video clip used. Select one from the list below, upload a new one or import an already uploaded video.

    @@ -84,7 +84,7 @@
      - {% include "widgets/video-box-unused.html" %} + <%include file="video-box-unused.html"/>
    @@ -129,7 +129,7 @@ @@ -140,7 +140,7 @@
    - +
    diff --git a/cms/templates/widgets/week-edit.html b/cms/templates/widgets/week-edit.html index 5e6ac199e9..ea6d29ef3b 100644 --- a/cms/templates/widgets/week-edit.html +++ b/cms/templates/widgets/week-edit.html @@ -1,4 +1,4 @@ -{% block content %} +<%block name="content">

    Week 3

    @@ -27,7 +27,7 @@
  • Problem title 13
  • Problem title 14
  • Video 3
  • - {% include "widgets/new-module.html"%} + <%include file="new-module.html"/>
    @@ -80,4 +80,4 @@ -{% endblock %} + diff --git a/cms/templates/widgets/week-new.html b/cms/templates/widgets/week-new.html index 42f4f78e1e..986dc6e05e 100644 --- a/cms/templates/widgets/week-new.html +++ b/cms/templates/widgets/week-new.html @@ -1,4 +1,4 @@ -{% block content %} +<%block name="content">

    Week 6

    @@ -18,7 +18,7 @@

    Scratchpad

      - {% include "widgets/new-module.html"%} + <%include file="new-module.html"/>
    @@ -59,4 +59,4 @@ -{% endblock %} + From f2080fde62335645a4d9ea787e8097e716d55f7a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 13 Jun 2012 14:43:01 -0400 Subject: [PATCH 051/252] Porting latest changes from ui_prototype --- cms/static/css/base-style.css | 684 ++++++++++++++------- cms/static/js/main.js | 165 ++--- cms/static/sass/_base.scss | 16 +- cms/static/sass/_calendar.scss | 286 +++++---- cms/static/sass/_module-header.scss | 128 ++++ cms/static/sass/_problem.scss | 47 +- cms/static/sass/_video.scss | 59 +- cms/static/sass/_week.scss | 4 +- cms/static/sass/base-style.scss | 14 +- cms/templates/index.html | 1 + cms/templates/widgets/header.html | 3 +- cms/templates/widgets/module-dropdown.html | 28 + cms/templates/widgets/navigation.html | 118 ++-- cms/templates/widgets/new-module.html | 16 +- cms/templates/widgets/problem-edit.html | 72 +-- cms/templates/widgets/problem-new.html | 33 +- cms/templates/widgets/sequnce-edit.html | 85 +++ cms/templates/widgets/video-edit.html | 184 ++---- cms/templates/widgets/video-new.html | 81 ++- cms/templates/widgets/week-edit.html | 94 +-- cms/templates/widgets/week-new.html | 114 ++-- 21 files changed, 1356 insertions(+), 876 deletions(-) create mode 100644 cms/static/sass/_module-header.scss create mode 100644 cms/templates/widgets/module-dropdown.html create mode 100644 cms/templates/widgets/sequnce-edit.html diff --git a/cms/static/css/base-style.css b/cms/static/css/base-style.css index 2cdeafe9e5..518443a41c 100644 --- a/cms/static/css/base-style.css +++ b/cms/static/css/base-style.css @@ -191,13 +191,28 @@ body { clear: both; } body > header nav h2 { font-size: 14px; - text-transform: uppercase; } + text-transform: uppercase; + float: left; } + body > header nav a.new-module { + float: right; } + body.content section.main-content { + border-left: 2px solid #000; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 74.423%; + float: left; + -webkit-box-shadow: -2px 0 3px #dddddd; + -moz-box-shadow: -2px 0 3px #dddddd; + box-shadow: -2px 0 3px #dddddd; } a { text-decoration: none; color: #888; } -input[type="submit"], .button, section.week-edit > header a, section.week-new > header a { +input[type="submit"], .button, section.cal section.new-section > a, section.week-edit > header a, +section.week-new > header a, +section.sequence-edit > header a, section.video-new > section section.upload a.upload-button, section.video-edit > section section.upload a.upload-button, section.video-new > section a.save-update, section.video-edit > section a.save-update, section.problem-new > section a.save, section.problem-edit > section a.save { border: 1px solid #ccc; background: #efefef; -webkit-border-radius: 3px; @@ -221,17 +236,36 @@ section.cal { clear: both; } section.cal > header { zoom: 1; - margin-bottom: 10px; } + margin-bottom: 10px; + background: #efefef; + border: 1px solid #ddd; } section.cal > header:before, section.cal > header:after { content: ""; display: table; } section.cal > header:after { clear: both; } - section.cal > header h1 { - float: left; - font-size: 18px; } + section.cal > header h2 { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 14px; + padding: 6px; + margin-left: 6px; + font-size: 12px; } section.cal > header ul { - float: right; } + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } section.cal > header ul li { display: -moz-inline-box; -moz-box-orient: vertical; @@ -239,42 +273,37 @@ section.cal { vertical-align: baseline; zoom: 1; *display: inline; - *vertical-align: auto; } + *vertical-align: auto; + margin-left: 6px; + padding-left: 6px; + border-left: 1px solid #ddd; + padding: 6px; } section.cal > header ul li a { - padding: 6px; - border: 1px solid #ddd; - display: block; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -ms-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; - background: #efefef; } - section.cal > header ul li.dropdown { - position: relative; } - section.cal > header ul li.dropdown ul { - display: none; - position: absolute; - background: #fff; - border: 1px solid #ddd; } - section.cal > header ul li.dropdown ul li { - padding: 6px; - display: block; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; } - section.cal > header ul li.dropdown ul li:hover { - background-color: #efefef; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; } - section.cal > header ul li.dropdown:hover ul { - display: block; } - section.cal > header ul li.dropdown:hover a { - -webkit-border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - -ms-border-radius: 3px 3px 0 0; - -o-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; - border-bottom: 0; } + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.cal > header ul li ul { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.cal > header ul li ul li { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + padding: 0; + border-left: 0; } section.cal ol { list-style: none; zoom: 1; @@ -297,75 +326,26 @@ section.cal { box-sizing: border-box; float: left; width: 25.0%; } - section.cal ol > li:last-child { - text-align: center; - background: #eee; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; } - section.cal ol > li:last-child p { - width: 100%; - height: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; } - section.cal ol > li:last-child p a { - display: block; - width: 100%; - height: 100%; } - section.cal ol > li:last-child section.new-week header { - background: #fff; - text-align: left; } - section.cal ol > li:last-child section.new-week form { - background: #fff; - width: 50%; - padding: 6px; - border: 1px solid #000; - margin: 0 auto; - -webkit-box-shadow: 0 0 2px #333333; - -moz-box-shadow: 0 0 2px #333333; - box-shadow: 0 0 2px #333333; - position: relative; } - section.cal ol > li:last-child section.new-week form:before { - background: #fff; - border-left: 1px solid #000; - border-top: 1px solid #000; - content: " "; - display: block; - height: 10px; - left: 50%; - position: absolute; - top: -6px; - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); - width: 10px; - z-index: 0; } - section.cal ol > li:last-child section.new-week form select { - margin-bottom: 6px; - width: 100%; } - section.cal ol > li:last-child section.new-week form select option { - padding: 10px 0 !important; } - section.cal ol > li:last-child section.new-week form input[type="submit"] { - display: block; - margin-bottom: 6px; - width: 100%; } - section.cal ol > li:last-child section.new-week form a:first-child { - float: left; } - section.cal ol > li:last-child section.new-week form a:last-child { - float: right; } section.cal ol > li header { border-bottom: 1px solid #000; - -webkit-box-shadow: 0 1px 2px #cccccc; - -moz-box-shadow: 0 1px 2px #cccccc; - box-shadow: 0 1px 2px #cccccc; + -webkit-box-shadow: 0 1px 2px #aaaaaa; + -moz-box-shadow: 0 1px 2px #aaaaaa; + box-shadow: 0 1px 2px #aaaaaa; display: block; - margin-bottom: 2px; - padding: 6px; } + margin-bottom: 2px; } section.cal ol > li header h1 { - font-size: 14px; } + font-size: 14px; + text-transform: uppercase; + border-bottom: 1px solid #ccc; + padding: 6px; } + section.cal ol > li header h1 a { + color: #000; + display: block; } + section.cal ol > li header ul li { + background: #fff; + color: #888; + border-bottom: 0; + font-size: 12px; } section.cal ol > li ul { list-style: none; margin-bottom: 1px; } @@ -373,8 +353,104 @@ section.cal { background: #efefef; border-bottom: 1px solid #666; padding: 6px; } - section.cal ol > li ul li.goal { - background: #fff; } + section.cal ol > li ul li.create-module { + position: relative; } + section.cal ol > li ul li.create-module > div { + display: none; + position: absolute; + top: 30px; + width: 90%; + background: rgba(0, 0, 0, 0.8); + padding: 10px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + -ms-border-radius: 3px; + -o-border-radius: 3px; + border-radius: 3px; + z-index: 99; } + section.cal ol > li ul li.create-module > div ul li { + border-bottom: 0; + background: none; } + section.cal ol > li ul li.create-module > div ul li input { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + border-color: #000; + padding: 6px; } + section.cal ol > li ul li.create-module > div ul li select { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + section.cal ol > li ul li.create-module > div ul li select option { + font-size: 14px; } + section.cal ol > li ul li.create-module > div ul li a { + float: right; } + section.cal ol > li ul li.create-module > div ul li a:first-child { + float: left; } + section.cal ol > li ul li.create-module:hover div { + display: block; } + section.cal section.new-section { + margin-top: 10px; } + section.cal section.new-section > a { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.cal section.new-section section { + display: none; } + section.cal section.new-section section header { + background: #fff; + text-align: left; } + section.cal section.new-section section form { + background: #fff; + width: 50%; + padding: 6px; + border: 1px solid #000; + margin: 0 auto; + -webkit-box-shadow: 0 0 2px #333333; + -moz-box-shadow: 0 0 2px #333333; + box-shadow: 0 0 2px #333333; + position: relative; } + section.cal section.new-section section form:before { + background: #fff; + border-left: 1px solid #000; + border-top: 1px solid #000; + content: " "; + display: block; + height: 10px; + left: 50%; + position: absolute; + top: -6px; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + width: 10px; + z-index: 0; } + section.cal section.new-section section form select { + margin-bottom: 6px; + width: 100%; } + section.cal section.new-section section form select option { + padding: 10px 0 !important; } + section.cal section.new-section section form input[type="submit"] { + display: block; + margin-bottom: 6px; + width: 100%; } + section.cal section.new-section section form a:first-child { + float: left; } + section.cal section.new-section section form a:last-child { + float: right; } + section.cal section.new-section:hover section { + display: block; } body.content section.cal { @@ -393,31 +469,49 @@ section.cal { box-sizing: border-box; width: 100%; } -section.week-edit > header, section.week-new > header { +section.week-edit > header, +section.week-new > header, +section.sequence-edit > header { border-bottom: 1px solid #ccc; zoom: 1; padding: 6px; } - section.week-edit > header:before, section.week-edit > header:after, section.week-new > header:before, section.week-new > header:after { + section.week-edit > header:before, section.week-edit > header:after, + section.week-new > header:before, + section.week-new > header:after, + section.sequence-edit > header:before, + section.sequence-edit > header:after { content: ""; display: table; } - section.week-edit > header:after, section.week-new > header:after { + section.week-edit > header:after, + section.week-new > header:after, + section.sequence-edit > header:after { clear: both; } - section.week-edit > header h1, section.week-new > header h1 { + section.week-edit > header h1, + section.week-new > header h1, + section.sequence-edit > header h1 { font-size: 18px; float: left; margin-top: 8px 6px; } - section.week-edit > header a, section.week-new > header a { + section.week-edit > header a, + section.week-new > header a, + section.sequence-edit > header a { float: right; display: block; } -section.week-edit section header, section.week-new section header { +section.week-edit section header, +section.week-new section header, +section.sequence-edit section header { background: #666; color: #fff; padding: 6px; border-bottom: 1px solid #333; -webkit-font-smoothing: antialiased; } - section.week-edit section header h2, section.week-new section header h2 { + section.week-edit section header h2, + section.week-new section header h2, + section.sequence-edit section header h2 { font-size: 14px; } -section.week-edit section.sidebar, section.week-new section.sidebar { +section.week-edit section.sidebar, +section.week-new section.sidebar, +section.sequence-edit section.sidebar { width: 34.368%; float: left; background: #ccc; @@ -425,49 +519,85 @@ section.week-edit section.sidebar, section.week-new section.sidebar { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } - section.week-edit section.sidebar section, section.week-new section.sidebar section { + section.week-edit section.sidebar section, + section.week-new section.sidebar section, + section.sequence-edit section.sidebar section { height: 50%; } - section.week-edit section.sidebar section > ul, section.week-new section.sidebar section > ul { + section.week-edit section.sidebar section > ul, + section.week-new section.sidebar section > ul, + section.sequence-edit section.sidebar section > ul { list-style: none; } - section.week-edit section.sidebar section > ul > li, section.week-new section.sidebar section > ul > li { + section.week-edit section.sidebar section > ul > li, + section.week-new section.sidebar section > ul > li, + section.sequence-edit section.sidebar section > ul > li { padding: 6px; border-bottom: 1px solid #666; background: #eee; } - section.week-edit section.sidebar section > ul > li.new-module, section.week-new section.sidebar section > ul > li.new-module { + section.week-edit section.sidebar section > ul > li.new-module, + section.week-new section.sidebar section > ul > li.new-module, + section.sequence-edit section.sidebar section > ul > li.new-module { position: relative; } - section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown { + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown, + section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown, + section.sequence-edit section.sidebar section > ul > li.new-module ul.new-dropdown { list-style: none; } - section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown li, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown li { + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown li, + section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown li, + section.sequence-edit section.sidebar section > ul > li.new-module ul.new-dropdown li { display: none; } - section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child { + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child, + section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child, + section.sequence-edit section.sidebar section > ul > li.new-module ul.new-dropdown li:first-child { display: block; } - section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown:hover li { + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li, + section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown:hover li, + section.sequence-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li { display: block; padding: 6px 0; } - section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child, section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child { + section.week-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child, + section.week-new section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child, + section.sequence-edit section.sidebar section > ul > li.new-module ul.new-dropdown:hover li:first-child { padding-top: 0; } - section.week-edit section.sidebar section p, section.week-new section.sidebar section p { + section.week-edit section.sidebar section p, + section.week-new section.sidebar section p, + section.sequence-edit section.sidebar section p { padding: 6px; border-bottom: 1px solid #666; } -section.week-edit section.weeks-content, section.week-new section.weeks-content { +section.week-edit section.weeks-content, +section.week-new section.weeks-content, +section.sequence-edit section.weeks-content { width: 65.632%; float: left; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } - section.week-edit section.weeks-content header, section.week-new section.weeks-content header { + section.week-edit section.weeks-content header, + section.week-new section.weeks-content header, + section.sequence-edit section.weeks-content header { zoom: 1; } - section.week-edit section.weeks-content header:before, section.week-edit section.weeks-content header:after, section.week-new section.weeks-content header:before, section.week-new section.weeks-content header:after { + section.week-edit section.weeks-content header:before, section.week-edit section.weeks-content header:after, + section.week-new section.weeks-content header:before, + section.week-new section.weeks-content header:after, + section.sequence-edit section.weeks-content header:before, + section.sequence-edit section.weeks-content header:after { content: ""; display: table; } - section.week-edit section.weeks-content header:after, section.week-new section.weeks-content header:after { + section.week-edit section.weeks-content header:after, + section.week-new section.weeks-content header:after, + section.sequence-edit section.weeks-content header:after { clear: both; } - section.week-edit section.weeks-content header h2, section.week-new section.weeks-content header h2 { + section.week-edit section.weeks-content header h2, + section.week-new section.weeks-content header h2, + section.sequence-edit section.weeks-content header h2 { float: left; } - section.week-edit section.weeks-content header form, section.week-new section.weeks-content header form { + section.week-edit section.weeks-content header form, + section.week-new section.weeks-content header form, + section.sequence-edit section.weeks-content header form { float: right; margin: -2px 0; } - section.week-edit section.weeks-content header form input, section.week-new section.weeks-content header form input { + section.week-edit section.weeks-content header form input, + section.week-new section.weeks-content header form input, + section.sequence-edit section.weeks-content header form input { border: 1px solid #000; background: #ddd; padding: 2px 4px; @@ -476,18 +606,30 @@ section.week-edit section.weeks-content, section.week-new section.weeks-content -ms-border-radius: 2px; -o-border-radius: 2px; border-radius: 2px; } - section.week-edit section.weeks-content section.filters, section.week-new section.weeks-content section.filters { + section.week-edit section.weeks-content section.filters, + section.week-new section.weeks-content section.filters, + section.sequence-edit section.weeks-content section.filters { border-bottom: 1px solid #999; } - section.week-edit section.weeks-content section.filters ul, section.week-new section.weeks-content section.filters ul { + section.week-edit section.weeks-content section.filters ul, + section.week-new section.weeks-content section.filters ul, + section.sequence-edit section.weeks-content section.filters ul { zoom: 1; list-style: none; padding: 6px; } - section.week-edit section.weeks-content section.filters ul:before, section.week-edit section.weeks-content section.filters ul:after, section.week-new section.weeks-content section.filters ul:before, section.week-new section.weeks-content section.filters ul:after { + section.week-edit section.weeks-content section.filters ul:before, section.week-edit section.weeks-content section.filters ul:after, + section.week-new section.weeks-content section.filters ul:before, + section.week-new section.weeks-content section.filters ul:after, + section.sequence-edit section.weeks-content section.filters ul:before, + section.sequence-edit section.weeks-content section.filters ul:after { content: ""; display: table; } - section.week-edit section.weeks-content section.filters ul:after, section.week-new section.weeks-content section.filters ul:after { + section.week-edit section.weeks-content section.filters ul:after, + section.week-new section.weeks-content section.filters ul:after, + section.sequence-edit section.weeks-content section.filters ul:after { clear: both; } - section.week-edit section.weeks-content section.filters ul li, section.week-new section.weeks-content section.filters ul li { + section.week-edit section.weeks-content section.filters ul li, + section.week-new section.weeks-content section.filters ul li, + section.sequence-edit section.weeks-content section.filters ul li { display: -moz-inline-box; -moz-box-orient: vertical; display: inline-block; @@ -495,95 +637,201 @@ section.week-edit section.weeks-content, section.week-new section.weeks-content zoom: 1; *display: inline; *vertical-align: auto; } - section.week-edit section.weeks-content section.filters ul li.advanced, section.week-new section.weeks-content section.filters ul li.advanced { + section.week-edit section.weeks-content section.filters ul li.advanced, + section.week-new section.weeks-content section.filters ul li.advanced, + section.sequence-edit section.weeks-content section.filters ul li.advanced { float: right; } - section.week-edit section.weeks-content section.modules ul, section.week-new section.weeks-content section.modules ul { + section.week-edit section.weeks-content section.modules ul, + section.week-new section.weeks-content section.modules ul, + section.sequence-edit section.weeks-content section.modules ul { list-style: none; } - section.week-edit section.weeks-content section.modules ul li, section.week-new section.weeks-content section.modules ul li { + section.week-edit section.weeks-content section.modules ul li, + section.week-new section.weeks-content section.modules ul li, + section.sequence-edit section.weeks-content section.modules ul li { padding: 6px; font-weight: bold; font-size: 16px; border-bottom: 1px solid #333; } - section.week-edit section.weeks-content section.modules ul li a, section.week-new section.weeks-content section.modules ul li a { + section.week-edit section.weeks-content section.modules ul li a, + section.week-new section.weeks-content section.modules ul li a, + section.sequence-edit section.weeks-content section.modules ul li a { color: #000; } -section.video-new, section.video-edit { - position: absolute; - top: 80px; - right: 0; - background: #fff; - width: 40.32%; - -webkit-box-shadow: 0 0 6px #666666; - -moz-box-shadow: 0 0 6px #666666; - box-shadow: 0 0 6px #666666; - border: 1px solid #333; - border-right: 0; - z-index: 4; } - section.video-new > header, section.video-edit > header { - background: #666; +section.video-new > section section.upload, section.video-edit > section section.upload { + padding: 6px; + margin-bottom: 10px; + border: 1px solid #ddd; } + section.video-new > section section.upload a.upload-button, section.video-edit > section section.upload a.upload-button { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; zoom: 1; - color: #fff; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; } - section.video-new > header:before, section.video-new > header:after, section.video-edit > header:before, section.video-edit > header:after { - content: ""; - display: table; } - section.video-new > header:after, section.video-edit > header:after { - clear: both; } - section.video-new > header h2, section.video-edit > header h2 { - float: left; - font-size: 14px; } - section.video-new > header a, section.video-edit > header a { - float: right; } - section.video-new section ul, section.video-edit section ul { - list-style: none; } - section.video-new section ul li, section.video-edit section ul li { - border-bottom: 1px solid #333; - padding: 10px 25px; } + *display: inline; + *vertical-align: auto; } +section.video-new > section section.in-use h2, section.video-edit > section section.in-use h2 { + font-size: 14px; } +section.video-new > section section.in-use div, section.video-edit > section section.in-use div { + background: #eee; + text-align: center; + padding: 6px; } +section.video-new > section a.save-update, section.video-edit > section a.save-update { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin-top: 20px; } -section.problem-new, section.problem-edit { - position: absolute; - top: 80px; - right: 0; - background: #fff; - width: 40.32%; - -webkit-box-shadow: 0 0 6px #666666; - -moz-box-shadow: 0 0 6px #666666; - box-shadow: 0 0 6px #666666; - border: 1px solid #333; - border-right: 0; - z-index: 4; } - section.problem-new > header, section.problem-edit > header { - background: #666; - zoom: 1; - color: #fff; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; } - section.problem-new > header:before, section.problem-new > header:after, section.problem-edit > header:before, section.problem-edit > header:after { - content: ""; - display: table; } - section.problem-new > header:after, section.problem-edit > header:after { - clear: both; } - section.problem-new > header h2, section.problem-edit > header h2 { - float: left; - font-size: 14px; } - section.problem-new > header a, section.problem-edit > header a { - float: right; } - section.problem-new section ul, section.problem-edit section ul { - list-style: none; } - section.problem-new section ul li, section.problem-edit section ul li { - border-bottom: 1px solid #333; - padding: 10px 25px; } - -body.content section.main-content { - border-left: 2px solid #000; +section.problem-new > section textarea, section.problem-edit > section textarea { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; - width: 74.423%; - float: left; - -webkit-box-shadow: -2px 0 3px #dddddd; - -moz-box-shadow: -2px 0 3px #dddddd; - box-shadow: -2px 0 3px #dddddd; } + display: block; + width: 100%; } +section.problem-new > section div.preview, section.problem-edit > section div.preview { + background: #eee; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + height: 40px; + padding: 10px; + width: 100%; } +section.problem-new > section a.save, section.problem-edit > section a.save { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin-top: 20px; } + +section.video-new, section.video-edit, section.problem-new, section.problem-edit { + position: absolute; + top: 72px; + right: 0; + background: #fff; + width: 48.845%; + -webkit-box-shadow: 0 0 6px #666666; + -moz-box-shadow: 0 0 6px #666666; + box-shadow: 0 0 6px #666666; + border: 1px solid #333; + border-right: 0; + z-index: 4; } + section.video-new > header, section.video-edit > header, section.problem-new > header, section.problem-edit > header { + background: #666; + zoom: 1; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; } + section.video-new > header:before, section.video-new > header:after, section.video-edit > header:before, section.video-edit > header:after, section.problem-new > header:before, section.problem-new > header:after, section.problem-edit > header:before, section.problem-edit > header:after { + content: ""; + display: table; } + section.video-new > header:after, section.video-edit > header:after, section.problem-new > header:after, section.problem-edit > header:after { + clear: both; } + section.video-new > header h2, section.video-edit > header h2, section.problem-new > header h2, section.problem-edit > header h2 { + float: left; + font-size: 14px; } + section.video-new > header a, section.video-edit > header a, section.problem-new > header a, section.problem-edit > header a { + color: #fff; } + section.video-new > header a.save-update, section.video-edit > header a.save-update, section.problem-new > header a.save-update, section.problem-edit > header a.save-update { + float: right; } + section.video-new > header a.cancel, section.video-edit > header a.cancel, section.problem-new > header a.cancel, section.problem-edit > header a.cancel { + float: left; } + section.video-new > section, section.video-edit > section, section.problem-new > section, section.problem-edit > section { + padding: 20px; } + section.video-new > section > header h1, section.video-edit > section > header h1, section.problem-new > section > header h1, section.problem-edit > section > header h1 { + font-size: 24px; + margin: 12px 0; } + section.video-new > section > header section.status-settings ul, section.video-edit > section > header section.status-settings ul, section.problem-new > section > header section.status-settings ul, section.problem-edit > section > header section.status-settings ul { + list-style: none; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + -ms-border-radius: 2px; + -o-border-radius: 2px; + border-radius: 2px; + border: 1px solid #999; + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.video-new > section > header section.status-settings ul li, section.video-edit > section > header section.status-settings ul li, section.problem-new > section > header section.status-settings ul li, section.problem-edit > section > header section.status-settings ul li { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + border-right: 1px solid #999; + padding: 6px; } + section.video-new > section > header section.status-settings ul li:last-child, section.video-edit > section > header section.status-settings ul li:last-child, section.problem-new > section > header section.status-settings ul li:last-child, section.problem-edit > section > header section.status-settings ul li:last-child { + border-right: 0; } + section.video-new > section > header section.status-settings ul li.current, section.video-edit > section > header section.status-settings ul li.current, section.problem-new > section > header section.status-settings ul li.current, section.problem-edit > section > header section.status-settings ul li.current { + background: #eee; } + section.video-new > section > header section.status-settings a.settings, section.video-edit > section > header section.status-settings a.settings, section.problem-new > section > header section.status-settings a.settings, section.problem-edit > section > header section.status-settings a.settings { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin: 0 20px; + border: 1px solid #999; + padding: 6px; } + section.video-new > section > header section.status-settings select, section.video-edit > section > header section.status-settings select, section.problem-new > section > header section.status-settings select, section.problem-edit > section > header section.status-settings select { + float: right; } + section.video-new > section > header section.meta, section.video-edit > section > header section.meta, section.problem-new > section > header section.meta, section.problem-edit > section > header section.meta { + background: #eee; + padding: 10px; + margin: 20px 0; + zoom: 1; } + section.video-new > section > header section.meta:before, section.video-new > section > header section.meta:after, section.video-edit > section > header section.meta:before, section.video-edit > section > header section.meta:after, section.problem-new > section > header section.meta:before, section.problem-new > section > header section.meta:after, section.problem-edit > section > header section.meta:before, section.problem-edit > section > header section.meta:after { + content: ""; + display: table; } + section.video-new > section > header section.meta:after, section.video-edit > section > header section.meta:after, section.problem-new > section > header section.meta:after, section.problem-edit > section > header section.meta:after { + clear: both; } + section.video-new > section > header section.meta div, section.video-edit > section > header section.meta div, section.problem-new > section > header section.meta div, section.problem-edit > section > header section.meta div { + float: left; + margin-right: 20px; } + section.video-new > section > header section.meta div h2, section.video-edit > section > header section.meta div h2, section.problem-new > section > header section.meta div h2, section.problem-edit > section > header section.meta div h2 { + font-size: 14px; + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.video-new > section > header section.meta div p, section.video-edit > section > header section.meta div p, section.problem-new > section > header section.meta div p, section.problem-edit > section > header section.meta div p { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.video-new > section section.notes, section.video-edit > section section.notes, section.problem-new > section section.notes, section.problem-edit > section section.notes { + margin-top: 20px; + padding: 6px; + background: #eee; + border: 1px solid #ccc; } + section.video-new > section section.notes textarea, section.video-edit > section section.notes textarea, section.problem-new > section section.notes textarea, section.problem-edit > section section.notes textarea { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + display: block; + width: 100%; } + section.video-new > section section.notes h2, section.video-edit > section section.notes h2, section.problem-new > section section.notes h2, section.problem-edit > section section.notes h2 { + font-size: 14px; + margin-bottom: 6px; } + section.video-new > section section.notes input[type="submit"], section.video-edit > section section.notes input[type="submit"], section.problem-new > section section.notes input[type="submit"], section.problem-edit > section section.notes input[type="submit"] { + margin-top: 10px; } diff --git a/cms/static/js/main.js b/cms/static/js/main.js index 45f94849f8..e7bdff0f14 100644 --- a/cms/static/js/main.js +++ b/cms/static/js/main.js @@ -6,96 +6,6 @@ $(document).ready(function(){ $('.editable-textarea').inlineEdit({control: 'textarea'}); }); - // $("a[rel*=leanModal]").leanModal(); - - // $(".remove").click(function(){ - // $(this).parents('li').hide(); - // }); - - // $("#show-sidebar").click(function(){ - // $("#video-selector").toggleClass('hidden'); - // return false; - // }); - - // $('.use-video').click(function() { - // var used = $('#used'); - // if (used.is(':visible')) { - // used.hide().show('slow'); - // } - // used.show(); - // $('.no-video').hide(); - // }); - - // $('.remove-video').click(function() { - // $('#used').hide(); - // $('.no-video').show(); - // }); - - // $('#new-upload').click(function() { - // $('.selected-files').toggle(); - // return false; - // }); - - // /* $('.block').append('✕<\/a>'); */ - - // $('a.delete').click(function() { - // $(this).parents('.block').hide(); - // }); - - // $('.speed-list > li').hover(function(){ - // $(this).children('.tooltip').toggle(); - // }); - - // $('.delete-speed').click(function(){ - // $(this).parents('li.speed').hide(); - // return false; - // }); - - // $('.edit-captions').click(function(){ - // var parentVid = $(this).parents('div'); - // parentVid.siblings('div.caption-box').toggle(); - // return false; - // }); - - // $('.close-box').click(function(){ - // $(this).parents('.caption-box').hide(); - // return false; - // }); - - // $('ul.dropdown').hide(); - // $('li.questions').click(function() { - // $('ul.dropdown').toggle(); - // return false; - // }); - - // $('#mchoice').click(function(){ - // $('div.used').append($('
    ').load("/widgets/multi-choice.html")); - // return false; - // }); - - // $('#text').click(function(){ - // $('div.used').append($('
    ').load("/widgets/text.html")); - // return false; - // }); - - // $('#numerical').click(function(){ - // $('div.used').append($('
    ').load("/widgets/text-question.html")); - // return false; - // }); - - // $('#equation').click(function(){ - // $('div.used').append($('
    ').load("/widgets/latex-equation.html")); - // return false; - // }); - - // $('#script').click(function(){ - // $('div.used').append($('
    ').load("/widgets/script-widget.html")); - // return false; - // }); - - // $("#mark").markItUp(myWikiSettings); - - var heighest = 0; $('.cal ol > li').each(function(){ heighest = ($(this).height() > heighest) ? $(this).height() : heighest; @@ -104,10 +14,7 @@ $(document).ready(function(){ $('.cal ol > li').css('height',heighest + 'px'); - $('.new-week').hide(); - $('.add-new-week').click(function() { - $(this).hide(); - $('.new-week').show(); + $('.add-new-section').click(function() { return false; }); @@ -117,43 +24,63 @@ $(document).ready(function(){ return false; }); - var windowHeight = $(window).resize().height(); - - $('.sidebar').css('height', windowHeight); - - $('.edit-week').click( function() { - $('body').addClass('content'); - $('body.content .cal').css('height', windowHeight); - $('section.week-new').show(); - return false; + $('.save-update').click(function(){ + $(this).parent().parent().hide(); + return false; }); - $('.cal ol li header h1 a').click( function() { - $('body').addClass('content'); - $('body.content .cal').css('height', windowHeight); - $('section.week-edit').show(); - return false; - }); + setHeight = function(){ + var windowHeight = $(this).height(); + var calHeight = windowHeight - 29; + var sidebarHeight = windowHeight - 73; + $('.sidebar').css('height', sidebarHeight); + $('body.content .cal').css('height', calHeight); + + $('.edit-week').click( function() { + $('body').addClass('content'); + $('body.content .cal').css('height', calHeight); + $('section.week-new').show(); + return false; + }); + + $('.cal ol li header h1 a').click( function() { + $('body').addClass('content'); + $('body.content .cal').css('height', calHeight); + $('section.week-edit').show(); + return false; + }); + + $('a.sequence-edit').click(function(){ + $('body').addClass('content'); + $('body.content .cal').css('height', calHeight); + $('section.sequence-edit').show(); + return false; + }); + } + + $(document).ready(setHeight); + $(window).bind('resize', setHeight); $('.video-new a').click(function(){ - $('section.video-new').show(); - return false; + $('section.video-new').show(); + return false; }); - $('.video-edit a').click(function(){ - $('section.video-edit').show(); - return false; + $('a.video-edit').click(function(){ + $('section.video-edit').show(); + return false; }); $('.problem-new a').click(function(){ - $('section.problem-new').show(); - return false; + $('section.problem-new').show(); + return false; }); - $('.problem-edit a').click(function(){ - $('section.problem-edit').show(); - return false; + $('a.problem-edit').click(function(){ + $('section.problem-edit').show(); + return false; }); + }); diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index d9f3971d9e..d06998c38a 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -32,7 +32,22 @@ body { h2 { font-size: 14px; text-transform: uppercase; + float: left; } + + a.new-module { + float: right; + } + } + } + + &.content { + section.main-content { + border-left: 2px solid #000; + @include box-sizing(border-box); + width: flex-grid(9); + float: left; + @include box-shadow( -2px 0 3px #ddd ); } } } @@ -48,4 +63,3 @@ input[type="submit"], .button { @include border-radius(3px); padding: 6px; } - diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss index b149346ad6..06ac62f549 100644 --- a/cms/static/sass/_calendar.scss +++ b/cms/static/sass/_calendar.scss @@ -7,59 +7,40 @@ section.cal { > header { @include clearfix; margin-bottom: 10px; + background: #efefef; + border: 1px solid #ddd; - h1 { - float: left; - font-size: 18px; + h2 { + @include inline-block(); + text-transform: uppercase; + letter-spacing: 1px; + font-size: 14px; + padding: 6px; + margin-left: 6px; + font-size: 12px; } ul { - float: right; + @include inline-block; li { @include inline-block; + margin-left: 6px; + padding-left: 6px; + border-left: 1px solid #ddd; + padding: 6px; a { - padding: 6px; - border: 1px solid #ddd; - display: block; - @include border-radius(3px); - background: #efefef; + @include inline-block(); } - &.dropdown { - position: relative; + ul { + @include inline-block(); - ul { - display: none; - position: absolute; - background: #fff; - border: 1px solid #ddd; - - li { - padding: 6px; - display: block; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - - &:hover { - background-color: #efefef; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - } - } - } - - &:hover { - - ul { - display: block; - } - - a { - @include border-radius(3px 3px 0 0); - border-bottom: 0; - } + li { + @include inline-block(); + padding: 0; + border-left: 0; } } } @@ -81,91 +62,31 @@ section.cal { float: left; width: flex-grid(3) + ((flex-gutter() * 3) / 4); - &:last-child { - text-align: center; - background: #eee; - @include box-sizing(border-box); - - p { - width: 100%; - height: 100%; - @include box-sizing(border-box); - - a { - display: block; - width: 100%; - height: 100%; - } - } - - section.new-week { - header { - background: #fff; - text-align: left; - } - - form { - background: #fff; - width: 50%; - padding: 6px; - border: 1px solid #000; - margin: 0 auto; - @include box-shadow(0 0 2px #333); - position: relative; - - &:before { - background: #fff; - border-left: 1px solid #000; - border-top: 1px solid #000; - content: " "; - display: block; - height: 10px; - left: 50%; - position: absolute; - top: -6px; - @include transform(rotate(45deg)); - width: 10px; - z-index: 0; - } - - select { - margin-bottom: 6px; - width: 100%; - - option { - padding: 10px 0 !important; - } - } - - input[type="submit"] { - display: block; - margin-bottom: 6px; - width: 100%; - } - - a { - - &:first-child { - float: left; - } - - &:last-child { - float: right; - } - } - } - } - } - header { border-bottom: 1px solid #000; - @include box-shadow(0 1px 2px #ccc); + @include box-shadow(0 1px 2px #aaa); display: block; margin-bottom: 2px; - padding: 6px; h1 { font-size: 14px; + text-transform: uppercase; + border-bottom: 1px solid #ccc; + padding: 6px; + + a { + color: #000; + display: block; + } + } + + ul { + li { + background: #fff; + color: #888; + border-bottom: 0; + font-size: 12px; + } } } @@ -178,13 +99,136 @@ section.cal { border-bottom: 1px solid #666; padding: 6px; - &.goal { - background: #fff; + &.create-module { + position: relative; + + > div { + display: none; + @include position(absolute, 30px 0 0 0); + width: 90%; + background: rgba(#000, .8); + padding: 10px; + @include box-sizing(border-box); + @include border-radius(3px); + z-index: 99; + + ul { + li { + border-bottom: 0; + background: none; + + input { + width: 100%; + @include box-sizing(border-box); + border-color: #000; + padding: 6px; + } + + select { + width: 100%; + @include box-sizing(border-box); + + option { + font-size: 14px; + } + } + + a { + float: right; + + &:first-child { + float: left; + } + } + } + } + } + + &:hover { + div { + display: block; + } + } } } } } } + + section.new-section { + margin-top: 10px; + + > a { + @extend .button; + @include inline-block(); + } + + section { + display: none; + + header { + background: #fff; + text-align: left; + } + + form { + background: #fff; + width: 50%; + padding: 6px; + border: 1px solid #000; + margin: 0 auto; + @include box-shadow(0 0 2px #333); + position: relative; + + &:before { + background: #fff; + border-left: 1px solid #000; + border-top: 1px solid #000; + content: " "; + display: block; + height: 10px; + left: 50%; + position: absolute; + top: -6px; + @include transform(rotate(45deg)); + width: 10px; + z-index: 0; + } + + select { + margin-bottom: 6px; + width: 100%; + + option { + padding: 10px 0 !important; + } + } + + input[type="submit"] { + display: block; + margin-bottom: 6px; + width: 100%; + } + + a { + + &:first-child { + float: left; + } + + &:last-child { + float: right; + } + } + } + } + + &:hover { + section { + display: block; + } + } + } } body.content diff --git a/cms/static/sass/_module-header.scss b/cms/static/sass/_module-header.scss new file mode 100644 index 0000000000..e2af263618 --- /dev/null +++ b/cms/static/sass/_module-header.scss @@ -0,0 +1,128 @@ +section.video-new, section.video-edit, section.problem-new, section.problem-edit { + position: absolute; + top: 72px; + right: 0; + background: #fff; + width: flex-grid(6); + @include box-shadow(0 0 6px #666); + border: 1px solid #333; + border-right: 0; + z-index: 4; + + > header { + background: #666; + @include clearfix; + color: #fff; + padding: 6px; + border-bottom: 1px solid #333; + -webkit-font-smoothing: antialiased; + + h2 { + float: left; + font-size: 14px; + } + + a { + color: #fff; + + &.save-update { + float: right; + } + + &.cancel { + float: left; + } + } + + } + + > section { + padding: 20px; + + > header { + h1 { + font-size: 24px; + margin: 12px 0; + } + + section { + &.status-settings { + ul { + list-style: none; + @include border-radius(2px); + border: 1px solid #999; + @include inline-block(); + + li { + @include inline-block(); + border-right: 1px solid #999; + padding: 6px; + + &:last-child { + border-right: 0; + } + + &.current { + background: #eee; + } + } + } + + a.settings { + @include inline-block(); + margin: 0 20px; + border: 1px solid #999; + padding: 6px; + } + + select { + float: right; + } + } + + &.meta { + background: #eee; + padding: 10px; + margin: 20px 0; + @include clearfix(); + + div { + float: left; + margin-right: 20px; + + h2 { + font-size: 14px; + @include inline-block(); + } + + p { + @include inline-block(); + } + } + } + } + } + + section.notes { + margin-top: 20px; + padding: 6px; + background: #eee; + border: 1px solid #ccc; + + textarea { + @include box-sizing(border-box); + display: block; + width: 100%; + } + + h2 { + font-size: 14px; + margin-bottom: 6px; + } + + input[type="submit"]{ + margin-top: 10px; + } + } + } +} diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_problem.scss index c513ce38d9..66acacf65c 100644 --- a/cms/static/sass/_problem.scss +++ b/cms/static/sass/_problem.scss @@ -1,40 +1,23 @@ section.problem-new, section.problem-edit { - position: absolute; - top: 80px; - right: 0; - background: #fff; - width: flex-grid(5); - @include box-shadow(0 0 6px #666); - border: 1px solid #333; - border-right: 0; - z-index: 4; - - > header { - background: #666; - @include clearfix; - color: #fff; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; - - h2 { - float: left; - font-size: 14px; + > section { + textarea { + @include box-sizing(border-box); + display: block; + width: 100%; } - a { - float: right; + div.preview { + background: #eee; + @include box-sizing(border-box); + height: 40px; + padding: 10px; + width: 100%; } - } - section { - ul { - list-style: none; - - li { - border-bottom: 1px solid #333; - padding: 10px 25px; - } + a.save { + @extend .button; + @include inline-block(); + margin-top: 20px; } } } diff --git a/cms/static/sass/_video.scss b/cms/static/sass/_video.scss index cb234108f4..b68176e2db 100644 --- a/cms/static/sass/_video.scss +++ b/cms/static/sass/_video.scss @@ -1,40 +1,33 @@ section.video-new, section.video-edit { - position: absolute; - top: 80px; - right: 0; - background: #fff; - width: flex-grid(5); - @include box-shadow(0 0 6px #666); - border: 1px solid #333; - border-right: 0; - z-index: 4; + > section { - > header { - background: #666; - @include clearfix; - color: #fff; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; + section.upload { + padding: 6px; + margin-bottom: 10px; + border: 1px solid #ddd; - h2 { - float: left; - font-size: 14px; - } - - a { - float: right; - } - } - - section { - ul { - list-style: none; - - li { - border-bottom: 1px solid #333; - padding: 10px 25px; + a.upload-button { + @extend .button; + @include inline-block(); } } + + section.in-use { + h2 { + font-size: 14px; + } + + div { + background: #eee; + text-align: center; + padding: 6px; + } + } + + a.save-update { + @extend .button; + @include inline-block(); + margin-top: 20px; + } } } diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss index a628b0f164..1a32557db5 100644 --- a/cms/static/sass/_week.scss +++ b/cms/static/sass/_week.scss @@ -1,4 +1,6 @@ -section.week-edit, section.week-new { +section.week-edit, +section.week-new, +section.sequence-edit { > header { border-bottom: 1px solid #ccc; diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 6a2dfbd0d2..133e1bda1b 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -3,16 +3,4 @@ @import 'base'; @import 'calendar'; -@import 'week', 'video', 'problem'; - -body { - &.content { - section.main-content { - border-left: 2px solid #000; - @include box-sizing(border-box); - width: flex-grid(9); - float: left; - @include box-shadow( -2px 0 3px #ddd ); - } - } -} +@import 'week', 'video', 'problem', 'module-header'; diff --git a/cms/templates/index.html b/cms/templates/index.html index efd9f9a242..11c226ae3d 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -9,6 +9,7 @@
    <%include file="widgets/week-edit.html"/> <%include file="widgets/week-new.html"/> + <%include file="widgets/sequnce-edit.html"/> <%include file="widgets/video-edit.html"/> <%include file="widgets/video-new.html"/> <%include file="widgets/problem-edit.html"/> diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 86a2afb0f9..ec550fec37 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,5 +1,6 @@
    diff --git a/cms/templates/widgets/module-dropdown.html b/cms/templates/widgets/module-dropdown.html new file mode 100644 index 0000000000..7c6e1e068c --- /dev/null +++ b/cms/templates/widgets/module-dropdown.html @@ -0,0 +1,28 @@ +
  • + + + Add new module + +
    +
    + +
    +
    +
  • diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 049e5da537..2edef08a12 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -1,9 +1,8 @@
    -

    Circuts & Electronics

    - +

    Filter content:

      -
    • Sequences
        @@ -17,9 +16,20 @@
      • Deadlines + +
          +
        • Today
        • +
        • Tomorrow
        • +
        • This week
        • +
        • In 2 weeks
        • +
        • This month
        • +
      • Goals +
          +
        • Hide
        • +
    @@ -28,93 +38,131 @@
  • Week 1

    +
      +
    • Goal title: This is a goal that will be in the header of the week
    • +
    • Goal title two: This is another fgoal for this week so that students have two things to learn
    • +
      -
    • Goal 1
    • -
    • Goal 2
    • -
    • Lecture Sequence
    • -
    • Lecture Sequence
    • -
    • Lab
    • +
    • Lecture Sequence
    • +
    • Lecture Sequence
    • +
    • Lab
    • Homework
    • -
    • + Add new sequence
    • + <%include file="module-dropdown.html"/>
  • Week 2

    +
      +
    • Another title This is the goal for the week
    • +
      -
    • Goal 1
    • Lecture Sequence
    • Lecture Sequence
    • Lab
    • Homework
    • -
    • + Add new sequence
    • + <%include file="module-dropdown.html"/>
  • Week 3

    +
      +
    • Another title This is the goal for the week
    • +
      -
    • Goal 1
    • Lab
    • Lab
    • Homework
    • -
    • + Add new sequence
    • + <%include file="module-dropdown.html"/>
  • Week 4

    +
      +
    • Another title This is the goal for the week
    • +
    • Goal title two: This is another fgoal for this week so that students have two things to learn
    • +
      -
    • Goal 1
    • Lecture Sequence
    • Lab
    • Homework
    • Homework
    • -
    • + Add new sequence
    • + <%include file="module-dropdown.html"/>
  • Week 5

    +
      +
    • Please create a learning goal for this week
    • +
      -
    • + Add new sequence
    • + <%include file="module-dropdown.html"/>
  • -

    - + Add New -

    +
    +

    Week 6

    +
      +
    • Please create a learning goal for this week
    • +
    +
    -
    -
    -

    Week 6

    -
    +
      + <%include file="module-dropdown.html"/> +
    +
  • +
  • +
    +

    Week 7

    +
      +
    • Please create a learning goal for this week
    • +
    +
    -
    - -
    +
      + <%include file="module-dropdown.html"/> +
    +
  • + + +
    + + Add New Section + +
    + +
    - - - - + + + + + diff --git a/cms/templates/widgets/new-module.html b/cms/templates/widgets/new-module.html index 070b6462bb..6b7794944a 100644 --- a/cms/templates/widgets/new-module.html +++ b/cms/templates/widgets/new-module.html @@ -1,9 +1,7 @@ -
  • - -
  • + diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html index b1d5796a9f..49f67e3e26 100644 --- a/cms/templates/widgets/problem-edit.html +++ b/cms/templates/widgets/problem-edit.html @@ -1,34 +1,13 @@ -
    +
    - cancel - Save & Update + Cancel + Save & Update
    -

    Old Problem

    -
    - - -
    - Settings -
    - - -
    -
    -
    -

    Tags:

    -

    Click to edit

    -
    - +

    New Problem

    +

    Last modified:

    mm/dd/yy

    @@ -39,6 +18,31 @@

    Anant Agarwal

    + +
    + + Settings + + +
    +
    +
    +

    Tags:

    +

    Click to edit

    +
    + +
    +

    Goal

    +

    Click to edit

    +
    +
    @@ -49,6 +53,10 @@
    +

    Add notes

    + + +
    • Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

      @@ -58,16 +66,8 @@

      Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

      Anant Agarwal

    • -
    +
    -
    -

    Add notes

    - - -
    -
    - - Save & Update + Save & Update
    - diff --git a/cms/templates/widgets/problem-new.html b/cms/templates/widgets/problem-new.html index 9f91dd1177..d986f5a9ef 100644 --- a/cms/templates/widgets/problem-new.html +++ b/cms/templates/widgets/problem-new.html @@ -1,39 +1,36 @@
    - cancel - Save & Update + Cancel + Save & Update
    -

    New Problem

    -
    +

    New Problem

    + +
    - Settings + Settings +
    -
    +

    Tags:

    Click to edit

    - - - - - - - - - +
    +

    Goal

    +

    Click to edit

    +
    @@ -45,8 +42,10 @@

    Add notes

    + +
    - Save & Update + Save & Update
    diff --git a/cms/templates/widgets/sequnce-edit.html b/cms/templates/widgets/sequnce-edit.html new file mode 100644 index 0000000000..75d4504b68 --- /dev/null +++ b/cms/templates/widgets/sequnce-edit.html @@ -0,0 +1,85 @@ +<%block name="content"> +
    +
    + Done +

    Lecture Sequence name

    + Settings +
    + + + +
    +
    +

    Sequence Content

    +
    + +
    +
    + +
    +
      +
    • + + +
    • + +
    • + + +
    • +
    • + +
    • + +
    • + Advanced filters +
    • +
    +
    + +
    + +
    +
    +
    + + diff --git a/cms/templates/widgets/video-edit.html b/cms/templates/widgets/video-edit.html index ac4c921918..0c82e743fe 100644 --- a/cms/templates/widgets/video-edit.html +++ b/cms/templates/widgets/video-edit.html @@ -1,151 +1,59 @@ -
    +
    +
    + Cancel + Save & Update +
    -
    -
    +
    +
    +

    Untitled Video

    -
    -
    - Created 22/03/12 - by Piotr Mitros -
    +
    +
    -
    -
    -

    Video title

    + Settings -
    - Tag mitx, s4v1, circuits, anant -
    + +
    -
    -
    - Keyword - S4V1 - +
    +
    +

    Tags:

    +

    Click to edit

    +
    +
    +

    Goal

    +

    mitx, s4v1, circuits, anant

    +
    +
    + - Due Date - 21/03/12 - - Status - - - -
    - -
    - -
    -
    -
    - or - use an already uploaded one -
    -
    - -
    -
    -
      -
    • video-name-@0-75x.extension
    • -
    • video-name-@1x.extension
    • -
    • video-name-@1-25x.extension
    • -
    • video-name-@1-5x.extension
    • -
    -
    - - - Cancel -
    - -
    - -
    -
    -
      -

      Video clip in use

      - <%include file="video-box.html"/> -
    -

    No video clip used. Select one from the list below, upload a new one or import an already uploaded video.

    -
    -
    - -
    -
    - - -
    -
    -
      - <%include file="video-box-unused.html"/> -
    -
    -
    - -
    - -
    -

    Annotations

    - - -
    - +
    + Upload new video clip + Or + use an already uploaded one
    - -
    -
    -

    Notes

    - - - -
    - -
    -
    -
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. -

    By Piotr Mitros 10 hours ago

    -
    -
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    By Piotr Mitros 10 hours ago

    -
    -
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

    By Piotr Mitros 10 hours ago

    +
    +

    Video in use

    +
    +

    No video clip used. Select one from the list below, upload a new one or import already existing video

    -
    - +
    +

    Add notes

    + + +
    -
    - -
    - Select the source video or directly enter its ID -
    - -
    - -
    - -
    - - - Cancel -
    + Save & Update
    +
    diff --git a/cms/templates/widgets/video-new.html b/cms/templates/widgets/video-new.html index 2555169c05..fecbaa423c 100644 --- a/cms/templates/widgets/video-new.html +++ b/cms/templates/widgets/video-new.html @@ -1,48 +1,85 @@
    - cancel - Save & Update + Cancel + Save & Update

    Untitled Video

    -
    -
    -
    - -
    -
    - Save & Update +
    + Upload new video clip + Or + use an already uploaded one + +
    +
    +
      +
    • video-name-@0-75x.extension
    • +
    • video-name-@1x.extension
    • +
    • video-name-@1-25x.extension
    • +
    • video-name-@1-5x.extension
    • +
    +
    + + + Cancel +
    + +
    +

    Video in use

    +
    +

    No video clip used. Select one from the list below, upload a new one or import already existing video

    +
    +
    + +
    +

    Add notes

    + + +
    + + Save & Update
    diff --git a/cms/templates/widgets/week-edit.html b/cms/templates/widgets/week-edit.html index ea6d29ef3b..348185be80 100644 --- a/cms/templates/widgets/week-edit.html +++ b/cms/templates/widgets/week-edit.html @@ -1,37 +1,20 @@ -<%block name="content">

    Week 3

    - Done +

    + new goal

    + +
    +

    Weeks goals:

    +
      +
    • +

      Goal title

      +

      This is the goal body

      +
    • +
    +
    -
    @@ -70,14 +53,55 @@
    + +
    +
    +

    Scratchpad

    +
    + +
      +
    • Problem title 11
    • +
    • Problem title 13
    • +
    • Problem title 14
    • +
    • Video 3
    • + <%include file="new-module.html"/> +
    +
    - diff --git a/cms/templates/widgets/week-new.html b/cms/templates/widgets/week-new.html index 986dc6e05e..fe286a74b3 100644 --- a/cms/templates/widgets/week-new.html +++ b/cms/templates/widgets/week-new.html @@ -1,62 +1,86 @@ -<%block name="content">

    Week 6

    - Done -
    +

    + new goal

    -
    diff --git a/cms/templates/widgets/week-new.html b/cms/templates/widgets/week-new.html index fe286a74b3..46aba9b778 100644 --- a/cms/templates/widgets/week-new.html +++ b/cms/templates/widgets/week-new.html @@ -1,10 +1,13 @@

    Week 6

    -

    + new goal

    -

    Weeks goals:

    +
    +

    Weeks goals:

    +

    +

    +
    +
    • Create new goal

      @@ -40,32 +43,32 @@
    • -
      - -
      +
      + +
    -
    -
    -
    -

    Scratch pad

    -
    +
    -
      -
    • - <%include file="new-module.html"/> -
    • -
    -
    +
    +
    +

    Weeks Content

    +
    +
    -
    -
    -

    Weeks Content

    -
    +
    +
    +

    Scratch pad

    +
    -
    +
      +
    • + <%include file="new-module.html"/> +
    • +
    +
    @@ -83,4 +86,5 @@ New Lab +
    From 01ca43346fed4834f8590b213896343393a53203 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Thu, 14 Jun 2012 12:16:29 -0400 Subject: [PATCH 060/252] Use existential operator --- lms/static/coffee/src/modules/video/video_player.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/static/coffee/src/modules/video/video_player.coffee b/lms/static/coffee/src/modules/video/video_player.coffee index 0dd52a128b..20df378267 100644 --- a/lms/static/coffee/src/modules/video/video_player.coffee +++ b/lms/static/coffee/src/modules/video/video_player.coffee @@ -135,7 +135,7 @@ class @VideoPlayer @video.speed volume: (value) -> - if value != undefined + if value? @player.setVolume value else @player.getVolume() From 6fee7928fc9961c08fbcbc7f7f35631a71ffbacf Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 14 Jun 2012 16:15:50 -0400 Subject: [PATCH 061/252] Successfully read course children out of mongodb --- .../management/commands/import.py | 29 +++++---- cms/djangoapps/contentstore/views.py | 5 +- cms/lib/keystore/__init__.py | 34 ++++++++++ cms/lib/keystore/exceptions.py | 4 ++ cms/lib/keystore/mongo.py | 63 ++++++++++++++++--- 5 files changed, 109 insertions(+), 26 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 78984f4119..690e3dbea0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -6,11 +6,13 @@ #import mitxmako.middleware #from courseware import content_parser #from django.contrib.auth.models import User +import os.path +from StringIO import StringIO from mako.template import Template from mako.lookup import TemplateLookup from django.core.management.base import BaseCommand -from contentstore.models import create_item, update_item, update_children +from keystore.django import keystore from lxml import etree @@ -20,16 +22,15 @@ class Command(BaseCommand): def handle(self, *args, **options): print args data_dir = args[0] - course_file = 'course.xml' parser = etree.XMLParser(remove_comments = True) lookup = TemplateLookup(directories=[data_dir]) template = lookup.get_template("course.xml") course_string = template.render(groups=[]) - course = etree.XML(course_string, parser=parser) + course = etree.parse(StringIO(course_string), parser=parser) - elements = course.xpath("//*") + elements = list(course.iter()) tag_to_category = {# Inside HTML ==> Skip these # Custom tags @@ -39,11 +40,11 @@ class Command(BaseCommand): 'image': 'Custom', 'discuss': 'Custom', # Simple lists - 'chapter': 'Sequence', - 'course': 'Sequence', - 'sequential': 'Sequence', - 'vertical': 'Sequence', - 'section': 'Sequence', + 'chapter': 'Chapter', + 'course': 'Course', + 'sequential': 'LectureSequence', + 'vertical': 'ProblemSet', + 'section': 'Section', # True types 'video': 'VideoSegment', 'html': 'HTML', @@ -114,7 +115,7 @@ class Command(BaseCommand): results[e.attrib['url']] = {'data':{'text':text}} def handle_problem(e): - data = open(data_dir+'problems/'+e.attrib['filename']+'.xml').read() + data = open(os.path.join(data_dir, 'problems', e.attrib['filename']+'.xml')).read() results[e.attrib['url']] = {'data':{'statement':data}} element_actions = {# Inside HTML ==> Skip these @@ -149,10 +150,8 @@ class Command(BaseCommand): for k in results: print k - create_item(k, 'Piotr Mitros') + keystore.create_item(k, 'Piotr Mitros') if 'data' in results[k]: - update_item(k, results[k]['data']) + keystore.update_item(k, results[k]['data']) if 'children' in results[k]: - update_children(k, results[k]['children']) - - + keystore.update_children(k, results[k]['children']) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 38e9e8ad35..429fb6c26b 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,14 +1,11 @@ from mitxmako.shortcuts import render_to_response -from keystore import Location from keystore.django import keystore from django.contrib.auth.decorators import login_required @login_required def calendar(request, org, course): - weeks = keystore.get_children_for_item( - Location(['i4x', org, course, 'Course', 'course']) - ) + weeks = keystore.get_children_for_item(['i4x', org, course, 'Course', None]) return render_to_response('calendar.html', {'weeks': weeks}) diff --git a/cms/lib/keystore/__init__.py b/cms/lib/keystore/__init__.py index 5e6374cf4a..d0a24be797 100644 --- a/cms/lib/keystore/__init__.py +++ b/cms/lib/keystore/__init__.py @@ -60,6 +60,40 @@ class KeyStore(object): with the specified location. If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + + Searches for all matches of a partially specifed location, but raises an + keystore.exceptions.InsufficientSpecificationError if more + than a single object matches the query. + + location: Something that can be passed to Location + """ + raise NotImplementedError + + def create_item(self, location, editor): + """ + Create an empty item at the specified location with the supplied editor + + location: Something that can be passed to Location + """ + raise NotImplementedError + + def update_item(self, location, data): + """ + Set the data in the item specified by the location to + data + + location: Something that can be passed to Location + data: A nested dictionary of problem data + """ + raise NotImplementedError + + def update_children(self, location, children): + """ + Set the children for the item specified by the location to + data + + location: Something that can be passed to Location + children: A list of child item identifiers """ raise NotImplementedError diff --git a/cms/lib/keystore/exceptions.py b/cms/lib/keystore/exceptions.py index b66470859f..08fd9b11d0 100644 --- a/cms/lib/keystore/exceptions.py +++ b/cms/lib/keystore/exceptions.py @@ -5,3 +5,7 @@ Exceptions thrown by KeyStore objects class ItemNotFoundError(Exception): pass + + +class InsufficientSpecificationError(Exception): + pass diff --git a/cms/lib/keystore/mongo.py b/cms/lib/keystore/mongo.py index fc190ee098..d29afb4bd2 100644 --- a/cms/lib/keystore/mongo.py +++ b/cms/lib/keystore/mongo.py @@ -1,6 +1,6 @@ import pymongo -from . import KeyStore -from .exceptions import ItemNotFoundError +from . import KeyStore, Location +from .exceptions import ItemNotFoundError, InsufficientSpecificationError class MongoKeyStore(KeyStore): @@ -12,15 +12,64 @@ class MongoKeyStore(KeyStore): host=host, port=port )[db][collection] + + # Force mongo to report errors, at the expense of performance + self.collection.safe = True def get_children_for_item(self, location): - item = self.collection.find_one( - {'location': location.dict()}, + query = dict( + ('location.{key}'.format(key=key), val) + for (key, val) + in Location(location).dict().items() + if val is not None + ) + items = self.collection.find( + query, fields={'children': True}, sort=[('revision', pymongo.ASCENDING)], + limit=1, + ) + if items.count() > 1: + raise InsufficientSpecificationError(location) + + if items.count() == 0: + raise ItemNotFoundError(location) + + return items[0]['children'] + + def create_item(self, location, editor): + """ + Create an empty item at the specified location with the supplied editor + + location: Something that can be passed to Location + """ + self.collection.insert({ + 'location': Location(location).dict(), + 'editor': editor + }) + + def update_item(self, location, data): + """ + Set the data in the item specified by the location to + data + + location: Something that can be passed to Location + data: A nested dictionary of problem data + """ + self.collection.update( + {'location': Location(location).dict()}, + {'$set': {'data': data}} ) - if item is None: - raise ItemNotFoundError() + def update_children(self, location, children): + """ + Set the children for the item specified by the location to + data - return item['children'] + location: Something that can be passed to Location + children: A list of child item identifiers + """ + self.collection.update( + {'location': Location(location).dict()}, + {'$set': {'children': children}} + ) From 57b605dbd3a791c3c73392f185df5e1e86b89ed1 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Thu, 14 Jun 2012 17:15:23 -0400 Subject: [PATCH 062/252] Added more styels to the section view and default section view --- cms/static/css/base-style.css | 170 +++++++++++++++++++------- cms/static/img/drag-handle.png | Bin 0 -> 98 bytes cms/static/sass/_base.scss | 47 ++++++- cms/static/sass/_calendar.scss | 6 + cms/static/sass/_week.scss | 99 +++++++++------ cms/templates/widgets/header.html | 19 ++- cms/templates/widgets/new-module.html | 2 +- cms/templates/widgets/week-edit.html | 42 +++++-- cms/templates/widgets/week-new.html | 93 +++++++------- 9 files changed, 336 insertions(+), 142 deletions(-) create mode 100644 cms/static/img/drag-handle.png diff --git a/cms/static/css/base-style.css b/cms/static/css/base-style.css index c84ab046ff..c4bfc6c415 100644 --- a/cms/static/css/base-style.css +++ b/cms/static/css/base-style.css @@ -193,8 +193,19 @@ body { font-size: 14px; text-transform: uppercase; float: left; } - body > header nav a.new-module { - float: right; } + body > header nav ul { + float: left; } + body > header nav ul.user-nav { + float: right; } + body > header nav ul li { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin-left: 15px; } body.content section.main-content { border-left: 2px solid #000; -webkit-box-sizing: border-box; @@ -213,7 +224,11 @@ a { input { font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; } -input[type="submit"], .button, section.cal section.new-section > a, section.video-new > section section.upload a.upload-button, section.video-edit > section section.upload a.upload-button, section.video-new > section a.save-update, section.video-edit > section a.save-update, section.problem-new > section a.save, section.problem-edit > section a.save { +input[type="submit"], .button, section.cal section.new-section > a, section.week-edit > section.content > div section.modules.empty a, +section.week-new > section.content > div section.modules.empty a, +section.sequence-edit > section.content > div section.modules.empty a, section.week-edit > section.content > div section.scratch-pad ol li ul li.empty a, +section.week-new > section.content > div section.scratch-pad ol li ul li.empty a, +section.sequence-edit > section.content > div section.scratch-pad ol li ul li.empty a, section.video-new > section section.upload a.upload-button, section.video-edit > section section.upload a.upload-button, section.video-new > section a.save-update, section.video-edit > section a.save-update, section.problem-new > section a.save, section.problem-edit > section a.save { border: 1px solid #ccc; background: #efefef; -webkit-border-radius: 3px; @@ -223,6 +238,26 @@ input[type="submit"], .button, section.cal section.new-section > a, section.vide border-radius: 3px; padding: 6px; } +.new-module { + position: relative; } + .new-module a { + padding: 6px; + display: block; } + .new-module ul.new-dropdown { + list-style: none; + position: absolute; } + .new-module ul.new-dropdown li { + display: none; + padding: 6px; } + .new-module:hover ul.new-dropdown { + display: block; } + +.draggable { + width: 7px; + min-height: 14px; + background: url("../img/drag-handle.png") no-repeat center; + text-indent: -9999px; } + section.cal { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -484,11 +519,35 @@ section.cal { overflow: scroll; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; - box-sizing: border-box; } + box-sizing: border-box; + opacity: .4; + -webkit-transition-property: all; + -moz-transition-property: all; + -ms-transition-property: all; + -o-transition-property: all; + transition-property: all; + -webkit-transition-duration: 0.15s; + -moz-transition-duration: 0.15s; + -ms-transition-duration: 0.15s; + -o-transition-duration: 0.15s; + transition-duration: 0.15s; + -webkit-transition-timing-function: ease-out; + -moz-transition-timing-function: ease-out; + -ms-transition-timing-function: ease-out; + -o-transition-timing-function: ease-out; + transition-timing-function: ease-out; + -webkit-transition-delay: 0; + -moz-transition-delay: 0; + -ms-transition-delay: 0; + -o-transition-delay: 0; + transition-delay: 0; } body.content section.cal > header ul { display: none; } body.content + section.cal:hover { + opacity: 1; } + body.content section.cal ol li { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -637,6 +696,22 @@ section.sequence-edit > section.content { display: table-cell; width: 65.632%; border-right: 1px solid #333; } + section.week-edit > section.content > div section.modules.empty, + section.week-new > section.content > div section.modules.empty, + section.sequence-edit > section.content > div section.modules.empty { + text-align: center; + vertical-align: middle; } + section.week-edit > section.content > div section.modules.empty a, + section.week-new > section.content > div section.modules.empty a, + section.sequence-edit > section.content > div section.modules.empty a { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin-top: 10px; } section.week-edit > section.content > div section.modules ol, section.week-new > section.content > div section.modules ol, section.sequence-edit > section.content > div section.modules ol { @@ -661,17 +736,27 @@ section.sequence-edit > section.content { section.week-edit > section.content > div section.modules ol li ol li, section.week-new > section.content > div section.modules ol li ol li, section.sequence-edit > section.content > div section.modules ol li ol li { - padding: 6px 0 6px 6px; } - section.week-edit > section.content > div section.modules ol li ol li h3, - section.week-new > section.content > div section.modules ol li ol li h3, - section.sequence-edit > section.content > div section.modules ol li ol li h3 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; } - section.week-edit > section.content > div section.modules ol li ol li ol, - section.week-new > section.content > div section.modules ol li ol li ol, - section.sequence-edit > section.content > div section.modules ol li ol li ol { - border-left: 1px solid; } + padding: 6px; } + section.week-edit > section.content > div section.modules ol li ol li.group, + section.week-new > section.content > div section.modules ol li ol li.group, + section.sequence-edit > section.content > div section.modules ol li ol li.group { + padding: 0; + border-left: 4px solid #999; } + section.week-edit > section.content > div section.modules ol li ol li.group header, + section.week-new > section.content > div section.modules ol li ol li.group header, + section.sequence-edit > section.content > div section.modules ol li ol li.group header { + padding: 3px 6px; + background: none; } + section.week-edit > section.content > div section.modules ol li ol li.group header h3, + section.week-new > section.content > div section.modules ol li ol li.group header h3, + section.sequence-edit > section.content > div section.modules ol li ol li.group header h3 { + text-transform: uppercase; + letter-spacing: 1px; + font-size: 12px; } + section.week-edit > section.content > div section.modules ol li ol li.group ol li:last-child, + section.week-new > section.content > div section.modules ol li ol li.group ol li:last-child, + section.sequence-edit > section.content > div section.modules ol li ol li.group ol li:last-child { + border-bottom: 0; } section.week-edit > section.content > div section.scratch-pad, section.week-new > section.content > div section.scratch-pad, section.sequence-edit > section.content > div section.scratch-pad { @@ -679,7 +764,8 @@ section.sequence-edit > section.content { -moz-box-sizing: border-box; box-sizing: border-box; display: table-cell; - width: 34.368%; } + width: 34.368%; + vertical-align: top; } section.week-edit > section.content > div section.scratch-pad ol, section.week-new > section.content > div section.scratch-pad ol, section.sequence-edit > section.content > div section.scratch-pad ol { @@ -702,40 +788,32 @@ section.sequence-edit > section.content { section.week-new > section.content > div section.scratch-pad ol li ul li, section.sequence-edit > section.content > div section.scratch-pad ol li ul li { padding: 6px; } + section.week-edit > section.content > div section.scratch-pad ol li ul li:last-child, + section.week-new > section.content > div section.scratch-pad ol li ul li:last-child, + section.sequence-edit > section.content > div section.scratch-pad ol li ul li:last-child { + border-bottom: 0; } + section.week-edit > section.content > div section.scratch-pad ol li ul li:hover a.draggable, + section.week-new > section.content > div section.scratch-pad ol li ul li:hover a.draggable, + section.sequence-edit > section.content > div section.scratch-pad ol li ul li:hover a.draggable { + opacity: 1; } + section.week-edit > section.content > div section.scratch-pad ol li ul li.empty, + section.week-new > section.content > div section.scratch-pad ol li ul li.empty, + section.sequence-edit > section.content > div section.scratch-pad ol li ul li.empty { + padding: 12px; } + section.week-edit > section.content > div section.scratch-pad ol li ul li.empty a, + section.week-new > section.content > div section.scratch-pad ol li ul li.empty a, + section.sequence-edit > section.content > div section.scratch-pad ol li ul li.empty a { + display: block; + text-align: center; } + section.week-edit > section.content > div section.scratch-pad ol li ul li a.draggable, + section.week-new > section.content > div section.scratch-pad ol li ul li a.draggable, + section.sequence-edit > section.content > div section.scratch-pad ol li ul li a.draggable { + float: right; + opacity: .3; } section.week-edit > section.content > div section.scratch-pad ol li ul li a, section.week-new > section.content > div section.scratch-pad ol li ul li a, section.sequence-edit > section.content > div section.scratch-pad ol li ul li a { color: #000; } - section.week-edit > section.content > div section.scratch-pad ol li.new-module, - section.week-new > section.content > div section.scratch-pad ol li.new-module, - section.sequence-edit > section.content > div section.scratch-pad ol li.new-module { - position: relative; } - section.week-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown, - section.week-new > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown, - section.sequence-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown { - list-style: none; } - section.week-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown li, - section.week-new > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown li, - section.sequence-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown li { - display: none; } - section.week-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown li:first-child, - section.week-new > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown li:first-child, - section.sequence-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown li:first-child { - display: block; } - section.week-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown:hover li, - section.week-new > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown:hover li, - section.sequence-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown:hover li { - display: block; - padding: 6px 0; } - section.week-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown:hover li:first-child, - section.week-new > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown:hover li:first-child, - section.sequence-edit > section.content > div section.scratch-pad ol li.new-module ul.new-dropdown:hover li:first-child { - padding-top: 0; } - section.week-edit > section.content > div section.scratch-pad p, - section.week-new > section.content > div section.scratch-pad p, - section.sequence-edit > section.content > div section.scratch-pad p { - padding: 6px; - border-bottom: 1px solid #666; } section.video-new > section section.upload, section.video-edit > section section.upload { padding: 6px; diff --git a/cms/static/img/drag-handle.png b/cms/static/img/drag-handle.png new file mode 100644 index 0000000000000000000000000000000000000000..3c9c3c1ebcee37ec1fcfd82aa204228ecd629598 GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)N!3HEB$FfNSDNRoo$B>F!Z+i`a$~c&!Yh(WZ v_v73tWm2xNh|g`ZvcZL`KR=$S&zsN6c8$yMo{i8-pe6=SS3j3^P6 header ul { display: none; } + &:hover { + opacity: 1; + } + ol { li { @include box-sizing(border-box); diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss index 1c03ff7962..de7b1e6f9e 100644 --- a/cms/static/sass/_week.scss +++ b/cms/static/sass/_week.scss @@ -86,6 +86,17 @@ section.sequence-edit { width: flex-grid(6, 9); border-right: 1px solid #333; + &.empty { + text-align: center; + vertical-align: middle; + + a { + @extend .button; + @include inline-block(); + margin-top: 10px; + } + } + ol { list-style: none; border-bottom: 1px solid #333; @@ -105,16 +116,30 @@ section.sequence-edit { list-style: none; li { - padding: 6px 0 6px 6px; + padding: 6px; - h3 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; - } + &.group { + padding: 0; + border-left: 4px solid #999; - ol { - border-left: 1px solid; + header { + padding: 3px 6px; + background: none; + + h3 { + text-transform: uppercase; + letter-spacing: 1px; + font-size: 12px; + } + } + + ol { + li { + &:last-child { + border-bottom: 0; + } + } + } } } } @@ -126,6 +151,7 @@ section.sequence-edit { @include box-sizing(border-box); display: table-cell; width: flex-grid(3, 9) + flex-gutter(9); + vertical-align: top; ol { list-style: none; @@ -145,48 +171,41 @@ section.sequence-edit { li { padding: 6px; + &:last-child { + border-bottom: 0; + } + + &:hover { + a.draggable { + opacity: 1; + } + } + + &.empty { + padding: 12px; + + a { + @extend .button; + display: block; + text-align: center; + } + } + + a.draggable { + float: right; + opacity: .3; + } + a { color: #000; } } } - &.new-module { - position: relative; - - ul.new-dropdown { - list-style: none; - - li { - display: none; - - &:first-child { - display: block; - } - } - - &:hover { - li { - display: block; - padding: 6px 0; - - &:first-child { - padding-top: 0; - } - } - } - } - } } } - - p { - padding: 6px; - border-bottom: 1px solid #666; - } } } } } } - diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index ec550fec37..49965e4026 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -1,6 +1,21 @@
    diff --git a/cms/templates/widgets/new-module.html b/cms/templates/widgets/new-module.html index 6b7794944a..8b12e5763a 100644 --- a/cms/templates/widgets/new-module.html +++ b/cms/templates/widgets/new-module.html @@ -1,5 +1,5 @@ ++ add new
      -
    1. Problem title 11
    2. -
    3. Problem title 13
    4. -
    5. Problem title 14
    6. -
    7. Video 3
    8. +
    9. + Problem title 11 + handle +
    10. +
    11. + Problem Group + handle +
    12. +
    13. + Problem title 14 + handle +
    14. +
    15. + Video 3 + handle +
  • @@ -68,7 +80,7 @@
      -
    1. +
    2. Problem group @@ -97,10 +109,22 @@

        -
      1. Problem title 11
      2. -
      3. Problem title 13
      4. -
      5. Problem title 14
      6. -
      7. Video 3
      8. +
      9. + Problem title 11 + handle +
      10. +
      11. + Problem Group + handle +
      12. +
      13. + Problem title 14 + handle +
      14. +
      15. + Video 3 + handle +
    3. diff --git a/cms/templates/widgets/week-new.html b/cms/templates/widgets/week-new.html index 46aba9b778..be08587755 100644 --- a/cms/templates/widgets/week-new.html +++ b/cms/templates/widgets/week-new.html @@ -1,23 +1,23 @@
      -

      Week 6

      +

      Week 3

      Weeks goals:

      -

      +

      +

      + new goal

      • -

        Create new goal

        -

        Goals are overarching themes for the week

        +

        Please add a goal for this section

      -
      +
      +
      • @@ -43,48 +43,59 @@
      • -
        - -
        +
      - -
      - -
      -
      -

      Weeks Content

      -
      +
      +
      +

      This are no groups or units in this section yet

      + Add a Group + Add a Unit
      -
      -
      -

      Scratch pad

      -
      +
      +
        +
      1. +
        +

        Section Scratch

        +
        + +
      2. +
      3. +
        +

        Course Scratch

        +
        + +
      4. -
          -
        • - <%include file="new-module.html"/> -
        • -
        -
      +
    4. + <%include file="new-module.html"/> +
    5. +
  • - -
    - +
    + From 93478d53dfa65008a09be1a62d79744b1bc2bc45 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 15 Jun 2012 10:11:04 -0400 Subject: [PATCH 063/252] Added scratch on the calendar view and added handles on the calendar view --- cms/static/css/base-style.css | 52 +++-- cms/static/sass/_base.scss | 2 + cms/static/sass/_week.scss | 33 ++- cms/templates/widgets/navigation.html | 100 ++++++-- cms/templates/widgets/week-edit.html | 317 +++++++++++++++----------- 5 files changed, 329 insertions(+), 175 deletions(-) diff --git a/cms/static/css/base-style.css b/cms/static/css/base-style.css index c4bfc6c415..abed82cc05 100644 --- a/cms/static/css/base-style.css +++ b/cms/static/css/base-style.css @@ -256,7 +256,9 @@ section.sequence-edit > section.content > div section.scratch-pad ol li ul li.em width: 7px; min-height: 14px; background: url("../img/drag-handle.png") no-repeat center; - text-indent: -9999px; } + text-indent: -9999px; + display: block; + float: right; } section.cal { -webkit-box-sizing: border-box; @@ -679,14 +681,27 @@ section.sequence-edit > section.content { section.week-new > section.content > div section header, section.sequence-edit > section.content > div section header { background: #eee; - padding: 10px; - border-bottom: 1px solid #ccc; } + padding: 6px; + border-bottom: 1px solid #ccc; + zoom: 1; } + section.week-edit > section.content > div section header:before, section.week-edit > section.content > div section header:after, + section.week-new > section.content > div section header:before, + section.week-new > section.content > div section header:after, + section.sequence-edit > section.content > div section header:before, + section.sequence-edit > section.content > div section header:after { + content: ""; + display: table; } + section.week-edit > section.content > div section header:after, + section.week-new > section.content > div section header:after, + section.sequence-edit > section.content > div section header:after { + clear: both; } section.week-edit > section.content > div section header h2, section.week-new > section.content > div section header h2, section.sequence-edit > section.content > div section header h2 { text-transform: uppercase; letter-spacing: 1px; - font-size: 12px; } + font-size: 12px; + float: left; } section.week-edit > section.content > div section.modules, section.week-new > section.content > div section.modules, section.sequence-edit > section.content > div section.modules { @@ -737,26 +752,37 @@ section.sequence-edit > section.content { section.week-new > section.content > div section.modules ol li ol li, section.sequence-edit > section.content > div section.modules ol li ol li { padding: 6px; } + section.week-edit > section.content > div section.modules ol li ol li:hover a.draggable, + section.week-new > section.content > div section.modules ol li ol li:hover a.draggable, + section.sequence-edit > section.content > div section.modules ol li ol li:hover a.draggable { + opacity: 1; } + section.week-edit > section.content > div section.modules ol li ol li a.draggable, + section.week-new > section.content > div section.modules ol li ol li a.draggable, + section.sequence-edit > section.content > div section.modules ol li ol li a.draggable { + float: right; + opacity: .5; } section.week-edit > section.content > div section.modules ol li ol li.group, section.week-new > section.content > div section.modules ol li ol li.group, section.sequence-edit > section.content > div section.modules ol li ol li.group { - padding: 0; - border-left: 4px solid #999; } + padding: 0; } section.week-edit > section.content > div section.modules ol li ol li.group header, section.week-new > section.content > div section.modules ol li ol li.group header, section.sequence-edit > section.content > div section.modules ol li ol li.group header { - padding: 3px 6px; + padding: 6px; background: none; } section.week-edit > section.content > div section.modules ol li ol li.group header h3, section.week-new > section.content > div section.modules ol li ol li.group header h3, section.sequence-edit > section.content > div section.modules ol li ol li.group header h3 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; } - section.week-edit > section.content > div section.modules ol li ol li.group ol li:last-child, - section.week-new > section.content > div section.modules ol li ol li.group ol li:last-child, - section.sequence-edit > section.content > div section.modules ol li ol li.group ol li:last-child { + font-size: 14px; } + section.week-edit > section.content > div section.modules ol li ol li.group ol, + section.week-new > section.content > div section.modules ol li ol li.group ol, + section.sequence-edit > section.content > div section.modules ol li ol li.group ol { + border-left: 4px solid #999; border-bottom: 0; } + section.week-edit > section.content > div section.modules ol li ol li.group ol li:last-child, + section.week-new > section.content > div section.modules ol li ol li.group ol li:last-child, + section.sequence-edit > section.content > div section.modules ol li ol li.group ol li:last-child { + border-bottom: 0; } section.week-edit > section.content > div section.scratch-pad, section.week-new > section.content > div section.scratch-pad, section.sequence-edit > section.content > div section.scratch-pad { diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 4bc24e662f..3a2ef86363 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -108,4 +108,6 @@ input[type="submit"], .button { min-height: 14px; background: url('../img/drag-handle.png') no-repeat center; text-indent: -9999px; + display: block; + float: right; } diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss index de7b1e6f9e..e5283cbc55 100644 --- a/cms/static/sass/_week.scss +++ b/cms/static/sass/_week.scss @@ -67,16 +67,19 @@ section.sequence-edit { display: table; border: 1px solid; width: 100%; + section { header { background: #eee; - padding: 10px; + padding: 6px; border-bottom: 1px solid #ccc; + @include clearfix; h2 { text-transform: uppercase; letter-spacing: 1px; font-size: 12px; + float: left; } } @@ -118,22 +121,34 @@ section.sequence-edit { li { padding: 6px; + &:hover { + a.draggable { + opacity: 1; + } + } + + a.draggable { + float: right; + opacity: .5; + } + &.group { padding: 0; - border-left: 4px solid #999; header { - padding: 3px 6px; + padding: 6px; background: none; h3 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; + font-size: 14px; } } + ol { + border-left: 4px solid #999; + border-bottom: 0; + li { &:last-child { border-bottom: 0; @@ -185,9 +200,9 @@ section.sequence-edit { padding: 12px; a { - @extend .button; - display: block; - text-align: center; + @extend .button; + display: block; + text-align: center; } } diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 808c569e74..28b92979d9 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -45,10 +45,22 @@ @@ -61,10 +73,22 @@ @@ -77,9 +101,19 @@ @@ -93,10 +127,22 @@ @@ -137,6 +183,30 @@ <%include file="module-dropdown.html"/> +
  • +
    +

    Course Scratch Pad

    +
    + + +
  • diff --git a/cms/templates/widgets/week-edit.html b/cms/templates/widgets/week-edit.html index 6e59c44468..2e37520fde 100644 --- a/cms/templates/widgets/week-edit.html +++ b/cms/templates/widgets/week-edit.html @@ -10,160 +10,201 @@
    • -

      Goal title: This is the goal body and is wehre the goal will be further explained

      +

      Goal title: This is the goal body and is where the goal will be further explained

    -
    -
      -
    • - - -
    • - -
    • - - -
    • -
    • - -
    • - -
    • - Advanced filters -
    • - -
    • - -
    • -
    -
    -
    -
    -
      +
      +
    +
    -
    -
      -
    1. -
      -

      Section Scratch

      -
      -
        -
      • Problem title 11
      • -
      • Problem title 13
      • -
      • Problem title 14
      • -
      • Video 3
      • -
      -
    2. -
    3. -
      -

      Course Scratch

      -
      -
        -
      • Problem title 11
      • -
      • Problem title 13
      • -
      • Problem title 14
      • -
      • Video 3
      • -
      -
    4. +
      +
      +
        +
      1. +
        +

        Lecture Sequence

        + handle +
        -
      2. - <%include file="new-module.html"/> -
      3. -
      -
      +
        +
      1. + Problem title 11 + handle +
      2. +
      3. + Problem Group + handle +
      4. +
      5. + Problem title 14 + handle +
      6. +
      7. + Video 3 + handle +
      8. +
      + +
    5. +
      +

      Lecture Sequence

      + handle +
      + +
        +
      1. +
        +

        + Problem group + handle +

        +
        +
          +
        1. + Problem title 11 + handle +
        2. +
        3. + Problem title 11 + handle +
        4. +
        5. + Problem title 11 + handle +
        6. +
        +
      2. +
      3. + Problem title 13 + handle +
      4. +
      5. + Problem title 14 + handle +
      6. +
      7. + Video 3 + handle +
      8. +
      +
    6. +
    7. +
      +

      Homework 1a

      +
      + +
        +
      1. + Problem title 11 + handle +
      2. +
      3. + Problem Group + handle +
      4. +
      5. + Problem title 14 + handle +
      6. +
      7. + Video 3 + handle +
      8. +
      +
    8. + + + + +
    +
    + +
    +
      +
    1. +
      +

      Section Scratch

      +
      + +
    2. +
    3. +
      +

      Course Scratch

      +
      + + +
    4. + + + + +
    +
    From cb1f6838ba3dc6dd00d0f4baad98b9fe5bc76a47 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 15 Jun 2012 13:27:24 -0400 Subject: [PATCH 064/252] Added more styles and markup for sequence --- cms/static/css/base-style.css | 112 +++++++++---- cms/static/sass/_calendar.scss | 2 +- cms/static/sass/_week.scss | 68 +++++--- cms/templates/widgets/sequnce-edit.html | 204 ++++++++++++++++++------ cms/templates/widgets/week-edit.html | 129 +++++++++++++-- cms/templates/widgets/week-new.html | 10 +- 6 files changed, 407 insertions(+), 118 deletions(-) diff --git a/cms/static/css/base-style.css b/cms/static/css/base-style.css index abed82cc05..9ebc6bd839 100644 --- a/cms/static/css/base-style.css +++ b/cms/static/css/base-style.css @@ -460,7 +460,7 @@ section.cal { section.cal section.new-section section { display: none; position: absolute; - top: 32px; + top: 30px; background: rgba(0, 0, 0, 0.8); min-width: 300px; padding: 10px; @@ -575,46 +575,96 @@ section.sequence-edit > header { section.week-new > header:after, section.sequence-edit > header:after { clear: both; } - section.week-edit > header h1, - section.week-new > header h1, - section.sequence-edit > header h1 { - font-size: 18px; - margin: 8px 6px; - text-transform: uppercase; - letter-spacing: 1px; } + section.week-edit > header div, + section.week-new > header div, + section.sequence-edit > header div { + zoom: 1; + padding: 6px 20px; } + section.week-edit > header div:before, section.week-edit > header div:after, + section.week-new > header div:before, + section.week-new > header div:after, + section.sequence-edit > header div:before, + section.sequence-edit > header div:after { + content: ""; + display: table; } + section.week-edit > header div:after, + section.week-new > header div:after, + section.sequence-edit > header div:after { + clear: both; } + section.week-edit > header div h1, + section.week-new > header div h1, + section.sequence-edit > header div h1 { + font-size: 18px; + text-transform: uppercase; + letter-spacing: 1px; + float: left; } + section.week-edit > header div p, + section.week-new > header div p, + section.sequence-edit > header div p { + float: right; } + section.week-edit > header div.week, + section.week-new > header div.week, + section.sequence-edit > header div.week { + background: #eee; + font-size: 12px; + border-bottom: 1px solid #ccc; } + section.week-edit > header div.week h2, + section.week-new > header div.week h2, + section.sequence-edit > header div.week h2 { + font-size: 12px; + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin-right: 20px; } + section.week-edit > header div.week ul, + section.week-new > header div.week ul, + section.sequence-edit > header div.week ul { + list-style: none; + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; } + section.week-edit > header div.week ul li, + section.week-new > header div.week ul li, + section.sequence-edit > header div.week ul li { + display: -moz-inline-box; + -moz-box-orient: vertical; + display: inline-block; + vertical-align: baseline; + zoom: 1; + *display: inline; + *vertical-align: auto; + margin-right: 10px; } + section.week-edit > header div.week ul li p, + section.week-new > header div.week ul li p, + section.sequence-edit > header div.week ul li p { + float: none; } section.week-edit > header section.goals, section.week-new > header section.goals, section.sequence-edit > header section.goals { background: #eee; - padding: 6px; + padding: 6px 20px; border-top: 1px solid #ccc; } - section.week-edit > header section.goals header h2, - section.week-new > header section.goals header h2, - section.sequence-edit > header section.goals header h2 { - font-size: 14px; - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.week-edit > header section.goals header p, - section.week-new > header section.goals header p, - section.sequence-edit > header section.goals header p { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } section.week-edit > header section.goals ul, section.week-new > header section.goals ul, section.sequence-edit > header section.goals ul { list-style: none; - margin-top: 6px; color: #999; } + section.week-edit > header section.goals ul li, + section.week-new > header section.goals ul li, + section.sequence-edit > header section.goals ul li { + margin-bottom: 6px; } + section.week-edit > header section.goals ul li:last-child, + section.week-new > header section.goals ul li:last-child, + section.sequence-edit > header section.goals ul li:last-child { + margin-bottom: 0; } section.week-edit > section.content, section.week-new > section.content, section.sequence-edit > section.content { diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss index ca91337366..fa10c65950 100644 --- a/cms/static/sass/_calendar.scss +++ b/cms/static/sass/_calendar.scss @@ -176,7 +176,7 @@ section.cal { section { display: none; - @include position(absolute, 32px 0 0 0); + @include position(absolute, 30px 0 0 0); background: rgba(#000, .8); min-width: 300px; padding: 10px; diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss index e5283cbc55..b638a36f5c 100644 --- a/cms/static/sass/_week.scss +++ b/cms/static/sass/_week.scss @@ -6,34 +6,64 @@ section.sequence-edit { border-bottom: 2px solid #333; @include clearfix(); - h1 { - font-size: 18px; - margin: 8px 6px; - text-transform: uppercase; - letter-spacing: 1px; + div { + @include clearfix(); + padding: 6px 20px; + + h1 { + font-size: 18px; + text-transform: uppercase; + letter-spacing: 1px; + float: left; + } + + p { + float: right; + } + + &.week { + background: #eee; + font-size: 12px; + border-bottom: 1px solid #ccc; + + h2 { + font-size: 12px; + @include inline-block(); + margin-right: 20px; + } + + ul { + list-style: none; + @include inline-block(); + + li { + @include inline-block(); + margin-right: 10px; + + p { + float: none; + } + } + } + } } section.goals { background: #eee; - padding: 6px; + padding: 6px 20px; border-top: 1px solid #ccc; - header { - h2 { - font-size: 14px; - @include inline-block(); - } - - p { - @include inline-block(); - } - - } - ul { list-style: none; - margin-top: 6px; color: #999; + + li { + margin-bottom: 6px; + + &:last-child { + margin-bottom: 0; + } + } } } } diff --git a/cms/templates/widgets/sequnce-edit.html b/cms/templates/widgets/sequnce-edit.html index 75d4504b68..b69b523bc4 100644 --- a/cms/templates/widgets/sequnce-edit.html +++ b/cms/templates/widgets/sequnce-edit.html @@ -1,47 +1,20 @@ -<%block name="content">
    - Done -

    Lecture Sequence name

    - Settings +
    +

    Week 1

    +
      +
    • +

      Goal title: This is the goal body and is where the goal will be further explained

      +
    • +
    +
    +
    +

    Lecture sequence

    +

    Group type: Ordered Sequence

    +
    - - -
    -
    -

    Sequence Content

    -
    - -
    -
    - +
    -
    - -
    +
    +
    +
      +
    1. +
        +
      1. + Problem title 11 + handle +
      2. +
      3. + Problem Group + handle +
      4. +
      5. + Problem title 14 + handle +
      6. +
      7. + Video 3 + handle +
      8. +
      9. +
        +

        + Problem group + handle +

        +
        +
          +
        1. + Problem title 11 + handle +
        2. +
        3. + Problem title 11 + handle +
        4. +
        5. + Problem title 11 + handle +
        6. +
        +
      10. +
      11. + Problem title 13 + handle +
      12. +
      13. + Problem title 14 + handle +
      14. +
      15. + Video 3 + handle +
      16. +
      17. + Problem title 11 + handle +
      18. +
      19. + Problem Group + handle +
      20. +
      21. + Problem title 14 + handle +
      22. +
      23. + Video 3 + handle +
      24. +
      +
    2. + + + + +
    +
    + +
    +
      +
    1. +
      +

      Section Scratch

      +
      + +
    2. +
    3. +
      +

      Course Scratch

      +
      + + +
    4. + + + + +
    +
    +
    - diff --git a/cms/templates/widgets/week-edit.html b/cms/templates/widgets/week-edit.html index 2e37520fde..95ca631be0 100644 --- a/cms/templates/widgets/week-edit.html +++ b/cms/templates/widgets/week-edit.html @@ -1,16 +1,14 @@
    -

    Week 3

    +
    +

    Week 3

    +

    + new goal

    +
    -
    -

    Weeks goals:

    -

    + new goal

    -
    -
    • -

      Goal title: This is the goal body and is where the goal will be further explained

      +

      Goal title: This is the goal body and is where the goal will be further explained

    @@ -62,7 +60,40 @@ Problem title 11 handle -
  • +
  • +
    +

    + Problem group + handle +

    +
    +
      +
    1. + Problem title 11 + handle +
    2. +
    3. + Problem title 11 + handle +
    4. +
    5. + Problem title 11 + handle +
    6. +
    7. + Problem title 13 + handle +
    8. +
    9. + Problem title 14 + handle +
    10. +
    11. + Video 3 + handle +
    12. +
    +
  • Problem Group handle
  • @@ -70,11 +101,77 @@ Problem title 14 handle -
  • +
  • +
    +

    + Problem group + handle +

    +
    +
      +
    1. + Problem title 11 + handle +
    2. +
    3. + Problem title 11 + handle +
    4. +
    5. + Problem title 11 + handle +
    6. +
    7. + Problem title 13 + handle +
    8. +
    9. + Problem title 14 + handle +
    10. +
    11. + Video 3 + handle +
    12. +
    +
  • Video 3 handle
  • - +
  • +
    +

    + Problem group + handle +

    +
    +
      +
    1. + Problem title 11 + handle +
    2. +
    3. + Problem title 11 + handle +
    4. +
    5. + Problem title 11 + handle +
    6. +
    7. + Problem title 13 + handle +
    8. +
    9. + Problem title 14 + handle +
    10. +
    11. + Video 3 + handle +
    12. +
    +
  • @@ -103,6 +200,18 @@ Problem title 11 handle
  • +
  • + Problem title 13 + handle +
  • +
  • + Problem title 14 + handle +
  • +
  • + Video 3 + handle +
  • diff --git a/cms/templates/widgets/week-new.html b/cms/templates/widgets/week-new.html index be08587755..c62730c1bc 100644 --- a/cms/templates/widgets/week-new.html +++ b/cms/templates/widgets/week-new.html @@ -1,13 +1,11 @@
    -

    Week 3

    +
    +

    Week 3

    +

    + new goal

    +
    -
    -

    Weeks goals:

    -

    + new goal

    -
    -
    • Please add a goal for this section

      From 5ad2824c59a47a267d180e36286475a2d7c85d02 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 7 Jun 2012 17:16:56 -0400 Subject: [PATCH 065/252] fix typo in comment in student view --- lms/djangoapps/student/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/student/views.py b/lms/djangoapps/student/views.py index eb71f5ba6a..a157da7bef 100644 --- a/lms/djangoapps/student/views.py +++ b/lms/djangoapps/student/views.py @@ -89,7 +89,7 @@ def login_user(request, error=""): @ensure_csrf_cookie def logout_user(request): - ''' HTTP request to log in the user. Redirects to marketing page''' + ''' HTTP request to log out the user. Redirects to marketing page''' logout(request) return redirect('/') From 15ac575301eb4fb63ead0ca62c4ab497ed934505 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 14 Jun 2012 11:28:52 -0400 Subject: [PATCH 066/252] added rednose to requirements (adds color output to nose) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9dc67682fb..a62baad94b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ newrelic glob2 django_nose nosexcover +rednose From e061e8642433e95151c3b355efa09059d5f8a41f Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 14 Jun 2012 17:07:50 -0400 Subject: [PATCH 067/252] Clean and refactor courseware/views.py and module_render.py * Refactor index() so that it makes sense to me and hopefully others :) * Rename preloaded cache of student modules to student_module_cache * Fix line length and whitespace throughout * add docstrings and other comments * a few behavior-preserving tweaks to the code to make it clearer. * Separate codepaths for with-module and without-module in index view * Remove default chapter + section, since they don't exist anyway in course.xml --- lms/djangoapps/courseware/module_render.py | 227 +++++++++------- lms/djangoapps/courseware/views.py | 301 +++++++++++++-------- 2 files changed, 316 insertions(+), 212 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index fe6ebdd585..7c4de7d499 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -26,8 +26,29 @@ class I4xSystem(object): This is an abstraction such that x_modules can function independent of the courseware (e.g. import into other types of courseware, LMS, or if we want to have a sandbox server for user-contributed content) + + I4xSystem objects are passed to x_modules to provide access to system + functionality. ''' - def __init__(self, ajax_url, track_function, render_function, render_template, filestore=None): + def __init__(self, ajax_url, track_function, render_function, + render_template, filestore=None): + ''' + Create a closure around the system environment. + + ajax_url - the url where ajax calls to the encapsulating module go. + track_function - function of (event_type, event), intended for logging + or otherwise tracking the event. + TODO: Not used, and has inconsistent args in different + files. Update or remove. + render_function - function that takes (module_xml) and renders it, + returning a dictionary with a context for rendering the + module to html. Dictionary will contain keys 'content' + and 'type'. + render_template - a function that takes (template_file, context), and returns + rendered html. + filestore - A filestore ojbect. Defaults to an instance of OSFS based at + settings.DATA_DIR. + ''' self.ajax_url = ajax_url self.track_function = track_function if not filestore: @@ -35,37 +56,47 @@ class I4xSystem(object): else: self.filestore = filestore if settings.DEBUG: - log.info("[courseware.module_render.I4xSystem] filestore path = %s" % filestore) + log.info("[courseware.module_render.I4xSystem] filestore path = %s", + filestore) self.render_function = render_function self.render_template = render_template self.exception404 = Http404 self.DEBUG = settings.DEBUG - def get(self,attr): # uniform access to attributes (like etree) + def get(self, attr): + ''' provide uniform access to attributes (like etree).''' return self.__dict__.get(attr) - def set(self,attr,val): # uniform access to attributes (like etree) + + def set(self,attr,val): + '''provide uniform access to attributes (like etree)''' self.__dict__[attr] = val + def __repr__(self): return repr(self.__dict__) + def __str__(self): return str(self.__dict__) -def object_cache(cache, user, module_type, module_id): - # We don't look up on user -- all queries include user - # Additional lookup would require a DB hit the way Django - # is broken. +def smod_cache_lookup(cache, module_type, module_id): + ''' + Look for a student module with the given type and id in the cache. + + cache -- list of student modules + + returns first found object, or None + ''' for o in cache: - if o.module_type == module_type and \ - o.module_id == module_id: + if o.module_type == module_type and o.module_id == module_id: return o return None def make_track_function(request): ''' We want the capa problem (and other modules) to be able to track/log what happens inside them without adding dependencies on - Django or the rest of the codebase. We do this by passing a - tracking function to them. This generates a closure for each request - that gives a clean interface on both sides. + Django or the rest of the codebase. + + To do this in a clean way, we pass a tracking function to the module, + which calls it to log events. ''' import track.views @@ -75,85 +106,91 @@ def make_track_function(request): def grade_histogram(module_id): ''' Print out a histogram of grades on a given problem. - Part of staff member debug info. + Part of staff member debug info. ''' from django.db import connection cursor = connection.cursor() - cursor.execute("select courseware_studentmodule.grade,COUNT(courseware_studentmodule.student_id) from courseware_studentmodule where courseware_studentmodule.module_id=%s group by courseware_studentmodule.grade", [module_id]) + q = """SELECT courseware_studentmodule.grade, + COUNT(courseware_studentmodule.student_id) + FROM courseware_studentmodule + WHERE courseware_studentmodule.module_id=%s + GROUP BY courseware_studentmodule.grade""" + # Passing module_id this way prevents sql-injection. + cursor.execute(q, [module_id]) grades = list(cursor.fetchall()) - grades.sort(key=lambda x:x[0]) # Probably not necessary - if (len(grades) == 1 and grades[0][0] is None): + grades.sort(key=lambda x: x[0]) # Add ORDER BY to sql query? + if len(grades) == 1 and grades[0][0] is None: return [] return grades -def get_module(user, request, xml_module, module_object_preload, position=None): - ''' Get the appropriate xmodule and StudentModule. +def get_module(user, request, module_xml, student_module_cache, position=None): + ''' Get an instance of the xmodule class corresponding to module_xml, + setting the state based on an existing StudentModule, or creating one if none + exists. Arguments: - user : current django User - request : current django HTTPrequest - - xml_module : lxml etree of xml subtree for the current module - - module_object_preload : list of StudentModule objects, one of which may match this module type and id - - position : extra information from URL for user-specified position within module + - module_xml : lxml etree of xml subtree for the requested module + - student_module_cache : list of StudentModule objects, one of which may + match this module type and id + - position : extra information from URL for user-specified + position within module Returns: - a tuple (xmodule instance, student module, module type). - ''' - module_type=xml_module.tag - module_class=xmodule.get_module_class(module_type) - module_id=xml_module.get('id') #module_class.id_attribute) or "" + module_type = module_xml.tag + module_class = xmodule.get_module_class(module_type) + module_id = module_xml.get('id') - # Grab state from database - smod = object_cache(module_object_preload, - user, - module_type, - module_id) + # Grab xmodule state from StudentModule cache + smod = smod_cache_lookup(student_module_cache, module_type, module_id) + state = smod.state if smod else None - if not smod: # If nothing in the database... - state=None - else: - state = smod.state - - # get coursename if stored + # get coursename if present in request coursename = multicourse_settings.get_coursename_from_request(request) if coursename and settings.ENABLE_MULTICOURSE: - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course + # path to XML for the course + xp = multicourse_settings.get_course_xmlpath(coursename) data_root = settings.DATA_DIR + xp else: data_root = settings.DATA_DIR - # Create a new instance - ajax_url = settings.MITX_ROOT_URL + '/modx/'+module_type+'/'+module_id+'/' + # Setup system context for module instance + ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/' + def render_function(module_xml): + return render_x_module(user, request, module_xml, student_module_cache, position) system = I4xSystem(track_function = make_track_function(request), - render_function = lambda x: render_x_module(user, request, x, module_object_preload, position), + render_function = render_function, render_template = render_to_string, ajax_url = ajax_url, filestore = OSFS(data_root), ) - system.set('position',position) # pass URL specified position along to module, through I4xSystem - instance=module_class(system, - etree.tostring(xml_module), - module_id, - state=state) + # pass position specified in URL to module through I4xSystem + system.set('position', position) + instance = module_class(system, + etree.tostring(module_xml), + module_id, + state=state) - # If instance wasn't already in the database, and this - # isn't a guest user, create it + # If StudentModule for this instance wasn't already in the database, + # and this isn't a guest user, create it. if not smod and user.is_authenticated(): - smod=StudentModule(student=user, - module_type = module_type, - module_id=module_id, - state=instance.get_state()) + smod = StudentModule(student=user, module_type = module_type, + module_id=module_id, state=instance.get_state()) smod.save() - module_object_preload.append(smod) + # Add to cache. The caller and the system context have references + # to it, so the change persists past the return + student_module_cache.append(smod) return (instance, smod, module_type) -def render_x_module(user, request, xml_module, module_object_preload, position=None): +def render_x_module(user, request, module_xml, student_module_cache, position=None): ''' Generic module for extensions. This renders to HTML. modules include sequential, vertical, problem, video, html @@ -164,37 +201,36 @@ def render_x_module(user, request, xml_module, module_object_preload, position=N - user : current django User - request : current django HTTPrequest - - xml_module : lxml etree of xml subtree for the current module - - module_object_preload : list of StudentModule objects, one of which may match this module type and id + - module_xml : lxml etree of xml subtree for the current module + - student_module_cache : list of StudentModule objects, one of which may match this module type and id - position : extra information from URL for user-specified position within module Returns: - - dict which is context for HTML rendering of the specified module - + - dict which is context for HTML rendering of the specified module. Will have + key 'content', and will have 'type' key if passed a valid module. ''' - if xml_module==None : - return {"content":""} + if module_xml is None : + return {"content": ""} - (instance, smod, module_type) = get_module(user, request, xml_module, module_object_preload, position) + (instance, smod, module_type) = get_module( + user, request, module_xml, student_module_cache, position) - # Grab content content = instance.get_html() # special extra information about each problem, only for users who are staff if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff: - module_id = xml_module.get('id') + module_id = module_xml.get('id') histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 - content=content+render_to_string("staff_problem_info.html", {'xml':etree.tostring(xml_module), - 'module_id' : module_id, - 'histogram': json.dumps(histogram), - 'render_histogram' : render_histogram}) + staff_context = {'xml': etree.tostring(module_xml), + 'module_id': module_id, + 'histogram': json.dumps(histogram), + 'render_histogram': render_histogram} + content += render_to_string("staff_problem_info.html", staff_context) - content = {'content':content, - 'type':module_type} - - return content + context = {'content': content, 'type': module_type} + return context def modx_dispatch(request, module=None, dispatch=None, id=None): ''' Generic view for extensions. This is where AJAX calls go. @@ -210,32 +246,38 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): - id -- the module id. Used to look up the student module. e.g. filenamexformularesponse ''' + # ''' (fix emacs broken parsing) if not request.user.is_authenticated(): return redirect('/') + # python concats adjacent strings + error_msg = ("We're sorry, this module is temporarily unavailable." + "Our staff is working to fix it as soon as possible") + + # Grab the student information for the module from the database s = StudentModule.objects.filter(student=request.user, module_id=id) - #s = StudentModule.get_with_caching(request.user, id) - if len(s) == 0 or s is None: - log.debug("Couldnt find module for user and id " + str(module) + " " + str(request.user) + " "+ str(id)) + + # s = StudentModule.get_with_caching(request.user, id) + if s is None or len(s) == 0: + log.debug("Couldn't find module '%s' for user '%s' and id '%s'", + module, request.user, id) raise Http404 s = s[0] oldgrade = s.grade oldstate = s.state - # TODO: if dispatch is left at default value None, this will go boom. What's the correct - # behavior? - dispatch=dispatch.split('?')[0] + # If there are arguments, get rid of them + if '?' in dispatch: + dispatch = dispatch.split('?')[0] - ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/' - - # get coursename if stored + ajax_url = '{root}/modx/{module}/{id}'.format(root = settings.MITX_ROOT_URL, + module=module, id=id) coursename = multicourse_settings.get_coursename_from_request(request) - if coursename and settings.ENABLE_MULTICOURSE: - xp = multicourse_settings.get_course_xmlpath(coursename) # path to XML for the course + xp = multicourse_settings.get_course_xmlpath(coursename) data_root = settings.DATA_DIR + xp else: data_root = settings.DATA_DIR @@ -244,11 +286,13 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): try: xml = content_parser.module_xml(request.user, module, 'id', id, coursename) except: - log.exception("Unable to load module during ajax call. module=%s, dispatch=%s, id=%s" % (module, dispatch, id)) + log.exception( + "Unable to load module during ajax call. module=%s, dispatch=%s, id=%s", + module, dispatch, id) if accepts(request, 'text/html'): return render_to_response("module-error.html", {}) else: - response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"})) + response = HttpResponse(json.dumps({'success': error_msg})) return response # Create the module @@ -260,24 +304,23 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ) try: - instance=xmodule.get_module_class(module)(system, - xml, - id, - state=oldstate) + module_class = xmodule.get_module_class(module) + instance = module_class(system, xml, id, state=oldstate) except: log.exception("Unable to load module instance during ajax call") if accepts(request, 'text/html'): return render_to_response("module-error.html", {}) else: - response = HttpResponse(json.dumps({'success': "We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible"})) + response = HttpResponse(json.dumps({'success': error_msg})) return response # Let the module handle the AJAX - ajax_return=instance.handle_ajax(dispatch, request.POST) + ajax_return = instance.handle_ajax(dispatch, request.POST) + # Save the state back to the database - s.state=instance.get_state() + s.state = instance.get_state() if instance.get_score(): - s.grade=instance.get_score()['score'] + s.grade = instance.get_score()['score'] if s.grade != oldgrade or s.state != oldstate: s.save() # Return whatever the module wanted to return to the client/caller diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index fcd0104455..7bc8b323ce 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -41,66 +41,71 @@ def gradebook(request): coursename = multicourse_settings.get_coursename_from_request(request) student_objects = User.objects.all()[:100] - student_info = [{'username' :s.username, - 'id' : s.id, + student_info = [{'username': s.username, + 'id': s.id, 'email': s.email, - 'grade_info' : grades.grade_sheet(s,coursename), - 'realname' : UserProfile.objects.get(user = s).name + 'grade_info': grades.grade_sheet(s, coursename), + 'realname': UserProfile.objects.get(user = s).name } for s in student_objects] - return render_to_response('gradebook.html',{'students':student_info}) + return render_to_response('gradebook.html', {'students': student_info}) @login_required @cache_control(no_cache=True, no_store=True, must_revalidate=True) -def profile(request, student_id = None): +def profile(request, student_id=None): ''' User profile. Show username, location, etc, as well as grades . We need to allow the user to change some of these settings .''' if student_id is None: student = request.user - else: + else: if 'course_admin' not in content_parser.user_groups(request.user): raise Http404 student = User.objects.get( id = int(student_id)) - user_info = UserProfile.objects.get(user=student) # request.user.profile_cache # + user_info = UserProfile.objects.get(user=student) # request.user.profile_cache # coursename = multicourse_settings.get_coursename_from_request(request) - context={'name':user_info.name, - 'username':student.username, - 'location':user_info.location, - 'language':user_info.language, - 'email':student.email, - 'format_url_params' : content_parser.format_url_params, - 'csrf':csrf(request)['csrf_token'] - } - context.update(grades.grade_sheet(student,coursename)) + context = {'name': user_info.name, + 'username': student.username, + 'location': user_info.location, + 'language': user_info.language, + 'email': student.email, + 'format_url_params': content_parser.format_url_params, + 'csrf': csrf(request)['csrf_token'] + } + context.update(grades.grade_sheet(student, coursename)) return render_to_response('profile.html', context) -def render_accordion(request,course,chapter,section): + +def render_accordion(request, course, chapter, section): ''' Draws navigation bar. Takes current position in accordion as parameter. Returns (initialization_javascript, content)''' if not course: course = "6.002 Spring 2012" - toc=content_parser.toc_from_xml(content_parser.course_file(request.user,course), chapter, section) - active_chapter=1 + toc = content_parser.toc_from_xml( + content_parser.course_file(request.user, course), chapter, section) + + active_chapter = 1 for i in range(len(toc)): if toc[i]['active']: - active_chapter=i - context=dict([['active_chapter',active_chapter], - ['toc',toc], - ['course_name',course], - ['format_url_params',content_parser.format_url_params], - ['csrf',csrf(request)['csrf_token']]] + \ + active_chapter = i + + context=dict([('active_chapter', active_chapter), + ('toc', toc), + ('course_name', course), + ('format_url_params', content_parser.format_url_params), + ('csrf', csrf(request)['csrf_token'])] + template_imports.items()) - return render_to_string('accordion.html',context) + return render_to_string('accordion.html', context) + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def render_section(request, section): - ''' TODO: Consolidate with index + ''' TODO: Consolidate with index ''' user = request.user if not settings.COURSEWARE_ENABLED: @@ -120,15 +125,15 @@ def render_section(request, section): } module_ids = dom.xpath("//@id") - + if user.is_authenticated(): - module_object_preload = list(StudentModule.objects.filter(student=user, + student_module_cache = list(StudentModule.objects.filter(student=user, module_id__in=module_ids)) else: - module_object_preload = [] - + student_module_cache = [] + try: - module = render_x_module(user, request, dom, module_object_preload) + module = render_x_module(user, request, dom, student_module_cache) except: log.exception("Unable to load module") context.update({ @@ -138,18 +143,67 @@ def render_section(request, section): return render_to_response('courseware.html', context) context.update({ - 'init':module.get('init_js', ''), - 'content':module['content'], + 'init': module.get('init_js', ''), + 'content': module['content'], }) result = render_to_response('courseware.html', context) return result +def get_course(request, course): + ''' Figure out what the correct course is. + + Needed to preserve backwards compatibility with non-multi-course version. + TODO: Can this go away once multicourse becomes standard? + ''' + + if course==None: + if not settings.ENABLE_MULTICOURSE: + course = "6.002 Spring 2012" + elif 'coursename' in request.session: + course = request.session['coursename'] + else: + course = settings.COURSE_DEFAULT + return course + +def get_module_xml(user, course, chapter, section): + ''' Look up the module xml for the given course/chapter/section path. + + Takes the user to look up the course file. + + Returns None if there was a problem, or the lxml etree for the module. + ''' + try: + # this is the course.xml etree + dom = content_parser.course_file(user, course) + except: + log.exception("Unable to parse courseware xml") + return None + + # this is the module's parent's etree + path = "//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]" + dom_module = dom.xpath(path, course=course, chapter=chapter, section=section) + + module_wrapper = dom_module[0] if len(dom_module) > 0 else None + if module_wrapper is None: + module = None + elif module_wrapper.get("src"): + module = content_parser.section_file( + user=user, section=module_wrapper.get("src"), coursename=course) + else: + # Copy the element out of the module's etree + module = etree.XML(etree.tostring(module_wrapper[0])) + return module + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -def index(request, course=None, chapter="Using the System", section="Hints",position=None): +def index(request, course=None, chapter=None, section=None, + position=None): ''' Displays courseware accordion, and any associated content. + If course, chapter, and section aren't all specified, just returns + the accordion. If they are specified, returns an error if they don't + point to a valid module. Arguments: @@ -162,110 +216,113 @@ def index(request, course=None, chapter="Using the System", section="Hints",posi Returns: - HTTPresponse + ''' + def clean(s): + ''' Fixes URLs -- we convert spaces to _ in URLs to prevent + funny encoding characters and keep the URLs readable. This undoes + that transformation. - ''' - user = request.user + TODO: Properly replace underscores. (Q: what is properly?) + ''' + return s.replace('_', ' ') + + def get_submodule_ids(module_xml): + ''' + Get a list with ids of the modules within this module. + ''' + return module_xml.xpath("//@id") + + def preload_student_modules(module_xml): + ''' + Find any StudentModule objects for this user that match + one of the given module_ids. Used as a cache to avoid having + each rendered module hit the db separately. + + Returns the list, or None on error. + ''' + if request.user.is_authenticated(): + module_ids = get_submodule_ids(module_xml) + return list(StudentModule.objects.filter(student=request.user, + module_id__in=module_ids)) + else: + return [] + + def get_module_context(): + ''' + Look up the module object and render it. If all goes well, returns + {'init': module-init-js, 'content': module-rendered-content} + + If there's an error, returns + {'content': module-error message} + ''' + # Can't modify variables of outer scope, so need new ones + chapter_ = clean(chapter) + section_ = clean(section) + + user = request.user + + module_xml = get_module_xml(user, course, chapter_, section_) + if module_xml is None: + log.exception("couldn't get module_xml: course/chapter/section: '%s/%s/%s'", + course, chapter_, section_) + return {'content' : render_to_string("module-error.html", {})} + + student_module_cache = preload_student_modules(module_xml) + + try: + module_context = render_x_module(user, request, module_xml, + student_module_cache, position) + except: + log.exception("Unable to load module") + return {'content' : render_to_string("module-error.html", {})} + + return {'init': module_context.get('init_js', ''), + 'content': module_context['content']} + if not settings.COURSEWARE_ENABLED: return redirect('/') - if course==None: - if not settings.ENABLE_MULTICOURSE: - course = "6.002 Spring 2012" - elif 'coursename' in request.session: - course = request.session['coursename'] - else: - course = settings.COURSE_DEFAULT - - # Fixes URLs -- we don't get funny encoding characters from spaces - # so they remain readable - ## TODO: Properly replace underscores - course=course.replace("_"," ") - chapter=chapter.replace("_"," ") - section=section.replace("_"," ") - - # use multicourse module to determine if "course" is valid - #if course!=settings.COURSE_NAME.replace('_',' '): + course = clean(get_course(request, course)) if not multicourse_settings.is_valid_course(course): return redirect('/') - request.session['coursename'] = course # keep track of current course being viewed in django's request.session - - try: - # this is the course.xml etree - dom = content_parser.course_file(user,course) # also pass course to it, for course-specific XML path - except: - log.exception("Unable to parse courseware xml") - return render_to_response('courseware-error.html', {}) - - # this is the module's parent's etree - dom_module = dom.xpath("//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]", - course=course, chapter=chapter, section=section) - - #print "DM", dom_module - - if len(dom_module) == 0: - module_wrapper = None - else: - module_wrapper = dom_module[0] - - if module_wrapper is None: - module = None - elif module_wrapper.get("src"): - module = content_parser.section_file(user=user, section=module_wrapper.get("src"), coursename=course) - else: - # this is the module's etree - module = etree.XML(etree.tostring(module_wrapper[0])) # Copy the element out of the tree - - module_ids = [] - if module is not None: - module_ids = module.xpath("//@id", - course=course, chapter=chapter, section=section) - - if user.is_authenticated(): - module_object_preload = list(StudentModule.objects.filter(student=user, - module_id__in=module_ids)) - else: - module_object_preload = [] + # keep track of current course being viewed in django's request.session + request.session['coursename'] = course context = { 'csrf': csrf(request)['csrf_token'], 'accordion': render_accordion(request, course, chapter, section), - 'COURSE_TITLE':multicourse_settings.get_course_title(course), + 'COURSE_TITLE': multicourse_settings.get_course_title(course), + 'init': '', + 'content': '' } - try: - module_context = render_x_module(user, request, module, module_object_preload, position) - except: - log.exception("Unable to load module") - context.update({ - 'init': '', - 'content': render_to_string("module-error.html", {}), - }) - return render_to_response('courseware.html', context) - - context.update({ - 'init': module_context.get('init_js', ''), - 'content': module_context['content'], - }) + look_for_module = chapter is not None and section is not None + if look_for_module: + context.update(get_module_context()) result = render_to_response('courseware.html', context) return result def jump_to(request, probname=None): ''' - Jump to viewing a specific problem. The problem is specified by a problem name - currently the filename (minus .xml) - of the problem. Maybe this should change to a more generic tag, eg "name" given as an attribute in . + Jump to viewing a specific problem. The problem is specified by a + problem name - currently the filename (minus .xml) of the problem. + Maybe this should change to a more generic tag, eg "name" given as + an attribute in . - We do the jump by (1) reading course.xml to find the first instance of with the given filename, then - (2) finding the parent element of the problem, then (3) rendering that parent element with a specific computed position - value (if it is ). + We do the jump by (1) reading course.xml to find the first + instance of with the given filename, then (2) finding + the parent element of the problem, then (3) rendering that parent + element with a specific computed position value (if it is + ). ''' # get coursename if stored coursename = multicourse_settings.get_coursename_from_request(request) # begin by getting course.xml tree - xml = content_parser.course_file(request.user,coursename) + xml = content_parser.course_file(request.user, coursename) # look for problem of given name pxml = xml.xpath('//problem[@filename="%s"]' % probname) @@ -279,12 +336,16 @@ def jump_to(request, probname=None): section = None branch = parent for k in range(4): # max depth of recursion - if branch.tag=='section': section = branch.get('name') - if branch.tag=='chapter': chapter = branch.get('name') + if branch.tag == 'section': + section = branch.get('name') + if branch.tag == 'chapter': + chapter = branch.get('name') branch = branch.getparent() position = None - if parent.tag=='sequential': - position = parent.index(pxml)+1 # position in sequence - - return index(request,course=coursename,chapter=chapter,section=section,position=position) + if parent.tag == 'sequential': + position = parent.index(pxml) + 1 # position in sequence + + return index(request, + course=coursename, chapter=chapter, + section=section, position=position) From ce74f9779f77834e6775e1b33117948abf3c6bdb Mon Sep 17 00:00:00 2001 From: ichuang Date: Fri, 15 Jun 2012 22:40:02 -0400 Subject: [PATCH 068/252] modify capa_problem to add functionality --- common/lib/capa/capa_problem.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index e70fa6ceff..4ee2a2113d 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -111,6 +111,7 @@ class LoncapaProblem(object): file_text = re.sub("endouttext\s*/", "/text", file_text) self.tree = etree.XML(file_text) # parse problem XML file into an element tree + self._process_includes() # handle any tags # construct script processor context (eg for customresponse problems) self.context = self._extract_context(self.tree, seed=self.seed) @@ -242,6 +243,36 @@ class LoncapaProblem(object): # ======= Private Methods Below ======== + def _process_includes(self): + ''' + Handle any tags by reading in the specified file and inserting it + into our XML tree. Fail gracefully if debugging. + ''' + includes = self.tree.findall('.//include') + for inc in includes: + file = inc.get('file') + if file is not None: + try: + ifp = self.system.filestore.open(file) # open using I4xSystem OSFS filestore + except Exception,err: + log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True))) + log.error('Cannot find file %s in %s' % (file,self.system.filestore)) + if not self.system.get('DEBUG'): # if debugging, don't fail - just log error + raise + else: continue + try: + incxml = etree.XML(ifp.read()) # read in and convert to XML + except Exception,err: + log.error('Error %s in problem xml include: %s' % (err,etree.tostring(inc,pretty_print=True))) + log.error('Cannot parse XML in %s' % (file)) + if not self.system.get('DEBUG'): # if debugging, don't fail - just log error + raise + else: continue + parent = inc.getparent() # insert new XML into tree in place of inlcude + parent.insert(parent.index(inc),incxml) + parent.remove(inc) + log.debug('Included %s into %s' % (file,self.fileobject)) + def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private ''' Extract content of from the problem.xml file, and exec it in the From fe45de3833ebc0ab06d9ed6fff696ee0387177f0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 10:08:31 -0400 Subject: [PATCH 069/252] Read week headings from mongodb --- .../management/commands/import.py | 2 +- cms/djangoapps/contentstore/views.py | 13 +- cms/lib/keystore/__init__.py | 7 +- cms/lib/keystore/mongo.py | 19 ++- cms/templates/widgets/navigation.html | 123 +----------------- common/lib/xmodule/seq_module.py | 8 ++ common/lib/xmodule/setup.py | 14 ++ common/lib/xmodule/x_module.py | 75 +++++++++-- requirements.txt | 1 + 9 files changed, 115 insertions(+), 147 deletions(-) create mode 100644 common/lib/xmodule/setup.py diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 690e3dbea0..3e0ccdd5e8 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -82,7 +82,7 @@ class Command(BaseCommand): def handle_list(e): if e.attrib.get("class", None) == "tutorials": return - children = [{'url':le.attrib['url']} for le in e.getchildren()] + children = [le.attrib['url'] for le in e.getchildren()] results[e.attrib['url']] = {'children':children} def handle_video(e): diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 429fb6c26b..64bde14869 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -3,11 +3,10 @@ from keystore.django import keystore from django.contrib.auth.decorators import login_required -@login_required -def calendar(request, org, course): - weeks = keystore.get_children_for_item(['i4x', org, course, 'Course', None]) - return render_to_response('calendar.html', {'weeks': weeks}) - - def index(request): - return render_to_response('index.html', {}) + # FIXME (cpennington): These need to be read in from the active user + org = 'mit.edu' + course = '6002xs12' + course = keystore.get_item(['i4x', org, course, 'Course', None]) + weeks = course.get_children() + return render_to_response('index.html', {'weeks': weeks}) diff --git a/cms/lib/keystore/__init__.py b/cms/lib/keystore/__init__.py index d0a24be797..61c797241d 100644 --- a/cms/lib/keystore/__init__.py +++ b/cms/lib/keystore/__init__.py @@ -37,7 +37,7 @@ class Location(object): self.update(location.list()) def url(self): - return "i4x://{org}/{course}/{category}/{name}".format(**self.dict()) + return "{tag}://{org}/{course}/{category}/{name}".format(**self.dict()) def list(self): return [self.tag, self.org, self.course, self.category, self.name] @@ -54,10 +54,9 @@ class Location(object): class KeyStore(object): - def get_children_for_item(self, location): + def get_item(self, location): """ - Returns the children for the most recent revision of the object - with the specified location. + Returns an XModuleDescriptor instance for the item at location If no object is found at that location, raises keystore.exceptions.ItemNotFoundError diff --git a/cms/lib/keystore/mongo.py b/cms/lib/keystore/mongo.py index d29afb4bd2..d9760909c9 100644 --- a/cms/lib/keystore/mongo.py +++ b/cms/lib/keystore/mongo.py @@ -1,6 +1,7 @@ import pymongo from . import KeyStore, Location from .exceptions import ItemNotFoundError, InsufficientSpecificationError +from xmodule.x_module import XModuleDescriptor class MongoKeyStore(KeyStore): @@ -12,11 +13,22 @@ class MongoKeyStore(KeyStore): host=host, port=port )[db][collection] - + # Force mongo to report errors, at the expense of performance self.collection.safe = True - def get_children_for_item(self, location): + def get_item(self, location): + """ + Returns an XModuleDescriptor instance for the item at location + + If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + + Searches for all matches of a partially specifed location, but raises an + keystore.exceptions.InsufficientSpecificationError if more + than a single object matches the query. + + location: Something that can be passed to Location + """ query = dict( ('location.{key}'.format(key=key), val) for (key, val) @@ -25,7 +37,6 @@ class MongoKeyStore(KeyStore): ) items = self.collection.find( query, - fields={'children': True}, sort=[('revision', pymongo.ASCENDING)], limit=1, ) @@ -35,7 +46,7 @@ class MongoKeyStore(KeyStore): if items.count() == 0: raise ItemNotFoundError(location) - return items[0]['children'] + return XModuleDescriptor.load_from_json(items[0], self.get_item) def create_item(self, location, editor): """ diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 28b92979d9..75d581dd73 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -35,9 +35,10 @@
      + % for week in weeks:
    1. -

      Week 1

      +

      ${week.name}

      • Goal title: This is a goal that will be in the header of the week
      • Goal title two: This is another goal for this week so that students have two things to learn
      • @@ -64,125 +65,7 @@ <%include file="module-dropdown.html"/>
    2. -
    3. -
      -

      Week 2

      -
        -
      • Another title This is the goal for the week
      • -
      -
      - - -
    4. -
    5. -
      -

      Week 3

      -
        -
      • Another title This is the goal for the week
      • -
      -
      - - -
    6. -
    7. -
      -

      Week 4

      -
        -
      • Another title This is the goal for the week
      • -
      • Goal title two: This is another fgoal for this week so that students have two things to learn
      • -
      -
      - - -
    8. - -
    9. -
      -

      Week 5

      -
        -
      • Please create a learning goal for this week
      • -
      -
      - -
        - <%include file="module-dropdown.html"/> -
      -
    10. -
    11. -
      -

      Week 6

      -
        -
      • Please create a learning goal for this week
      • -
      -
      - -
        - <%include file="module-dropdown.html"/> -
      -
    12. -
    13. -
      -

      Week 7

      -
        -
      • Please create a learning goal for this week
      • -
      -
      - -
        - <%include file="module-dropdown.html"/> -
      -
    14. + %endfor
    15. Course Scratch Pad

      diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index f643eaab4a..4ae4fb3813 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -95,3 +95,11 @@ class Module(XModule): self.position = int(system.get('position')) self.rendered = False + + +class CourseModuleDescriptor(XModuleDescriptor): + pass + + +class ChapterModuleDescriptor(XModuleDescriptor): + pass diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py new file mode 100644 index 0000000000..6a659b6852 --- /dev/null +++ b/common/lib/xmodule/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup, find_packages + +setup( + name="XModule", + version="0.1", + packages=find_packages(), + install_requires=['distribute'], + entry_points={ + 'xmodule.v1': [ + "Course = seq_module:CourseModuleDescriptor", + "Chapter = seq_module:ChapterModuleDescriptor", + ] + } +) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index d783694fee..23025df50b 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -1,8 +1,34 @@ from lxml import etree +import pkg_resources +import logging +from keystore import Location + +log = logging.getLogger('mitx.' + __name__) def dummy_track(event_type, event): pass + +class ModuleMissingError(Exception): + pass + + +class Plugin(object): + @classmethod + def load_class(cls, identifier): + classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) + if len(classes) > 1: + log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( + entry_point=cls.entry_point, + id=identifier, + classes=", ".join([class_.module_name for class_ in classes]))) + + if len(classes) == 0: + raise ModuleMissingError(identifier) + + return classes[0].load() + + class XModule(object): ''' Implements a generic learning module. Initialized on access with __init__, first time with state=None, and @@ -24,8 +50,8 @@ class XModule(object): or a CAPA input type ''' return ['xmodule'] - def get_name(): - name = self.__xmltree.get(name) + def get_name(self): + name = self.__xmltree.get('name') if name: return name else: @@ -98,15 +124,42 @@ class XModule(object): return "" -class XModuleDescriptor(object): - def __init__(self, xml = None, json = None): - if not xml and not json: - raise "XModuleDescriptor must be initalized with XML or JSON" - if not xml: - raise NotImplementedError("Code does not have support for JSON yet") - - self.xml = xml - self.json = json +class XModuleDescriptor(Plugin): + + entry_point = "xmodule.v1" + + @staticmethod + def load_from_json(json_data, load_item): + class_ = XModuleDescriptor.load_class(json_data['location']['category']) + return class_.from_json(json_data, load_item) + + @classmethod + def from_json(cls, json_data, load_item): + """ + Creates an instance of this descriptor from the supplied json_data. + + json_data: Json data specifying the data, children, and metadata for the descriptor + load_item: A function that takes an i4x url and returns a module descriptor + """ + return cls(load_item=load_item, **json_data) + + def __init__(self, + load_item, + data=None, + children=None, + **kwargs): + self.load_item = load_item + self.data = data if data is not None else {} + self.children = children if children is not None else [] + self.name = Location(kwargs.get('location')).name + self._child_instances = None + + def get_children(self): + """Returns a list of XModuleDescriptor instances for the children of this module""" + if self._child_instances is None: + self._child_instances = [self.load_item(child) for child in self.children] + return self._child_instances + def get_xml(self): ''' For conversions between JSON and legacy XML representations. diff --git a/requirements.txt b/requirements.txt index 5e95e1bf9e..85cc16b828 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ sympy newrelic glob2 pymongo +-e common/lib/xmodule From e67dfb70e4d54da8e078dee94c37fd34a5d9d39c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 11:09:11 -0400 Subject: [PATCH 070/252] Add categories and XModuleDescriptors for all module types that are used at the top level of a course --- .../management/commands/import.py | 27 +++++++++++++------ cms/templates/widgets/navigation.html | 25 ++++++----------- common/lib/xmodule/seq_module.py | 19 ++++++++++--- common/lib/xmodule/setup.py | 11 ++++++-- common/lib/xmodule/video_module.py | 4 +++ common/lib/xmodule/x_module.py | 9 +++++-- 6 files changed, 63 insertions(+), 32 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 3e0ccdd5e8..8b33f32b94 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -32,7 +32,7 @@ class Command(BaseCommand): elements = list(course.iter()) - tag_to_category = {# Inside HTML ==> Skip these + tag_to_category = { # Custom tags 'videodev': 'Custom', 'slides': 'Custom', @@ -40,33 +40,44 @@ class Command(BaseCommand): 'image': 'Custom', 'discuss': 'Custom', # Simple lists - 'chapter': 'Chapter', + 'chapter': 'Week', 'course': 'Course', 'sequential': 'LectureSequence', 'vertical': 'ProblemSet', - 'section': 'Section', + 'section': { + 'Lab': 'Lab', + 'Lecture Sequence': 'LectureSequence', + 'Homework': 'Homework', + 'Tutorial Index': 'TutorialIndex', + 'Video': 'VideoSegment', + 'Midterm': 'Exam', + 'Final': 'Exam', + None: 'Section', + }, # True types 'video': 'VideoSegment', 'html': 'HTML', 'problem': 'Problem', } - - name_index=0 + name_index = 0 for e in elements: name = e.attrib.get('name', None) for f in elements: if f != e and f.attrib.get('name', None) == name: name = None if not name: - name = "{tag}_{index}".format(tag = e.tag,index = name_index) + name = "{tag}_{index}".format(tag=e.tag, index=name_index) name_index = name_index + 1 if e.tag in tag_to_category: category = tag_to_category[e.tag] + if isinstance(category, dict): + category = category[e.get('format')] category = category.replace('/', '-') name = name.replace('/', '-') - e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format(category = category, - name = name)) + e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format( + category=category, + name=name)) def handle_skip(e): diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 75d581dd73..1f75dab470 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -40,28 +40,19 @@

      ${week.name}

        -
      • Goal title: This is a goal that will be in the header of the week
      • -
      • Goal title two: This is another goal for this week so that students have two things to learn
      • + % for goal in week.get_goals(): +
      • ${goal.name}:${goal.data}
      • + % endfor
    16. diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 4ae4fb3813..91ff6d2671 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -97,9 +97,22 @@ class Module(XModule): self.rendered = False -class CourseModuleDescriptor(XModuleDescriptor): - pass +class WeekDescriptor(XModuleDescriptor): + + def get_goals(self): + """ + Return a list of Goal XModuleDescriptors that are children + of this Week + """ + return [child for child in self.get_children() if child.type == 'Goal'] + + def get_non_goals(self): + """ + Return a list of non-Goal XModuleDescriptors that are children of + this Week + """ + return [child for child in self.get_children() if child.type != 'Goal'] -class ChapterModuleDescriptor(XModuleDescriptor): +class SectionDescriptor(XModuleDescriptor): pass diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 6a659b6852..1140037259 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -7,8 +7,15 @@ setup( install_requires=['distribute'], entry_points={ 'xmodule.v1': [ - "Course = seq_module:CourseModuleDescriptor", - "Chapter = seq_module:ChapterModuleDescriptor", + "Course = seq_module:SectionDescriptor", + "Week = seq_module:WeekDescriptor", + "Section = seq_module:SectionDescriptor", + "LectureSequence = seq_module:SectionDescriptor", + "Lab = seq_module:SectionDescriptor", + "Homework = seq_module:SectionDescriptor", + "TutorialIndex = seq_module:SectionDescriptor", + "Exam = seq_module:SectionDescriptor", + "VideoSegment = video_module:VideoSegmentDescriptor", ] } ) diff --git a/common/lib/xmodule/video_module.py b/common/lib/xmodule/video_module.py index 8f8a2c2ffd..d7c7f80291 100644 --- a/common/lib/xmodule/video_module.py +++ b/common/lib/xmodule/video_module.py @@ -57,3 +57,7 @@ class Module(XModule): self.annotations=[(e.get("name"),self.render_function(e)) \ for e in xmltree] + + +class VideoSegmentDescriptor(XModuleDescriptor): + pass diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 23025df50b..3560eecbdc 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -152,13 +152,18 @@ class XModuleDescriptor(Plugin): self.data = data if data is not None else {} self.children = children if children is not None else [] self.name = Location(kwargs.get('location')).name + self.type = Location(kwargs.get('location')).category self._child_instances = None - def get_children(self): + def get_children(self, categories=None): """Returns a list of XModuleDescriptor instances for the children of this module""" if self._child_instances is None: self._child_instances = [self.load_item(child) for child in self.children] - return self._child_instances + + if categories is None: + return self._child_instances + else: + return [child for child in self._child_instances if child.type in categories] def get_xml(self): From 96cbb3d33b146ae5be7d13a63a0a50ba96b942aa Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 18 Jun 2012 12:04:10 -0400 Subject: [PATCH 071/252] Small cleanups in response to Calen + Piotr's comments --- lms/djangoapps/courseware/module_render.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 7c4de7d499..87892e8fec 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -29,6 +29,9 @@ class I4xSystem(object): I4xSystem objects are passed to x_modules to provide access to system functionality. + + Note that these functions can be closures over e.g. a django request + and user, or other environment-specific info. ''' def __init__(self, ajax_url, track_function, render_function, render_template, filestore=None): @@ -91,12 +94,9 @@ def smod_cache_lookup(cache, module_type, module_id): return None def make_track_function(request): - ''' We want the capa problem (and other modules) to be able to - track/log what happens inside them without adding dependencies on - Django or the rest of the codebase. - - To do this in a clean way, we pass a tracking function to the module, - which calls it to log events. + ''' + Make a tracking function that logs what happened. + For use in I4xSystem. ''' import track.views @@ -162,11 +162,11 @@ def get_module(user, request, module_xml, student_module_cache, position=None): # Setup system context for module instance ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/' - def render_function(module_xml): + def render_x_module_wrapper(module_xml): return render_x_module(user, request, module_xml, student_module_cache, position) system = I4xSystem(track_function = make_track_function(request), - render_function = render_function, + render_function = render_x_module_wrapper, render_template = render_to_string, ajax_url = ajax_url, filestore = OSFS(data_root), @@ -251,7 +251,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): return redirect('/') # python concats adjacent strings - error_msg = ("We're sorry, this module is temporarily unavailable." + error_msg = ("We're sorry, this module is temporarily unavailable. " "Our staff is working to fix it as soon as possible") @@ -259,7 +259,6 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): s = StudentModule.objects.filter(student=request.user, module_id=id) - # s = StudentModule.get_with_caching(request.user, id) if s is None or len(s) == 0: log.debug("Couldn't find module '%s' for user '%s' and id '%s'", module, request.user, id) @@ -270,8 +269,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): oldstate = s.state # If there are arguments, get rid of them - if '?' in dispatch: - dispatch = dispatch.split('?')[0] + dispatch, _, _ = dispatch.partition('?') ajax_url = '{root}/modx/{module}/{id}'.format(root = settings.MITX_ROOT_URL, module=module, id=id) From f391e9c51c58ff8cd595099673647c321e4d76cd Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 14 May 2012 18:43:21 -0400 Subject: [PATCH 072/252] Courseware can have same random seed in multiple problems for exam. Slight hack. --- common/lib/xmodule/capa_module.py | 2 ++ lms/djangoapps/courseware/module_render.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index c7b52ae8ba..55534f8a3e 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -222,6 +222,8 @@ class Module(XModule): self.weight=only_one(dom2.xpath('/problem/@weight')) if self.rerandomize == 'never': seed = 1 + elif self.rerandomize == "per_student" and hasattr(system, 'id'): + seed = system.id else: seed = None try: diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 7c4de7d499..3b42ac51e7 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -31,7 +31,7 @@ class I4xSystem(object): functionality. ''' def __init__(self, ajax_url, track_function, render_function, - render_template, filestore=None): + render_template, request=None, filestore=None): ''' Create a closure around the system environment. @@ -46,6 +46,7 @@ class I4xSystem(object): and 'type'. render_template - a function that takes (template_file, context), and returns rendered html. + request - the request in progress filestore - A filestore ojbect. Defaults to an instance of OSFS based at settings.DATA_DIR. ''' @@ -62,6 +63,7 @@ class I4xSystem(object): self.render_template = render_template self.exception404 = Http404 self.DEBUG = settings.DEBUG + self.id = request.user.id if request is not None else 0 def get(self, attr): ''' provide uniform access to attributes (like etree).''' @@ -169,6 +171,7 @@ def get_module(user, request, module_xml, student_module_cache, position=None): render_function = render_function, render_template = render_to_string, ajax_url = ajax_url, + request = request, filestore = OSFS(data_root), ) # pass position specified in URL to module through I4xSystem @@ -300,6 +303,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): render_function = None, render_template = render_to_string, ajax_url = ajax_url, + request = request, filestore = OSFS(data_root), ) From 328b2df7c5571f5e38e53e96a413c6964f0fa8ce Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 12:21:12 -0400 Subject: [PATCH 073/252] Ignore .egg-info directories --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ebf06998b1..ef28575da5 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ log/ reports/ /src/ \#*\# +*.egg-info From 7e73f0ede09debeebba547f8e15ec43ecab11463 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 18 Jun 2012 13:12:47 -0400 Subject: [PATCH 074/252] put lambda function back in, as requested during code review --- lms/djangoapps/courseware/module_render.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 87892e8fec..214d53bbb4 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -162,11 +162,10 @@ def get_module(user, request, module_xml, student_module_cache, position=None): # Setup system context for module instance ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/' - def render_x_module_wrapper(module_xml): - return render_x_module(user, request, module_xml, student_module_cache, position) - + system = I4xSystem(track_function = make_track_function(request), - render_function = render_x_module_wrapper, + render_function = lambda xml: render_x_module( + user, request, xml, student_module_cache, position), render_template = render_to_string, ajax_url = ajax_url, filestore = OSFS(data_root), From 5404345b1ff38d7ecd21432835dd739a63ee3a79 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 13:21:06 -0400 Subject: [PATCH 075/252] Make tests pass when running on cms --- .../management/commands/ftpserve.py | 61 ------------------- cms/envs/dev.py | 2 +- cms/envs/test.py | 54 ++++++++++++++++ .../{lib => djangoapps}/cache_toolbox/COPYING | 0 .../cache_toolbox/README.rst | 0 .../cache_toolbox/__init__.py | 0 .../cache_toolbox/app_settings.py | 0 .../{lib => djangoapps}/cache_toolbox/core.py | 0 .../cache_toolbox/middleware.py | 0 .../cache_toolbox/model.py | 0 .../cache_toolbox/relation.py | 0 .../cache_toolbox/templatetags/__init__.py | 0 .../templatetags/cache_toolbox.py | 0 common/{lib => djangoapps}/util/__init__.py | 0 common/{lib => djangoapps}/util/cache.py | 0 common/{lib => djangoapps}/util/memcache.py | 0 common/{lib => djangoapps}/util/middleware.py | 0 common/{lib => djangoapps}/util/models.py | 0 common/{lib => djangoapps}/util/tests.py | 0 common/{lib => djangoapps}/util/views.py | 0 {cms => common}/lib/keystore/__init__.py | 0 {cms => common}/lib/keystore/django.py | 0 {cms => common}/lib/keystore/exceptions.py | 0 {cms => common}/lib/keystore/mongo.py | 0 lms/envs/dev.py | 2 +- rakefile | 2 +- 26 files changed, 57 insertions(+), 64 deletions(-) delete mode 100644 cms/djangoapps/contentstore/management/commands/ftpserve.py create mode 100644 cms/envs/test.py rename common/{lib => djangoapps}/cache_toolbox/COPYING (100%) rename common/{lib => djangoapps}/cache_toolbox/README.rst (100%) rename common/{lib => djangoapps}/cache_toolbox/__init__.py (100%) rename common/{lib => djangoapps}/cache_toolbox/app_settings.py (100%) rename common/{lib => djangoapps}/cache_toolbox/core.py (100%) rename common/{lib => djangoapps}/cache_toolbox/middleware.py (100%) rename common/{lib => djangoapps}/cache_toolbox/model.py (100%) rename common/{lib => djangoapps}/cache_toolbox/relation.py (100%) rename common/{lib => djangoapps}/cache_toolbox/templatetags/__init__.py (100%) rename common/{lib => djangoapps}/cache_toolbox/templatetags/cache_toolbox.py (100%) rename common/{lib => djangoapps}/util/__init__.py (100%) rename common/{lib => djangoapps}/util/cache.py (100%) rename common/{lib => djangoapps}/util/memcache.py (100%) rename common/{lib => djangoapps}/util/middleware.py (100%) rename common/{lib => djangoapps}/util/models.py (100%) rename common/{lib => djangoapps}/util/tests.py (100%) rename common/{lib => djangoapps}/util/views.py (100%) rename {cms => common}/lib/keystore/__init__.py (100%) rename {cms => common}/lib/keystore/django.py (100%) rename {cms => common}/lib/keystore/exceptions.py (100%) rename {cms => common}/lib/keystore/mongo.py (100%) diff --git a/cms/djangoapps/contentstore/management/commands/ftpserve.py b/cms/djangoapps/contentstore/management/commands/ftpserve.py deleted file mode 100644 index f0a1c19dbf..0000000000 --- a/cms/djangoapps/contentstore/management/commands/ftpserve.py +++ /dev/null @@ -1,61 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth.models import User -import contentstore.tasks - -from pyftpdlib import ftpserver -import os - -class DjangoAuthorizer(object): - def validate_authentication(self, username, password): - try: - u=User.objects.get(username=username) - except User.DoesNotExist: - return False - # TODO: Check security groups - return u.check_password(password) - def has_user(self, username): - print "????",username - return True - def has_perm(self, username, perm, path=None): - print "!!!!!",username, perm, path - return True - def get_home_dir(self, username): - d = "/tmp/ftp/"+username - try: - os.mkdir(d) - except OSError: - pass - return "/tmp/ftp/"+username - def get_perms(self, username): - return 'elradfmw' - def get_msg_login(self, username): - return 'Hello' - def get_msg_quit(self, username): - return 'Goodbye' - def __init__(self): - pass - def impersonate_user(self, username, password): - pass - def terminate_impersonation(self, username): - pass - -def on_upload(ftp_handler, filename): - source = ftp_handler.remote_ip - author = ftp_handler.username - print filename, author, source - # We pass on this for now: - # contentstore.tasks.on_upload - # It is a changing API, and it makes testing the FTP server slow. - -class Command(BaseCommand): - help = \ -''' Run FTP server.''' - def handle(self, *args, **options): - authorizer = DjangoAuthorizer() #ftpserver.DummyAuthorizer() - handler = ftpserver.FTPHandler - handler.on_file_received = on_upload - - handler.authorizer = authorizer - address = ("127.0.0.1", 2121) - ftpd = ftpserver.FTPServer(address, handler) - ftpd.serve_forever() diff --git a/cms/envs/dev.py b/cms/envs/dev.py index f7277b3d3f..332f52f145 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -38,6 +38,6 @@ CACHES = { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 'KEY_PREFIX': 'general', 'VERSION': 4, - 'KEY_FUNCTION': 'util.cache.memcache_safe_key', + 'KEY_FUNCTION': 'util.memcache.safe_key', } } diff --git a/cms/envs/test.py b/cms/envs/test.py new file mode 100644 index 0000000000..1a20d9e6f8 --- /dev/null +++ b/cms/envs/test.py @@ -0,0 +1,54 @@ +""" +This config file runs the simplest dev environment using sqlite, and db-based +sessions. Assumes structure: + +/envroot/ + /db # This is where it'll write the database file + /mitx # The location of this repo + /log # Where we're going to write log files +""" +from .common import * +import os + +# Nose Test Runner +INSTALLED_APPS += ('django_nose',) +NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive'] +for app in os.listdir(PROJECT_ROOT / 'djangoapps'): + NOSE_ARGS += ['--cover-package', app] +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +KEYSTORE = { + 'host': 'localhost', + 'db': 'mongo_base', + 'collection': 'key_store', +} + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ENV_ROOT / "db" / "mitx.db", + } +} + +CACHES = { + # This is the cache used for most things. Askbot will not work without a + # functioning cache -- it relies on caching to load its settings in places. + # In staging/prod envs, the sessions also live here. + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'mitx_loc_mem_cache', + 'KEY_FUNCTION': 'util.memcache.safe_key', + }, + + # The general cache is what you get if you use our util.cache. It's used for + # things like caching the course.xml file for different A/B test groups. + # We set it to be a DummyCache to force reloading of course.xml in dev. + # In staging environments, we would grab VERSION from data uploaded by the + # push process. + 'general': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + 'KEY_PREFIX': 'general', + 'VERSION': 4, + 'KEY_FUNCTION': 'util.memcache.safe_key', + } +} diff --git a/common/lib/cache_toolbox/COPYING b/common/djangoapps/cache_toolbox/COPYING similarity index 100% rename from common/lib/cache_toolbox/COPYING rename to common/djangoapps/cache_toolbox/COPYING diff --git a/common/lib/cache_toolbox/README.rst b/common/djangoapps/cache_toolbox/README.rst similarity index 100% rename from common/lib/cache_toolbox/README.rst rename to common/djangoapps/cache_toolbox/README.rst diff --git a/common/lib/cache_toolbox/__init__.py b/common/djangoapps/cache_toolbox/__init__.py similarity index 100% rename from common/lib/cache_toolbox/__init__.py rename to common/djangoapps/cache_toolbox/__init__.py diff --git a/common/lib/cache_toolbox/app_settings.py b/common/djangoapps/cache_toolbox/app_settings.py similarity index 100% rename from common/lib/cache_toolbox/app_settings.py rename to common/djangoapps/cache_toolbox/app_settings.py diff --git a/common/lib/cache_toolbox/core.py b/common/djangoapps/cache_toolbox/core.py similarity index 100% rename from common/lib/cache_toolbox/core.py rename to common/djangoapps/cache_toolbox/core.py diff --git a/common/lib/cache_toolbox/middleware.py b/common/djangoapps/cache_toolbox/middleware.py similarity index 100% rename from common/lib/cache_toolbox/middleware.py rename to common/djangoapps/cache_toolbox/middleware.py diff --git a/common/lib/cache_toolbox/model.py b/common/djangoapps/cache_toolbox/model.py similarity index 100% rename from common/lib/cache_toolbox/model.py rename to common/djangoapps/cache_toolbox/model.py diff --git a/common/lib/cache_toolbox/relation.py b/common/djangoapps/cache_toolbox/relation.py similarity index 100% rename from common/lib/cache_toolbox/relation.py rename to common/djangoapps/cache_toolbox/relation.py diff --git a/common/lib/cache_toolbox/templatetags/__init__.py b/common/djangoapps/cache_toolbox/templatetags/__init__.py similarity index 100% rename from common/lib/cache_toolbox/templatetags/__init__.py rename to common/djangoapps/cache_toolbox/templatetags/__init__.py diff --git a/common/lib/cache_toolbox/templatetags/cache_toolbox.py b/common/djangoapps/cache_toolbox/templatetags/cache_toolbox.py similarity index 100% rename from common/lib/cache_toolbox/templatetags/cache_toolbox.py rename to common/djangoapps/cache_toolbox/templatetags/cache_toolbox.py diff --git a/common/lib/util/__init__.py b/common/djangoapps/util/__init__.py similarity index 100% rename from common/lib/util/__init__.py rename to common/djangoapps/util/__init__.py diff --git a/common/lib/util/cache.py b/common/djangoapps/util/cache.py similarity index 100% rename from common/lib/util/cache.py rename to common/djangoapps/util/cache.py diff --git a/common/lib/util/memcache.py b/common/djangoapps/util/memcache.py similarity index 100% rename from common/lib/util/memcache.py rename to common/djangoapps/util/memcache.py diff --git a/common/lib/util/middleware.py b/common/djangoapps/util/middleware.py similarity index 100% rename from common/lib/util/middleware.py rename to common/djangoapps/util/middleware.py diff --git a/common/lib/util/models.py b/common/djangoapps/util/models.py similarity index 100% rename from common/lib/util/models.py rename to common/djangoapps/util/models.py diff --git a/common/lib/util/tests.py b/common/djangoapps/util/tests.py similarity index 100% rename from common/lib/util/tests.py rename to common/djangoapps/util/tests.py diff --git a/common/lib/util/views.py b/common/djangoapps/util/views.py similarity index 100% rename from common/lib/util/views.py rename to common/djangoapps/util/views.py diff --git a/cms/lib/keystore/__init__.py b/common/lib/keystore/__init__.py similarity index 100% rename from cms/lib/keystore/__init__.py rename to common/lib/keystore/__init__.py diff --git a/cms/lib/keystore/django.py b/common/lib/keystore/django.py similarity index 100% rename from cms/lib/keystore/django.py rename to common/lib/keystore/django.py diff --git a/cms/lib/keystore/exceptions.py b/common/lib/keystore/exceptions.py similarity index 100% rename from cms/lib/keystore/exceptions.py rename to common/lib/keystore/exceptions.py diff --git a/cms/lib/keystore/mongo.py b/common/lib/keystore/mongo.py similarity index 100% rename from cms/lib/keystore/mongo.py rename to common/lib/keystore/mongo.py diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 17e1e96f45..decd92d136 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -44,7 +44,7 @@ CACHES = { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 'KEY_PREFIX': 'general', 'VERSION': 4, - 'KEY_FUNCTION': 'util.cache.memcache_safe_key', + 'KEY_FUNCTION': 'util.memcache.safe_key', } } diff --git a/rakefile b/rakefile index 6bd582783e..43f5697bb8 100644 --- a/rakefile +++ b/rakefile @@ -66,7 +66,7 @@ end desc "Run all django tests on our djangoapps for the #{system}" task task_name => report_dir do ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") - sh(django_admin(:lms, :test, 'test', *Dir['lms/djangoapps'].each)) + sh(django_admin(system, :test, 'test', *Dir["#{system}/djangoapps/*"].each)) end task :test => task_name From 79e81d69b4be8c6d082aa25626f6f442df830ddb Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 13:41:53 -0400 Subject: [PATCH 076/252] Delay initializition of the MongoKeyStore until required --- .../contentstore/management/commands/import.py | 7 +++---- cms/djangoapps/contentstore/views.py | 2 +- common/lib/keystore/django.py | 11 ++++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 8b33f32b94..d6064e1e3d 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -160,9 +160,8 @@ class Command(BaseCommand): element_actions[e.tag](e) for k in results: - print k - keystore.create_item(k, 'Piotr Mitros') + keystore().create_item(k, 'Piotr Mitros') if 'data' in results[k]: - keystore.update_item(k, results[k]['data']) + keystore().update_item(k, results[k]['data']) if 'children' in results[k]: - keystore.update_children(k, results[k]['children']) + keystore().update_children(k, results[k]['children']) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 64bde14869..e29c41ea59 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -7,6 +7,6 @@ def index(request): # FIXME (cpennington): These need to be read in from the active user org = 'mit.edu' course = '6002xs12' - course = keystore.get_item(['i4x', org, course, 'Course', None]) + course = keystore().get_item(['i4x', org, course, 'Course', None]) weeks = course.get_children() return render_to_response('index.html', {'weeks': weeks}) diff --git a/common/lib/keystore/django.py b/common/lib/keystore/django.py index b6ffb83b5c..b88c74b8a3 100644 --- a/common/lib/keystore/django.py +++ b/common/lib/keystore/django.py @@ -9,4 +9,13 @@ from __future__ import absolute_import from django.conf import settings from .mongo import MongoKeyStore -keystore = MongoKeyStore(**settings.KEYSTORE) +_KEYSTORE = None + + +def keystore(): + global _KEYSTORE + + if _KEYSTORE is None: + _KEYSTORE = MongoKeyStore(**settings.KEYSTORE) + + return _KEYSTORE From f8434b3a6d7d29372aaddab8c62cadf3ca1fb027 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 13:48:26 -0400 Subject: [PATCH 077/252] Move libraries that can only be imported in the context of django into common/djangoapps --- common/{lib => djangoapps}/django_future/__init__.py | 0 common/{lib => djangoapps}/django_future/csrf.py | 0 common/{lib => djangoapps}/monitoring/__init__.py | 0 common/{lib => djangoapps}/monitoring/exceptions.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename common/{lib => djangoapps}/django_future/__init__.py (100%) rename common/{lib => djangoapps}/django_future/csrf.py (100%) rename common/{lib => djangoapps}/monitoring/__init__.py (100%) rename common/{lib => djangoapps}/monitoring/exceptions.py (100%) diff --git a/common/lib/django_future/__init__.py b/common/djangoapps/django_future/__init__.py similarity index 100% rename from common/lib/django_future/__init__.py rename to common/djangoapps/django_future/__init__.py diff --git a/common/lib/django_future/csrf.py b/common/djangoapps/django_future/csrf.py similarity index 100% rename from common/lib/django_future/csrf.py rename to common/djangoapps/django_future/csrf.py diff --git a/common/lib/monitoring/__init__.py b/common/djangoapps/monitoring/__init__.py similarity index 100% rename from common/lib/monitoring/__init__.py rename to common/djangoapps/monitoring/__init__.py diff --git a/common/lib/monitoring/exceptions.py b/common/djangoapps/monitoring/exceptions.py similarity index 100% rename from common/lib/monitoring/exceptions.py rename to common/djangoapps/monitoring/exceptions.py From 9ff0effd718664b0ccd29d92cb389825776470e9 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 14:08:15 -0400 Subject: [PATCH 078/252] Remove copy/paste instructor module. It was a bad idea anyway. =) --- cms/djangoapps/instructor/__init__.py | 0 cms/djangoapps/instructor/models.py | 61 --------------------------- cms/djangoapps/instructor/tests.py | 16 ------- cms/djangoapps/instructor/views.py | 49 --------------------- 4 files changed, 126 deletions(-) delete mode 100644 cms/djangoapps/instructor/__init__.py delete mode 100644 cms/djangoapps/instructor/models.py delete mode 100644 cms/djangoapps/instructor/tests.py delete mode 100644 cms/djangoapps/instructor/views.py diff --git a/cms/djangoapps/instructor/__init__.py b/cms/djangoapps/instructor/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cms/djangoapps/instructor/models.py b/cms/djangoapps/instructor/models.py deleted file mode 100644 index 906aeee2f1..0000000000 --- a/cms/djangoapps/instructor/models.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -WE'RE USING MIGRATIONS! - -If you make changes to this model, be sure to create an appropriate migration -file and check it in at the same time as your model changes. To do that, - -1. Go to the mitx dir -2. ./manage.py schemamigration user --auto description_of_your_change -3. Add the migration file created in mitx/courseware/migrations/ -""" -import uuid - -from django.db import models -from django.contrib.auth.models import User - - -class UserProfile(models.Model): - class Meta: - db_table = "auth_userprofile" - - ## CRITICAL TODO/SECURITY - # Sanitize all fields. - # This is not visible to other users, but could introduce holes later - user = models.OneToOneField(User, unique=True, db_index=True, related_name='profile') - name = models.CharField(blank=True, max_length=255, db_index=True) - org = models.CharField(blank=True, max_length=255, db_index=True) - - -class Registration(models.Model): - ''' Allows us to wait for e-mail before user is registered. A - registration profile is created when the user creates an - account, but that account is inactive. Once the user clicks - on the activation key, it becomes active. ''' - class Meta: - db_table = "auth_registration" - - user = models.ForeignKey(User, unique=True) - activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) - - def register(self, user): - # MINOR TODO: Switch to crypto-secure key - self.activation_key = uuid.uuid4().hex - self.user = user - self.save() - - def activate(self): - self.user.is_active = True - self.user.save() - #self.delete() - - -class PendingNameChange(models.Model): - user = models.OneToOneField(User, unique=True, db_index=True) - new_name = models.CharField(blank=True, max_length=255) - rationale = models.CharField(blank=True, max_length=1024) - - -class PendingEmailChange(models.Model): - user = models.OneToOneField(User, unique=True, db_index=True) - new_email = models.CharField(blank=True, max_length=255, db_index=True) - activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True) diff --git a/cms/djangoapps/instructor/tests.py b/cms/djangoapps/instructor/tests.py deleted file mode 100644 index 501deb776c..0000000000 --- a/cms/djangoapps/instructor/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/cms/djangoapps/instructor/views.py b/cms/djangoapps/instructor/views.py deleted file mode 100644 index fbb341b468..0000000000 --- a/cms/djangoapps/instructor/views.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging - -from django.views.decorators.http import require_http_methods, require_POST, require_GET -from django.contrib.auth import logout, authenticate, login -from django.shortcuts import redirect -from mitxmako.shortcuts import render_to_response - -from django_future.csrf import ensure_csrf_cookie - -log = logging.getLogger("mitx.student") - - -@require_http_methods(['GET', 'POST']) -def do_login(request): - if request.method == 'POST': - return post_login(request) - elif request.method == 'GET': - return get_login(request) - - -@require_POST -@ensure_csrf_cookie -def post_login(request): - username = request.POST['username'] - password = request.POST['password'] - user = authenticate(username=username, password=password) - if user is not None: - if user.is_active: - login(request, user) - return redirect(request.POST.get('next', '/')) - else: - raise Exception("Can't log in, account disabled") - else: - raise Exception("Can't log in, invalid authentication") - - -@require_GET -@ensure_csrf_cookie -def get_login(request): - return render_to_response('login.html', { - 'next': request.GET.get('next') - }) - - -@ensure_csrf_cookie -def logout_user(request): - ''' HTTP request to log in the user. Redirects to marketing page''' - logout(request) - return redirect('/') From 605b1ae0fd16bc3fec0fd4becb321f79f7644dab Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 18 Jun 2012 14:24:45 -0400 Subject: [PATCH 079/252] Remove old template that is no longer relevant --- cms/templates/calendar.html | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 cms/templates/calendar.html diff --git a/cms/templates/calendar.html b/cms/templates/calendar.html deleted file mode 100644 index 05b2f88806..0000000000 --- a/cms/templates/calendar.html +++ /dev/null @@ -1,3 +0,0 @@ -% for week in weeks: -${week} -% endfor From 8bad7dc99d004a3671416d6ed5edd7d4033a4f2c Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 18 Jun 2012 17:06:17 -0400 Subject: [PATCH 080/252] Code standards --- doc/code_standards.txt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/code_standards.txt b/doc/code_standards.txt index 02953b3677..06be87fd6a 100644 --- a/doc/code_standards.txt +++ b/doc/code_standards.txt @@ -75,7 +75,21 @@ no hard standards. review it (this may change as the team grows). * Each contributor is responsible for finding a person to review their code. If it is not clear to the contributor who is appropriate, each - project has an owner + project has an owner who is the default go-to. + +2.1 Rapid pull + +Unmerged code can lead to merge conflicts, and slow down +development. We have an experimental procedure for handling rapid +pulls and merges. To qualify: + +* A piece of code must only have minor issues remaining (nothing which +we would be uncomfortable placing on a server). +* Either the requester or the puller takes ownership for guaranteeing +that those issues are resolved within a short timeframe. +* Both the requester and the puller must be comfortable with it. + +If code qualified, it can be merged, and repaired in master. 3. Documentation Standards From e108c2cd4421b5b20cfd6ebcbeb8d2e1c2fa5aaa Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 18 Jun 2012 17:09:17 -0400 Subject: [PATCH 081/252] Rapid pull/history --- doc/code_standards.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/code_standards.txt b/doc/code_standards.txt index 06be87fd6a..33ec4cecab 100644 --- a/doc/code_standards.txt +++ b/doc/code_standards.txt @@ -88,6 +88,8 @@ we would be uncomfortable placing on a server). * Either the requester or the puller takes ownership for guaranteeing that those issues are resolved within a short timeframe. * Both the requester and the puller must be comfortable with it. +* Both the requester and the owner must have a history of/ability to +resolve remaining issues quickly. If code qualified, it can be merged, and repaired in master. From 25834412ce23e2c5cd77bea4001f1df5ee798cf0 Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Mon, 18 Jun 2012 17:40:39 -0400 Subject: [PATCH 082/252] Documentation for rapid merge --- doc/code_standards.txt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/code_standards.txt b/doc/code_standards.txt index 33ec4cecab..a4833b8d36 100644 --- a/doc/code_standards.txt +++ b/doc/code_standards.txt @@ -84,14 +84,22 @@ development. We have an experimental procedure for handling rapid pulls and merges. To qualify: * A piece of code must only have minor issues remaining (nothing which -we would be uncomfortable placing on a server). + we would be uncomfortable placing on a server). * Either the requester or the puller takes ownership for guaranteeing -that those issues are resolved within a short timeframe. + that those issues are resolved within a short timeframe. * Both the requester and the puller must be comfortable with it. * Both the requester and the owner must have a history of/ability to -resolve remaining issues quickly. + resolve remaining issues quickly. -If code qualified, it can be merged, and repaired in master. +If code qualifies: +* It can be merged, and repaired in master. +* The pull message should specify '## pending fixes/OWNER' where ## is + the pull request number, and OWNER is the owner. +* All required fixes are documented in github in the (now closed) pull + request, and should be marked off there when applied (potentially, + directly to master). +* Once all fixes are applied, the final commit should specify + '## closed'. 3. Documentation Standards From 199f94aa993b6dd26df051becd38c6290dcb3634 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:24:22 -0400 Subject: [PATCH 083/252] Conform to new TODO standards --- cms/djangoapps/contentstore/views.py | 2 +- doc/code_standards.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index e29c41ea59..ad846fb369 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -4,7 +4,7 @@ from django.contrib.auth.decorators import login_required def index(request): - # FIXME (cpennington): These need to be read in from the active user + # TODO (cpennington): These need to be read in from the active user org = 'mit.edu' course = '6002xs12' course = keystore().get_item(['i4x', org, course, 'Course', None]) diff --git a/doc/code_standards.txt b/doc/code_standards.txt index 02953b3677..a6a3b3e556 100644 --- a/doc/code_standards.txt +++ b/doc/code_standards.txt @@ -83,3 +83,6 @@ no hard standards. * When impossible, it should live in the github repo. * Discussion should live on github, Basecamp or Pivotal, depending on context. +* Notes for later fixes should in general be put into Pivotal as stories. + If they are left in the code, they should be prefixed by + # TODO () From 0379d1cb9577c181e186cc709622f79690cf76fe Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:25:29 -0400 Subject: [PATCH 084/252] Search for course specifically in navigation view --- cms/djangoapps/contentstore/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index ad846fb369..a87520ab13 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -7,6 +7,7 @@ def index(request): # TODO (cpennington): These need to be read in from the active user org = 'mit.edu' course = '6002xs12' - course = keystore().get_item(['i4x', org, course, 'Course', None]) + name = '6.002 Spring 2012' + course = keystore().get_item(['i4x', org, course, 'Course', name]) weeks = course.get_children() return render_to_response('index.html', {'weeks': weeks}) From 703103e7675dba223e8dd883945bd3f5d2ab44ba Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:25:41 -0400 Subject: [PATCH 085/252] Get rid of references to askbot --- cms/envs/common.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 8d402b6fa9..80056af1d2 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -27,15 +27,12 @@ from path import path PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms COMMON_ROOT = PROJECT_ROOT.dirname() / "common" ENV_ROOT = PROJECT_ROOT.dirname().dirname() # virtualenv dir /mitx is in -ASKBOT_ROOT = ENV_ROOT / "askbot-devel" COURSES_ROOT = ENV_ROOT / "data" # FIXME: To support multiple courses, we should walk the courses dir at startup DATA_DIR = COURSES_ROOT sys.path.append(ENV_ROOT) -sys.path.append(ASKBOT_ROOT) -sys.path.append(ASKBOT_ROOT / "askbot" / "deps") sys.path.append(PROJECT_ROOT / 'djangoapps') sys.path.append(PROJECT_ROOT / 'lib') sys.path.append(COMMON_ROOT / 'djangoapps') From f1ffff1dc0dba30bb8ed2d509107117a5375a728 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:27:29 -0400 Subject: [PATCH 086/252] Cleanup and test Location, and add the ability to specify a revision --- common/lib/keystore/__init__.py | 108 ++++++++++++++++++---- common/lib/keystore/exceptions.py | 3 + common/lib/keystore/test/test_location.py | 52 +++++++++++ 3 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 common/lib/keystore/test/test_location.py diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 61c797241d..592cde7b4d 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -10,47 +10,117 @@ the following attributes: revision: What revision of the item this is """ +import re +from .exceptions import InvalidLocationError + +URL_RE = re.compile(""" + (?P[^:]+):// + (?P[^/]+)/ + (?P[^/]+)/ + (?P[^/]+)/ + (?P[^/]+) + (/(?P[^/]+))? + """, re.VERBOSE) + class Location(object): - ''' Encodes a location. - Can be: - * String (url) - * Tuple - * Dictionary + ''' + Encodes a location. + + Locations representations of URLs of the + form {tag}://{org}/{course}/{category}/{name}[/{revision}] + + However, they can also be represented a dictionaries (specifying each component), + tuples or list (specified in order), or as strings of the url ''' def __init__(self, location): + """ + Create a new location that is a clone of the specifed one. + + location - Can be any of the following types: + string: should be of the form {tag}://{org}/{course}/{category}/{name}[/{revision}] + list: should be of the form [tag, org, course, category, name, revision] + dict: should be of the form { + 'tag': tag, + 'org': org, + 'course': course, + 'category': category, + 'name': name, + 'revision': revision, + } + Location: another Location object + + None of the components of a location may contain the '/' character + """ self.update(location) def update(self, location): + """ + Update this instance with data from another Location object. + + location: can take the same forms as specified by `__init__` + """ + self.tag = self.org = self.course = self.category = self.name = self.revision = None + if isinstance(location, basestring): - self.tag = location.split('/')[0][:-1] - (self.org, self.course, self.category, self.name) = location.split('/')[2:] + match = URL_RE.match(location) + if match is None: + raise InvalidLocationError(location) + else: + self.update(match.groupdict()) elif isinstance(location, list): - (self.tag, self.org, self.course, self.category, self.name) = location + if len(location) not in (5, 6): + raise InvalidLocationError(location) + + (self.tag, self.org, self.course, self.category, self.name) = location[0:5] + self.revision = location[5] if len(location) == 6 else None elif isinstance(location, dict): - self.tag = location['tag'] - self.org = location['org'] - self.course = location['course'] - self.category = location['category'] - self.name = location['name'] + try: + self.tag = location['tag'] + self.org = location['org'] + self.course = location['course'] + self.category = location['category'] + self.name = location['name'] + except KeyError: + raise InvalidLocationError(location) + self.revision = location.get('revision') elif isinstance(location, Location): self.update(location.list()) + else: + raise InvalidLocationError(location) + + for val in self.list(): + if val is not None and '/' in val: + raise InvalidLocationError(location) + + def __str__(self): + return self.url() def url(self): - return "{tag}://{org}/{course}/{category}/{name}".format(**self.dict()) + """ + Return a string containing the URL for this location + """ + url = "{tag}://{org}/{course}/{category}/{name}".format(**self.dict()) + if self.revision: + url += "/" + self.revision + return url def list(self): - return [self.tag, self.org, self.course, self.category, self.name] + """ + Return a list representing this location + """ + return [self.tag, self.org, self.course, self.category, self.name, self.revision] def dict(self): + """ + Return a dictionary representing this location + """ return {'tag': self.tag, 'org': self.org, 'course': self.course, 'category': self.category, - 'name': self.name} - - def to_json(self): - return self.dict() + 'name': self.name, + 'revision': self.revision} class KeyStore(object): diff --git a/common/lib/keystore/exceptions.py b/common/lib/keystore/exceptions.py index 08fd9b11d0..4c8c55ffe9 100644 --- a/common/lib/keystore/exceptions.py +++ b/common/lib/keystore/exceptions.py @@ -9,3 +9,6 @@ class ItemNotFoundError(Exception): class InsufficientSpecificationError(Exception): pass + +class InvalidLocationError(Exception): + pass diff --git a/common/lib/keystore/test/test_location.py b/common/lib/keystore/test/test_location.py new file mode 100644 index 0000000000..f10f03c0b0 --- /dev/null +++ b/common/lib/keystore/test/test_location.py @@ -0,0 +1,52 @@ +from nose.tools import assert_equals, assert_raises +from keystore import Location +from keystore.exceptions import InvalidLocationError + + +def check_string_roundtrip(url): + assert_equals(url, Location(url).url()) + assert_equals(url, str(Location(url))) + + +def test_string_roundtrip(): + check_string_roundtrip("tag://org/course/category/name") + check_string_roundtrip("tag://org/course/category/name/revision") + check_string_roundtrip("tag://org/course/category/name with spaces/revision") + + +def test_dict(): + input_dict = { + 'tag': 'tag', + 'course': 'course', + 'category': 'category', + 'name': 'name', + 'org': 'org' + } + assert_equals("tag://org/course/category/name", Location(input_dict).url()) + assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict()) + + input_dict['revision'] = 'revision' + assert_equals("tag://org/course/category/name/revision", Location(input_dict).url()) + assert_equals(input_dict, Location(input_dict).dict()) + + +def test_list(): + input_list = ['tag', 'org', 'course', 'category', 'name'] + assert_equals("tag://org/course/category/name", Location(input_list).url()) + assert_equals(input_list + [None], Location(input_list).list()) + + input_list.append('revision') + assert_equals("tag://org/course/category/name/revision", Location(input_list).url()) + assert_equals(input_list, Location(input_list).list()) + + +def test_location(): + input_list = ['tag', 'org', 'course', 'category', 'name'] + assert_equals("tag://org/course/category/name", Location(Location(input_list)).url()) + + +def test_invalid_locations(): + assert_raises(InvalidLocationError, Location, "foo") + assert_raises(InvalidLocationError, Location, ["foo", "bar"]) + assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"]) + assert_raises(InvalidLocationError, Location, None) From 6daa0f1aa06092598892cb37a7f3c5f3541fa0c2 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:27:44 -0400 Subject: [PATCH 087/252] Fix string layout for readability --- cms/manage.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cms/manage.py b/cms/manage.py index 3e4eedc9ff..f8773c0641 100644 --- a/cms/manage.py +++ b/cms/manage.py @@ -5,7 +5,9 @@ try: imp.find_module('settings') # Assumed to be in the same directory. except ImportError: import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. " + "It appears you've customized things.\nYou'll have to run django-admin.py, " + "passing it your settings module.\n" % __file__) sys.exit(1) import settings From 58085f8ed98c59a2d1569099e833f5002c3e5f5d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:27:56 -0400 Subject: [PATCH 088/252] Remove unused urls --- cms/urls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cms/urls.py b/cms/urls.py index 781c2c261f..d2e6415827 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -5,7 +5,5 @@ from django.conf.urls.defaults import patterns, url # admin.autodiscover() urlpatterns = patterns('', - url(r'^(?P[^/]+)/(?P[^/]+)/calendar/', 'contentstore.views.calendar', name='calendar'), - url(r'^accounts/login/', 'instructor.views.do_login', name='login'), url(r'^$', 'contentstore.views.index', name='index'), ) From 677c25ee687167430b0fbc1e88cbe6d6f6173a26 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:28:22 -0400 Subject: [PATCH 089/252] Remove unused code --- common/lib/keystore/__init__.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 592cde7b4d..f5ca6f4164 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -165,19 +165,3 @@ class KeyStore(object): children: A list of child item identifiers """ raise NotImplementedError - - -class KeyStoreItem(object): - """ - An object from a KeyStore, which can be saved back to that keystore - """ - def __init__(self, location, children, data, editor, parents, revision): - self.location = location - self.children = children - self.data = data - self.editor = editor - self.parents = parents - self.revision = revision - - def save(self): - raise NotImplementedError From 5562def5b7137be944fdbc93bcd7e7b32ea0fbf2 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:29:48 -0400 Subject: [PATCH 090/252] Add documentation of mongo query syntax usage --- common/lib/keystore/mongo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index d9760909c9..9b6327c8e9 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -67,6 +67,9 @@ class MongoKeyStore(KeyStore): location: Something that can be passed to Location data: A nested dictionary of problem data """ + + # See http://www.mongodb.org/display/DOCS/Updating for + # atomic update syntax self.collection.update( {'location': Location(location).dict()}, {'$set': {'data': data}} @@ -80,6 +83,9 @@ class MongoKeyStore(KeyStore): location: Something that can be passed to Location children: A list of child item identifiers """ + + # See http://www.mongodb.org/display/DOCS/Updating for + # atomic update syntax self.collection.update( {'location': Location(location).dict()}, {'$set': {'children': children}} From 6fb35c4773db0b5920a51cdd5f0b6d6eb316b01f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:31:13 -0400 Subject: [PATCH 091/252] Cleanup intertwined descriptor and keystore code --- cms/envs/dev.py | 8 +-- cms/templates/widgets/navigation.html | 12 +++-- common/lib/keystore/__init__.py | 32 +++++------- common/lib/keystore/django.py | 16 +++--- common/lib/keystore/mongo.py | 47 +++++++++-------- common/lib/xmodule/seq_module.py | 17 ------- common/lib/xmodule/setup.py | 5 +- common/lib/xmodule/x_module.py | 72 +++++++++++++++++++++------ doc/overview.md | 34 +++++++++++-- 9 files changed, 147 insertions(+), 96 deletions(-) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 332f52f145..16bed60729 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -7,9 +7,11 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG KEYSTORE = { - 'host': 'localhost', - 'db': 'mongo_base', - 'collection': 'key_store', + 'default': { + 'host': 'localhost', + 'db': 'mongo_base', + 'collection': 'key_store', + } } DATABASES = { diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 1f75dab470..2d5af9ead1 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -40,14 +40,18 @@

      ${week.name}

        - % for goal in week.get_goals(): -
      • ${goal.name}:${goal.data}
      • - % endfor + % if week.goals: + % for goal in week.goals: +
      • ${goal}
      • + % endfor + % else: +
      • Please create a learning goal for this week
      • + % endif
        - % for module in week.get_non_goals(): + % for module in week.get_children():
      • ${module.name} handle diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index f5ca6f4164..0e77a02a87 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -1,13 +1,6 @@ """ -This module provides an abstraction for working objects that conceptually have -the following attributes: - - location: An identifier for an item, of which there might be many revisions - children: A list of urls for other items required to fully define this object - data: A set of nested data needed to define this object - editor: The editor/owner of the object - parents: Url pointers for objects that this object was derived from - revision: What revision of the item this is +This module provides an abstraction for working with XModuleDescriptors +that are stored in a database an accessible using their Location as an identifier """ import re @@ -123,27 +116,26 @@ class Location(object): 'revision': self.revision} -class KeyStore(object): +class ModuleStore(object): + """ + An abstract interface for a database backend that stores XModuleDescriptor instances + """ def get_item(self, location): """ - Returns an XModuleDescriptor instance for the item at location + Returns an XModuleDescriptor instance for the item at location. + If location.revision is None, returns the most item with the most + recent revision + If any segment of the location is None except revision, raises + keystore.exceptions.InsufficientSpecificationError If no object is found at that location, raises keystore.exceptions.ItemNotFoundError - Searches for all matches of a partially specifed location, but raises an - keystore.exceptions.InsufficientSpecificationError if more - than a single object matches the query. - location: Something that can be passed to Location """ raise NotImplementedError + # TODO (cpennington): Replace with clone_item def create_item(self, location, editor): - """ - Create an empty item at the specified location with the supplied editor - - location: Something that can be passed to Location - """ raise NotImplementedError def update_item(self, location, data): diff --git a/common/lib/keystore/django.py b/common/lib/keystore/django.py index b88c74b8a3..2ba3f0756e 100644 --- a/common/lib/keystore/django.py +++ b/common/lib/keystore/django.py @@ -1,21 +1,21 @@ """ Module that provides a connection to the keystore specified in the django settings. -Passes settings.KEYSTORE as kwargs to MongoKeyStore +Passes settings.KEYSTORE as kwargs to MongoModuleStore """ from __future__ import absolute_import from django.conf import settings -from .mongo import MongoKeyStore +from .mongo import MongoModuleStore -_KEYSTORE = None +_KEYSTORES = {} -def keystore(): - global _KEYSTORE +def keystore(name='default'): + global _KEYSTORES - if _KEYSTORE is None: - _KEYSTORE = MongoKeyStore(**settings.KEYSTORE) + if name not in _KEYSTORES: + _KEYSTORES[name] = MongoModuleStore(**settings.KEYSTORE[name]) - return _KEYSTORE + return _KEYSTORES[name] diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index 9b6327c8e9..1bef298fde 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -1,12 +1,12 @@ import pymongo -from . import KeyStore, Location +from . import ModuleStore, Location from .exceptions import ItemNotFoundError, InsufficientSpecificationError -from xmodule.x_module import XModuleDescriptor +from xmodule.x_module import XModuleDescriptor, XModuleSystem -class MongoKeyStore(KeyStore): +class MongoModuleStore(ModuleStore): """ - A Mongodb backed KeyStore + A Mongodb backed ModuleStore """ def __init__(self, host, db, collection, port=27017): self.collection = pymongo.connection.Connection( @@ -19,34 +19,33 @@ class MongoKeyStore(KeyStore): def get_item(self, location): """ - Returns an XModuleDescriptor instance for the item at location + Returns an XModuleDescriptor instance for the item at location. + If location.revision is None, returns the most item with the most + recent revision + If any segment of the location is None except revision, raises + keystore.exceptions.InsufficientSpecificationError If no object is found at that location, raises keystore.exceptions.ItemNotFoundError - Searches for all matches of a partially specifed location, but raises an - keystore.exceptions.InsufficientSpecificationError if more - than a single object matches the query. - location: Something that can be passed to Location """ - query = dict( - ('location.{key}'.format(key=key), val) - for (key, val) - in Location(location).dict().items() - if val is not None - ) - items = self.collection.find( + + query = {} + for key, val in Location(location).dict().iteritems(): + if key != 'revision' and val is None: + raise InsufficientSpecificationError(location) + + if val is not None: + query['location.{key}'.format(key=key)] = val + + item = self.collection.find_one( query, sort=[('revision', pymongo.ASCENDING)], - limit=1, ) - if items.count() > 1: - raise InsufficientSpecificationError(location) - - if items.count() == 0: + if item is None: raise ItemNotFoundError(location) - return XModuleDescriptor.load_from_json(items[0], self.get_item) + return XModuleDescriptor.load_from_json(item, XModuleSystem(self.get_item)) def create_item(self, location, editor): """ @@ -72,7 +71,7 @@ class MongoKeyStore(KeyStore): # atomic update syntax self.collection.update( {'location': Location(location).dict()}, - {'$set': {'data': data}} + {'$set': {'definition.data': data}} ) def update_children(self, location, children): @@ -88,5 +87,5 @@ class MongoKeyStore(KeyStore): # atomic update syntax self.collection.update( {'location': Location(location).dict()}, - {'$set': {'children': children}} + {'$set': {'definition.children': children}} ) diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 91ff6d2671..b394227aa7 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -97,22 +97,5 @@ class Module(XModule): self.rendered = False -class WeekDescriptor(XModuleDescriptor): - - def get_goals(self): - """ - Return a list of Goal XModuleDescriptors that are children - of this Week - """ - return [child for child in self.get_children() if child.type == 'Goal'] - - def get_non_goals(self): - """ - Return a list of non-Goal XModuleDescriptors that are children of - this Week - """ - return [child for child in self.get_children() if child.type != 'Goal'] - - class SectionDescriptor(XModuleDescriptor): pass diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 1140037259..7f3370ed37 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -5,10 +5,13 @@ setup( version="0.1", packages=find_packages(), install_requires=['distribute'], + + # See http://guide.python-distribute.org/creation.html#entry-points + # for a description of entry_points entry_points={ 'xmodule.v1': [ "Course = seq_module:SectionDescriptor", - "Week = seq_module:WeekDescriptor", + "Week = seq_module:SectionDescriptor", "Section = seq_module:SectionDescriptor", "LectureSequence = seq_module:SectionDescriptor", "Lab = seq_module:SectionDescriptor", diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 3560eecbdc..9f960843d9 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -21,7 +21,7 @@ class Plugin(object): log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( entry_point=cls.entry_point, id=identifier, - classes=", ".join([class_.module_name for class_ in classes]))) + classes=", ".join(class_.module_name for class_ in classes))) if len(classes) == 0: raise ModuleMissingError(identifier) @@ -125,47 +125,82 @@ class XModule(object): class XModuleDescriptor(Plugin): + """ + An XModuleDescriptor is a specification for an element of a course. This could + be a problem, an organizational element (a group of content), or a segment of video, + for example. + XModuleDescriptors are independent and agnostic to the current student state on a + problem. They handle the editing interface used by instructors to create a problem, + and can generate XModules (which do know about student state). + """ entry_point = "xmodule.v1" @staticmethod - def load_from_json(json_data, load_item): + def load_from_json(json_data, system): + """ + This method instantiates the correct subclass of XModuleDescriptor based + on the contents of json_data. + + json_data must contain a 'location' element, and must be suitable to be + passed into the subclasses `from_json` method. + """ class_ = XModuleDescriptor.load_class(json_data['location']['category']) - return class_.from_json(json_data, load_item) + return class_.from_json(json_data, system) @classmethod - def from_json(cls, json_data, load_item): + def from_json(cls, json_data, system): """ Creates an instance of this descriptor from the supplied json_data. + This may be overridden by subclasses json_data: Json data specifying the data, children, and metadata for the descriptor - load_item: A function that takes an i4x url and returns a module descriptor + system: An XModuleSystem for interacting with external resources """ - return cls(load_item=load_item, **json_data) + return cls(system=system, **json_data) def __init__(self, - load_item, - data=None, - children=None, + system, + definition=None, **kwargs): - self.load_item = load_item - self.data = data if data is not None else {} - self.children = children if children is not None else [] + """ + Construct a new XModuleDescriptor. The only required arguments are the + system, used for interaction with external resources, and the definition, + which specifies all the data needed to edit and display the problem (but none + of the associated metadata that handles recordkeeping around the problem). + + This allows for maximal flexibility to add to the interface while preserving + backwards compatibility. + + system: An XModuleSystem for interacting with external resources + definition: A dict containing `data` and `children` representing the problem definition + + Current arguments passed in kwargs: + location: A keystore.Location object indicating the name and ownership of this problem + goals: A list of strings of learning goals associated with this module + """ + self.system = system + self.definition = definition if definition is not None else {} self.name = Location(kwargs.get('location')).name self.type = Location(kwargs.get('location')).category + + # For now, we represent goals as a list of strings, but this + # is one of the things that we are going to be iterating on heavily + # to find the best teaching method + self.goals = kwargs.get('goals', []) + self._child_instances = None def get_children(self, categories=None): """Returns a list of XModuleDescriptor instances for the children of this module""" if self._child_instances is None: - self._child_instances = [self.load_item(child) for child in self.children] + self._child_instances = [self.system.load_item(child) for child in self.definition['children']] if categories is None: return self._child_instances else: return [child for child in self._child_instances if child.type in categories] - def get_xml(self): ''' For conversions between JSON and legacy XML representations. ''' @@ -192,3 +227,12 @@ class XModuleDescriptor(Plugin): # Full ==> what we edit # ''' # raise NotImplementedError + + +class DescriptorSystem(object): + def __init__(self, load_item): + """ + load_item: Takes a Location and returns and XModuleDescriptor + """ + + self.load_item = load_item diff --git a/doc/overview.md b/doc/overview.md index 304d5161b0..6d187dca91 100644 --- a/doc/overview.md +++ b/doc/overview.md @@ -44,10 +44,27 @@ You should be familiar with the following. If you're not, go read some docs... ### Common libraries -- x_modules -- generic learning modules. *x* can be sequence, video, template, html, vertical, capa, etc. These are the things that one puts inside sections in the course structure. Modules know how to render themselves to html, how to score themselves, and handle ajax calls from the front end. - - x_modules take a 'system context' parameter, which helps isolate xmodules from any particular application, so they can be used in many places. The modules should make no references to Django (though there are still a few left). The system context knows how to render things, track events, complain about 404s, etc. - - TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here) - - in `common/lib/xmodule` +- xmodule: generic learning modules. *x* can be sequence, video, template, html, + vertical, capa, etc. These are the things that one puts inside sections + in the course structure. + + - XModuleDescriptor: This defines the problem and all data and UI needed to edit + that problem. It is unaware of any student data, but can be used to retrieve + an XModule, which is aware of that student state. + + - XModule: The XModule is a problem instance that is particular to a student. It knows + how to render itself to html to display the problem, how to score itself, + and how to handle ajax calls from the front end. + + - Both XModule and XModuleDescriptor take system context parameters. These are named + ModuleSystem and DescriptorSystem respectively. These help isolate the XModules + from any interactions with external resources that they require. + + For instance, the DescriptorSystem has a function to load an XModuleDescriptor + from a Location object, and the ModuleSystem knows how to render things, + track events, and complain about 404s + - TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here) + - in `common/lib/xmodule` - capa modules -- defines `LoncapaProblem` and many related things. - in `common/lib/capa` @@ -76,7 +93,14 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro - See `lms/urls.py` for the wirings of urls to views. -- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`. +- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`. + +### CMS + +The CMS is a django site, with root in `cms`. It can run in a number of different +environments, defined in `cms/envs`. + +- Core rendering path: Still TBD ### Other modules From 7914baccaa660617b1bf306d94c8cfbe4b848beb Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:36:22 -0400 Subject: [PATCH 092/252] Change name of XModuleSystem to DescriptorSystem at usage sites --- common/lib/keystore/mongo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index 1bef298fde..29115a33a7 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -1,7 +1,7 @@ import pymongo from . import ModuleStore, Location from .exceptions import ItemNotFoundError, InsufficientSpecificationError -from xmodule.x_module import XModuleDescriptor, XModuleSystem +from xmodule.x_module import XModuleDescriptor, DescriptorSystem class MongoModuleStore(ModuleStore): @@ -45,7 +45,7 @@ class MongoModuleStore(ModuleStore): if item is None: raise ItemNotFoundError(location) - return XModuleDescriptor.load_from_json(item, XModuleSystem(self.get_item)) + return XModuleDescriptor.load_from_json(item, DescriptorSystem(self.get_item)) def create_item(self, location, editor): """ From b91a1d48b6ef79c6ee163025786a135019a4cbda Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 11:44:54 -0400 Subject: [PATCH 093/252] Remove reference to instructor module --- cms/envs/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 80056af1d2..c3c8ee85a8 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -142,5 +142,4 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'contentstore', - 'instructor', ) From 47bf71ae10ad3c8814debe661768972cbbf94f05 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 14:17:05 -0400 Subject: [PATCH 094/252] Remove extra word in doc string --- common/lib/keystore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 0e77a02a87..fc06a4d780 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -123,7 +123,7 @@ class ModuleStore(object): def get_item(self, location): """ Returns an XModuleDescriptor instance for the item at location. - If location.revision is None, returns the most item with the most + If location.revision is None, returns the item with the most recent revision If any segment of the location is None except revision, raises From 282372736d7869b601fd11a9d88726fd819365fc Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 14:24:14 -0400 Subject: [PATCH 095/252] Add comment about None in Locations --- common/lib/keystore/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index fc06a4d780..2605424517 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -44,6 +44,9 @@ class Location(object): Location: another Location object None of the components of a location may contain the '/' character + + Components may be set to None, which may be interpreted by some contexts to mean + wildcard selection """ self.update(location) From cac53cf1a06989da761a07751d3f06453d146384 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 14:12:03 -0400 Subject: [PATCH 096/252] Switch the cms over to using django-pipeline --- cms/envs/common.py | 63 +++++++++++++++++-- cms/models.py | 0 cms/static/sass/.gitignore | 1 + cms/templates/base.html | 6 ++ .../djangoapps/pipeline_mako/__init__.py | 6 +- .../pipeline_mako/templates/mako}/css.html | 0 .../templates/mako}/inline_js.html | 0 .../pipeline_mako/templates/mako}/js.html | 0 .../templates/static_content.html | 0 .../djangoapps}/static_replace.py | 0 lms/envs/common.py | 2 + requirements.txt | 2 +- 12 files changed, 72 insertions(+), 8 deletions(-) delete mode 100644 cms/models.py create mode 100644 cms/static/sass/.gitignore rename lms/lib/pipeline_mako.py => common/djangoapps/pipeline_mako/__init__.py (91%) rename {lms/templates/pipeline_mako => common/djangoapps/pipeline_mako/templates/mako}/css.html (100%) rename {lms/templates/pipeline_mako => common/djangoapps/pipeline_mako/templates/mako}/inline_js.html (100%) rename {lms/templates/pipeline_mako => common/djangoapps/pipeline_mako/templates/mako}/js.html (100%) rename {lms => common/djangoapps/pipeline_mako}/templates/static_content.html (100%) rename {lms/lib => common/djangoapps}/static_replace.py (100%) diff --git a/cms/envs/common.py b/cms/envs/common.py index c3c8ee85a8..20d49b7ac5 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -23,6 +23,12 @@ import sys import tempfile from path import path +############################ FEATURE CONFIGURATION ############################# + +MITX_FEATURES = { + 'USE_DJANGO_PIPELINE': True, +} + ############################# SET PATH INFORMATION ############################# PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms COMMON_ROOT = PROJECT_ROOT.dirname() / "common" @@ -43,7 +49,10 @@ sys.path.append(COMMON_ROOT / 'lib') # This is where we stick our compiled template files. MAKO_MODULE_DIR = tempfile.mkdtemp('mako') MAKO_TEMPLATES = {} -MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates'] +MAKO_TEMPLATES['main'] = [ + PROJECT_ROOT / 'templates', + COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates' +] MITX_ROOT_URL = '' @@ -59,8 +68,8 @@ TEMPLATE_CONTEXT_PROCESSORS = ( # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'staticfiles.finders.FileSystemFinder', + 'staticfiles.finders.AppDirectoriesFinder', ) # List of callables that know how to import templates from various sources. @@ -132,14 +141,60 @@ USE_L10N = True # Messages MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' +############################### Pipeline ####################################### + +STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' + +PIPELINE_CSS = { + 'base-style': { + 'source_filenames': ['sass/base-style.scss'], + 'output_filename': 'css/base-style.css', + }, +} + +PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] + +PIPELINE_JS = { +} + +PIPELINE_COMPILERS = [ + 'pipeline.compilers.sass.SASSCompiler', + 'pipeline.compilers.coffee.CoffeeScriptCompiler', +] + +PIPELINE_SASS_ARGUMENTS = '-t compressed -r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT) + +PIPELINE_CSS_COMPRESSOR = None +PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.yui.YUICompressor' + +STATICFILES_IGNORE_PATTERNS = ( + "sass/*", + "coffee/*", + "*.py", + "*.pyc" +) + +PIPELINE_YUI_BINARY = 'yui-compressor' +PIPELINE_SASS_BINARY = 'sass' +PIPELINE_COFFEE_SCRIPT_BINARY = 'coffee' + +# Setting that will only affect the MITx version of django-pipeline until our changes are merged upstream +PIPELINE_COMPILE_INPLACE = True + ############################ APPS ##################################### INSTALLED_APPS = ( + # Standard apps 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', - 'django.contrib.staticfiles', + + # For CMS 'contentstore', + + # For asset pipelining + 'pipeline', + 'staticfiles', ) diff --git a/cms/models.py b/cms/models.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cms/static/sass/.gitignore b/cms/static/sass/.gitignore new file mode 100644 index 0000000000..b3a5267117 --- /dev/null +++ b/cms/static/sass/.gitignore @@ -0,0 +1 @@ +*.css diff --git a/cms/templates/base.html b/cms/templates/base.html index a23a31d9a5..271f73614d 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -1,10 +1,16 @@ +<%namespace name='static' file='static_content.html'/> + + % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: + <%static:css group='base-style'/> + % else: + % endif <%block name="title"></%block> diff --git a/lms/lib/pipeline_mako.py b/common/djangoapps/pipeline_mako/__init__.py similarity index 91% rename from lms/lib/pipeline_mako.py rename to common/djangoapps/pipeline_mako/__init__.py index 34e65a63ac..f100d95916 100644 --- a/lms/lib/pipeline_mako.py +++ b/common/djangoapps/pipeline_mako/__init__.py @@ -21,7 +21,7 @@ def compressed_css(package_name): return render_individual_css(package, paths) def render_css(package, path): - template_name = package.template_name or "pipeline_mako/css.html" + template_name = package.template_name or "mako/css.html" context = package.extra_context context.update({ 'type': guess_type(path, 'text/css'), @@ -50,7 +50,7 @@ def compressed_js(package_name): return render_individual_js(package, paths, templates) def render_js(package, path): - template_name = package.template_name or "pipeline_mako/js.html" + template_name = package.template_name or "mako/js.html" context = package.extra_context context.update({ 'type': guess_type(path, 'text/javascript'), @@ -63,7 +63,7 @@ def render_inline_js(package, js): context.update({ 'source': js }) - return render_to_string("pipeline_mako/inline_js.html", context) + return render_to_string("mako/inline_js.html", context) def render_individual_js(package, paths, templates=None): tags = [render_js(package, js) for js in paths] diff --git a/lms/templates/pipeline_mako/css.html b/common/djangoapps/pipeline_mako/templates/mako/css.html similarity index 100% rename from lms/templates/pipeline_mako/css.html rename to common/djangoapps/pipeline_mako/templates/mako/css.html diff --git a/lms/templates/pipeline_mako/inline_js.html b/common/djangoapps/pipeline_mako/templates/mako/inline_js.html similarity index 100% rename from lms/templates/pipeline_mako/inline_js.html rename to common/djangoapps/pipeline_mako/templates/mako/inline_js.html diff --git a/lms/templates/pipeline_mako/js.html b/common/djangoapps/pipeline_mako/templates/mako/js.html similarity index 100% rename from lms/templates/pipeline_mako/js.html rename to common/djangoapps/pipeline_mako/templates/mako/js.html diff --git a/lms/templates/static_content.html b/common/djangoapps/pipeline_mako/templates/static_content.html similarity index 100% rename from lms/templates/static_content.html rename to common/djangoapps/pipeline_mako/templates/static_content.html diff --git a/lms/lib/static_replace.py b/common/djangoapps/static_replace.py similarity index 100% rename from lms/lib/static_replace.py rename to common/djangoapps/static_replace.py diff --git a/lms/envs/common.py b/lms/envs/common.py index cc46271130..60834f9d91 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -73,7 +73,9 @@ MAKO_TEMPLATES['course'] = [DATA_DIR] MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections'] MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags'] MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', + COMMON_ROOT / 'templates', COMMON_ROOT / 'lib' / 'capa' / 'templates', + COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates', DATA_DIR / 'info', DATA_DIR / 'problems'] diff --git a/requirements.txt b/requirements.txt index 55e4dd665e..a72f72a7da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ python-memcached django-celery path.py django_debug_toolbar --e git+git://github.com/MITx/django-pipeline.git@incremental_compile#egg=django-pipeline +-e git+git://github.com/MITx/django-pipeline.git#egg=django-pipeline django-staticfiles>=1.2.1 django-masquerade fs From cc22125971616e1b6e9a3f513ef9261d7fe3968a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 14:20:10 -0400 Subject: [PATCH 097/252] Remove old pre-compiled css --- cms/static/css/base-style.css | 1071 --------------------------------- cms/static/css/style.css | 0 2 files changed, 1071 deletions(-) delete mode 100644 cms/static/css/base-style.css delete mode 100644 cms/static/css/style.css diff --git a/cms/static/css/base-style.css b/cms/static/css/base-style.css deleted file mode 100644 index 9ebc6bd839..0000000000 --- a/cms/static/css/base-style.css +++ /dev/null @@ -1,1071 +0,0 @@ -html, body, div, span, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -abbr, address, cite, code, -del, dfn, em, img, ins, kbd, q, samp, -small, strong, sub, sup, var, -b, i, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, figcaption, figure, -footer, header, hgroup, menu, nav, section, summary, -time, mark, audio, video { - margin: 0; - padding: 0; - border: 0; - outline: 0; - vertical-align: baseline; - background: transparent; } - -html, body { - font-size: 100%; } - -article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { - display: block; } - -audio, canvas, video { - display: inline-block; } - -audio:not([controls]) { - display: none; } - -[hidden] { - display: none; } - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; } - -html, button, input, select, textarea { - font-family: sans-serif; } - -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; } -a:hover, a:active { - outline: 0; } - -abbr[title] { - border-bottom: 1px dotted; } - -b, strong { - font-weight: bold; } - -blockquote { - margin: 1em 40px; } - -dfn { - font-style: italic; } - -mark { - background: #ff0; - color: #000; } - -pre, code, kbd, samp { - font-family: monospace, serif; - _font-family: 'courier new', monospace; - font-size: 1em; } - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; } - -blockquote, q { - quotes: none; } - blockquote:before, blockquote:after, q:before, q:after { - content: ''; - content: none; } - -small { - font-size: 75%; } - -sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; } - -sup { - top: -0.5em; } - -sub { - bottom: -0.25em; } - -nav ul, nav ol { - list-style: none; - list-style-image: none; } - -img { - border: 0; - height: auto; - max-width: 100%; - -ms-interpolation-mode: bicubic; } - -svg:not(:root) { - overflow: hidden; } - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; } - -legend { - border: 0; - padding: 0; - white-space: normal; } - -button, input, select, textarea { - font-size: 100%; - margin: 0; - vertical-align: baseline; } - -button, input { - line-height: normal; } - -button, input[type="button"], input[type="reset"], input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; } - -button[disabled], input[disabled] { - cursor: default; } - -input[type="checkbox"], input[type="radio"] { - box-sizing: border-box; - padding: 0; } - -input[type="search"] { - -webkit-appearance: textfield; - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; - box-sizing: content-box; } - -input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; } - -button::-moz-focus-inner, input::-moz-focus-inner { - border: 0; - padding: 0; } - -textarea { - overflow: auto; - vertical-align: top; } - -table { - border-collapse: collapse; - border-spacing: 0; } - -html { - height: 100%; } - -body { - zoom: 1; - height: 100%; - font: 14px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; } - body:before, body:after { - content: ""; - display: table; } - body:after { - clear: both; } - body > section { - display: table; - width: 100%; } - body > header { - background: #000; - color: #fff; - display: block; - float: none; - padding: 6px 20px; - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; } - body > header nav { - zoom: 1; } - body > header nav:before, body > header nav:after { - content: ""; - display: table; } - body > header nav:after { - clear: both; } - body > header nav h2 { - font-size: 14px; - text-transform: uppercase; - float: left; } - body > header nav ul { - float: left; } - body > header nav ul.user-nav { - float: right; } - body > header nav ul li { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-left: 15px; } - body.content section.main-content { - border-left: 2px solid #000; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - width: 74.423%; - float: left; - -webkit-box-shadow: -2px 0 3px #dddddd; - -moz-box-shadow: -2px 0 3px #dddddd; - box-shadow: -2px 0 3px #dddddd; } - -a { - text-decoration: none; - color: #888; } - -input { - font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; } - -input[type="submit"], .button, section.cal section.new-section > a, section.week-edit > section.content > div section.modules.empty a, -section.week-new > section.content > div section.modules.empty a, -section.sequence-edit > section.content > div section.modules.empty a, section.week-edit > section.content > div section.scratch-pad ol li ul li.empty a, -section.week-new > section.content > div section.scratch-pad ol li ul li.empty a, -section.sequence-edit > section.content > div section.scratch-pad ol li ul li.empty a, section.video-new > section section.upload a.upload-button, section.video-edit > section section.upload a.upload-button, section.video-new > section a.save-update, section.video-edit > section a.save-update, section.problem-new > section a.save, section.problem-edit > section a.save { - border: 1px solid #ccc; - background: #efefef; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -ms-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; - padding: 6px; } - -.new-module { - position: relative; } - .new-module a { - padding: 6px; - display: block; } - .new-module ul.new-dropdown { - list-style: none; - position: absolute; } - .new-module ul.new-dropdown li { - display: none; - padding: 6px; } - .new-module:hover ul.new-dropdown { - display: block; } - -.draggable { - width: 7px; - min-height: 14px; - background: url("../img/drag-handle.png") no-repeat center; - text-indent: -9999px; - display: block; - float: right; } - -section.cal { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 25px; - zoom: 1; } - section.cal:before, section.cal:after { - content: ""; - display: table; } - section.cal:after { - clear: both; } - section.cal > header { - zoom: 1; - margin-bottom: 10px; - background: #efefef; - border: 1px solid #ddd; } - section.cal > header:before, section.cal > header:after { - content: ""; - display: table; } - section.cal > header:after { - clear: both; } - section.cal > header h2 { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - text-transform: uppercase; - letter-spacing: 1px; - font-size: 14px; - padding: 6px; - margin-left: 6px; - font-size: 12px; } - section.cal > header ul { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.cal > header ul li { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-left: 6px; - padding-left: 6px; - border-left: 1px solid #ddd; - padding: 6px; } - section.cal > header ul li a { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.cal > header ul li ul { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.cal > header ul li ul li { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - padding: 0; - border-left: 0; } - section.cal ol { - list-style: none; - zoom: 1; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - border-left: 1px solid #333; - border-top: 1px solid #333; - width: 100%; } - section.cal ol:before, section.cal ol:after { - content: ""; - display: table; } - section.cal ol:after { - clear: both; } - section.cal ol > li { - border-right: 1px solid #333; - border-bottom: 1px solid; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - float: left; - width: 25.0%; } - section.cal ol > li header { - border-bottom: 1px solid #000; - -webkit-box-shadow: 0 1px 2px #aaaaaa; - -moz-box-shadow: 0 1px 2px #aaaaaa; - box-shadow: 0 1px 2px #aaaaaa; - display: block; - margin-bottom: 2px; } - section.cal ol > li header h1 { - font-size: 14px; - text-transform: uppercase; - border-bottom: 1px solid #ccc; - padding: 6px; } - section.cal ol > li header h1 a { - color: #000; - display: block; } - section.cal ol > li header ul li { - background: #fff; - color: #888; - border-bottom: 0; - font-size: 12px; } - section.cal ol > li ul { - list-style: none; - margin-bottom: 1px; } - section.cal ol > li ul li { - background: #efefef; - border-bottom: 1px solid #666; - padding: 6px; } - section.cal ol > li ul li.create-module { - position: relative; } - section.cal ol > li ul li.create-module > div { - display: none; - position: absolute; - top: 30px; - width: 90%; - background: rgba(0, 0, 0, 0.9); - padding: 10px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -ms-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; - z-index: 99; } - section.cal ol > li ul li.create-module > div:before { - content: " "; - display: block; - background: rgba(0, 0, 0, 0.8); - width: 10px; - height: 10px; - position: absolute; - top: -5px; - left: 50%; - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); } - section.cal ol > li ul li.create-module > div ul li { - border-bottom: 0; - background: none; } - section.cal ol > li ul li.create-module > div ul li input { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - border-color: #000; - padding: 6px; } - section.cal ol > li ul li.create-module > div ul li select { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; } - section.cal ol > li ul li.create-module > div ul li select option { - font-size: 14px; } - section.cal ol > li ul li.create-module > div ul li a { - float: right; } - section.cal ol > li ul li.create-module > div ul li a:first-child { - float: left; } - section.cal ol > li ul li.create-module:hover div { - display: block; } - section.cal section.new-section { - margin-top: 10px; - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - position: relative; } - section.cal section.new-section > a { - display: block; } - section.cal section.new-section section { - display: none; - position: absolute; - top: 30px; - background: rgba(0, 0, 0, 0.8); - min-width: 300px; - padding: 10px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - -ms-border-radius: 3px; - -o-border-radius: 3px; - border-radius: 3px; - z-index: 99; } - section.cal section.new-section section:before { - content: " "; - display: block; - background: rgba(0, 0, 0, 0.8); - width: 10px; - height: 10px; - position: absolute; - top: -5px; - left: 20%; - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); } - section.cal section.new-section section form ul { - list-style: none; } - section.cal section.new-section section form ul li { - border-bottom: 0; - background: none; - margin-bottom: 6px; } - section.cal section.new-section section form ul li input { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - border-color: #000; - padding: 6px; } - section.cal section.new-section section form ul li select { - width: 100%; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; } - section.cal section.new-section section form ul li select option { - font-size: 14px; } - section.cal section.new-section section form ul li a { - float: right; } - section.cal section.new-section section form ul li a:first-child { - float: left; } - section.cal section.new-section:hover section { - display: block; } - -body.content -section.cal { - width: 25.577%; - float: left; - overflow: scroll; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - opacity: .4; - -webkit-transition-property: all; - -moz-transition-property: all; - -ms-transition-property: all; - -o-transition-property: all; - transition-property: all; - -webkit-transition-duration: 0.15s; - -moz-transition-duration: 0.15s; - -ms-transition-duration: 0.15s; - -o-transition-duration: 0.15s; - transition-duration: 0.15s; - -webkit-transition-timing-function: ease-out; - -moz-transition-timing-function: ease-out; - -ms-transition-timing-function: ease-out; - -o-transition-timing-function: ease-out; - transition-timing-function: ease-out; - -webkit-transition-delay: 0; - -moz-transition-delay: 0; - -ms-transition-delay: 0; - -o-transition-delay: 0; - transition-delay: 0; } - body.content - section.cal > header ul { - display: none; } - body.content - section.cal:hover { - opacity: 1; } - body.content - section.cal ol li { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - width: 100%; } - body.content - section.cal ol li.create-module { - display: none; } - -section.week-edit > header, -section.week-new > header, -section.sequence-edit > header { - border-bottom: 2px solid #333; - zoom: 1; } - section.week-edit > header:before, section.week-edit > header:after, - section.week-new > header:before, - section.week-new > header:after, - section.sequence-edit > header:before, - section.sequence-edit > header:after { - content: ""; - display: table; } - section.week-edit > header:after, - section.week-new > header:after, - section.sequence-edit > header:after { - clear: both; } - section.week-edit > header div, - section.week-new > header div, - section.sequence-edit > header div { - zoom: 1; - padding: 6px 20px; } - section.week-edit > header div:before, section.week-edit > header div:after, - section.week-new > header div:before, - section.week-new > header div:after, - section.sequence-edit > header div:before, - section.sequence-edit > header div:after { - content: ""; - display: table; } - section.week-edit > header div:after, - section.week-new > header div:after, - section.sequence-edit > header div:after { - clear: both; } - section.week-edit > header div h1, - section.week-new > header div h1, - section.sequence-edit > header div h1 { - font-size: 18px; - text-transform: uppercase; - letter-spacing: 1px; - float: left; } - section.week-edit > header div p, - section.week-new > header div p, - section.sequence-edit > header div p { - float: right; } - section.week-edit > header div.week, - section.week-new > header div.week, - section.sequence-edit > header div.week { - background: #eee; - font-size: 12px; - border-bottom: 1px solid #ccc; } - section.week-edit > header div.week h2, - section.week-new > header div.week h2, - section.sequence-edit > header div.week h2 { - font-size: 12px; - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-right: 20px; } - section.week-edit > header div.week ul, - section.week-new > header div.week ul, - section.sequence-edit > header div.week ul { - list-style: none; - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.week-edit > header div.week ul li, - section.week-new > header div.week ul li, - section.sequence-edit > header div.week ul li { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-right: 10px; } - section.week-edit > header div.week ul li p, - section.week-new > header div.week ul li p, - section.sequence-edit > header div.week ul li p { - float: none; } - section.week-edit > header section.goals, - section.week-new > header section.goals, - section.sequence-edit > header section.goals { - background: #eee; - padding: 6px 20px; - border-top: 1px solid #ccc; } - section.week-edit > header section.goals ul, - section.week-new > header section.goals ul, - section.sequence-edit > header section.goals ul { - list-style: none; - color: #999; } - section.week-edit > header section.goals ul li, - section.week-new > header section.goals ul li, - section.sequence-edit > header section.goals ul li { - margin-bottom: 6px; } - section.week-edit > header section.goals ul li:last-child, - section.week-new > header section.goals ul li:last-child, - section.sequence-edit > header section.goals ul li:last-child { - margin-bottom: 0; } -section.week-edit > section.content, -section.week-new > section.content, -section.sequence-edit > section.content { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 20px; } - section.week-edit > section.content section.filters, - section.week-new > section.content section.filters, - section.sequence-edit > section.content section.filters { - zoom: 1; - margin-bottom: 10px; - background: #efefef; - border: 1px solid #ddd; } - section.week-edit > section.content section.filters:before, section.week-edit > section.content section.filters:after, - section.week-new > section.content section.filters:before, - section.week-new > section.content section.filters:after, - section.sequence-edit > section.content section.filters:before, - section.sequence-edit > section.content section.filters:after { - content: ""; - display: table; } - section.week-edit > section.content section.filters:after, - section.week-new > section.content section.filters:after, - section.sequence-edit > section.content section.filters:after { - clear: both; } - section.week-edit > section.content section.filters ul, - section.week-new > section.content section.filters ul, - section.sequence-edit > section.content section.filters ul { - zoom: 1; - list-style: none; - padding: 6px; } - section.week-edit > section.content section.filters ul:before, section.week-edit > section.content section.filters ul:after, - section.week-new > section.content section.filters ul:before, - section.week-new > section.content section.filters ul:after, - section.sequence-edit > section.content section.filters ul:before, - section.sequence-edit > section.content section.filters ul:after { - content: ""; - display: table; } - section.week-edit > section.content section.filters ul:after, - section.week-new > section.content section.filters ul:after, - section.sequence-edit > section.content section.filters ul:after { - clear: both; } - section.week-edit > section.content section.filters ul li, - section.week-new > section.content section.filters ul li, - section.sequence-edit > section.content section.filters ul li { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.week-edit > section.content section.filters ul li.advanced, - section.week-new > section.content section.filters ul li.advanced, - section.sequence-edit > section.content section.filters ul li.advanced { - float: right; } - section.week-edit > section.content > div, - section.week-new > section.content > div, - section.sequence-edit > section.content > div { - display: table; - border: 1px solid; - width: 100%; } - section.week-edit > section.content > div section header, - section.week-new > section.content > div section header, - section.sequence-edit > section.content > div section header { - background: #eee; - padding: 6px; - border-bottom: 1px solid #ccc; - zoom: 1; } - section.week-edit > section.content > div section header:before, section.week-edit > section.content > div section header:after, - section.week-new > section.content > div section header:before, - section.week-new > section.content > div section header:after, - section.sequence-edit > section.content > div section header:before, - section.sequence-edit > section.content > div section header:after { - content: ""; - display: table; } - section.week-edit > section.content > div section header:after, - section.week-new > section.content > div section header:after, - section.sequence-edit > section.content > div section header:after { - clear: both; } - section.week-edit > section.content > div section header h2, - section.week-new > section.content > div section header h2, - section.sequence-edit > section.content > div section header h2 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; - float: left; } - section.week-edit > section.content > div section.modules, - section.week-new > section.content > div section.modules, - section.sequence-edit > section.content > div section.modules { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - display: table-cell; - width: 65.632%; - border-right: 1px solid #333; } - section.week-edit > section.content > div section.modules.empty, - section.week-new > section.content > div section.modules.empty, - section.sequence-edit > section.content > div section.modules.empty { - text-align: center; - vertical-align: middle; } - section.week-edit > section.content > div section.modules.empty a, - section.week-new > section.content > div section.modules.empty a, - section.sequence-edit > section.content > div section.modules.empty a { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-top: 10px; } - section.week-edit > section.content > div section.modules ol, - section.week-new > section.content > div section.modules ol, - section.sequence-edit > section.content > div section.modules ol { - list-style: none; - border-bottom: 1px solid #333; } - section.week-edit > section.content > div section.modules ol li, - section.week-new > section.content > div section.modules ol li, - section.sequence-edit > section.content > div section.modules ol li { - border-bottom: 1px solid #333; } - section.week-edit > section.content > div section.modules ol li:last-child, - section.week-new > section.content > div section.modules ol li:last-child, - section.sequence-edit > section.content > div section.modules ol li:last-child { - border-bottom: 0; } - section.week-edit > section.content > div section.modules ol li a, - section.week-new > section.content > div section.modules ol li a, - section.sequence-edit > section.content > div section.modules ol li a { - color: #000; } - section.week-edit > section.content > div section.modules ol li ol, - section.week-new > section.content > div section.modules ol li ol, - section.sequence-edit > section.content > div section.modules ol li ol { - list-style: none; } - section.week-edit > section.content > div section.modules ol li ol li, - section.week-new > section.content > div section.modules ol li ol li, - section.sequence-edit > section.content > div section.modules ol li ol li { - padding: 6px; } - section.week-edit > section.content > div section.modules ol li ol li:hover a.draggable, - section.week-new > section.content > div section.modules ol li ol li:hover a.draggable, - section.sequence-edit > section.content > div section.modules ol li ol li:hover a.draggable { - opacity: 1; } - section.week-edit > section.content > div section.modules ol li ol li a.draggable, - section.week-new > section.content > div section.modules ol li ol li a.draggable, - section.sequence-edit > section.content > div section.modules ol li ol li a.draggable { - float: right; - opacity: .5; } - section.week-edit > section.content > div section.modules ol li ol li.group, - section.week-new > section.content > div section.modules ol li ol li.group, - section.sequence-edit > section.content > div section.modules ol li ol li.group { - padding: 0; } - section.week-edit > section.content > div section.modules ol li ol li.group header, - section.week-new > section.content > div section.modules ol li ol li.group header, - section.sequence-edit > section.content > div section.modules ol li ol li.group header { - padding: 6px; - background: none; } - section.week-edit > section.content > div section.modules ol li ol li.group header h3, - section.week-new > section.content > div section.modules ol li ol li.group header h3, - section.sequence-edit > section.content > div section.modules ol li ol li.group header h3 { - font-size: 14px; } - section.week-edit > section.content > div section.modules ol li ol li.group ol, - section.week-new > section.content > div section.modules ol li ol li.group ol, - section.sequence-edit > section.content > div section.modules ol li ol li.group ol { - border-left: 4px solid #999; - border-bottom: 0; } - section.week-edit > section.content > div section.modules ol li ol li.group ol li:last-child, - section.week-new > section.content > div section.modules ol li ol li.group ol li:last-child, - section.sequence-edit > section.content > div section.modules ol li ol li.group ol li:last-child { - border-bottom: 0; } - section.week-edit > section.content > div section.scratch-pad, - section.week-new > section.content > div section.scratch-pad, - section.sequence-edit > section.content > div section.scratch-pad { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - display: table-cell; - width: 34.368%; - vertical-align: top; } - section.week-edit > section.content > div section.scratch-pad ol, - section.week-new > section.content > div section.scratch-pad ol, - section.sequence-edit > section.content > div section.scratch-pad ol { - list-style: none; - border-bottom: 1px solid #999; } - section.week-edit > section.content > div section.scratch-pad ol li, - section.week-new > section.content > div section.scratch-pad ol li, - section.sequence-edit > section.content > div section.scratch-pad ol li { - border-bottom: 1px solid #999; - background: #f9f9f9; } - section.week-edit > section.content > div section.scratch-pad ol li:last-child, - section.week-new > section.content > div section.scratch-pad ol li:last-child, - section.sequence-edit > section.content > div section.scratch-pad ol li:last-child { - border-bottom: 0; } - section.week-edit > section.content > div section.scratch-pad ol li ul, - section.week-new > section.content > div section.scratch-pad ol li ul, - section.sequence-edit > section.content > div section.scratch-pad ol li ul { - list-style: none; } - section.week-edit > section.content > div section.scratch-pad ol li ul li, - section.week-new > section.content > div section.scratch-pad ol li ul li, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li { - padding: 6px; } - section.week-edit > section.content > div section.scratch-pad ol li ul li:last-child, - section.week-new > section.content > div section.scratch-pad ol li ul li:last-child, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li:last-child { - border-bottom: 0; } - section.week-edit > section.content > div section.scratch-pad ol li ul li:hover a.draggable, - section.week-new > section.content > div section.scratch-pad ol li ul li:hover a.draggable, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li:hover a.draggable { - opacity: 1; } - section.week-edit > section.content > div section.scratch-pad ol li ul li.empty, - section.week-new > section.content > div section.scratch-pad ol li ul li.empty, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li.empty { - padding: 12px; } - section.week-edit > section.content > div section.scratch-pad ol li ul li.empty a, - section.week-new > section.content > div section.scratch-pad ol li ul li.empty a, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li.empty a { - display: block; - text-align: center; } - section.week-edit > section.content > div section.scratch-pad ol li ul li a.draggable, - section.week-new > section.content > div section.scratch-pad ol li ul li a.draggable, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li a.draggable { - float: right; - opacity: .3; } - section.week-edit > section.content > div section.scratch-pad ol li ul li a, - section.week-new > section.content > div section.scratch-pad ol li ul li a, - section.sequence-edit > section.content > div section.scratch-pad ol li ul li a { - color: #000; } - -section.video-new > section section.upload, section.video-edit > section section.upload { - padding: 6px; - margin-bottom: 10px; - border: 1px solid #ddd; } - section.video-new > section section.upload a.upload-button, section.video-edit > section section.upload a.upload-button { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } -section.video-new > section section.in-use h2, section.video-edit > section section.in-use h2 { - font-size: 14px; } -section.video-new > section section.in-use div, section.video-edit > section section.in-use div { - background: #eee; - text-align: center; - padding: 6px; } -section.video-new > section a.save-update, section.video-edit > section a.save-update { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-top: 20px; } - -section.problem-new > section textarea, section.problem-edit > section textarea { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - display: block; - width: 100%; } -section.problem-new > section div.preview, section.problem-edit > section div.preview { - background: #eee; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 40px; - padding: 10px; - width: 100%; } -section.problem-new > section a.save, section.problem-edit > section a.save { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin-top: 20px; } - -section.video-new, section.video-edit, section.problem-new, section.problem-edit { - position: absolute; - top: 72px; - right: 0; - background: #fff; - width: 48.845%; - -webkit-box-shadow: 0 0 6px #666666; - -moz-box-shadow: 0 0 6px #666666; - box-shadow: 0 0 6px #666666; - border: 1px solid #333; - border-right: 0; - z-index: 4; } - section.video-new > header, section.video-edit > header, section.problem-new > header, section.problem-edit > header { - background: #666; - zoom: 1; - color: #fff; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; } - section.video-new > header:before, section.video-new > header:after, section.video-edit > header:before, section.video-edit > header:after, section.problem-new > header:before, section.problem-new > header:after, section.problem-edit > header:before, section.problem-edit > header:after { - content: ""; - display: table; } - section.video-new > header:after, section.video-edit > header:after, section.problem-new > header:after, section.problem-edit > header:after { - clear: both; } - section.video-new > header h2, section.video-edit > header h2, section.problem-new > header h2, section.problem-edit > header h2 { - float: left; - font-size: 14px; } - section.video-new > header a, section.video-edit > header a, section.problem-new > header a, section.problem-edit > header a { - color: #fff; } - section.video-new > header a.save-update, section.video-edit > header a.save-update, section.problem-new > header a.save-update, section.problem-edit > header a.save-update { - float: right; } - section.video-new > header a.cancel, section.video-edit > header a.cancel, section.problem-new > header a.cancel, section.problem-edit > header a.cancel { - float: left; } - section.video-new > section, section.video-edit > section, section.problem-new > section, section.problem-edit > section { - padding: 20px; } - section.video-new > section > header h1, section.video-edit > section > header h1, section.problem-new > section > header h1, section.problem-edit > section > header h1 { - font-size: 24px; - margin: 12px 0; } - section.video-new > section > header section.status-settings ul, section.video-edit > section > header section.status-settings ul, section.problem-new > section > header section.status-settings ul, section.problem-edit > section > header section.status-settings ul { - list-style: none; - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - -ms-border-radius: 2px; - -o-border-radius: 2px; - border-radius: 2px; - border: 1px solid #999; - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.video-new > section > header section.status-settings ul li, section.video-edit > section > header section.status-settings ul li, section.problem-new > section > header section.status-settings ul li, section.problem-edit > section > header section.status-settings ul li { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - border-right: 1px solid #999; - padding: 6px; } - section.video-new > section > header section.status-settings ul li:last-child, section.video-edit > section > header section.status-settings ul li:last-child, section.problem-new > section > header section.status-settings ul li:last-child, section.problem-edit > section > header section.status-settings ul li:last-child { - border-right: 0; } - section.video-new > section > header section.status-settings ul li.current, section.video-edit > section > header section.status-settings ul li.current, section.problem-new > section > header section.status-settings ul li.current, section.problem-edit > section > header section.status-settings ul li.current { - background: #eee; } - section.video-new > section > header section.status-settings a.settings, section.video-edit > section > header section.status-settings a.settings, section.problem-new > section > header section.status-settings a.settings, section.problem-edit > section > header section.status-settings a.settings { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; - margin: 0 20px; - border: 1px solid #999; - padding: 6px; } - section.video-new > section > header section.status-settings select, section.video-edit > section > header section.status-settings select, section.problem-new > section > header section.status-settings select, section.problem-edit > section > header section.status-settings select { - float: right; } - section.video-new > section > header section.meta, section.video-edit > section > header section.meta, section.problem-new > section > header section.meta, section.problem-edit > section > header section.meta { - background: #eee; - padding: 10px; - margin: 20px 0; - zoom: 1; } - section.video-new > section > header section.meta:before, section.video-new > section > header section.meta:after, section.video-edit > section > header section.meta:before, section.video-edit > section > header section.meta:after, section.problem-new > section > header section.meta:before, section.problem-new > section > header section.meta:after, section.problem-edit > section > header section.meta:before, section.problem-edit > section > header section.meta:after { - content: ""; - display: table; } - section.video-new > section > header section.meta:after, section.video-edit > section > header section.meta:after, section.problem-new > section > header section.meta:after, section.problem-edit > section > header section.meta:after { - clear: both; } - section.video-new > section > header section.meta div, section.video-edit > section > header section.meta div, section.problem-new > section > header section.meta div, section.problem-edit > section > header section.meta div { - float: left; - margin-right: 20px; } - section.video-new > section > header section.meta div h2, section.video-edit > section > header section.meta div h2, section.problem-new > section > header section.meta div h2, section.problem-edit > section > header section.meta div h2 { - font-size: 14px; - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.video-new > section > header section.meta div p, section.video-edit > section > header section.meta div p, section.problem-new > section > header section.meta div p, section.problem-edit > section > header section.meta div p { - display: -moz-inline-box; - -moz-box-orient: vertical; - display: inline-block; - vertical-align: baseline; - zoom: 1; - *display: inline; - *vertical-align: auto; } - section.video-new > section section.notes, section.video-edit > section section.notes, section.problem-new > section section.notes, section.problem-edit > section section.notes { - margin-top: 20px; - padding: 6px; - background: #eee; - border: 1px solid #ccc; } - section.video-new > section section.notes textarea, section.video-edit > section section.notes textarea, section.problem-new > section section.notes textarea, section.problem-edit > section section.notes textarea { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - display: block; - width: 100%; } - section.video-new > section section.notes h2, section.video-edit > section section.notes h2, section.problem-new > section section.notes h2, section.problem-edit > section section.notes h2 { - font-size: 14px; - margin-bottom: 6px; } - section.video-new > section section.notes input[type="submit"], section.video-edit > section section.notes input[type="submit"], section.problem-new > section section.notes input[type="submit"], section.problem-edit > section section.notes input[type="submit"] { - margin-top: 10px; } diff --git a/cms/static/css/style.css b/cms/static/css/style.css deleted file mode 100644 index e69de29bb2..0000000000 From e865b972698f8a0c91f52b90071760d6a65e15a5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 19 Jun 2012 14:21:38 -0400 Subject: [PATCH 098/252] Rename test directory for consistancy --- common/lib/keystore/{test => tests}/test_location.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename common/lib/keystore/{test => tests}/test_location.py (100%) diff --git a/common/lib/keystore/test/test_location.py b/common/lib/keystore/tests/test_location.py similarity index 100% rename from common/lib/keystore/test/test_location.py rename to common/lib/keystore/tests/test_location.py From afaed831586c32eae21d8e28c4937c4b994e8003 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 19 Jun 2012 14:27:34 -0400 Subject: [PATCH 099/252] Update jQuery and jQuery-UI to latest version --- lms/static/js/jquery-1.6.2.min.js | 18 - lms/static/js/jquery-ui-1.8.16.custom.min.js | 791 ------------------- lms/static/js/jquery-ui.min.js | 125 +++ lms/static/js/jquery.min.js | 4 + lms/templates/main.html | 4 +- 5 files changed, 131 insertions(+), 811 deletions(-) delete mode 100644 lms/static/js/jquery-1.6.2.min.js delete mode 100644 lms/static/js/jquery-ui-1.8.16.custom.min.js create mode 100755 lms/static/js/jquery-ui.min.js create mode 100644 lms/static/js/jquery.min.js diff --git a/lms/static/js/jquery-1.6.2.min.js b/lms/static/js/jquery-1.6.2.min.js deleted file mode 100644 index 48590ecb96..0000000000 --- a/lms/static/js/jquery-1.6.2.min.js +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * jQuery JavaScript Library v1.6.2 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Jun 30 14:16:56 2011 -0400 - */ -(function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"":"")+""),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
        a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
        ",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
        t
        ",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i. -shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

        ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
        ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/",""],legend:[1,"
        ","
        "],thead:[1,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],col:[2,"","
        "],area:[1,"",""],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div
        ","
        "]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j -)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
        ").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b
        ";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); \ No newline at end of file diff --git a/lms/static/js/jquery-ui-1.8.16.custom.min.js b/lms/static/js/jquery-ui-1.8.16.custom.min.js deleted file mode 100644 index 14c9064f7f..0000000000 --- a/lms/static/js/jquery-ui-1.8.16.custom.min.js +++ /dev/null @@ -1,791 +0,0 @@ -/*! - * jQuery UI 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.16", -keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({propAttr:c.fn.prop||c.fn.attr,_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d= -this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this, -"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart": -"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight, -outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a, -"tabindex"),d=isNaN(b);return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&& -a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted= -false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); -;/* - * jQuery UI Position 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Position - */ -(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.setTimeout){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j={top:b.of.pageY, -left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/2;if(b.at[1]==="bottom")j.top+= -k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+(parseInt(c.curCSS(this,"marginRight",true))||0),w=m+q+(parseInt(c.curCSS(this,"marginBottom",true))||0),i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]==="center")i.top-= -m/2;i.left=Math.round(i.left);i.top=Math.round(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();b.left= -d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];b.left+= -a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=c(b), -g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery); -;/* - * jQuery UI Draggable 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Draggables - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== -"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= -this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;if(b.iframeFix)d(b.iframeFix===true?"iframe":b.iframeFix).each(function(){d('
        ').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")});return true},_mouseStart:function(a){var b=this.options; -this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}); -this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);d.ui.ddmanager&&d.ui.ddmanager.dragStart(this,a);return true}, -_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b= -false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration, -10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},_mouseUp:function(a){this.options.iframeFix===true&&d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)});d.ui.ddmanager&&d.ui.ddmanager.dragStop(this,a);return d.ui.mouse.prototype._mouseUp.call(this,a)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle|| -!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone().removeAttr("id"):this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&& -a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent= -this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"), -10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"), -10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[a.containment=="document"?0:d(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,a.containment=="document"?0:d(window).scrollTop()-this.offset.relative.top-this.offset.parent.top, -(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"?0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){a=d(a.containment);var b=a[0];if(b){a.offset();var c=d(b).css("overflow")!= -"hidden";this.containment=[(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0),(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0),(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"), -10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom];this.relative_container=a}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+ -this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&& -!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,h=a.pageY;if(this.originalPosition){var g;if(this.containment){if(this.relative_container){g=this.relative_container.offset();g=[this.containment[0]+g.left,this.containment[1]+g.top,this.containment[2]+g.left,this.containment[3]+g.top]}else g=this.containment;if(a.pageX-this.offset.click.leftg[2])e=g[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>g[3])h=g[3]+this.offset.click.top}if(b.grid){h=b.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/b.grid[1])*b.grid[1]:this.originalPageY;h=g?!(h-this.offset.click.topg[3])?h:!(h-this.offset.click.topg[2])?e:!(e-this.offset.click.left=0;i--){var j=c.snapElements[i].left,l=j+c.snapElements[i].width,k=c.snapElements[i].top,m=k+c.snapElements[i].height;if(j-e=j&&f<=l||h>=j&&h<=l||fl)&&(e>= -i&&e<=k||g>=i&&g<=k||ek);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f
  • ').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(), -top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle= -this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne", -nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d
    ');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== -String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),l=0;l=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,l);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); -this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){if(!a.disabled){e(this).removeClass("ui-resizable-autohide");b._handles.show()}},function(){if(!a.disabled)if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy(); -var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a= -false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(),d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"}); -this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset=this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff= -{width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio:this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis]; -if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize",b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false}, -_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height;f=f?0:c.sizeDiff.width;f={width:c.helper.width()-f,height:c.helper.height()-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f, -{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop",b);this._helper&&this.helper.remove();return false},_updateVirtualBoundaries:function(b){var a=this.options,c,d,f;a={minWidth:k(a.minWidth)?a.minWidth:0,maxWidth:k(a.maxWidth)?a.maxWidth:Infinity,minHeight:k(a.minHeight)?a.minHeight:0,maxHeight:k(a.maxHeight)?a.maxHeight: -Infinity};if(this._aspectRatio||b){b=a.minHeight*this.aspectRatio;d=a.minWidth/this.aspectRatio;c=a.maxHeight*this.aspectRatio;f=a.maxWidth/this.aspectRatio;if(b>a.minWidth)a.minWidth=b;if(d>a.minHeight)a.minHeight=d;if(cb.width,h=k(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height,l=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&l)b.left=i-a.minWidth;if(d&&l)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left= -null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a
    ');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+ -a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+ -c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]); -b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable,{version:"1.8.16"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(), -10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize,function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top- -f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var l=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:l.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n=(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(l.css("position"))){c._revertToRelativePosition=true;l.css({position:"absolute",top:"auto",left:"auto"})}l.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType? -e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition=false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a= -e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left-a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing, -step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize",b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement= -e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top","Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset; -var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset,f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left: -a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left=a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top- -d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition, -f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25, -display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative",height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b= -e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height= -d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},k=function(b){return!isNaN(parseInt(b,10))}})(jQuery); -;/* - * jQuery UI Selectable 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Selectables - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"), -selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("
    ")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX, -c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting", -c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d= -this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.righti||a.bottomb&&a.rightg&&a.bottom *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){var a=this.options;this.containerCache={};this.element.addClass("ui-sortable"); -this.refresh();this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a=== -"disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&& -!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem=c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top, -left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]}; -this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment();if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!= -document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start",a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a); -return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0], -e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a,c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset(); -c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp({target:null});this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"): -this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate",null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}if(this.placeholder){this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null, -dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem):d(this.domPosition.parent).prepend(this.currentItem)}return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")}, -toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute||"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith(); -if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!=this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), -this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a=this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable");if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h=0;b--){var c=this.items[b];if(!(c.instance!=this.currentContainer&&this.currentContainer&&c.item[0]!=this.currentItem[0])){var e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b= -this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width=this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f= -d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f},update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")|| -0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b=null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out", -a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this));this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h- -f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g- -this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])?g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive",g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this, -this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over=0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop", -a,this._uiHash());for(e=0;e li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"); -a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom"); -if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var h=d.closest(".ui-accordion-header");a.active=h.length?h:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion", -function(f){return a._keydown(f)}).next().attr("role","tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(f){a._clickHandler.call(a,f,this);f.preventDefault()})},_createIcons:function(){var a= -this.options;if(a.icons){c("").addClass("ui-icon "+a.icons.header).prependTo(this.headers);this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"); -this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons(); -b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,h=this.headers.index(a.target),f=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:f=this.headers[(h+1)%d];break;case b.LEFT:case b.UP:f=this.headers[(h-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target); -a.preventDefault()}if(f){c(a.target).attr("tabIndex",-1);c(f).attr("tabIndex",0);f.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+ -c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options; -if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){var h=this.active;j=a.next();g=this.active.next();e={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):j,oldContent:g};var f=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(j,g,e,b,f);h.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header); -if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected);a.next().addClass("ui-accordion-content-active")}}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);this.active.next().addClass("ui-accordion-content-active");var g=this.active.next(), -e={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:g},j=this.active=c([]);this._toggle(j,g,e)}},_toggle:function(a,b,d,h,f){var g=this,e=g.options;g.toShow=a;g.toHide=b;g.data=d;var j=function(){if(g)return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data);g.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&h?{toShow:c([]),toHide:b,complete:j,down:f,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:f,autoHeight:e.autoHeight|| -e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;h=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!h[k]&&!c.easing[k])k="slide";h[k]||(h[k]=function(l){this.slide(l,{easing:k,duration:i||700})});h[k](d)}else{if(e.collapsible&&h)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false", -"aria-selected":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");if(this.toHide.length)this.toHide.parent()[0].className=this.toHide.parent()[0].className;this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.16", -animations:{slide:function(a,b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),h=0,f={},g={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){g[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/); -f[i]={value:j[1],unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(g,{step:function(j,i){if(i.prop=="height")h=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=h*f[i.prop].value+f[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide", -paddingTop:"hide",paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery); -;/* - * jQuery UI Autocomplete 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Autocomplete - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.position.js - */ -(function(d){var e=0;d.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:false,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var a=this,b=this.element[0].ownerDocument,g;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!(a.options.disabled||a.element.propAttr("readOnly"))){g= -false;var f=d.ui.keyCode;switch(c.keyCode){case f.PAGE_UP:a._move("previousPage",c);break;case f.PAGE_DOWN:a._move("nextPage",c);break;case f.UP:a._move("previous",c);c.preventDefault();break;case f.DOWN:a._move("next",c);c.preventDefault();break;case f.ENTER:case f.NUMPAD_ENTER:if(a.menu.active){g=true;c.preventDefault()}case f.TAB:if(!a.menu.active)return;a.menu.select(c);break;case f.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!= -a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);break}}}).bind("keypress.autocomplete",function(c){if(g){g=false;c.preventDefault()}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)}; -this.menu=d("
      ").addClass("ui-autocomplete").appendTo(d(this.options.appendTo||"body",b)[0]).mousedown(function(c){var f=a.menu.element[0];d(c.target).closest(".ui-menu-item").length||setTimeout(function(){d(document).one("mousedown",function(h){h.target!==a.element[0]&&h.target!==f&&!d.ui.contains(f,h.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,f){f=f.item.data("item.autocomplete");false!==a._trigger("focus",c,{item:f})&&/^key/.test(c.originalEvent.type)&& -a.element.val(f.value)},selected:function(c,f){var h=f.item.data("item.autocomplete"),i=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();a.previous=i;setTimeout(function(){a.previous=i;a.selectedItem=h},1)}false!==a._trigger("select",c,{item:h})&&a.element.val(h.value);a.term=a.element.val();a.close(c);a.selectedItem=h},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"); -d.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");this.menu.element.remove();d.Widget.prototype.destroy.call(this)},_setOption:function(a,b){d.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(d(b||"body",this.element[0].ownerDocument)[0]);a==="disabled"&& -b&&this.xhr&&this.xhr.abort()},_initSource:function(){var a=this,b,g;if(d.isArray(this.options.source)){b=this.options.source;this.source=function(c,f){f(d.ui.autocomplete.filter(b,c.term))}}else if(typeof this.options.source==="string"){g=this.options.source;this.source=function(c,f){a.xhr&&a.xhr.abort();a.xhr=d.ajax({url:g,data:c,dataType:"json",autocompleteRequest:++e,success:function(h){this.autocompleteRequest===e&&f(h)},error:function(){this.autocompleteRequest===e&&f([])}})}}else this.source= -this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(d("").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});d.extend(d.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, -"\\$&")},filter:function(a,b){var g=new RegExp(d.ui.autocomplete.escapeRegex(b),"i");return d.grep(a,function(c){return g.test(c.label||c.value||c)})}})})(jQuery); -(function(d){d.widget("ui.menu",{_create:function(){var e=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(a){if(d(a.target).closest(".ui-menu-item a").length){a.preventDefault();e.select(a)}});this.refresh()},refresh:function(){var e=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", --1).mouseenter(function(a){e.activate(a,d(this).parent())}).mouseleave(function(){e.deactivate()})},activate:function(e,a){this.deactivate();if(this.hasScroll()){var b=a.offset().top-this.element.offset().top,g=this.element.scrollTop(),c=this.element.height();if(b<0)this.element.scrollTop(g+b);else b>=c&&this.element.scrollTop(g+b-c+a.height())}this.active=a.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",e,{item:a})},deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id"); -this._trigger("blur");this.active=null}},next:function(e){this.move("next",".ui-menu-item:first",e)},previous:function(e){this.move("prev",".ui-menu-item:last",e)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(e,a,b){if(this.active){e=this.active[e+"All"](".ui-menu-item").eq(0);e.length?this.activate(b,e):this.activate(b,this.element.children(a))}else this.activate(b, -this.element.children(a))},nextPage:function(e){if(this.hasScroll())if(!this.active||this.last())this.activate(e,this.element.children(".ui-menu-item:first"));else{var a=this.active.offset().top,b=this.element.height(),g=this.element.children(".ui-menu-item").filter(function(){var c=d(this).offset().top-a-b+d(this).height();return c<10&&c>-10});g.length||(g=this.element.children(".ui-menu-item:last"));this.activate(e,g)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| -this.last()?":first":":last"))},previousPage:function(e){if(this.hasScroll())if(!this.active||this.first())this.activate(e,this.element.children(".ui-menu-item:last"));else{var a=this.active.offset().top,b=this.element.height();result=this.element.children(".ui-menu-item").filter(function(){var g=d(this).offset().top-a+b-d(this).height();return g<10&&g>-10});result.length||(result=this.element.children(".ui-menu-item:first"));this.activate(e,result)}else this.activate(e,this.element.children(".ui-menu-item").filter(!this.active|| -this.first()?":last":":first"))},hasScroll:function(){return this.element.height()").addClass("ui-button-text").html(this.options.label).appendTo(a.empty()).text(),e=this.options.icons,f=e.primary&&e.secondary,d=[];if(e.primary||e.secondary){if(this.options.text)d.push("ui-button-text-icon"+(f?"s":e.primary?"-primary":"-secondary"));e.primary&&a.prepend("");e.secondary&&a.append("");if(!this.options.text){d.push(f?"ui-button-icons-only": -"ui-button-icon-only");this.hasTitle||a.attr("title",c)}}else d.push("ui-button-text-only");a.addClass(d.join(" "))}}});b.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(a,c){a==="disabled"&&this.buttons.button("option",a,c);b.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var a=this.element.css("direction")=== -"ltr";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return b(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(a?"ui-corner-left":"ui-corner-right").end().filter(":last").addClass(a?"ui-corner-right":"ui-corner-left").end().end()},destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return b(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"); -b.Widget.prototype.destroy.call(this)}})})(jQuery); -;/* - * jQuery UI Dialog 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Dialog - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - * jquery.ui.button.js - * jquery.ui.draggable.js - * jquery.ui.mouse.js - * jquery.ui.position.js - * jquery.ui.resizable.js - */ -(function(c,l){var m={buttons:true,height:true,maxHeight:true,maxWidth:true,minHeight:true,minWidth:true,width:true},n={maxHeight:true,maxWidth:true,minHeight:true,minWidth:true},o=c.attrFn||{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true,click:true};c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false, -position:{my:"center",at:"center",collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title");if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",e=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("
      ")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+ -b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&!i.isDefaultPrevented()&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog","aria-labelledby":e}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var f=(a.uiDialogTitlebar=c("
      ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g), -h=c('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i);return false}).appendTo(f);(a.uiDialogTitlebarCloseText=c("")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("").addClass("ui-dialog-title").attr("id", -e).html(d).prependTo(f);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;f.find("*").add(f).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"); -a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d,e;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog");b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!== -b.uiDialog[0]){e=c(this).css("z-index");isNaN(e)||(d=Math.max(d,e))}});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,e=d.options;if(e.modal&&!a||!e.stack&&!e.modal)return d._trigger("focus",b);if(e.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ=e.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()};c.ui.dialog.maxZ+=1; -d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;a._size();a._position(b.position);d.show(b.show);a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(e){if(e.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),f=g.filter(":first");g=g.filter(":last");if(e.target===g[0]&&!e.shiftKey){f.focus(1);return false}else if(e.target=== -f[0]&&e.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false,e=c("
      ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("
      ").addClass("ui-dialog-buttonset").appendTo(e);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a, -function(){return!(d=true)});if(d){c.each(a,function(f,h){h=c.isFunction(h)?{click:h,text:f}:h;var i=c('').click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.each(h,function(j,k){if(j!=="click")j in o?i[j](k):i.attr(j,k)});c.fn.button&&i.button()});e.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(f){return{position:f.position,offset:f.offset}}var b=this,d=b.options,e=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close", -handle:".ui-dialog-titlebar",containment:"document",start:function(f,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",f,a(h))},drag:function(f,h){b._trigger("drag",f,a(h))},stop:function(f,h){d.position=[h.position.left-e.scrollLeft(),h.position.top-e.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g);b._trigger("dragStop",f,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(f){return{originalPosition:f.originalPosition, -originalSize:f.originalSize,position:f.position,size:f.size}}a=a===l?this.options.resizable:a;var d=this,e=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:a,start:function(f,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",f,b(h))},resize:function(f,h){d._trigger("resize", -f,b(h))},stop:function(f,h){c(this).removeClass("ui-dialog-resizing");e.height=c(this).height();e.width=c(this).width();d._trigger("resizeStop",f,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(a){var b=[],d=[0,0],e;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "): -[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,f){if(+b[g]===b[g]){d[g]=b[g];b[g]=f}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(e=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(c.extend({of:window},a));e||this.uiDialog.hide()},_setOptions:function(a){var b=this,d={},e=false;c.each(a,function(g,f){b._setOption(g,f); -if(g in m)e=true;if(g in n)d[g]=f});e&&this._size();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",d)},_setOption:function(a,b){var d=this,e=d.uiDialog;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":e.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?e.addClass("ui-dialog-disabled"): -e.removeClass("ui-dialog-disabled");break;case "draggable":var g=e.is(":data(draggable)");g&&!b&&e.draggable("destroy");!g&&b&&d._makeDraggable();break;case "position":d._position(b);break;case "resizable":(g=e.is(":data(resizable)"))&&!b&&e.resizable("destroy");g&&typeof b==="string"&&e.resizable("option","handles",b);!g&&b!==false&&d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break}c.Widget.prototype._setOption.apply(d,arguments)},_size:function(){var a= -this.options,b,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();d=Math.max(0,a.minHeight-b);if(a.height==="auto")if(c.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();a=this.element.css("height","auto").height();e||this.uiDialog.hide();this.element.height(Math.max(a,d))}else this.element.height(Math.max(a.height- -b,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.16",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "), -create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()
      ").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){var b=c.inArray(a,this.instances);b!=-1&&this.oldInstances.push(this.instances.splice(b,1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var d=0;c.each(this.instances,function(){d=Math.max(d,this.css("z-index"))});this.maxZ=d},height:function(){var a,b;if(c.browser.msie&& -c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a
      ").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(b.range==="min"||b.range==="max"?" ui-slider-range-"+b.range:""))}for(var j=c.length;j"); -this.handles=c.add(d(e.join("")).appendTo(a.element));this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(g){g.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur();else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(g){d(this).data("index.ui-slider-handle", -g)});this.handles.keydown(function(g){var k=true,l=d(this).data("index.ui-slider-handle"),i,h,m;if(!a.options.disabled){switch(g.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:k=false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");i=a._start(g,l);if(i===false)return}break}m=a.options.step;i=a.options.values&&a.options.values.length? -(h=a.values(l)):(h=a.value());switch(g.keyCode){case d.ui.keyCode.HOME:h=a._valueMin();break;case d.ui.keyCode.END:h=a._valueMax();break;case d.ui.keyCode.PAGE_UP:h=a._trimAlignValue(i+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:h=a._trimAlignValue(i-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(i===a._valueMax())return;h=a._trimAlignValue(i+m);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(i===a._valueMin())return;h=a._trimAlignValue(i- -m);break}a._slide(g,l,h);return k}}).keyup(function(g){var k=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(g,k);a._change(g,k);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");this._mouseDestroy(); -return this},_mouseCapture:function(a){var b=this.options,c,f,e,j,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});f=this._valueMax()-this._valueMin()+1;j=this;this.handles.each(function(k){var l=Math.abs(c-j.values(k));if(f>l){f=l;e=d(this);g=k}});if(b.range===true&&this.values(1)===b.min){g+=1;e=d(this.handles[g])}if(this._start(a,g)===false)return false; -this._mouseSliding=true;j._handleIndex=g;e.addClass("ui-state-active").focus();b=e.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-e.width()/2,top:a.pageY-b.top-e.height()/2-(parseInt(e.css("borderTopWidth"),10)||0)-(parseInt(e.css("borderBottomWidth"),10)||0)+(parseInt(e.css("marginTop"),10)||0)};this.handles.hasClass("ui-state-hover")||this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= -this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= -this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); -c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var f;if(this.options.values&&this.options.values.length){f=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>f||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}else if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;f=arguments[0];for(e=0;e=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= -this.options.range,b=this.options,c=this,f=!this._animateOff?b.animate:false,e,j={},g,k,l,i;if(this.options.values&&this.options.values.length)this.handles.each(function(h){e=(c.values(h)-c._valueMin())/(c._valueMax()-c._valueMin())*100;j[c.orientation==="horizontal"?"left":"bottom"]=e+"%";d(this).stop(1,1)[f?"animate":"css"](j,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(h===0)c.range.stop(1,1)[f?"animate":"css"]({left:e+"%"},b.animate);if(h===1)c.range[f?"animate":"css"]({width:e- -g+"%"},{queue:false,duration:b.animate})}else{if(h===0)c.range.stop(1,1)[f?"animate":"css"]({bottom:e+"%"},b.animate);if(h===1)c.range[f?"animate":"css"]({height:e-g+"%"},{queue:false,duration:b.animate})}g=e});else{k=this.value();l=this._valueMin();i=this._valueMax();e=i!==l?(k-l)/(i-l)*100:0;j[c.orientation==="horizontal"?"left":"bottom"]=e+"%";this.handle.stop(1,1)[f?"animate":"css"](j,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[f?"animate":"css"]({width:e+"%"}, -b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[f?"animate":"css"]({width:100-e+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[f?"animate":"css"]({height:e+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[f?"animate":"css"]({height:100-e+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.16"})})(jQuery); -;/* - * jQuery UI Tabs 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Tabs - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
      ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
    • #{label}
    • "},_create:function(){this._tabify(true)},_setOption:function(b,e){if(b=="selected")this.options.collapsible&& -e==this.options.selected||this.select(e);else{this.options[b]=e;this._tabify()}},_tabId:function(b){return b.title&&b.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(b){return b.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[b].concat(d.makeArray(arguments)))},_ui:function(b,e){return{tab:b,panel:e,index:this.anchors.index(b)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b= -d(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(b){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var a=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| -(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))a.panels=a.panels.add(a.element.find(a._sanitizeSelector(i)));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=a._tabId(f);f.href="#"+i;f=a.element.find("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(a.panels[g-1]||a.list);f.data("destroy.tabs",true)}a.panels=a.panels.add(f)}else c.disabled.push(g)});if(b){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); -this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(a._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= -this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return a.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); -if(c.selected>=0&&this.anchors.length){a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash)).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");a.element.queue("tabs",function(){a._trigger("show",null,a._ui(a.anchors[c.selected],a.element.find(a._sanitizeSelector(a.anchors[c.selected].hash))[0]))});this.load(c.selected)}d(window).bind("unload",function(){a.lis.add(a.anchors).unbind(".tabs");a.lis=a.anchors=a.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")); -this.element[c.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);b=0;for(var j;j=this.lis[b];b++)d(j)[d.inArray(b,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+ -g)};this.lis.bind("mouseover.tabs",function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal", -function(){e(f,o);a._trigger("show",null,a._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");a._trigger("show",null,a._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);a.element.dequeue("tabs")})}:function(g,f){a.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");a.element.dequeue("tabs")}; -this.anchors.bind(c.event+".tabs",function(){var g=this,f=d(g).closest("li"),i=a.panels.filter(":not(.ui-tabs-hide)"),l=a.element.find(a._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||a.panels.filter(":animated").length||a._trigger("select",null,a._ui(this,l[0]))===false){this.blur();return false}c.selected=a.anchors.index(this);a.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected= --1;c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&a._cookie(c.selected,c.cookie);a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this));this.blur();return false}c.cookie&&a._cookie(c.selected,c.cookie);if(l.length){i.length&&a.element.queue("tabs",function(){s(g,i)});a.element.queue("tabs",function(){r(g,l)});a.load(a.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier."; -d.browser.msie&&this.blur()});this.anchors.bind("click.tabs",function(){return false})},_getIndex:function(b){if(typeof b=="string")b=this.anchors.index(this.anchors.filter("[href$="+b+"]"));return b},destroy:function(){var b=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e= -d.data(this,"href.tabs");if(e)this.href=e;var a=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){a.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});b.cookie&&this._cookie(null,b.cookie);return this},add:function(b, -e,a){if(a===p)a=this.anchors.length;var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,b).replace(/#\{label\}/g,e));b=!b.indexOf("#")?b.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=c.element.find("#"+b);j.length||(j=d(h.panelTemplate).attr("id",b).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(a>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[a]); -j.insertBefore(this.panels[a])}h.disabled=d.map(h.disabled,function(k){return k>=a?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[a],this.panels[a]));return this},remove:function(b){b=this._getIndex(b);var e=this.options,a=this.lis.eq(b).remove(),c=this.panels.eq(b).remove(); -if(a.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(b+(b+1=b?--h:h});this._tabify();this._trigger("remove",null,this._ui(a.find("a")[0],c[0]));return this},enable:function(b){b=this._getIndex(b);var e=this.options;if(d.inArray(b,e.disabled)!=-1){this.lis.eq(b).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(a){return a!=b});this._trigger("enable",null, -this._ui(this.anchors[b],this.panels[b]));return this}},disable:function(b){b=this._getIndex(b);var e=this.options;if(b!=e.selected){this.lis.eq(b).addClass("ui-state-disabled");e.disabled.push(b);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[b],this.panels[b]))}return this},select:function(b){b=this._getIndex(b);if(b==-1)if(this.options.collapsible&&this.options.selected!=-1)b=this.options.selected;else return this;this.anchors.eq(b).trigger(this.options.event+".tabs");return this}, -load:function(b){b=this._getIndex(b);var e=this,a=this.options,c=this.anchors.eq(b)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(b).addClass("ui-state-processing");if(a.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(a.spinner)}this.xhr=d.ajax(d.extend({},a.ajaxOptions,{url:h,success:function(k,n){e.element.find(e._sanitizeSelector(c.hash)).html(k);e._cleanup();a.cache&&d.data(c, -"cache.tabs",true);e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[b],e.panels[b]));try{a.ajaxOptions.error(k,n,b,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this}, -url:function(b,e){this.anchors.eq(b).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.16"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(b,e){var a=this,c=this.options,h=a._rotate||(a._rotate=function(j){clearTimeout(a.rotation);a.rotation=setTimeout(function(){var k=c.selected;a.select(++k
      '))}function N(a){return a.bind("mouseout", -function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");b.length&&b.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(b){b=d(b.target).closest("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a");if(!(d.datepicker._isDisabledDatepicker(J.inline?a.parent()[0]:J.input[0])||!b.length)){b.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); -b.addClass("ui-state-hover");b.hasClass("ui-datepicker-prev")&&b.addClass("ui-datepicker-prev-hover");b.hasClass("ui-datepicker-next")&&b.addClass("ui-datepicker-next-hover")}})}function H(a,b){d.extend(a,b);for(var c in b)if(b[c]==null||b[c]==C)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.16"}});var B=(new Date).getTime(),J;d.extend(M.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv}, -setDefaults:function(a){H(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]=f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_-])/g, -"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:N(d('
      '))}},_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker", -function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b);b.settings.disabled&&this._disableDatepicker(a)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&&b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c== -"focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f==""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker(): -d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a, -b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b),true);this._updateDatepicker(b);this._updateAlternate(b);b.settings.disabled&&this._disableDatepicker(a);b.dpDiv.css("display","block")}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+= -1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}H(a.settings,e||{});b=b&&b.constructor==Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/ -2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]);d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b= -d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}},_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e= -a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().removeClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b=d(a),c=d.data(a, -"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span"){b=b.children("."+this._inlineClass);b.children().addClass("ui-state-disabled");b.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=d.map(this._disabledInputs,function(f){return f== -a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false;for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target||a;if(a.nodeName.toLowerCase()!="input")a=d("input", -a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);if(d.datepicker._curInst&&d.datepicker._curInst!=b){d.datepicker._datepickerShowing&&d.datepicker._triggerOnClose(d.datepicker._curInst);d.datepicker._curInst.dpDiv.stop(true,true)}var c=d.datepicker._get(b,"beforeShow");c=c?c.apply(a,[a,b]):{};if(c!==false){H(b.settings,c);b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value= -"";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a);d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.empty();b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b); -c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&&d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){var i=b.dpDiv.find("iframe.ui-datepicker-cover");if(i.length){var g=d.datepicker._getBorders(b.dpDiv);i.css({left:-g[0],top:-g[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex(d(a).zIndex()+1);d.datepicker._datepickerShowing= -true;d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f,h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}}},_updateDatepicker:function(a){this.maxRows=4;var b=d.datepicker._getBorders(a.dpDiv);J=a;a.dpDiv.empty().append(this._generateHTML(a));var c=a.dpDiv.find("iframe.ui-datepicker-cover");c.length&&c.css({left:-b[0],top:-b[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}); -a.dpDiv.find("."+this._dayOverClass+" a").mouseover();b=this._getNumberOfMonths(a);c=b[1];a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");c>1&&a.dpDiv.addClass("ui-datepicker-multi-"+c).css("width",17*c+"em");a.dpDiv[(b[0]!=1||b[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl");a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&& -!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var e=a.yearshtml;setTimeout(function(){e===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml);e=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(), -h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(),j=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>j&&j>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b= -this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1||d.expr.filters.hidden(a));)a=a[b?"previousSibling":"nextSibling"];a=d(a).offset();return[a.left,a.top]},_triggerOnClose:function(a){var b=this._get(a,"onClose");if(b)b.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a])},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b); -this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();d.datepicker._triggerOnClose(b);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")}, -_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&&!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"): -0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth;b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e["selected"+(c=="M"? -"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a=d(a); -this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a,"altField"); -if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"? -b.toString():b+"";if(b=="")return null;var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;e=typeof e!="string"?e:(new Date).getFullYear()%100+parseInt(e,10);for(var f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,j=c=-1,l=-1,u=-1,k=false,o=function(p){(p=A+1-1){j=1;l=u;do{e=this._getDaysInMonth(c,j-1);if(l<=e)break;j++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,j-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=j||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd", -COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames: -null)||this._defaults.monthNames;var i=function(o){(o=k+1 -12?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e?"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&& -a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),j=this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay? -new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),k=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=k&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a,"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-j,1)),this._getFormatConfig(a)); -n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+n+"";var s=this._get(a,"nextText");s=!h?s:this.formatDate(s,this._daylightSavingAdjust(new Date(m, -g+j,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+s+"":f?"":''+s+"";j=this._get(a,"currentText");s=this._get(a,"gotoCurrent")&& -a.currentDay?u:b;j=!h?j:this.formatDate(j,s,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
      '+(c?h:"")+(this._isInRange(a,s)?'":"")+(c?"":h)+"
      ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;j=this._get(a,"showWeek");s=this._get(a,"dayNames");this._get(a,"dayNamesShort");var q=this._get(a,"dayNamesMin"),A=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),D=this._get(a,"showOtherMonths"),K=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var E=this._getDefaultDate(a),w="",x=0;x1)switch(G){case 0:y+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:y+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:y+=" ui-datepicker-group-middle";t="";break}y+='">'}y+='
      '+(/all|left/.test(t)&& -x==0?c?f:n:"")+(/all|right/.test(t)&&x==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,k,o,x>0||G>0,A,v)+'
      ';var z=j?'":"";for(t=0;t<7;t++){var r=(t+h)%7;z+="=5?' class="ui-datepicker-week-end"':"")+'>'+q[r]+""}y+=z+"";z=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, -z);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;z=Math.ceil((t+z)/7);this.maxRows=z=l?this.maxRows>z?this.maxRows:z:z;r=this._daylightSavingAdjust(new Date(m,g,1-t));for(var Q=0;Q";var R=!j?"":'";for(t=0;t<7;t++){var I=p?p.apply(a.input?a.input[0]:null,[r]):[true,""],F=r.getMonth()!=g,L=F&&!K||!I[0]||k&&ro;R+='";r.setDate(r.getDate()+1);r=this._daylightSavingAdjust(r)}y+=R+""}g++;if(g>11){g=0;m++}y+="
      '+this._get(a,"weekHeader")+"
      '+this._get(a,"calculateWeek")(r)+""+(F&&!D?" ":L?''+ -r.getDate()+"":''+r.getDate()+"")+"
      "+(l?""+(i[0]>0&&G==i[1]-1?'
      ':""):"");O+=y}w+=O}w+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': -"");a._keyEvent=false;return w},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var j=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),k='
      ',o="";if(h||!j)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(k+=o+(h||!(j&&l)?" ":""));if(!a.yearshtml){a.yearshtml="";if(h||!l)k+=''+c+"";else{g=this._get(a,"yearRange").split(":");var s=(new Date).getFullYear();i=function(q){q=q.match(/c[+-].*/)?c+parseInt(q.substring(1),10):q.match(/[+-].*/)?s+parseInt(q,10):parseInt(q,10);return isNaN(q)?s:q};b=i(g[0]);g=Math.max(b,i(g[1]||""));b=e?Math.max(b, -e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(a.yearshtml+='";k+=a.yearshtml;a.yearshtml=null}}k+=this._get(a,"yearSuffix");if(u)k+=(h||!(j&&l)?" ":"")+o;k+="
      ";return k},_adjustInstDate:function(a,b,c){var e=a.drawYear+(c=="Y"?b:0),f=a.drawMonth+ -(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");if(b)b.apply(a.input? -a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a);c=this._daylightSavingAdjust(new Date(c, -e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a, -"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker=function(a){if(!this.length)return this; -if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));return this.each(function(){typeof a== -"string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new M;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.16";window["DP_jQuery_"+B]=d})(jQuery); -;/* - * jQuery UI Progressbar 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Progressbar - * - * Depends: - * jquery.ui.core.js - * jquery.ui.widget.js - */ -(function(b,d){b.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()});this.valueDiv=b("
      ").appendTo(this.element);this.oldValue=this._value();this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); -this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===d)return this._value();this._setOption("value",a);return this},_setOption:function(a,c){if(a==="value"){this.options.value=c;this._refreshValue();this._value()===this.options.max&&this._trigger("complete")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100* -this._value()/this.options.max},_refreshValue:function(){var a=this.value(),c=this._percentage();if(this.oldValue!==a){this.oldValue=a;this._trigger("change")}this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(c.toFixed(0)+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.16"})})(jQuery); -;/* - * jQuery UI Effects 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/ - */ -jQuery.effects||function(f,j){function m(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1], -16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return n.transparent;return n[f.trim(c).toLowerCase()]}function s(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return m(b)}function o(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle, -a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function p(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in t||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function u(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d= -a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}function l(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects[c])return true;return false}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor", -"borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=s(b.elem,a);b.end=m(b.end);b.colorInit=true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0, -0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211, -211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b, -d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})}; -f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this, -[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.16",save:function(c,a){for(var b=0;b").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}), -d=document.activeElement;c.wrap(b);if(c[0]===d||f.contains(c[0],d))f(d).focus();b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(e,g){a[g]=c.css(g);if(isNaN(parseInt(a[g],10)))a[g]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){var a,b=document.activeElement; -if(c.parent().is(".ui-effects-wrapper")){a=c.parent().replaceWith(c);if(c[0]===b||f.contains(c[0],b))f(b).focus();return a}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)}); -return d.call(this,b)},_show:f.fn.show,show:function(c){if(l(c))return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(l(c))return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(l(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this, -arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/ -2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b, -d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c, -a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b, -d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ -e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); -;/* - * jQuery UI Effects Fade 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fade - * - * Depends: - * jquery.effects.core.js - */ -(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); -;/* - * jQuery UI Effects Fold 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Fold - * - * Depends: - * jquery.effects.core.js - */ -(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","bottom","left","right"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1], -10)/100*f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); -;/* - * jQuery UI Effects Highlight 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Highlight - * - * Depends: - * jquery.effects.core.js - */ -(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& -this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); -;/* - * jQuery UI Effects Pulsate 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Effects/Pulsate - * - * Depends: - * jquery.effects.core.js - */ -(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); -b.dequeue()})})}})(jQuery); -; \ No newline at end of file diff --git a/lms/static/js/jquery-ui.min.js b/lms/static/js/jquery-ui.min.js new file mode 100755 index 0000000000..3fe9ccb7b7 --- /dev/null +++ b/lms/static/js/jquery-ui.min.js @@ -0,0 +1,125 @@ +/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.core.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;return!b.href||!g||f.nodeName.toLowerCase()!=="map"?!1:(h=a("img[usemap=#"+g+"]")[0],!!h&&d(h))}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}a.ui=a.ui||{};if(a.ui.version)return;a.extend(a.ui,{version:"1.8.21",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;return a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0),/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a.each(["Width","Height"],function(c,d){function h(b,c,d,f){return a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)}),c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){return c===b?g["inner"+d].call(this):this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){return typeof b!="number"?g["outer"+d].call(this,b):this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!d||!a.element[0].parentNode)return;for(var e=0;e0?!0:(b[d]=1,e=b[d]>0,b[d]=0,e)},isOverAxis:function(a,b,c){return a>b&&a=9||!!b.button?this._mouseStarted?(this._mouseDrag(b),b.preventDefault()):(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,b)!==!1,this._mouseStarted?this._mouseDrag(b):this._mouseUp(b)),!this._mouseStarted):this._mouseUp(b)},_mouseUp:function(b){return a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,b.target==this._mouseDownEvent.target&&a.data(b.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(b)),!1},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(a){return this.mouseDelayMet},_mouseStart:function(a){},_mouseDrag:function(a){},_mouseStop:function(a){},_mouseCapture:function(a){return!0}})})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.position.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.ui=a.ui||{};var c=/left|center|right/,d=/top|center|bottom/,e="center",f={},g=a.fn.position,h=a.fn.offset;a.fn.position=function(b){if(!b||!b.of)return g.apply(this,arguments);b=a.extend({},b);var h=a(b.of),i=h[0],j=(b.collision||"flip").split(" "),k=b.offset?b.offset.split(" "):[0,0],l,m,n;return i.nodeType===9?(l=h.width(),m=h.height(),n={top:0,left:0}):i.setTimeout?(l=h.width(),m=h.height(),n={top:h.scrollTop(),left:h.scrollLeft()}):i.preventDefault?(b.at="left top",l=m=0,n={top:b.of.pageY,left:b.of.pageX}):(l=h.outerWidth(),m=h.outerHeight(),n=h.offset()),a.each(["my","at"],function(){var a=(b[this]||"").split(" ");a.length===1&&(a=c.test(a[0])?a.concat([e]):d.test(a[0])?[e].concat(a):[e,e]),a[0]=c.test(a[0])?a[0]:e,a[1]=d.test(a[1])?a[1]:e,b[this]=a}),j.length===1&&(j[1]=j[0]),k[0]=parseInt(k[0],10)||0,k.length===1&&(k[1]=k[0]),k[1]=parseInt(k[1],10)||0,b.at[0]==="right"?n.left+=l:b.at[0]===e&&(n.left+=l/2),b.at[1]==="bottom"?n.top+=m:b.at[1]===e&&(n.top+=m/2),n.left+=k[0],n.top+=k[1],this.each(function(){var c=a(this),d=c.outerWidth(),g=c.outerHeight(),h=parseInt(a.curCSS(this,"marginLeft",!0))||0,i=parseInt(a.curCSS(this,"marginTop",!0))||0,o=d+h+(parseInt(a.curCSS(this,"marginRight",!0))||0),p=g+i+(parseInt(a.curCSS(this,"marginBottom",!0))||0),q=a.extend({},n),r;b.my[0]==="right"?q.left-=d:b.my[0]===e&&(q.left-=d/2),b.my[1]==="bottom"?q.top-=g:b.my[1]===e&&(q.top-=g/2),f.fractions||(q.left=Math.round(q.left),q.top=Math.round(q.top)),r={left:q.left-h,top:q.top-i},a.each(["left","top"],function(c,e){a.ui.position[j[c]]&&a.ui.position[j[c]][e](q,{targetWidth:l,targetHeight:m,elemWidth:d,elemHeight:g,collisionPosition:r,collisionWidth:o,collisionHeight:p,offset:k,my:b.my,at:b.at})}),a.fn.bgiframe&&c.bgiframe(),c.offset(a.extend(q,{using:b.using}))})},a.ui.position={fit:{left:function(b,c){var d=a(window),e=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft();b.left=e>0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]===e)return;var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0},top:function(b,c){if(c.at[1]===e)return;var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];return!c||!c.ownerDocument?null:b?a.isFunction(b)?this.each(function(c){a(this).offset(b.call(this,c,a(this).offset()))}):this.each(function(){a.offset.setOffset(this,b)}):h.call(this)}),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&a.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.draggable.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.widget("ui.draggable",a.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},destroy:function(){if(!this.element.data("draggable"))return;return this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options;return this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")?!1:(this.handle=this._getHandle(b),this.handle?(c.iframeFix&&a(c.iframeFix===!0?"iframe":c.iframeFix).each(function(){a('
      ').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(a(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(b){var c=this.options;return this.helper=this._createHelper(b),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),a.ui.ddmanager&&(a.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,c.cursorAt&&this._adjustOffsetFromHelper(c.cursorAt),c.containment&&this._setContainment(),this._trigger("start",b)===!1?(this._clear(),!1):(this._cacheHelperProportions(),a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this._mouseDrag(b,!0),a.ui.ddmanager&&a.ui.ddmanager.dragStart(this,b),!0)},_mouseDrag:function(b,c){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute");if(!c){var d=this._uiHash();if(this._trigger("drag",b,d)===!1)return this._mouseUp({}),!1;this.position=d.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";return a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),!1},_mouseStop:function(b){var c=!1;a.ui.ddmanager&&!this.options.dropBehaviour&&(c=a.ui.ddmanager.drop(this,b)),this.dropped&&(c=this.dropped,this.dropped=!1);var d=this.element[0],e=!1;while(d&&(d=d.parentNode))d==document&&(e=!0);if(!e&&this.options.helper==="original")return!1;if(this.options.revert=="invalid"&&!c||this.options.revert=="valid"&&c||this.options.revert===!0||a.isFunction(this.options.revert)&&this.options.revert.call(this.element,c)){var f=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){f._trigger("stop",b)!==!1&&f._clear()})}else this._trigger("stop",b)!==!1&&this._clear();return!1},_mouseUp:function(b){return this.options.iframeFix===!0&&a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),a.ui.ddmanager&&a.ui.ddmanager.dragStop(this,b),a.ui.mouse.prototype._mouseUp.call(this,b)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?!0:!1;return a(this.options.handle,this.element).find("*").andSelf().each(function(){this==b.target&&(c=!0)}),c},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b])):c.helper=="clone"?this.element.clone().removeAttr("id"):this.element;return d.parents("body").length||d.appendTo(c.appendTo=="parent"?this.element[0].parentNode:c.appendTo),d[0]!=this.element[0]&&!/(fixed|absolute)/.test(d.css("position"))&&d.css("position","absolute"),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[b.containment=="document"?0:a(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,b.containment=="document"?0:a(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(b.containment=="document"?0:a(window).scrollLeft())+a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(b.containment=="document"?0:a(window).scrollTop())+(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)&&b.containment.constructor!=Array){var c=a(b.containment),d=c[0];if(!d)return;var e=c.offset(),f=a(d).css("overflow")!="hidden";this.containment=[(parseInt(a(d).css("borderLeftWidth"),10)||0)+(parseInt(a(d).css("paddingLeft"),10)||0),(parseInt(a(d).css("borderTopWidth"),10)||0)+(parseInt(a(d).css("paddingTop"),10)||0),(f?Math.max(d.scrollWidth,d.offsetWidth):d.offsetWidth)-(parseInt(a(d).css("borderLeftWidth"),10)||0)-(parseInt(a(d).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(f?Math.max(d.scrollHeight,d.offsetHeight):d.offsetHeight)-(parseInt(a(d).css("borderTopWidth"),10)||0)-(parseInt(a(d).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=c}else b.containment.constructor==Array&&(this.containment=b.containment)},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName),f=b.pageX,g=b.pageY;if(this.originalPosition){var h;if(this.containment){if(this.relative_container){var i=this.relative_container.offset();h=[this.containment[0]+i.left,this.containment[1]+i.top,this.containment[2]+i.left,this.containment[3]+i.top]}else h=this.containment;b.pageX-this.offset.click.lefth[2]&&(f=h[2]+this.offset.click.left),b.pageY-this.offset.click.top>h[3]&&(g=h[3]+this.offset.click.top)}if(c.grid){var j=c.grid[1]?this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1]:this.originalPageY;g=h?j-this.offset.click.toph[3]?j-this.offset.click.toph[2]?k-this.offset.click.left=0;k--){var l=d.snapElements[k].left,m=l+d.snapElements[k].width,n=d.snapElements[k].top,o=n+d.snapElements[k].height;if(!(l-f=k&&g<=l||h>=k&&h<=l||gl)&&(e>=i&&e<=j||f>=i&&f<=j||ej);default:return!1}},a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(b,c){var d=a.ui.ddmanager.droppables[b.options.scope]||[],e=c?c.type:null,f=(b.currentItem||b.element).find(":data(droppable)").andSelf();g:for(var h=0;h').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("resizable",this.element.data("resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=c.handles||(a(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se");if(this.handles.constructor==String){this.handles=="all"&&(this.handles="n,e,s,w,se,sw,ne,nw");var d=this.handles.split(",");this.handles={};for(var e=0;e');h.css({zIndex:c.zIndex}),"se"==f&&h.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[f]=".ui-resizable-"+f,this.element.append(h)}}this._renderAxis=function(b){b=b||this.element;for(var c in this.handles){this.handles[c].constructor==String&&(this.handles[c]=a(this.handles[c],this.element).show());if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var d=a(this.handles[c],this.element),e=0;e=/sw|ne|nw|se|n|s/.test(c)?d.outerHeight():d.outerWidth();var f=["padding",/ne|nw|n/.test(c)?"Top":/se|sw|s/.test(c)?"Bottom":/^e$/.test(c)?"Right":"Left"].join("");b.css(f,e),this._proportionallyResize()}if(!a(this.handles[c]).length)continue}},this._renderAxis(this.element),this._handles=a(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){if(!b.resizing){if(this.className)var a=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=a&&a[1]?a[1]:"se"}}),c.autoHide&&(this._handles.hide(),a(this.element).addClass("ui-resizable-autohide").hover(function(){if(c.disabled)return;a(this).removeClass("ui-resizable-autohide"),b._handles.show()},function(){if(c.disabled)return;b.resizing||(a(this).addClass("ui-resizable-autohide"),b._handles.hide())})),this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(b){a(b).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var c=this.element;c.after(this.originalElement.css({position:c.css("position"),width:c.outerWidth(),height:c.outerHeight(),top:c.css("top"),left:c.css("left")})).remove()}return this.originalElement.css("resize",this.originalResizeStyle),b(this.originalElement),this},_mouseCapture:function(b){var c=!1;for(var d in this.handles)a(this.handles[d])[0]==b.target&&(c=!0);return!this.options.disabled&&c},_mouseStart:function(b){var d=this.options,e=this.element.position(),f=this.element;this.resizing=!0,this.documentScroll={top:a(document).scrollTop(),left:a(document).scrollLeft()},(f.is(".ui-draggable")||/absolute/.test(f.css("position")))&&f.css({position:"absolute",top:e.top,left:e.left}),this._renderProxy();var g=c(this.helper.css("left")),h=c(this.helper.css("top"));d.containment&&(g+=a(d.containment).scrollLeft()||0,h+=a(d.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:g,top:h},this.size=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalSize=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalPosition={left:g,top:h},this.sizeDiff={width:f.outerWidth()-f.width(),height:f.outerHeight()-f.height()},this.originalMousePosition={left:b.pageX,top:b.pageY},this.aspectRatio=typeof d.aspectRatio=="number"?d.aspectRatio:this.originalSize.width/this.originalSize.height||1;var i=a(".ui-resizable-"+this.axis).css("cursor");return a("body").css("cursor",i=="auto"?this.axis+"-resize":i),f.addClass("ui-resizable-resizing"),this._propagate("start",b),!0},_mouseDrag:function(b){var c=this.helper,d=this.options,e={},f=this,g=this.originalMousePosition,h=this.axis,i=b.pageX-g.left||0,j=b.pageY-g.top||0,k=this._change[h];if(!k)return!1;var l=k.apply(this,[b,i,j]),m=a.browser.msie&&a.browser.version<7,n=this.sizeDiff;this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)l=this._updateRatio(l,b);return l=this._respectSize(l,b),this._propagate("resize",b),c.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"}),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),this._updateCache(l),this._trigger("resize",b,this.ui()),!1},_mouseStop:function(b){this.resizing=!1;var c=this.options,d=this;if(this._helper){var e=this._proportionallyResizeElements,f=e.length&&/textarea/i.test(e[0].nodeName),g=f&&a.ui.hasScroll(e[0],"left")?0:d.sizeDiff.height,h=f?0:d.sizeDiff.width,i={width:d.helper.width()-h,height:d.helper.height()-g},j=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,k=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;c.animate||this.element.css(a.extend(i,{top:k,left:j})),d.helper.height(d.size.height),d.helper.width(d.size.width),this._helper&&!c.animate&&this._proportionallyResize()}return a("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",b),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(a){var b=this.options,c,e,f,g,h;h={minWidth:d(b.minWidth)?b.minWidth:0,maxWidth:d(b.maxWidth)?b.maxWidth:Infinity,minHeight:d(b.minHeight)?b.minHeight:0,maxHeight:d(b.maxHeight)?b.maxHeight:Infinity};if(this._aspectRatio||a)c=h.minHeight*this.aspectRatio,f=h.minWidth/this.aspectRatio,e=h.maxHeight*this.aspectRatio,g=h.maxWidth/this.aspectRatio,c>h.minWidth&&(h.minWidth=c),f>h.minHeight&&(h.minHeight=f),ea.width,k=d(a.height)&&e.minHeight&&e.minHeight>a.height;j&&(a.width=e.minWidth),k&&(a.height=e.minHeight),h&&(a.width=e.maxWidth),i&&(a.height=e.maxHeight);var l=this.originalPosition.left+this.originalSize.width,m=this.position.top+this.size.height,n=/sw|nw|w/.test(g),o=/nw|ne|n/.test(g);j&&n&&(a.left=l-e.minWidth),h&&n&&(a.left=l-e.maxWidth),k&&o&&(a.top=m-e.minHeight),i&&o&&(a.top=m-e.maxHeight);var p=!a.width&&!a.height;return p&&!a.left&&a.top?a.top=null:p&&!a.top&&a.left&&(a.left=null),a},_proportionallyResize:function(){var b=this.options;if(!this._proportionallyResizeElements.length)return;var c=this.helper||this.element;for(var d=0;d');var d=a.browser.msie&&a.browser.version<7,e=d?1:0,f=d?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+f,height:this.element.outerHeight()+f,position:"absolute",left:this.elementOffset.left-e+"px",top:this.elementOffset.top-e+"px",zIndex:++c.zIndex}),this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(a,b,c){return{width:this.originalSize.width+b}},w:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{left:f.left+b,width:e.width-b}},n:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{top:f.top+c,height:e.height-c}},s:function(a,b,c){return{height:this.originalSize.height+c}},se:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},sw:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,c,d]))},ne:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},nw:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,c,d]))}},_propagate:function(b,c){a.ui.plugin.call(this,b,[c,this.ui()]),b!="resize"&&this._trigger(b,c,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),a.extend(a.ui.resizable,{version:"1.8.21"}),a.ui.plugin.add("resizable","alsoResize",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=function(b){a(b).each(function(){var b=a(this);b.data("resizable-alsoresize",{width:parseInt(b.width(),10),height:parseInt(b.height(),10),left:parseInt(b.css("left"),10),top:parseInt(b.css("top"),10)})})};typeof e.alsoResize=="object"&&!e.alsoResize.parentNode?e.alsoResize.length?(e.alsoResize=e.alsoResize[0],f(e.alsoResize)):a.each(e.alsoResize,function(a){f(a)}):f(e.alsoResize)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.originalSize,g=d.originalPosition,h={height:d.size.height-f.height||0,width:d.size.width-f.width||0,top:d.position.top-g.top||0,left:d.position.left-g.left||0},i=function(b,d){a(b).each(function(){var b=a(this),e=a(this).data("resizable-alsoresize"),f={},g=d&&d.length?d:b.parents(c.originalElement[0]).length?["width","height"]:["width","height","top","left"];a.each(g,function(a,b){var c=(e[b]||0)+(h[b]||0);c&&c>=0&&(f[b]=c||null)}),b.css(f)})};typeof e.alsoResize=="object"&&!e.alsoResize.nodeType?a.each(e.alsoResize,function(a,b){i(a,b)}):i(e.alsoResize)},stop:function(b,c){a(this).removeData("resizable-alsoresize")}}),a.ui.plugin.add("resizable","animate",{stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d._proportionallyResizeElements,g=f.length&&/textarea/i.test(f[0].nodeName),h=g&&a.ui.hasScroll(f[0],"left")?0:d.sizeDiff.height,i=g?0:d.sizeDiff.width,j={width:d.size.width-i,height:d.size.height-h},k=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,l=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;d.element.animate(a.extend(j,l&&k?{top:l,left:k}:{}),{duration:e.animateDuration,easing:e.animateEasing,step:function(){var c={width:parseInt(d.element.css("width"),10),height:parseInt(d.element.css("height"),10),top:parseInt(d.element.css("top"),10),left:parseInt(d.element.css("left"),10)};f&&f.length&&a(f[0]).css({width:c.width,height:c.height}),d._updateCache(c),d._propagate("resize",b)}})}}),a.ui.plugin.add("resizable","containment",{start:function(b,d){var e=a(this).data("resizable"),f=e.options,g=e.element,h=f.containment,i=h instanceof a?h.get(0):/parent/.test(h)?g.parent().get(0):h;if(!i)return;e.containerElement=a(i);if(/document/.test(h)||h==document)e.containerOffset={left:0,top:0},e.containerPosition={left:0,top:0},e.parentData={element:a(document),left:0,top:0,width:a(document).width(),height:a(document).height()||document.body.parentNode.scrollHeight};else{var j=a(i),k=[];a(["Top","Right","Left","Bottom"]).each(function(a,b){k[a]=c(j.css("padding"+b))}),e.containerOffset=j.offset(),e.containerPosition=j.position(),e.containerSize={height:j.innerHeight()-k[3],width:j.innerWidth()-k[1]};var l=e.containerOffset,m=e.containerSize.height,n=e.containerSize.width,o=a.ui.hasScroll(i,"left")?i.scrollWidth:n,p=a.ui.hasScroll(i)?i.scrollHeight:m;e.parentData={element:i,left:l.left,top:l.top,width:o,height:p}}},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.containerSize,g=d.containerOffset,h=d.size,i=d.position,j=d._aspectRatio||b.shiftKey,k={top:0,left:0},l=d.containerElement;l[0]!=document&&/static/.test(l.css("position"))&&(k=g),i.left<(d._helper?g.left:0)&&(d.size.width=d.size.width+(d._helper?d.position.left-g.left:d.position.left-k.left),j&&(d.size.height=d.size.width/d.aspectRatio),d.position.left=e.helper?g.left:0),i.top<(d._helper?g.top:0)&&(d.size.height=d.size.height+(d._helper?d.position.top-g.top:d.position.top),j&&(d.size.width=d.size.height*d.aspectRatio),d.position.top=d._helper?g.top:0),d.offset.left=d.parentData.left+d.position.left,d.offset.top=d.parentData.top+d.position.top;var m=Math.abs((d._helper?d.offset.left-k.left:d.offset.left-k.left)+d.sizeDiff.width),n=Math.abs((d._helper?d.offset.top-k.top:d.offset.top-g.top)+d.sizeDiff.height),o=d.containerElement.get(0)==d.element.parent().get(0),p=/relative|absolute/.test(d.containerElement.css("position"));o&&p&&(m-=d.parentData.left),m+d.size.width>=d.parentData.width&&(d.size.width=d.parentData.width-m,j&&(d.size.height=d.size.width/d.aspectRatio)),n+d.size.height>=d.parentData.height&&(d.size.height=d.parentData.height-n,j&&(d.size.width=d.size.height*d.aspectRatio))},stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.position,g=d.containerOffset,h=d.containerPosition,i=d.containerElement,j=a(d.helper),k=j.offset(),l=j.outerWidth()-d.sizeDiff.width,m=j.outerHeight()-d.sizeDiff.height;d._helper&&!e.animate&&/relative/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m}),d._helper&&!e.animate&&/static/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m})}}),a.ui.plugin.add("resizable","ghost",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size;d.ghost=d.originalElement.clone(),d.ghost.css({opacity:.25,display:"block",position:"relative",height:f.height,width:f.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof e.ghost=="string"?e.ghost:""),d.ghost.appendTo(d.helper)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.ghost.css({position:"relative",height:d.size.height,width:d.size.width})},stop:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.helper&&d.helper.get(0).removeChild(d.ghost.get(0))}}),a.ui.plugin.add("resizable","grid",{resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size,g=d.originalSize,h=d.originalPosition,i=d.axis,j=e._aspectRatio||b.shiftKey;e.grid=typeof e.grid=="number"?[e.grid,e.grid]:e.grid;var k=Math.round((f.width-g.width)/(e.grid[0]||1))*(e.grid[0]||1),l=Math.round((f.height-g.height)/(e.grid[1]||1))*(e.grid[1]||1);/^(se|s|e)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l):/^(ne)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l):/^(sw)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.left=h.left-k):(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l,d.position.left=h.left-k)}});var c=function(a){return parseInt(a,10)||0},d=function(a){return!isNaN(parseInt(a,10))}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.selectable.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.widget("ui.selectable",a.ui.mouse,{options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch"},_create:function(){var b=this;this.element.addClass("ui-selectable"),this.dragged=!1;var c;this.refresh=function(){c=a(b.options.filter,b.element[0]),c.addClass("ui-selectee"),c.each(function(){var b=a(this),c=b.offset();a.data(this,"selectable-item",{element:this,$element:b,left:c.left,top:c.top,right:c.left+b.outerWidth(),bottom:c.top+b.outerHeight(),startselected:!1,selected:b.hasClass("ui-selected"),selecting:b.hasClass("ui-selecting"),unselecting:b.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=c.addClass("ui-selectee"),this._mouseInit(),this.helper=a("
      ")},destroy:function(){return this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable"),this._mouseDestroy(),this},_mouseStart:function(b){var c=this;this.opos=[b.pageX,b.pageY];if(this.options.disabled)return;var d=this.options;this.selectees=a(d.filter,this.element[0]),this._trigger("start",b),a(d.appendTo).append(this.helper),this.helper.css({left:b.clientX,top:b.clientY,width:0,height:0}),d.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var d=a.data(this,"selectable-item");d.startselected=!0,!b.metaKey&&!b.ctrlKey&&(d.$element.removeClass("ui-selected"),d.selected=!1,d.$element.addClass("ui-unselecting"),d.unselecting=!0,c._trigger("unselecting",b,{unselecting:d.element}))}),a(b.target).parents().andSelf().each(function(){var d=a.data(this,"selectable-item");if(d){var e=!b.metaKey&&!b.ctrlKey||!d.$element.hasClass("ui-selected");return d.$element.removeClass(e?"ui-unselecting":"ui-selected").addClass(e?"ui-selecting":"ui-unselecting"),d.unselecting=!e,d.selecting=e,d.selected=e,e?c._trigger("selecting",b,{selecting:d.element}):c._trigger("unselecting",b,{unselecting:d.element}),!1}})},_mouseDrag:function(b){var c=this;this.dragged=!0;if(this.options.disabled)return;var d=this.options,e=this.opos[0],f=this.opos[1],g=b.pageX,h=b.pageY;if(e>g){var i=g;g=e,e=i}if(f>h){var i=h;h=f,f=i}return this.helper.css({left:e,top:f,width:g-e,height:h-f}),this.selectees.each(function(){var i=a.data(this,"selectable-item");if(!i||i.element==c.element[0])return;var j=!1;d.tolerance=="touch"?j=!(i.left>g||i.righth||i.bottome&&i.rightf&&i.bottom *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3},_create:function(){var a=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},destroy:function(){a.Widget.prototype.destroy.call(this),this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var b=this.items.length-1;b>=0;b--)this.items[b].item.removeData(this.widgetName+"-item");return this},_setOption:function(b,c){b==="disabled"?(this.options[b]=c,this.widget()[c?"addClass":"removeClass"]("ui-sortable-disabled")):a.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(b,c){var d=this;if(this.reverting)return!1;if(this.options.disabled||this.options.type=="static")return!1;this._refreshItems(b);var e=null,f=this,g=a(b.target).parents().each(function(){if(a.data(this,d.widgetName+"-item")==f)return e=a(this),!1});a.data(b.target,d.widgetName+"-item")==f&&(e=a(b.target));if(!e)return!1;if(this.options.handle&&!c){var h=!1;a(this.options.handle,e).find("*").andSelf().each(function(){this==b.target&&(h=!0)});if(!h)return!1}return this.currentItem=e,this._removeCurrentsFromItems(),!0},_mouseStart:function(b,c,d){var e=this.options,f=this;this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(b),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,e.cursorAt&&this._adjustOffsetFromHelper(e.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!=this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),e.containment&&this._setContainment(),e.cursor&&(a("body").css("cursor")&&(this._storedCursor=a("body").css("cursor")),a("body").css("cursor",e.cursor)),e.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",e.opacity)),e.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",e.zIndex)),this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",b,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions();if(!d)for(var g=this.containers.length-1;g>=0;g--)this.containers[g]._trigger("activate",b,f._uiHash(this));return a.ui.ddmanager&&(a.ui.ddmanager.current=this),a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(b),!0},_mouseDrag:function(b){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs);if(this.options.scroll){var c=this.options,d=!1;this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-b.pageY=0;e--){var f=this.items[e],g=f.item[0],h=this._intersectsWithPointer(f);if(!h)continue;if(g!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=g&&!a.ui.contains(this.placeholder[0],g)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],g):!0)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(f))this._rearrange(b,f);else break;this._trigger("change",b,this._uiHash());break}}return this._contactContainers(b),a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),this._trigger("sort",b,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(b,c){if(!b)return;a.ui.ddmanager&&!this.options.dropBehaviour&&a.ui.ddmanager.drop(this,b);if(this.options.revert){var d=this,e=d.placeholder.offset();d.reverting=!0,a(this.helper).animate({left:e.left-this.offset.parent.left-d.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-d.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){d._clear(b)})}else this._clear(b,c);return!1},cancel:function(){var b=this;if(this.dragging){this._mouseUp({target:null}),this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("deactivate",null,b._uiHash(this)),this.containers[c].containerCache.over&&(this.containers[c]._trigger("out",null,b._uiHash(this)),this.containers[c].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),a.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?a(this.domPosition.prev).after(this.currentItem):a(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];return b=b||{},a(c).each(function(){var c=(a(b.item||this).attr(b.attribute||"id")||"").match(b.expression||/(.+)[-=_](.+)/);c&&d.push((b.key||c[1]+"[]")+"="+(b.key&&b.expression?c[1]:c[2]))}),!d.length&&b.key&&d.push(b.key+"="),d.join("&")},toArray:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];return b=b||{},c.each(function(){d.push(a(b.item||this).attr(b.attribute||"id")||"")}),d},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,d=this.positionAbs.top,e=d+this.helperProportions.height,f=a.left,g=f+a.width,h=a.top,i=h+a.height,j=this.offset.click.top,k=this.offset.click.left,l=d+j>h&&d+jf&&b+ka[this.floating?"width":"height"]?l:f0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){return this._refreshItems(a),this.refreshPositions(),this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(b){var c=this,d=[],e=[],f=this._connectWith();if(f&&b)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&e.push([a.isFunction(j.options.items)?j.options.items.call(j.element):a(j.options.items,j.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),j])}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(var g=e.length-1;g>=0;g--)e[g][0].each(function(){d.push(this)});return a(d)},_removeCurrentsFromItems:function(){var a=this.currentItem.find(":data("+this.widgetName+"-item)");for(var b=0;b=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&(e.push([a.isFunction(j.options.items)?j.options.items.call(j.element[0],b,{item:this.currentItem}):a(j.options.items,j.element),j]),this.containers.push(j))}}for(var g=e.length-1;g>=0;g--){var k=e[g][1],l=e[g][0];for(var i=0,m=l.length;i=0;c--){var d=this.items[c];if(d.instance!=this.currentContainer&&this.currentContainer&&d.item[0]!=this.currentItem[0])continue;var e=this.options.toleranceElement?a(this.options.toleranceElement,d.item):d.item;b||(d.width=e.outerWidth(),d.height=e.outerHeight());var f=e.offset();d.left=f.left,d.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(var c=this.containers.length-1;c>=0;c--){var f=this.containers[c].element.offset();this.containers[c].containerCache.left=f.left,this.containers[c].containerCache.top=f.top,this.containers[c].containerCache.width=this.containers[c].element.outerWidth(),this.containers[c].containerCache.height=this.containers[c].element.outerHeight()}return this},_createPlaceholder:function(b){var c=b||this,d=c.options;if(!d.placeholder||d.placeholder.constructor==String){var e=d.placeholder;d.placeholder={element:function(){var b=a(document.createElement(c.currentItem[0].nodeName)).addClass(e||c.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];return e||(b.style.visibility="hidden"),b},update:function(a,b){if(e&&!d.forcePlaceholderSize)return;b.height()||b.height(c.currentItem.innerHeight()-parseInt(c.currentItem.css("paddingTop")||0,10)-parseInt(c.currentItem.css("paddingBottom")||0,10)),b.width()||b.width(c.currentItem.innerWidth()-parseInt(c.currentItem.css("paddingLeft")||0,10)-parseInt(c.currentItem.css("paddingRight")||0,10))}}}c.placeholder=a(d.placeholder.element.call(c.element,c.currentItem)),c.currentItem.after(c.placeholder),d.placeholder.update(c,c.placeholder)},_contactContainers:function(b){var c=null,d=null;for(var e=this.containers.length-1;e>=0;e--){if(a.ui.contains(this.currentItem[0],this.containers[e].element[0]))continue;if(this._intersectsWith(this.containers[e].containerCache)){if(c&&a.ui.contains(this.containers[e].element[0],c.element[0]))continue;c=this.containers[e],d=e}else this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",b,this._uiHash(this)),this.containers[e].containerCache.over=0)}if(!c)return;if(this.containers.length===1)this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1;else if(this.currentContainer!=this.containers[d]){var f=1e4,g=null,h=this.positionAbs[this.containers[d].floating?"left":"top"];for(var i=this.items.length-1;i>=0;i--){if(!a.ui.contains(this.containers[d].element[0],this.items[i].item[0]))continue;var j=this.containers[d].floating?this.items[i].item.offset().left:this.items[i].item.offset().top;Math.abs(j-h)0?"down":"up")}if(!g&&!this.options.dropOnEmpty)return;this.currentContainer=this.containers[d],g?this._rearrange(b,g,null,!0):this._rearrange(b,null,this.containers[d].element,!0),this._trigger("change",b,this._uiHash()),this.containers[d]._trigger("change",b,this._uiHash(this)),this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1}},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b,this.currentItem])):c.helper=="clone"?this.currentItem.clone():this.currentItem;return d.parents("body").length||a(c.appendTo!="parent"?c.appendTo:this.currentItem[0].parentNode)[0].appendChild(d[0]),d[0]==this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(d[0].style.width==""||c.forceHelperSize)&&d.width(this.currentItem.width()),(d[0].style.height==""||c.forceHelperSize)&&d.height(this.currentItem.height()),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)){var c=a(b.containment)[0],d=a(b.containment).offset(),e=a(c).css("overflow")!="hidden";this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(e?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(e?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName);this.cssPosition=="relative"&&(this.scrollParent[0]==document||this.scrollParent[0]==this.offsetParent[0])&&(this.offset.relative=this._getRelativeOffset());var f=b.pageX,g=b.pageY;if(this.originalPosition){this.containment&&(b.pageX-this.offset.click.leftthis.containment[2]&&(f=this.containment[2]+this.offset.click.left),b.pageY-this.offset.click.top>this.containment[3]&&(g=this.containment[3]+this.offset.click.top));if(c.grid){var h=this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1];g=this.containment?h-this.offset.click.topthis.containment[3]?h-this.offset.click.topthis.containment[2]?i-this.offset.click.left=0;f--)a.ui.contains(this.containers[f].element[0],this.currentItem[0])&&!c&&(d.push(function(a){return function(b){a._trigger("receive",b,this._uiHash(this))}}.call(this,this.containers[f])),d.push(function(a){return function(b){a._trigger("update",b,this._uiHash(this))}}.call(this,this.containers[f])))}for(var f=this.containers.length-1;f>=0;f--)c||d.push(function(a){return function(b){a._trigger("deactivate",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over&&(d.push(function(a){return function(b){a._trigger("out",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over=0);this._storedCursor&&a("body").css("cursor",this._storedCursor),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex),this.dragging=!1;if(this.cancelHelperRemoval){if(!c){this._trigger("beforeStop",b,this._uiHash());for(var f=0;f li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:!1,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var b=this,c=b.options;b.running=0,b.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"),b.headers=b.element.find(c.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){if(c.disabled)return;a(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){if(c.disabled)return;a(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){if(c.disabled)return;a(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){if(c.disabled)return;a(this).removeClass("ui-state-focus")}),b.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");if(c.navigation){var d=b.element.find("a").filter(c.navigationFilter).eq(0);if(d.length){var e=d.closest(".ui-accordion-header");e.length?b.active=e:b.active=d.closest(".ui-accordion-content").prev()}}b.active=b._findActive(b.active||c.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top"),b.active.next().addClass("ui-accordion-content-active"),b._createIcons(),b.resize(),b.element.attr("role","tablist"),b.headers.attr("role","tab").bind("keydown.accordion",function(a){return b._keydown(a)}).next().attr("role","tabpanel"),b.headers.not(b.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide(),b.active.length?b.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):b.headers.eq(0).attr("tabIndex",0),a.browser.safari||b.headers.find("a").attr("tabIndex",-1),c.event&&b.headers.bind(c.event.split(" ").join(".accordion ")+".accordion",function(a){b._clickHandler.call(b,a,this),a.preventDefault()})},_createIcons:function(){var b=this.options;b.icons&&(a("").addClass("ui-icon "+b.icons.header).prependTo(this.headers),this.active.children(".ui-icon").toggleClass(b.icons.header).toggleClass(b.icons.headerSelected),this.element.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.children(".ui-icon").remove(),this.element.removeClass("ui-accordion-icons")},destroy:function(){var b=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"),this.headers.find("a").removeAttr("tabIndex"),this._destroyIcons();var c=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");return(b.autoHeight||b.fillHeight)&&c.css("height",""),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b=="active"&&this.activate(c),b=="icons"&&(this._destroyIcons(),c&&this._createIcons()),b=="disabled"&&this.headers.add(this.headers.next())[c?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(b){if(this.options.disabled||b.altKey||b.ctrlKey)return;var c=a.ui.keyCode,d=this.headers.length,e=this.headers.index(b.target),f=!1;switch(b.keyCode){case c.RIGHT:case c.DOWN:f=this.headers[(e+1)%d];break;case c.LEFT:case c.UP:f=this.headers[(e-1+d)%d];break;case c.SPACE:case c.ENTER:this._clickHandler({target:b.target},b.target),b.preventDefault()}return f?(a(b.target).attr("tabIndex",-1),a(f).attr("tabIndex",0),f.focus(),!1):!0},resize:function(){var b=this.options,c;if(b.fillSpace){if(a.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}c=this.element.parent().height(),a.browser.msie&&this.element.parent().css("overflow",d),this.headers.each(function(){c-=a(this).outerHeight(!0)}),this.headers.next().each(function(){a(this).height(Math.max(0,c-a(this).innerHeight()+a(this).height()))}).css("overflow","auto")}else b.autoHeight&&(c=0,this.headers.next().each(function(){c=Math.max(c,a(this).height("").height())}).height(c));return this},activate:function(a){this.options.active=a;var b=this._findActive(a)[0];return this._clickHandler({target:b},b),this},_findActive:function(b){return b?typeof b=="number"?this.headers.filter(":eq("+b+")"):this.headers.not(this.headers.not(b)):b===!1?a([]):this.headers.filter(":eq(0)")},_clickHandler:function(b,c){var d=this.options;if(d.disabled)return;if(!b.target){if(!d.collapsible)return;this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),this.active.next().addClass("ui-accordion-content-active");var e=this.active.next(),f={options:d,newHeader:a([]),oldHeader:d.active,newContent:a([]),oldContent:e},g=this.active=a([]);this._toggle(g,e,f);return}var h=a(b.currentTarget||c),i=h[0]===this.active[0];d.active=d.collapsible&&i?!1:this.headers.index(h);if(this.running||!d.collapsible&&i)return;var j=this.active,g=h.next(),e=this.active.next(),f={options:d,newHeader:i&&d.collapsible?a([]):h,oldHeader:this.active,newContent:i&&d.collapsible?a([]):g,oldContent:e},k=this.headers.index(this.active[0])>this.headers.index(h[0]);this.active=i?a([]):h,this._toggle(g,e,f,i,k),j.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),i||(h.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected),h.next().addClass("ui-accordion-content-active"));return},_toggle:function(b,c,d,e,f){var g=this,h=g.options;g.toShow=b,g.toHide=c,g.data=d;var i=function(){if(!g)return;return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data),g.running=c.size()===0?b.size():c.size();if(h.animated){var j={};h.collapsible&&e?j={toShow:a([]),toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace}:j={toShow:b,toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace},h.proxied||(h.proxied=h.animated),h.proxiedDuration||(h.proxiedDuration=h.duration),h.animated=a.isFunction(h.proxied)?h.proxied(j):h.proxied,h.duration=a.isFunction(h.proxiedDuration)?h.proxiedDuration(j):h.proxiedDuration;var k=a.ui.accordion.animations,l=h.duration,m=h.animated;m&&!k[m]&&!a.easing[m]&&(m="slide"),k[m]||(k[m]=function(a){this.slide(a,{easing:m,duration:l||700})}),k[m](j)}else h.collapsible&&e?b.toggle():(c.hide(),b.show()),i(!0);c.prev().attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).blur(),b.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(this.running)return;this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""}),this.toHide.removeClass("ui-accordion-content-active"),this.toHide.length&&(this.toHide.parent()[0].className=this.toHide.parent()[0].className),this._trigger("change",null,this.data)}}),a.extend(a.ui.accordion,{version:"1.8.21",animations:{slide:function(b,c){b=a.extend({easing:"swing",duration:300},b,c);if(!b.toHide.size()){b.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},b);return}if(!b.toShow.size()){b.toHide.animate({height:"hide",paddingTop:"hide",paddingBottom:"hide"},b);return}var d=b.toShow.css("overflow"),e=0,f={},g={},h=["height","paddingTop","paddingBottom"],i,j=b.toShow;i=j[0].style.width,j.width(j.parent().width()-parseFloat(j.css("paddingLeft"))-parseFloat(j.css("paddingRight"))-(parseFloat(j.css("borderLeftWidth"))||0)-(parseFloat(j.css("borderRightWidth"))||0)),a.each(h,function(c,d){g[d]="hide";var e=(""+a.css(b.toShow[0],d)).match(/^([\d+-.]+)(.*)$/);f[d]={value:e[1],unit:e[2]||"px"}}),b.toShow.css({height:0,overflow:"hidden"}).show(),b.toHide.filter(":hidden").each(b.complete).end().filter(":visible").animate(g,{step:function(a,c){c.prop=="height"&&(e=c.end-c.start===0?0:(c.now-c.start)/(c.end-c.start)),b.toShow[0].style[c.prop]=e*f[c.prop].value+f[c.prop].unit},duration:b.duration,easing:b.easing,complete:function(){b.autoHeight||b.toShow.css("height",""),b.toShow.css({width:i,overflow:d}),b.complete()}})},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1e3:200})}}})})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.autocomplete.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){var c=0;a.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var b=this,c=this.element[0].ownerDocument,d;this.isMultiLine=this.element.is("textarea"),this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(b.options.disabled||b.element.propAttr("readOnly"))return;d=!1;var e=a.ui.keyCode;switch(c.keyCode){case e.PAGE_UP:b._move("previousPage",c);break;case e.PAGE_DOWN:b._move("nextPage",c);break;case e.UP:b._keyEvent("previous",c);break;case e.DOWN:b._keyEvent("next",c);break;case e.ENTER:case e.NUMPAD_ENTER:b.menu.active&&(d=!0,c.preventDefault());case e.TAB:if(!b.menu.active)return;b.menu.select(c);break;case e.ESCAPE:b.element.val(b.term),b.close(c);break;default:clearTimeout(b.searching),b.searching=setTimeout(function(){b.term!=b.element.val()&&(b.selectedItem=null,b.search(null,c))},b.options.delay)}}).bind("keypress.autocomplete",function(a){d&&(d=!1,a.preventDefault())}).bind("focus.autocomplete",function(){if(b.options.disabled)return;b.selectedItem=null,b.previous=b.element.val()}).bind("blur.autocomplete",function(a){if(b.options.disabled)return;clearTimeout(b.searching),b.closing=setTimeout(function(){b.close(a),b._change(a)},150)}),this._initSource(),this.menu=a("
        ").addClass("ui-autocomplete").appendTo(a(this.options.appendTo||"body",c)[0]).mousedown(function(c){var d=b.menu.element[0];a(c.target).closest(".ui-menu-item").length||setTimeout(function(){a(document).one("mousedown",function(c){c.target!==b.element[0]&&c.target!==d&&!a.ui.contains(d,c.target)&&b.close()})},1),setTimeout(function(){clearTimeout(b.closing)},13)}).menu({focus:function(a,c){var d=c.item.data("item.autocomplete");!1!==b._trigger("focus",a,{item:d})&&/^key/.test(a.originalEvent.type)&&b.element.val(d.value)},selected:function(a,d){var e=d.item.data("item.autocomplete"),f=b.previous;b.element[0]!==c.activeElement&&(b.element.focus(),b.previous=f,setTimeout(function(){b.previous=f,b.selectedItem=e},1)),!1!==b._trigger("select",a,{item:e})&&b.element.val(e.value),b.term=b.element.val(),b.close(a),b.selectedItem=e},blur:function(a,c){b.menu.element.is(":visible")&&b.element.val()!==b.term&&b.element.val(b.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"),a.fn.bgiframe&&this.menu.element.bgiframe(),b.beforeunloadHandler=function(){b.element.removeAttr("autocomplete")},a(window).bind("beforeunload",b.beforeunloadHandler)},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"),this.menu.element.remove(),a(window).unbind("beforeunload",this.beforeunloadHandler),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b==="source"&&this._initSource(),b==="appendTo"&&this.menu.element.appendTo(a(c||"body",this.element[0].ownerDocument)[0]),b==="disabled"&&c&&this.xhr&&this.xhr.abort()},_initSource:function(){var b=this,c,d;a.isArray(this.options.source)?(c=this.options.source,this.source=function(b,d){d(a.ui.autocomplete.filter(c,b.term))}):typeof this.options.source=="string"?(d=this.options.source,this.source=function(c,e){b.xhr&&b.xhr.abort(),b.xhr=a.ajax({url:d,data:c,dataType:"json",success:function(a,b){e(a)},error:function(){e([])}})}):this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val(),this.term=this.element.val();if(a.length").data("item.autocomplete",c).append(a("").text(c.label)).appendTo(b)},_move:function(a,b){if(!this.menu.element.is(":visible")){this.search(null,b);return}if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term),this.menu.deactivate();return}this.menu[a](b)},widget:function(){return this.menu.element},_keyEvent:function(a,b){if(!this.isMultiLine||this.menu.element.is(":visible"))this._move(a,b),b.preventDefault()}}),a.extend(a.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},filter:function(b,c){var d=new RegExp(a.ui.autocomplete.escapeRegex(c),"i");return a.grep(b,function(a){return d.test(a.label||a.value||a)})}})})(jQuery),function(a){a.widget("ui.menu",{_create:function(){var b=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(c){if(!a(c.target).closest(".ui-menu-item a").length)return;c.preventDefault(),b.select(c)}),this.refresh()},refresh:function(){var b=this,c=this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem");c.children("a").addClass("ui-corner-all").attr("tabindex",-1).mouseenter(function(c){b.activate(c,a(this).parent())}).mouseleave(function(){b.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.scrollTop(),e=this.element.height();c<0?this.element.scrollTop(d+c):c>=e&&this.element.scrollTop(d+c-e+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end(),this._trigger("focus",a,{item:b})},deactivate:function(){if(!this.active)return;this.active.children("a").removeClass("ui-state-hover").removeAttr("id"),this._trigger("blur"),this.active=null},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(!this.active){this.activate(c,this.element.children(b));return}var d=this.active[a+"All"](".ui-menu-item").eq(0);d.length?this.activate(c,d):this.activate(c,this.element.children(b))},nextPage:function(b){if(this.hasScroll()){if(!this.active||this.last()){this.activate(b,this.element.children(".ui-menu-item:first"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c-d+a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:last")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.last()?":first":":last"))},previousPage:function(b){if(this.hasScroll()){if(!this.active||this.first()){this.activate(b,this.element.children(".ui-menu-item:last"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c+d-a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:first")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.first()?":last":":first"))},hasScroll:function(){return this.element.height()
        ",this.element[0].ownerDocument).addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary,f=[];d.primary||d.secondary?(this.options.text&&f.push("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary")),d.primary&&b.prepend(""),d.secondary&&b.append(""),this.options.text||(f.push(e?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||b.attr("title",c))):f.push("ui-button-text-only"),b.addClass(f.join(" "))}}),a.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c),a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var b=this.element.css("direction")==="rtl";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(b?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(b?"ui-corner-left":"ui-corner-right").end().end()},destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"),a.Widget.prototype.destroy.call(this)}})})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.dialog.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){var c="ui-dialog ui-widget ui-widget-content ui-corner-all ",d={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},e={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},f=a.attrFn||{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0,click:!0};a.widget("ui.dialog",{options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",collision:"fit",using:function(b){var c=a(this).css(b).offset().top;c<0&&a(this).css("top",b.top-c)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.options.title=this.options.title||this.originalTitle;var b=this,d=b.options,e=d.title||" ",f=a.ui.dialog.getTitleId(b.element),g=(b.uiDialog=a("
        ")).appendTo(document.body).hide().addClass(c+d.dialogClass).css({zIndex:d.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(c){d.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}).attr({role:"dialog","aria-labelledby":f}).mousedown(function(a){b.moveToTop(!1,a)}),h=b.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g),i=(b.uiDialogTitlebar=a("
        ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),j=a('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){j.addClass("ui-state-hover")},function(){j.removeClass("ui-state-hover")}).focus(function(){j.addClass("ui-state-focus")}).blur(function(){j.removeClass("ui-state-focus")}).click(function(a){return b.close(a),!1}).appendTo(i),k=(b.uiDialogTitlebarCloseText=a("")).addClass("ui-icon ui-icon-closethick").text(d.closeText).appendTo(j),l=a("").addClass("ui-dialog-title").attr("id",f).html(e).prependTo(i);a.isFunction(d.beforeclose)&&!a.isFunction(d.beforeClose)&&(d.beforeClose=d.beforeclose),i.find("*").add(i).disableSelection(),d.draggable&&a.fn.draggable&&b._makeDraggable(),d.resizable&&a.fn.resizable&&b._makeResizable(),b._createButtons(d.buttons),b._isOpen=!1,a.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;return a.overlay&&a.overlay.destroy(),a.uiDialog.hide(),a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),a.uiDialog.remove(),a.originalTitle&&a.element.attr("title",a.originalTitle),a},widget:function(){return this.uiDialog},close:function(b){var c=this,d,e;if(!1===c._trigger("beforeClose",b))return;return c.overlay&&c.overlay.destroy(),c.uiDialog.unbind("keypress.ui-dialog"),c._isOpen=!1,c.options.hide?c.uiDialog.hide(c.options.hide,function(){c._trigger("close",b)}):(c.uiDialog.hide(),c._trigger("close",b)),a.ui.dialog.overlay.resize(),c.options.modal&&(d=0,a(".ui-dialog").each(function(){this!==c.uiDialog[0]&&(e=a(this).css("z-index"),isNaN(e)||(d=Math.max(d,e)))}),a.ui.dialog.maxZ=d),c},isOpen:function(){return this._isOpen},moveToTop:function(b,c){var d=this,e=d.options,f;return e.modal&&!b||!e.stack&&!e.modal?d._trigger("focus",c):(e.zIndex>a.ui.dialog.maxZ&&(a.ui.dialog.maxZ=e.zIndex),d.overlay&&(a.ui.dialog.maxZ+=1,d.overlay.$el.css("z-index",a.ui.dialog.overlay.maxZ=a.ui.dialog.maxZ)),f={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()},a.ui.dialog.maxZ+=1,d.uiDialog.css("z-index",a.ui.dialog.maxZ),d.element.attr(f),d._trigger("focus",c),d)},open:function(){if(this._isOpen)return;var b=this,c=b.options,d=b.uiDialog;return b.overlay=c.modal?new a.ui.dialog.overlay(b):null,b._size(),b._position(c.position),d.show(c.show),b.moveToTop(!0),c.modal&&d.bind("keydown.ui-dialog",function(b){if(b.keyCode!==a.ui.keyCode.TAB)return;var c=a(":tabbable",this),d=c.filter(":first"),e=c.filter(":last");if(b.target===e[0]&&!b.shiftKey)return d.focus(1),!1;if(b.target===d[0]&&b.shiftKey)return e.focus(1),!1}),a(b.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus(),b._isOpen=!0,b._trigger("open"),b},_createButtons:function(b){var c=this,d=!1,e=a("
        ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=a("
        ").addClass("ui-dialog-buttonset").appendTo(e);c.uiDialog.find(".ui-dialog-buttonpane").remove(),typeof b=="object"&&b!==null&&a.each(b,function(){return!(d=!0)}),d&&(a.each(b,function(b,d){d=a.isFunction(d)?{click:d,text:b}:d;var e=a('').click(function(){d.click.apply(c.element[0],arguments)}).appendTo(g);a.each(d,function(a,b){if(a==="click")return;a in f?e[a](b):e.attr(a,b)}),a.fn.button&&e.button()}),e.appendTo(c.uiDialog))},_makeDraggable:function(){function f(a){return{position:a.position,offset:a.offset}}var b=this,c=b.options,d=a(document),e;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(d,g){e=c.height==="auto"?"auto":a(this).height(),a(this).height(a(this).height()).addClass("ui-dialog-dragging"),b._trigger("dragStart",d,f(g))},drag:function(a,c){b._trigger("drag",a,f(c))},stop:function(g,h){c.position=[h.position.left-d.scrollLeft(),h.position.top-d.scrollTop()],a(this).removeClass("ui-dialog-dragging").height(e),b._trigger("dragStop",g,f(h)),a.ui.dialog.overlay.resize()}})},_makeResizable:function(c){function h(a){return{originalPosition:a.originalPosition,originalSize:a.originalSize,position:a.position,size:a.size}}c=c===b?this.options.resizable:c;var d=this,e=d.options,f=d.uiDialog.css("position"),g=typeof c=="string"?c:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:g,start:function(b,c){a(this).addClass("ui-dialog-resizing"),d._trigger("resizeStart",b,h(c))},resize:function(a,b){d._trigger("resize",a,h(b))},stop:function(b,c){a(this).removeClass("ui-dialog-resizing"),e.height=a(this).height(),e.width=a(this).width(),d._trigger("resizeStop",b,h(c)),a.ui.dialog.overlay.resize()}}).css("position",f).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(b){var c=[],d=[0,0],e;if(b){if(typeof b=="string"||typeof b=="object"&&"0"in b)c=b.split?b.split(" "):[b[0],b[1]],c.length===1&&(c[1]=c[0]),a.each(["left","top"],function(a,b){+c[a]===c[a]&&(d[a]=c[a],c[a]=b)}),b={my:c.join(" "),at:c.join(" "),offset:d.join(" ")};b=a.extend({},a.ui.dialog.prototype.options.position,b)}else b=a.ui.dialog.prototype.options.position;e=this.uiDialog.is(":visible"),e||this.uiDialog.show(),this.uiDialog.css({top:0,left:0}).position(a.extend({of:window},b)),e||this.uiDialog.hide()},_setOptions:function(b){var c=this,f={},g=!1;a.each(b,function(a,b){c._setOption(a,b),a in d&&(g=!0),a in e&&(f[a]=b)}),g&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",f)},_setOption:function(b,d){var e=this,f=e.uiDialog;switch(b){case"beforeclose":b="beforeClose";break;case"buttons":e._createButtons(d);break;case"closeText":e.uiDialogTitlebarCloseText.text(""+d);break;case"dialogClass":f.removeClass(e.options.dialogClass).addClass(c+d);break;case"disabled":d?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case"draggable":var g=f.is(":data(draggable)");g&&!d&&f.draggable("destroy"),!g&&d&&e._makeDraggable();break;case"position":e._position(d);break;case"resizable":var h=f.is(":data(resizable)");h&&!d&&f.resizable("destroy"),h&&typeof d=="string"&&f.resizable("option","handles",d),!h&&d!==!1&&e._makeResizable(d);break;case"title":a(".ui-dialog-title",e.uiDialogTitlebar).html(""+(d||" "))}a.Widget.prototype._setOption.apply(e,arguments)},_size:function(){var b=this.options,c,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),b.minWidth>b.width&&(b.width=b.minWidth),c=this.uiDialog.css({height:"auto",width:b.width}).height(),d=Math.max(0,b.minHeight-c);if(b.height==="auto")if(a.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();var f=this.element.css("height","auto").height();e||this.uiDialog.hide(),this.element.height(Math.max(f,d))}else this.element.height(Math.max(b.height-c,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),a.extend(a.ui.dialog,{version:"1.8.21",uuid:0,maxZ:0,getTitleId:function(a){var b=a.attr("id");return b||(this.uuid+=1,b=this.uuid),"ui-dialog-title-"+b},overlay:function(b){this.$el=a.ui.dialog.overlay.create(b)}}),a.extend(a.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:a.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "),create:function(b){this.instances.length===0&&(setTimeout(function(){a.ui.dialog.overlay.instances.length&&a(document).bind(a.ui.dialog.overlay.events,function(b){if(a(b.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});return a.fn.bgiframe&&c.bgiframe(),this.instances.push(c),c},destroy:function(b){var c=a.inArray(b,this.instances);c!=-1&&this.oldInstances.push(this.instances.splice(c,1)[0]),this.instances.length===0&&a([document,window]).unbind(".dialog-overlay"),b.remove();var d=0;a.each(this.instances,function(){d=Math.max(d,this.css("z-index"))}),this.maxZ=d},height:function(){var b,c;return a.browser.msie&&a.browser.version<7?(b=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),c=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),b").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(d.range==="min"||d.range==="max"?" ui-slider-range-"+d.range:"")));for(var i=e.length;ic&&(f=c,g=a(this),i=b)}),c.range===!0&&this.values(1)===c.min&&(i+=1,g=a(this.handles[i])),j=this._start(b,i),j===!1?!1:(this._mouseSliding=!0,h._handleIndex=i,g.addClass("ui-state-active").focus(),k=g.offset(),l=!a(b.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:b.pageX-k.left-g.width()/2,top:b.pageY-k.top-g.height()/2-(parseInt(g.css("borderTopWidth"),10)||0)-(parseInt(g.css("borderBottomWidth"),10)||0)+(parseInt(g.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(b,i,e),this._animateOff=!0,!0))},_mouseStart:function(a){return!0},_mouseDrag:function(a){var b={x:a.pageX,y:a.pageY},c=this._normValueFromMouse(b);return this._slide(a,this._handleIndex,c),!1},_mouseStop:function(a){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(a,this._handleIndex),this._change(a,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b,c,d,e,f;return this.orientation==="horizontal"?(b=this.elementSize.width,c=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(b=this.elementSize.height,c=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),d=c/b,d>1&&(d=1),d<0&&(d=0),this.orientation==="vertical"&&(d=1-d),e=this._valueMax()-this._valueMin(),f=this._valueMin()+d*e,this._trimAlignValue(f)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};return this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("start",a,c)},_slide:function(a,b,c){var d,e,f;this.options.values&&this.options.values.length?(d=this.values(b?0:1),this.options.values.length===2&&this.options.range===!0&&(b===0&&c>d||b===1&&c1){this.options.values[b]=this._trimAlignValue(c),this._refreshValue(),this._change(null,b);return}if(!arguments.length)return this._values();if(!a.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(b):this.value();d=this.options.values,e=arguments[0];for(f=0;f=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b,d=a-c;return Math.abs(c)*2>=b&&(d+=c>0?b:-b),parseFloat(d.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var b=this.options.range,c=this.options,d=this,e=this._animateOff?!1:c.animate,f,g={},h,i,j,k;this.options.values&&this.options.values.length?this.handles.each(function(b,i){f=(d.values(b)-d._valueMin())/(d._valueMax()-d._valueMin())*100,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",a(this).stop(1,1)[e?"animate":"css"](g,c.animate),d.options.range===!0&&(d.orientation==="horizontal"?(b===0&&d.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({width:f-h+"%"},{queue:!1,duration:c.animate})):(b===0&&d.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({height:f-h+"%"},{queue:!1,duration:c.animate}))),h=f}):(i=this.value(),j=this._valueMin(),k=this._valueMax(),f=k!==j?(i-j)/(k-j)*100:0,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",this.handle.stop(1,1)[e?"animate":"css"](g,c.animate),b==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},c.animate),b==="max"&&this.orientation==="horizontal"&&this.range[e?"animate":"css"]({width:100-f+"%"},{queue:!1,duration:c.animate}),b==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},c.animate),b==="max"&&this.orientation==="vertical"&&this.range[e?"animate":"css"]({height:100-f+"%"},{queue:!1,duration:c.animate}))}}),a.extend(a.ui.slider,{version:"1.8.21"})})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.tabs.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){function e(){return++c}function f(){return++d}var c=0,d=0;a.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:!1,cookie:null,collapsible:!1,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
        ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
      • #{label}
      • "},_create:function(){this._tabify(!0)},_setOption:function(a,b){if(a=="selected"){if(this.options.collapsible&&b==this.options.selected)return;this.select(b)}else this.options[a]=b,this._tabify()},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+e()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+f());return a.cookie.apply(null,[b].concat(a.makeArray(arguments)))},_ui:function(a,b){return{tab:a,panel:b,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=a(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(c){function m(b,c){b.css("display",""),!a.support.opacity&&c.opacity&&b[0].style.removeAttribute("filter")}var d=this,e=this.options,f=/^#.+/;this.list=this.element.find("ol,ul").eq(0),this.lis=a(" > li:has(a[href])",this.list),this.anchors=this.lis.map(function(){return a("a",this)[0]}),this.panels=a([]),this.anchors.each(function(b,c){var g=a(c).attr("href"),h=g.split("#")[0],i;h&&(h===location.toString().split("#")[0]||(i=a("base")[0])&&h===i.href)&&(g=c.hash,c.href=g);if(f.test(g))d.panels=d.panels.add(d.element.find(d._sanitizeSelector(g)));else if(g&&g!=="#"){a.data(c,"href.tabs",g),a.data(c,"load.tabs",g.replace(/#.*$/,""));var j=d._tabId(c);c.href="#"+j;var k=d.element.find("#"+j);k.length||(k=a(e.panelTemplate).attr("id",j).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(d.panels[b-1]||d.list),k.data("destroy.tabs",!0)),d.panels=d.panels.add(k)}else e.disabled.push(b)}),c?(this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"),this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.lis.addClass("ui-state-default ui-corner-top"),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom"),e.selected===b?(location.hash&&this.anchors.each(function(a,b){if(b.hash==location.hash)return e.selected=a,!1}),typeof e.selected!="number"&&e.cookie&&(e.selected=parseInt(d._cookie(),10)),typeof e.selected!="number"&&this.lis.filter(".ui-tabs-selected").length&&(e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))),e.selected=e.selected||(this.lis.length?0:-1)):e.selected===null&&(e.selected=-1),e.selected=e.selected>=0&&this.anchors[e.selected]||e.selected<0?e.selected:0,e.disabled=a.unique(e.disabled.concat(a.map(this.lis.filter(".ui-state-disabled"),function(a,b){return d.lis.index(a)}))).sort(),a.inArray(e.selected,e.disabled)!=-1&&e.disabled.splice(a.inArray(e.selected,e.disabled),1),this.panels.addClass("ui-tabs-hide"),this.lis.removeClass("ui-tabs-selected ui-state-active"),e.selected>=0&&this.anchors.length&&(d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash)).removeClass("ui-tabs-hide"),this.lis.eq(e.selected).addClass("ui-tabs-selected ui-state-active"),d.element.queue("tabs",function(){d._trigger("show",null,d._ui(d.anchors[e.selected],d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash))[0]))}),this.load(e.selected)),a(window).bind("unload",function(){d.lis.add(d.anchors).unbind(".tabs"),d.lis=d.anchors=d.panels=null})):e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")),this.element[e.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible"),e.cookie&&this._cookie(e.selected,e.cookie);for(var g=0,h;h=this.lis[g];g++)a(h)[a.inArray(g,e.disabled)!=-1&&!a(h).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");e.cache===!1&&this.anchors.removeData("cache.tabs"),this.lis.add(this.anchors).unbind(".tabs");if(e.event!=="mouseover"){var i=function(a,b){b.is(":not(.ui-state-disabled)")&&b.addClass("ui-state-"+a)},j=function(a,b){b.removeClass("ui-state-"+a)};this.lis.bind("mouseover.tabs",function(){i("hover",a(this))}),this.lis.bind("mouseout.tabs",function(){j("hover",a(this))}),this.anchors.bind("focus.tabs",function(){i("focus",a(this).closest("li"))}),this.anchors.bind("blur.tabs",function(){j("focus",a(this).closest("li"))})}var k,l;e.fx&&(a.isArray(e.fx)?(k=e.fx[0],l=e.fx[1]):k=l=e.fx);var n=l?function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.hide().removeClass("ui-tabs-hide").animate(l,l.duration||"normal",function(){m(c,l),d._trigger("show",null,d._ui(b,c[0]))})}:function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.removeClass("ui-tabs-hide"),d._trigger("show",null,d._ui(b,c[0]))},o=k?function(a,b){b.animate(k,k.duration||"normal",function(){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),m(b,k),d.element.dequeue("tabs")})}:function(a,b,c){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),d.element.dequeue("tabs")};this.anchors.bind(e.event+".tabs",function(){var b=this,c=a(b).closest("li"),f=d.panels.filter(":not(.ui-tabs-hide)"),g=d.element.find(d._sanitizeSelector(b.hash));if(c.hasClass("ui-tabs-selected")&&!e.collapsible||c.hasClass("ui-state-disabled")||c.hasClass("ui-state-processing")||d.panels.filter(":animated").length||d._trigger("select",null,d._ui(this,g[0]))===!1)return this.blur(),!1;e.selected=d.anchors.index(this),d.abort();if(e.collapsible){if(c.hasClass("ui-tabs-selected"))return e.selected=-1,e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){o(b,f)}).dequeue("tabs"),this.blur(),!1;if(!f.length)return e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this)),this.blur(),!1}e.cookie&&d._cookie(e.selected,e.cookie);if(g.length)f.length&&d.element.queue("tabs",function(){o(b,f)}),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this));else throw"jQuery UI Tabs: Mismatching fragment identifier.";a.browser.msie&&this.blur()}),this.anchors.bind("click.tabs",function(){return!1})},_getIndex:function(a){return typeof a=="string"&&(a=this.anchors.index(this.anchors.filter("[href$='"+a+"']"))),a},destroy:function(){var b=this.options;return this.abort(),this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs"),this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.anchors.each(function(){var b=a.data(this,"href.tabs");b&&(this.href=b);var c=a(this).unbind(".tabs");a.each(["href","load","cache"],function(a,b){c.removeData(b+".tabs")})}),this.lis.unbind(".tabs").add(this.panels).each(function(){a.data(this,"destroy.tabs")?a(this).remove():a(this).removeClass(["ui-state-default","ui-corner-top","ui-tabs-selected","ui-state-active","ui-state-hover","ui-state-focus","ui-state-disabled","ui-tabs-panel","ui-widget-content","ui-corner-bottom","ui-tabs-hide"].join(" "))}),b.cookie&&this._cookie(null,b.cookie),this},add:function(c,d,e){e===b&&(e=this.anchors.length);var f=this,g=this.options,h=a(g.tabTemplate.replace(/#\{href\}/g,c).replace(/#\{label\}/g,d)),i=c.indexOf("#")?this._tabId(a("a",h)[0]):c.replace("#","");h.addClass("ui-state-default ui-corner-top").data("destroy.tabs",!0);var j=f.element.find("#"+i);return j.length||(j=a(g.panelTemplate).attr("id",i).data("destroy.tabs",!0)),j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide"),e>=this.lis.length?(h.appendTo(this.list),j.appendTo(this.list[0].parentNode)):(h.insertBefore(this.lis[e]),j.insertBefore(this.panels[e])),g.disabled=a.map(g.disabled,function(a,b){return a>=e?++a:a}),this._tabify(),this.anchors.length==1&&(g.selected=0,h.addClass("ui-tabs-selected ui-state-active"),j.removeClass("ui-tabs-hide"),this.element.queue("tabs",function(){f._trigger("show",null,f._ui(f.anchors[0],f.panels[0]))}),this.load(0)),this._trigger("add",null,this._ui(this.anchors[e],this.panels[e])),this},remove:function(b){b=this._getIndex(b);var c=this.options,d=this.lis.eq(b).remove(),e=this.panels.eq(b).remove();return d.hasClass("ui-tabs-selected")&&this.anchors.length>1&&this.select(b+(b+1=b?--a:a}),this._tabify(),this._trigger("remove",null,this._ui(d.find("a")[0],e[0])),this},enable:function(b){b=this._getIndex(b);var c=this.options;if(a.inArray(b,c.disabled)==-1)return;return this.lis.eq(b).removeClass("ui-state-disabled"),c.disabled=a.grep(c.disabled,function(a,c){return a!=b}),this._trigger("enable",null,this._ui(this.anchors[b],this.panels[b])),this},disable:function(a){a=this._getIndex(a);var b=this,c=this.options;return a!=c.selected&&(this.lis.eq(a).addClass("ui-state-disabled"),c.disabled.push(a),c.disabled.sort(),this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))),this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;return this.anchors.eq(a).trigger(this.options.event+".tabs"),this},load:function(b){b=this._getIndex(b);var c=this,d=this.options,e=this.anchors.eq(b)[0],f=a.data(e,"load.tabs");this.abort();if(!f||this.element.queue("tabs").length!==0&&a.data(e,"cache.tabs")){this.element.dequeue("tabs");return}this.lis.eq(b).addClass("ui-state-processing");if(d.spinner){var g=a("span",e);g.data("label.tabs",g.html()).html(d.spinner)}return this.xhr=a.ajax(a.extend({},d.ajaxOptions,{url:f,success:function(f,g){c.element.find(c._sanitizeSelector(e.hash)).html(f),c._cleanup(),d.cache&&a.data(e,"cache.tabs",!0),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.success(f,g)}catch(h){}},error:function(a,f,g){c._cleanup(),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.error(a,f,b,e)}catch(g){}}})),c.element.dequeue("tabs"),this},abort:function(){return this.element.queue([]),this.panels.stop(!1,!0),this.element.queue("tabs",this.element.queue("tabs").splice(-2,2)),this.xhr&&(this.xhr.abort(),delete this.xhr),this._cleanup(),this},url:function(a,b){return this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",b),this},length:function(){return this.anchors.length}}),a.extend(a.ui.tabs,{version:"1.8.21"}),a.extend(a.ui.tabs.prototype,{rotation:null,rotate:function(a,b){var c=this,d=this.options,e=c._rotate||(c._rotate=function(b){clearTimeout(c.rotation),c.rotation=setTimeout(function(){var a=d.selected;c.select(++a'))}function bindHover(a){var b="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return a.bind("mouseout",function(a){var c=$(a.target).closest(b);if(!c.length)return;c.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(c){var d=$(c.target).closest(b);if($.datepicker._isDisabledDatepicker(instActive.inline?a.parent()[0]:instActive.input[0])||!d.length)return;d.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),d.addClass("ui-state-hover"),d.hasClass("ui-datepicker-prev")&&d.addClass("ui-datepicker-prev-hover"),d.hasClass("ui-datepicker-next")&&d.addClass("ui-datepicker-next-hover")})}function extendRemove(a,b){$.extend(a,b);for(var c in b)if(b[c]==null||b[c]==undefined)a[c]=b[c];return a}function isArray(a){return a&&($.browser.safari&&typeof a=="object"&&a.length||a.constructor&&a.constructor.toString().match(/\Array\(\)/))}$.extend($.ui,{datepicker:{version:"1.8.21"}});var PROP_NAME="datepicker",dpuuid=(new Date).getTime(),instActive;$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){return extendRemove(this._defaults,a||{}),this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase(),inline=nodeName=="div"||nodeName=="span";target.id||(this.uuid+=1,target.id="dp"+this.uuid);var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{}),nodeName=="input"?this._connectDatepicker(target,inst):inline&&this._inlineDatepicker(target,inst)},_newInst:function(a,b){var c=a[0].id.replace(/([^A-Za-z0-9_-])/g,"\\\\$1");return{id:c,input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:b?bindHover($('
        ')):this.dpDiv}},_connectDatepicker:function(a,b){var c=$(a);b.append=$([]),b.trigger=$([]);if(c.hasClass(this.markerClassName))return;this._attachments(c,b),c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),this._autoSize(b),$.data(a,PROP_NAME,b),b.settings.disabled&&this._disableDatepicker(a)},_attachments:function(a,b){var c=this._get(b,"appendText"),d=this._get(b,"isRTL");b.append&&b.append.remove(),c&&(b.append=$(''+c+""),a[d?"before":"after"](b.append)),a.unbind("focus",this._showDatepicker),b.trigger&&b.trigger.remove();var e=this._get(b,"showOn");(e=="focus"||e=="both")&&a.focus(this._showDatepicker);if(e=="button"||e=="both"){var f=this._get(b,"buttonText"),g=this._get(b,"buttonImage");b.trigger=$(this._get(b,"buttonImageOnly")?$("").addClass(this._triggerClass).attr({src:g,alt:f,title:f}):$('').addClass(this._triggerClass).html(g==""?f:$("").attr({src:g,alt:f,title:f}))),a[d?"before":"after"](b.trigger),b.trigger.click(function(){return $.datepicker._datepickerShowing&&$.datepicker._lastInput==a[0]?$.datepicker._hideDatepicker():$.datepicker._datepickerShowing&&$.datepicker._lastInput!=a[0]?($.datepicker._hideDatepicker(),$.datepicker._showDatepicker(a[0])):$.datepicker._showDatepicker(a[0]),!1})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var d=function(a){var b=0,c=0;for(var d=0;db&&(b=a[d].length,c=d);return c};b.setMonth(d(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort"))),b.setDate(d(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=$(a);if(c.hasClass(this.markerClassName))return;c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),$.data(a,PROP_NAME,b),this._setDate(b,this._getDefaultDate(b),!0),this._updateDatepicker(b),this._updateAlternate(b),b.settings.disabled&&this._disableDatepicker(a),b.dpDiv.css("display","block")},_dialogDatepicker:function(a,b,c,d,e){var f=this._dialogInst;if(!f){this.uuid+=1;var g="dp"+this.uuid;this._dialogInput=$(''),this._dialogInput.keydown(this._doKeyDown),$("body").append(this._dialogInput),f=this._dialogInst=this._newInst(this._dialogInput,!1),f.settings={},$.data(this._dialogInput[0],PROP_NAME,f)}extendRemove(f.settings,d||{}),b=b&&b.constructor==Date?this._formatDate(f,b):b,this._dialogInput.val(b),this._pos=e?e.length?e:[e.pageX,e.pageY]:null;if(!this._pos){var h=document.documentElement.clientWidth,i=document.documentElement.clientHeight,j=document.documentElement.scrollLeft||document.body.scrollLeft,k=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[h/2-100+j,i/2-150+k]}return this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),f.settings.onSelect=c,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),$.blockUI&&$.blockUI(this.dpDiv),$.data(this._dialogInput[0],PROP_NAME,f),this},_destroyDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();$.removeData(a,PROP_NAME),d=="input"?(c.append.remove(),c.trigger.remove(),b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):(d=="div"||d=="span")&&b.removeClass(this.markerClassName).empty()},_enableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!1,c.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().removeClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b})},_disableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!0,c.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().addClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b}),this._disabledInputs[this._disabledInputs.length]=a},_isDisabledDatepicker:function(a){if(!a)return!1;for(var b=0;b-1}},_doKeyUp:function(a){var b=$.datepicker._getInst(a.target);if(b.input.val()!=b.lastVal)try{var c=$.datepicker.parseDate($.datepicker._get(b,"dateFormat"),b.input?b.input.val():null,$.datepicker._getFormatConfig(b));c&&($.datepicker._setDateFromField(b),$.datepicker._updateAlternate(b),$.datepicker._updateDatepicker(b))}catch(d){$.datepicker.log(d)}return!0},_showDatepicker:function(a){a=a.target||a,a.nodeName.toLowerCase()!="input"&&(a=$("input",a.parentNode)[0]);if($.datepicker._isDisabledDatepicker(a)||$.datepicker._lastInput==a)return;var b=$.datepicker._getInst(a);$.datepicker._curInst&&$.datepicker._curInst!=b&&($.datepicker._curInst.dpDiv.stop(!0,!0),b&&$.datepicker._datepickerShowing&&$.datepicker._hideDatepicker($.datepicker._curInst.input[0]));var c=$.datepicker._get(b,"beforeShow"),d=c?c.apply(a,[a,b]):{};if(d===!1)return;extendRemove(b.settings,d),b.lastVal=null,$.datepicker._lastInput=a,$.datepicker._setDateFromField(b),$.datepicker._inDialog&&(a.value=""),$.datepicker._pos||($.datepicker._pos=$.datepicker._findPos(a),$.datepicker._pos[1]+=a.offsetHeight);var e=!1;$(a).parents().each(function(){return e|=$(this).css("position")=="fixed",!e}),e&&$.browser.opera&&($.datepicker._pos[0]-=document.documentElement.scrollLeft,$.datepicker._pos[1]-=document.documentElement.scrollTop);var f={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null,b.dpDiv.empty(),b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),$.datepicker._updateDatepicker(b),f=$.datepicker._checkOffset(b,f,e),b.dpDiv.css({position:$.datepicker._inDialog&&$.blockUI?"static":e?"fixed":"absolute",display:"none",left:f.left+"px",top:f.top+"px"});if(!b.inline){var g=$.datepicker._get(b,"showAnim"),h=$.datepicker._get(b,"duration"),i=function(){var a=b.dpDiv.find("iframe.ui-datepicker-cover");if(!!a.length){var c=$.datepicker._getBorders(b.dpDiv);a.css({left:-c[0],top:-c[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex($(a).zIndex()+1),$.datepicker._datepickerShowing=!0,$.effects&&$.effects[g]?b.dpDiv.show(g,$.datepicker._get(b,"showOptions"),h,i):b.dpDiv[g||"show"](g?h:null,i),(!g||!h)&&i(),b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus(),$.datepicker._curInst=b}},_updateDatepicker:function(a){var b=this;b.maxRows=4;var c=$.datepicker._getBorders(a.dpDiv);instActive=a,a.dpDiv.empty().append(this._generateHTML(a));var d=a.dpDiv.find("iframe.ui-datepicker-cover");!d.length||d.css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}),a.dpDiv.find("."+this._dayOverClass+" a").mouseover();var e=this._getNumberOfMonths(a),f=e[1],g=17;a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),f>1&&a.dpDiv.addClass("ui-datepicker-multi-"+f).css("width",g*f+"em"),a.dpDiv[(e[0]!=1||e[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi"),a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),a==$.datepicker._curInst&&$.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var h=a.yearshtml;setTimeout(function(){h===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml),h=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(a){return{thin:1,medium:2,thick:3}[a]||a};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var d=a.dpDiv.outerWidth(),e=a.dpDiv.outerHeight(),f=a.input?a.input.outerWidth():0,g=a.input?a.input.outerHeight():0,h=document.documentElement.clientWidth+$(document).scrollLeft(),i=document.documentElement.clientHeight+$(document).scrollTop();return b.left-=this._get(a,"isRTL")?d-f:0,b.left-=c&&b.left==a.input.offset().left?$(document).scrollLeft():0,b.top-=c&&b.top==a.input.offset().top+g?$(document).scrollTop():0,b.left-=Math.min(b.left,b.left+d>h&&h>d?Math.abs(b.left+d-h):0),b.top-=Math.min(b.top,b.top+e>i&&i>e?Math.abs(e+g):0),b},_findPos:function(a){var b=this._getInst(a),c=this._get(b,"isRTL");while(a&&(a.type=="hidden"||a.nodeType!=1||$.expr.filters.hidden(a)))a=a[c?"previousSibling":"nextSibling"];var d=$(a).offset();return[d.left,d.top]},_hideDatepicker:function(a){var b=this._curInst;if(!b||a&&b!=$.data(a,PROP_NAME))return;if(this._datepickerShowing){var c=this._get(b,"showAnim"),d=this._get(b,"duration"),e=function(){$.datepicker._tidyDialog(b)};$.effects&&$.effects[c]?b.dpDiv.hide(c,$.datepicker._get(b,"showOptions"),d,e):b.dpDiv[c=="slideDown"?"slideUp":c=="fadeIn"?"fadeOut":"hide"](c?d:null,e),c||e(),this._datepickerShowing=!1;var f=this._get(b,"onClose");f&&f.apply(b.input?b.input[0]:null,[b.input?b.input.val():"",b]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),$.blockUI&&($.unblockUI(),$("body").append(this.dpDiv))),this._inDialog=!1}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(!$.datepicker._curInst)return;var b=$(a.target),c=$.datepicker._getInst(b[0]);(b[0].id!=$.datepicker._mainDivId&&b.parents("#"+$.datepicker._mainDivId).length==0&&!b.hasClass($.datepicker.markerClassName)&&!b.closest("."+$.datepicker._triggerClass).length&&$.datepicker._datepickerShowing&&(!$.datepicker._inDialog||!$.blockUI)||b.hasClass($.datepicker.markerClassName)&&$.datepicker._curInst!=c)&&$.datepicker._hideDatepicker()},_adjustDate:function(a,b,c){var d=$(a),e=this._getInst(d[0]);if(this._isDisabledDatepicker(d[0]))return;this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c),this._updateDatepicker(e)},_gotoToday:function(a){var b=$(a),c=this._getInst(b[0]);if(this._get(c,"gotoCurrent")&&c.currentDay)c.selectedDay=c.currentDay,c.drawMonth=c.selectedMonth=c.currentMonth,c.drawYear=c.selectedYear=c.currentYear;else{var d=new Date;c.selectedDay=d.getDate(),c.drawMonth=c.selectedMonth=d.getMonth(),c.drawYear=c.selectedYear=d.getFullYear()}this._notifyChange(c),this._adjustDate(b)},_selectMonthYear:function(a,b,c){var d=$(a),e=this._getInst(d[0]);e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10),this._notifyChange(e),this._adjustDate(d)},_selectDay:function(a,b,c,d){var e=$(a);if($(d).hasClass(this._unselectableClass)||this._isDisabledDatepicker(e[0]))return;var f=this._getInst(e[0]);f.selectedDay=f.currentDay=$("a",d).html(),f.selectedMonth=f.currentMonth=b,f.selectedYear=f.currentYear=c,this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))},_clearDate:function(a){var b=$(a),c=this._getInst(b[0]);this._selectDate(b,"")},_selectDate:function(a,b){var c=$(a),d=this._getInst(c[0]);b=b!=null?b:this._formatDate(d),d.input&&d.input.val(b),this._updateAlternate(d);var e=this._get(d,"onSelect");e?e.apply(d.input?d.input[0]:null,[b,d]):d.input&&d.input.trigger("change"),d.inline?this._updateDatepicker(d):(this._hideDatepicker(),this._lastInput=d.input[0],typeof d.input[0]!="object"&&d.input.focus(),this._lastInput=null)},_updateAlternate:function(a){var b=this._get(a,"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),d=this._getDate(a),e=this.formatDate(c,d,this._getFormatConfig(a));$(b).each(function(){$(this).val(e)})}},noWeekends:function(a){var b=a.getDay();return[b>0&&b<6,""]},iso8601Week:function(a){var b=new Date(a.getTime());b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"?b.toString():b+"";if(b=="")return null;var d=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;d=typeof d!="string"?d:(new Date).getFullYear()%100+parseInt(d,10);var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,g=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,h=(c?c.monthNames:null)||this._defaults.monthNames,i=-1,j=-1,k=-1,l=-1,m=!1,n=function(b){var c=s+1-1){j=1,k=l;do{var u=this._getDaysInMonth(i,j-1);if(k<=u)break;j++,k-=u}while(!0)}var t=this._daylightSavingAdjust(new Date(i,j-1,k));if(t.getFullYear()!=i||t.getMonth()+1!=j||t.getDate()!=k)throw"Invalid date";return t},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1e7,formatDate:function(a,b,c){if(!b)return"";var d=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,e=(c?c.dayNames:null)||this._defaults.dayNames,f=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,h=function(b){var c=m+112?a.getHours()+2:0),a):null},_setDate:function(a,b,c){var d=!b,e=a.selectedMonth,f=a.selectedYear,g=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=g.getDate(),a.drawMonth=a.selectedMonth=a.currentMonth=g.getMonth(),a.drawYear=a.selectedYear=a.currentYear=g.getFullYear(),(e!=a.selectedMonth||f!=a.selectedYear)&&!c&&this._notifyChange(a),this._adjustInstDate(a),a.input&&a.input.val(d?"":this._formatDate(a))},_getDate:function(a){var b=!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return b},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),d=this._get(a,"showButtonPanel"),e=this._get(a,"hideIfNoPrevNext"),f=this._get(a,"navigationAsDateFormat"),g=this._getNumberOfMonths(a),h=this._get(a,"showCurrentAtPos"),i=this._get(a,"stepMonths"),j=g[0]!=1||g[1]!=1,k=this._daylightSavingAdjust(a.currentDay?new Date(a.currentYear,a.currentMonth,a.currentDay):new Date(9999,9,9)),l=this._getMinMaxDate(a,"min"),m=this._getMinMaxDate(a,"max"),n=a.drawMonth-h,o=a.drawYear;n<0&&(n+=12,o--);if(m){var p=this._daylightSavingAdjust(new Date(m.getFullYear(),m.getMonth()-g[0]*g[1]+1,m.getDate()));p=l&&pp)n--,n<0&&(n=11,o--)}a.drawMonth=n,a.drawYear=o;var q=this._get(a,"prevText");q=f?this.formatDate(q,this._daylightSavingAdjust(new Date(o,n-i,1)),this._getFormatConfig(a)):q;var r=this._canAdjustMonth(a,-1,o,n)?''+q+"":e?"":''+q+"",s=this._get(a,"nextText");s=f?this.formatDate(s,this._daylightSavingAdjust(new Date(o,n+i,1)),this._getFormatConfig(a)):s;var t=this._canAdjustMonth(a,1,o,n)?''+s+"":e?"":''+s+"",u=this._get(a,"currentText"),v=this._get(a,"gotoCurrent")&&a.currentDay?k:b;u=f?this.formatDate(u,v,this._getFormatConfig(a)):u;var w=a.inline?"":'",x=d?'
        '+(c?w:"")+(this._isInRange(a,v)?'":"")+(c?"":w)+"
        ":"",y=parseInt(this._get(a,"firstDay"),10);y=isNaN(y)?0:y;var z=this._get(a,"showWeek"),A=this._get(a,"dayNames"),B=this._get(a,"dayNamesShort"),C=this._get(a,"dayNamesMin"),D=this._get(a,"monthNames"),E=this._get(a,"monthNamesShort"),F=this._get(a,"beforeShowDay"),G=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths"),I=this._get(a,"calculateWeek")||this.iso8601Week,J=this._getDefaultDate(a),K="";for(var L=0;L1)switch(N){case 0:Q+=" ui-datepicker-group-first",P=" ui-corner-"+(c?"right":"left");break;case g[1]-1:Q+=" ui-datepicker-group-last",P=" ui-corner-"+(c?"left":"right");break;default:Q+=" ui-datepicker-group-middle",P=""}Q+='">'}Q+='
        '+(/all|left/.test(P)&&L==0?c?t:r:"")+(/all|right/.test(P)&&L==0?c?r:t:"")+this._generateMonthYearHeader(a,n,o,l,m,L>0||N>0,D,E)+'
        '+"";var R=z?'":"";for(var S=0;S<7;S++){var T=(S+y)%7;R+="=5?' class="ui-datepicker-week-end"':"")+">"+''+C[T]+""}Q+=R+"";var U=this._getDaysInMonth(o,n);o==a.selectedYear&&n==a.selectedMonth&&(a.selectedDay=Math.min(a.selectedDay,U));var V=(this._getFirstDayOfMonth(o,n)-y+7)%7,W=Math.ceil((V+U)/7),X=j?this.maxRows>W?this.maxRows:W:W;this.maxRows=X;var Y=this._daylightSavingAdjust(new Date(o,n,1-V));for(var Z=0;Z";var _=z?'":"";for(var S=0;S<7;S++){var ba=F?F.apply(a.input?a.input[0]:null,[Y]):[!0,""],bb=Y.getMonth()!=n,bc=bb&&!H||!ba[0]||l&&Ym;_+='",Y.setDate(Y.getDate()+1),Y=this._daylightSavingAdjust(Y)}Q+=_+""}n++,n>11&&(n=0,o++),Q+="
        '+this._get(a,"weekHeader")+"
        '+this._get(a,"calculateWeek")(Y)+""+(bb&&!G?" ":bc?''+Y.getDate()+"":''+Y.getDate()+"")+"
        "+(j?""+(g[0]>0&&N==g[1]-1?'
        ':""):""),M+=Q}K+=M}return K+=x+($.browser.msie&&parseInt($.browser.version,10)<7&&!a.inline?'':""),a._keyEvent=!1,K},_generateMonthYearHeader:function(a,b,c,d,e,f,g,h){var i=this._get(a,"changeMonth"),j=this._get(a,"changeYear"),k=this._get(a,"showMonthAfterYear"),l='
        ',m="";if(f||!i)m+=''+g[b]+"";else{var n=d&&d.getFullYear()==c,o=e&&e.getFullYear()==c;m+='"}k||(l+=m+(f||!i||!j?" ":""));if(!a.yearshtml){a.yearshtml="";if(f||!j)l+=''+c+"";else{var q=this._get(a,"yearRange").split(":"),r=(new Date).getFullYear(),s=function(a){var b=a.match(/c[+-].*/)?c+parseInt(a.substring(1),10):a.match(/[+-].*/)?r+parseInt(a,10):parseInt(a,10);return isNaN(b)?r:b},t=s(q[0]),u=Math.max(t,s(q[1]||""));t=d?Math.max(t,d.getFullYear()):t,u=e?Math.min(u,e.getFullYear()):u,a.yearshtml+='",l+=a.yearshtml,a.yearshtml=null}}return l+=this._get(a,"yearSuffix"),k&&(l+=(f||!i||!j?" ":"")+m),l+="
        ",l},_adjustInstDate:function(a,b,c){var d=a.drawYear+(c=="Y"?b:0),e=a.drawMonth+(c=="M"?b:0),f=Math.min(a.selectedDay,this._getDaysInMonth(d,e))+(c=="D"?b:0),g=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(d,e,f)));a.selectedDay=g.getDate(),a.drawMonth=a.selectedMonth=g.getMonth(),a.drawYear=a.selectedYear=g.getFullYear(),(c=="M"||c=="Y")&&this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max"),e=c&&bd?d:e,e},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");b&&b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){var b=this._get(a,"numberOfMonths");return b==null?[1,1]:typeof b=="number"?[1,b]:b},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,d){var e=this._getNumberOfMonths(a),f=this._daylightSavingAdjust(new Date(c,d+(b<0?b:e[0]*e[1]),1));return b<0&&f.setDate(this._getDaysInMonth(f.getFullYear(),f.getMonth())),this._isInRange(a,f)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!d||b.getTime()<=d.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");return b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10),{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,d){b||(a.currentDay=a.selectedDay,a.currentMonth=a.selectedMonth,a.currentYear=a.selectedYear);var e=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(d,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),e,this._getFormatConfig(a))}}),$.fn.datepicker=function(a){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv),$.datepicker.initialized=!0);var b=Array.prototype.slice.call(arguments,1);return typeof a!="string"||a!="isDisabled"&&a!="getDate"&&a!="widget"?a=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b)):this.each(function(){typeof a=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this].concat(b)):$.datepicker._attachDatepicker(this,a)}):$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.8.21",window["DP_jQuery_"+dpuuid]=$})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.progressbar.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()}),this.valueDiv=a("
        ").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove(),a.Widget.prototype.destroy.apply(this,arguments)},value:function(a){return a===b?this._value():(this._setOption("value",a),this)},_setOption:function(b,c){b==="value"&&(this.options.value=c,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),a.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;return typeof a!="number"&&(a=0),Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var a=this.value(),b=this._percentage();this.oldValue!==a&&(this.oldValue=a,this._trigger("change")),this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(b.toFixed(0)+"%"),this.element.attr("aria-valuenow",a)}}),a.extend(a.ui.progressbar,{version:"1.8.21"})})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.core.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +jQuery.effects||function(a,b){function c(b){var c;return b&&b.constructor==Array&&b.length==3?b:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))?[parseInt(c[1],10),parseInt(c[2],10),parseInt(c[3],10)]:(c=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))?[parseFloat(c[1])*2.55,parseFloat(c[2])*2.55,parseFloat(c[3])*2.55]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))?[parseInt(c[1],16),parseInt(c[2],16),parseInt(c[3],16)]:(c=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))?[parseInt(c[1]+c[1],16),parseInt(c[2]+c[2],16),parseInt(c[3]+c[3],16)]:(c=/rgba\(0, 0, 0, 0\)/.exec(b))?e.transparent:e[a.trim(b).toLowerCase()]}function d(b,d){var e;do{e=a.curCSS(b,d);if(e!=""&&e!="transparent"||a.nodeName(b,"body"))break;d="backgroundColor"}while(b=b.parentNode);return c(e)}function h(){var a=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,b={},c,d;if(a&&a.length&&a[0]&&a[a[0]]){var e=a.length;while(e--)c=a[e],typeof a[c]=="string"&&(d=c.replace(/\-(\w)/g,function(a,b){return b.toUpperCase()}),b[d]=a[c])}else for(c in a)typeof a[c]=="string"&&(b[c]=a[c]);return b}function i(b){var c,d;for(c in b)d=b[c],(d==null||a.isFunction(d)||c in g||/scrollbar/.test(c)||!/color/i.test(c)&&isNaN(parseFloat(d)))&&delete b[c];return b}function j(a,b){var c={_:0},d;for(d in b)a[d]!=b[d]&&(c[d]=b[d]);return c}function k(b,c,d,e){typeof b=="object"&&(e=c,d=null,c=b,b=c.effect),a.isFunction(c)&&(e=c,d=null,c={});if(typeof c=="number"||a.fx.speeds[c])e=d,d=c,c={};return a.isFunction(d)&&(e=d,d=null),c=c||{},d=d||c.duration,d=a.fx.off?0:typeof d=="number"?d:d in a.fx.speeds?a.fx.speeds[d]:a.fx.speeds._default,e=e||c.complete,[b,c,d,e]}function l(b){return!b||typeof b=="number"||a.fx.speeds[b]?!0:typeof b=="string"&&!a.effects[b]?!0:!1}a.effects={},a.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(b,e){a.fx.step[e]=function(a){a.colorInit||(a.start=d(a.elem,e),a.end=c(a.end),a.colorInit=!0),a.elem.style[e]="rgb("+Math.max(Math.min(parseInt(a.pos*(a.end[0]-a.start[0])+a.start[0],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[1]-a.start[1])+a.start[1],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[2]-a.start[2])+a.start[2],10),255),0)+")"}});var e={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},f=["add","remove","toggle"],g={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};a.effects.animateClass=function(b,c,d,e){return a.isFunction(d)&&(e=d,d=null),this.queue(function(){var g=a(this),k=g.attr("style")||" ",l=i(h.call(this)),m,n=g.attr("class")||"";a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),m=i(h.call(this)),g.attr("class",n),g.animate(j(l,m),{queue:!1,duration:c,easing:d,complete:function(){a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),typeof g.attr("style")=="object"?(g.attr("style").cssText="",g.attr("style").cssText=k):g.attr("style",k),e&&e.apply(this,arguments),a.dequeue(this)}})})},a.fn.extend({_addClass:a.fn.addClass,addClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{add:b},c,d,e]):this._addClass(b)},_removeClass:a.fn.removeClass,removeClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{remove:b},c,d,e]):this._removeClass(b)},_toggleClass:a.fn.toggleClass,toggleClass:function(c,d,e,f,g){return typeof d=="boolean"||d===b?e?a.effects.animateClass.apply(this,[d?{add:c}:{remove:c},e,f,g]):this._toggleClass(c,d):a.effects.animateClass.apply(this,[{toggle:c},d,e,f])},switchClass:function(b,c,d,e,f){return a.effects.animateClass.apply(this,[{add:c,remove:b},d,e,f])}}),a.extend(a.effects,{version:"1.8.21",save:function(a,b){for(var c=0;c").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;try{e.id}catch(f){e=document.body}return b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;return b.parent().is(".ui-effects-wrapper")?(c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus(),c):b},setTransition:function(b,c,d,e){return e=e||{},a.each(c,function(a,c){var f=b.cssUnit(c);f[0]>0&&(e[c]=f[0]*d+f[1])}),e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];return a.fx.off||!i?h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)}):i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="show",this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="hide",this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);return c[1].mode="toggle",this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];return a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])}),d}}),a.easing.jswing=a.easing.swing,a.extend(a.easing,{def:"easeOutQuad",swing:function(b,c,d,e,f){return a.easing[a.easing.def](b,c,d,e,f)},easeInQuad:function(a,b,c,d,e){return d*(b/=e)*b+c},easeOutQuad:function(a,b,c,d,e){return-d*(b/=e)*(b-2)+c},easeInOutQuad:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b+c:-d/2*(--b*(b-2)-1)+c},easeInCubic:function(a,b,c,d,e){return d*(b/=e)*b*b+c},easeOutCubic:function(a,b,c,d,e){return d*((b=b/e-1)*b*b+1)+c},easeInOutCubic:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b*b+c:d/2*((b-=2)*b*b+2)+c},easeInQuart:function(a,b,c,d,e){return d*(b/=e)*b*b*b+c},easeOutQuart:function(a,b,c,d,e){return-d*((b=b/e-1)*b*b*b-1)+c},easeInOutQuart:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b*b*b+c:-d/2*((b-=2)*b*b*b-2)+c},easeInQuint:function(a,b,c,d,e){return d*(b/=e)*b*b*b*b+c},easeOutQuint:function(a,b,c,d,e){return d*((b=b/e-1)*b*b*b*b+1)+c},easeInOutQuint:function(a,b,c,d,e){return(b/=e/2)<1?d/2*b*b*b*b*b+c:d/2*((b-=2)*b*b*b*b+2)+c},easeInSine:function(a,b,c,d,e){return-d*Math.cos(b/e*(Math.PI/2))+d+c},easeOutSine:function(a,b,c,d,e){return d*Math.sin(b/e*(Math.PI/2))+c},easeInOutSine:function(a,b,c,d,e){return-d/2*(Math.cos(Math.PI*b/e)-1)+c},easeInExpo:function(a,b,c,d,e){return b==0?c:d*Math.pow(2,10*(b/e-1))+c},easeOutExpo:function(a,b,c,d,e){return b==e?c+d:d*(-Math.pow(2,-10*b/e)+1)+c},easeInOutExpo:function(a,b,c,d,e){return b==0?c:b==e?c+d:(b/=e/2)<1?d/2*Math.pow(2,10*(b-1))+c:d/2*(-Math.pow(2,-10*--b)+2)+c},easeInCirc:function(a,b,c,d,e){return-d*(Math.sqrt(1-(b/=e)*b)-1)+c},easeOutCirc:function(a,b,c,d,e){return d*Math.sqrt(1-(b=b/e-1)*b)+c},easeInOutCirc:function(a,b,c,d,e){return(b/=e/2)<1?-d/2*(Math.sqrt(1-b*b)-1)+c:d/2*(Math.sqrt(1-(b-=2)*b)+1)+c},easeInElastic:function(a,b,c,d,e){var f=1.70158,g=0,h=d;if(b==0)return c;if((b/=e)==1)return c+d;g||(g=e*.3);if(h").css({position:"absolute",visibility:"visible",left:-j*(g/d),top:-i*(h/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:g/d,height:h/c,left:f.left+j*(g/d)+(b.options.mode=="show"?(j-Math.floor(d/2))*(g/d):0),top:f.top+i*(h/c)+(b.options.mode=="show"?(i-Math.floor(c/2))*(h/c):0),opacity:b.options.mode=="show"?0:1}).animate({left:f.left+j*(g/d)+(b.options.mode=="show"?0:(j-Math.floor(d/2))*(g/d)),top:f.top+i*(h/c)+(b.options.mode=="show"?0:(i-Math.floor(c/2))*(h/c)),opacity:b.options.mode=="show"?1:0},b.duration||500);setTimeout(function(){b.options.mode=="show"?e.css({visibility:"visible"}):e.css({visibility:"visible"}).hide(),b.callback&&b.callback.apply(e[0]),e.dequeue(),a("div.ui-effects-explode").remove()},b.duration||500)})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.fade.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.fade=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide");c.animate({opacity:d},{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.fold.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.fold=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.size||15,g=!!b.options.horizFirst,h=b.duration?b.duration/2:a.fx.speeds._default/2;a.effects.save(c,d),c.show();var i=a.effects.createWrapper(c).css({overflow:"hidden"}),j=e=="show"!=g,k=j?["width","height"]:["height","width"],l=j?[i.width(),i.height()]:[i.height(),i.width()],m=/([0-9]+)%/.exec(f);m&&(f=parseInt(m[1],10)/100*l[e=="hide"?0:1]),e=="show"&&i.css(g?{height:0,width:f}:{height:f,width:0});var n={},p={};n[k[0]]=e=="show"?l[0]:f,p[k[1]]=e=="show"?l[1]:0,i.animate(n,h,b.options.easing).animate(p,h,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.highlight.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.highlight=function(b){return this.queue(function(){var c=a(this),d=["backgroundImage","backgroundColor","opacity"],e=a.effects.setMode(c,b.options.mode||"show"),f={backgroundColor:c.css("backgroundColor")};e=="hide"&&(f.opacity=0),a.effects.save(c,d),c.show().css({backgroundImage:"none",backgroundColor:b.options.color||"#ffff99"}).animate(f,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),e=="show"&&!a.support.opacity&&this.style.removeAttribute("filter"),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.21 - 2012-06-05 +* https://github.com/jquery/jquery-ui +* Includes: jquery.effects.pulsate.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.effects.pulsate=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"show"),e=(b.options.times||5)*2-1,f=b.duration?b.duration/2:a.fx.speeds._default/2,g=c.is(":visible"),h=0;g||(c.css("opacity",0).show(),h=1),(d=="hide"&&g||d=="show"&&!g)&&e--;for(var i=0;i').appendTo(document.body).addClass(b.options.className).css({top:g.top,left:g.left,height:c.innerHeight(),width:c.innerWidth(),position:"absolute"}).animate(f,b.duration,b.options.easing,function(){h.remove(),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);; \ No newline at end of file diff --git a/lms/static/js/jquery.min.js b/lms/static/js/jquery.min.js new file mode 100644 index 0000000000..16ad06c5ac --- /dev/null +++ b/lms/static/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
        a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
        "+""+"
        ",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
        t
        ",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
        ",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

        ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
        ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
        ","
        "],thead:[1,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],col:[2,"","
        "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
        ","
        "]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
        ").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/lms/templates/main.html b/lms/templates/main.html index 292910039a..ceab4ea9df 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -14,8 +14,8 @@ % endif - - + + % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: From a6fc27acd69d9d3f2447ccc25750c23c45d10253 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 19 Jun 2012 16:19:45 -0400 Subject: [PATCH 100/252] Fix accordion rendering bug * needed to clean the chapter and section vars before rendering --- lms/djangoapps/courseware/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 7bc8b323ce..3478c88d4b 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -256,15 +256,13 @@ def index(request, course=None, chapter=None, section=None, {'content': module-error message} ''' # Can't modify variables of outer scope, so need new ones - chapter_ = clean(chapter) - section_ = clean(section) user = request.user - module_xml = get_module_xml(user, course, chapter_, section_) + module_xml = get_module_xml(user, course, chapter, section) if module_xml is None: log.exception("couldn't get module_xml: course/chapter/section: '%s/%s/%s'", - course, chapter_, section_) + course, chapter, section) return {'content' : render_to_string("module-error.html", {})} student_module_cache = preload_student_modules(module_xml) @@ -289,6 +287,9 @@ def index(request, course=None, chapter=None, section=None, # keep track of current course being viewed in django's request.session request.session['coursename'] = course + chapter = clean(chapter) + section = clean(section) + context = { 'csrf': csrf(request)['csrf_token'], 'accordion': render_accordion(request, course, chapter, section), From 39c57af849dc87a4960cd618e2f12f65d13f6c3d Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 19 Jun 2012 16:37:33 -0400 Subject: [PATCH 101/252] remove obsolete comment --- lms/djangoapps/courseware/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 3478c88d4b..aa3d1b7781 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -255,8 +255,6 @@ def index(request, course=None, chapter=None, section=None, If there's an error, returns {'content': module-error message} ''' - # Can't modify variables of outer scope, so need new ones - user = request.user module_xml = get_module_xml(user, course, chapter, section) From a1269353699825cbb9e683f1561b4b6be18b5411 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 19 Jun 2012 18:15:34 -0400 Subject: [PATCH 102/252] Add Progress class and tests --- common/lib/xmodule/progress.py | 124 +++++++++++++++++++++++++++++++++ common/lib/xmodule/tests.py | 120 ++++++++++++++++++++++++++++++- common/lib/xmodule/x_module.py | 11 +++ 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 common/lib/xmodule/progress.py diff --git a/common/lib/xmodule/progress.py b/common/lib/xmodule/progress.py new file mode 100644 index 0000000000..1ce5d821f3 --- /dev/null +++ b/common/lib/xmodule/progress.py @@ -0,0 +1,124 @@ +''' +Progress class for modules. Represents where a student is in a module. +''' + +from collections import namedtuple +import numbers + +class Progress(object): + '''Represents a progress of a/b (a out of b done) + + a and b must be numeric, but not necessarily integer, with + 0 <= a <= b and b > 0. + + Progress can only represent Progress for modules where that makes sense. Other + modules (e.g. html) should return None from get_progress(). + ''' + + def __init__(self, a, b): + '''Construct a Progress object. a and b must be numbers, and must have + 0 <= a <= b and b > 0 + ''' + + # Want to do all checking at construction time, so explicitly check types + if not (isinstance(a, numbers.Number) and + isinstance(b, numbers.Number)): + raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b)) + + if not (0 <= a <= b and b > 0): + raise ValueError( + 'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b)) + + self._a = a + self._b = b + + def frac(self): + ''' Return tuple (a,b) representing progress of a/b''' + return (self._a, self._b) + + def percent(self): + ''' Returns a percentage progress as a float between 0 and 100. + + subclassing note: implemented in terms of frac(), assumes sanity + checking is done at construction time. + ''' + (a, b) = self.frac() + return 100.0 * a / b + + def started(self): + ''' Returns True if fractional progress is greater than 0. + + subclassing note: implemented in terms of frac(), assumes sanity + checking is done at construction time. + ''' + return self.frac()[0] > 0 + + + def inprogress(self): + ''' Returns True if fractional progress is strictly between 0 and 1. + + subclassing note: implemented in terms of frac(), assumes sanity + checking is done at construction time. + ''' + (a, b) = self.frac() + return a > 0 and a < b + + def done(self): + ''' Return True if this represents done. + + subclassing note: implemented in terms of frac(), assumes sanity + checking is done at construction time. + ''' + (a, b) = self.frac() + return a==b + + + def ternary_str(self): + ''' Return a string version of this progress: either + "none", "in_progress", or "done". + + subclassing note: implemented in terms of frac() + ''' + (a, b) = self.frac() + if a == 0: + return "none" + if a < b: + return "in_progress" + return "done" + + def __eq__(self, other): + ''' Two Progress objects are equal if they have identical values. + Implemented in terms of frac()''' + if not isinstance(other, Progress): + return False + (a, b) = self.frac() + (a2, b2) = other.frac() + return a == a2 and b == b2 + + def __ne__(self, other): + ''' The opposite of equal''' + return not self.__eq__(other) + + + def __str__(self): + ''' Return a string representation of this string. + + subclassing note: implemented in terms of frac(). + ''' + (a, b) = self.frac() + return "{0}/{1}".format(a, b) + + @staticmethod + def add_counts(a, b): + '''Add two progress indicators, assuming that each represents items done: + (a / b) + (c / d) = (a + c) / (b + d). + If either is None, returns the other. + ''' + if a is None: + return b + if b is None: + return a + # get numerators + denominators + (n, d) = a.frac() + (n2, d2) = b.frac() + return Progress(n + n2, d + d2) diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests.py index 370b3befe5..73096ce8da 100644 --- a/common/lib/xmodule/tests.py +++ b/common/lib/xmodule/tests.py @@ -13,8 +13,9 @@ import numpy import xmodule import capa.calc as calc import capa.capa_problem as lcp -from xmodule import graders +from xmodule import graders, x_module from xmodule.graders import Score, aggregate_scores +from xmodule.progress import Progress from nose.plugins.skip import SkipTest class I4xSystem(object): @@ -26,7 +27,9 @@ class I4xSystem(object): def __init__(self): self.ajax_url = '/' self.track_function = lambda x: None + self.filestore = None self.render_function = lambda x: {} # Probably incorrect + self.module_from_xml = lambda x: None # May need a real impl... self.exception404 = Exception self.DEBUG = True def __repr__(self): @@ -488,3 +491,118 @@ class GraderTest(unittest.TestCase): #TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions? +# -------------------------------------------------------------------------- +# Module progress tests + +class ProgressTest(unittest.TestCase): + ''' Test that basic Progress objects work. A Progress represents a + fraction between 0 and 1. + ''' + not_started = Progress(0, 17) + part_done = Progress(2, 6) + half_done = Progress(3, 6) + also_half_done = Progress(1, 2) + done = Progress(7, 7) + + def test_create_object(self): + # These should work: + p = Progress(0, 2) + p = Progress(1, 2) + p = Progress(2, 2) + + p = Progress(2.5, 5.0) + p = Progress(3.7, 12.3333) + + # These shouldn't + self.assertRaises(ValueError, Progress, 0, 0) + self.assertRaises(ValueError, Progress, 2, 0) + self.assertRaises(ValueError, Progress, 1, -2) + self.assertRaises(ValueError, Progress, 3, 2) + self.assertRaises(ValueError, Progress, -2, 5) + + self.assertRaises(TypeError, Progress, 0, "all") + # check complex numbers just for the heck of it :) + self.assertRaises(TypeError, Progress, 2j, 3) + + def test_frac(self): + p = Progress(1, 2) + (a, b) = p.frac() + self.assertEqual(a, 1) + self.assertEqual(b, 2) + + def test_percent(self): + self.assertEqual(self.not_started.percent(), 0) + self.assertAlmostEqual(self.part_done.percent(), 33.33333333333333) + self.assertEqual(self.half_done.percent(), 50) + self.assertEqual(self.done.percent(), 100) + + self.assertEqual(self.half_done.percent(), self.also_half_done.percent()) + + def test_started(self): + self.assertFalse(self.not_started.started()) + + self.assertTrue(self.part_done.started()) + self.assertTrue(self.half_done.started()) + self.assertTrue(self.done.started()) + + def test_inprogress(self): + # only true if working on it + self.assertFalse(self.done.inprogress()) + self.assertFalse(self.not_started.inprogress()) + + self.assertTrue(self.part_done.inprogress()) + self.assertTrue(self.half_done.inprogress()) + + def test_done(self): + self.assertTrue(self.done.done()) + self.assertFalse(self.half_done.done()) + self.assertFalse(self.not_started.done()) + + def test_str(self): + self.assertEqual(str(self.not_started), "0/17") + self.assertEqual(str(self.part_done), "2/6") + self.assertEqual(str(self.done), "7/7") + + def test_ternary_str(self): + self.assertEqual(self.not_started.ternary_str(), "none") + self.assertEqual(self.half_done.ternary_str(), "in_progress") + self.assertEqual(self.done.ternary_str(), "done") + + def test_add(self): + '''Test the Progress.add_counts() method''' + p = Progress(0, 2) + p2 = Progress(1, 3) + p3 = Progress(2, 5) + pNone = None + add = lambda a, b: Progress.add_counts(a, b).frac() + + self.assertEqual(add(p, p), (0, 4)) + self.assertEqual(add(p, p2), (1, 5)) + self.assertEqual(add(p2, p3), (3, 8)) + + self.assertEqual(add(p2, pNone), p2.frac()) + self.assertEqual(add(pNone, p2), p2.frac()) + + def test_equality(self): + '''Test that comparing Progress objects for equality + works correctly.''' + p = Progress(1, 2) + p2 = Progress(2, 4) + p3 = Progress(1, 2) + self.assertTrue(p == p3) + self.assertFalse(p == p2) + + # Check != while we're at it + self.assertTrue(p != p2) + self.assertFalse(p != p3) + + +class ModuleProgressTest(unittest.TestCase): + ''' Test that get_progress() does the right thing for the different modules + ''' + def test_xmodule_default(self): + '''Make sure default get_progress exists, returns None''' + xm = x_module.XModule(i4xs, "", "dummy") + p = xm.get_progress() + self.assertEqual(p, None) + diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 3560eecbdc..0361e08537 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -1,7 +1,9 @@ from lxml import etree import pkg_resources import logging + from keystore import Location +from progress import Progress log = logging.getLogger('mitx.' + __name__) @@ -118,6 +120,15 @@ class XModule(object): ''' return "Unimplemented" + def get_progress(self): + ''' Return a progress.Progress object that represents how far the student has gone + in this module. Must be implemented to get correct progress tracking behavior in + nesting modules like sequence and vertical. + + If this module has no notion of progress, return None. + ''' + return None + def handle_ajax(self, dispatch, get): ''' dispatch is last part of the URL. get is a dictionary-like object ''' From b963d8b534d44458d85e820cc410fd081df3b9c9 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 19 Jun 2012 18:16:14 -0400 Subject: [PATCH 103/252] Initial progress display. * add module_from_xml param to I4xSystem * use it to implement xmodule.get_children() * fix a few comments here and there * Render-time progress display for seq and vertical modules. - Computes fraction of subproblems done. * Pass problem state back to js during ajax calls. * general cleanup in capa_module.py * add progress_changed and progress fields to json returned from each ajax handler * Coffeescript changes to hook up sequence tracking of problem progress * net result: sequence 'a' tags now have a progress class * properly set css class on initial load * fire event when progress changes after ajax calls * also save state in 'progress' property of problems-wrapper tag * event handler finds those tags, computes updated progress --- common/lib/capa/capa_problem.py | 3 +- common/lib/xmodule/capa_module.py | 240 +++++++++++------- common/lib/xmodule/progress.py | 2 + common/lib/xmodule/seq_module.py | 30 ++- common/lib/xmodule/vertical_module.py | 9 + common/lib/xmodule/x_module.py | 8 + lms/djangoapps/courseware/grades.py | 2 + lms/djangoapps/courseware/module_render.py | 38 ++- lms/static/coffee/src/modules/problem.coffee | 21 +- lms/static/coffee/src/modules/sequence.coffee | 59 ++++- 10 files changed, 309 insertions(+), 103 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index e70fa6ceff..5a3359f919 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -168,7 +168,8 @@ class LoncapaProblem(object): def get_score(self): ''' Compute score for this problem. The score is the number of points awarded. - Returns an integer, from 0 to get_max_score(). + Returns a dictionary {'score': integer, from 0 to get_max_score(), + 'total': get_max_score()}. ''' correct = 0 for key in self.correct_map: diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 55534f8a3e..7c75d1666a 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -11,6 +11,7 @@ from datetime import timedelta from lxml import etree from x_module import XModule, XModuleDescriptor +from progress import Progress from capa.capa_problem import LoncapaProblem from capa.responsetypes import StudentInputError @@ -79,24 +80,41 @@ class Module(XModule): def get_xml_tags(c): return ["problem"] + def get_state(self): state = self.lcp.get_state() state['attempts'] = self.attempts return json.dumps(state) + def get_score(self): return self.lcp.get_score() + def max_score(self): return self.lcp.get_max_score() + + def get_progress(self): + ''' For now, just return score / max_score + ''' + d = self.get_score() + score = d['score'] + total = d['total'] + return Progress(score, total) + + def get_html(self): return self.system.render_template('problem_ajax.html', { 'id': self.item_id, 'ajax_url': self.ajax_url, }) + def get_problem_html(self, encapsulate=True): + '''Return html for the problem. Adds check, reset, save buttons + as necessary based on the problem config and state.''' + html = self.lcp.get_html() content = {'name': self.name, 'html': html, @@ -109,7 +127,7 @@ class Module(XModule): reset_button = True save_button = True - # If we're after deadline, or user has exhuasted attempts, + # If we're after deadline, or user has exhausted attempts, # question is read-only. if self.closed(): check_button = False @@ -154,11 +172,13 @@ class Module(XModule): 'attempts_used': self.attempts, 'attempts_allowed': self.max_attempts, 'explain': explain, + 'progress': self.get_progress(), } html = self.system.render_template('problem.html', context) if encapsulate: - html = '
        '.format(id=self.item_id, ajax_url=self.ajax_url) + html + "
        " + html = '
        '.format( + id=self.item_id, ajax_url=self.ajax_url) + html + "
        " return html @@ -170,7 +190,8 @@ class Module(XModule): dom2 = etree.fromstring(xml) - self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), default="closed") + self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), + default="closed") # TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed") self.explain_available = only_one(dom2.xpath('/problem/@explain_available')) @@ -190,19 +211,19 @@ class Module(XModule): self.grace_period = None self.close_date = self.display_due_date - self.max_attempts =only_one(dom2.xpath('/problem/@attempts')) - if len(self.max_attempts)>0: - self.max_attempts =int(self.max_attempts) + self.max_attempts = only_one(dom2.xpath('/problem/@attempts')) + if len(self.max_attempts) > 0: + self.max_attempts = int(self.max_attempts) else: - self.max_attempts =None + self.max_attempts = None - self.show_answer =only_one(dom2.xpath('/problem/@showanswer')) + self.show_answer = only_one(dom2.xpath('/problem/@showanswer')) - if self.show_answer =="": - self.show_answer ="closed" + if self.show_answer == "": + self.show_answer = "closed" - self.rerandomize =only_one(dom2.xpath('/problem/@rerandomize')) - if self.rerandomize =="" or self.rerandomize=="always" or self.rerandomize=="true": + self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize')) + if self.rerandomize == "" or self.rerandomize=="always" or self.rerandomize=="true": self.rerandomize="always" elif self.rerandomize=="false" or self.rerandomize=="per_student": self.rerandomize="per_student" @@ -253,23 +274,33 @@ class Module(XModule): def handle_ajax(self, dispatch, get): ''' - This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST + This is called by courseware.module_render, to handle an AJAX call. + "get" is request.POST. + + Returns a json dictionary: + { 'progress_changed' : True/False, + 'progress' : 'none'/'in_progress'/'done', + } ''' - if dispatch=='problem_get': - response = self.get_problem(get) - elif False: #self.close_date > - return json.dumps({"error":"Past due date"}) - elif dispatch=='problem_check': - response = self.check_problem(get) - elif dispatch=='problem_reset': - response = self.reset_problem(get) - elif dispatch=='problem_save': - response = self.save_problem(get) - elif dispatch=='problem_show': - response = self.get_answer(get) - else: - return "Error" - return response + handlers = { + 'problem_get': self.get_problem, + 'problem_check': self.check_problem, + 'problem_reset': self.reset_problem, + 'problem_save': self.save_problem, + 'problem_show': self.get_answer, + } + + if dispatch not in handlers: + return 'Error' + + before = self.get_progress() + d = handlers[dispatch](get) + after = self.get_progress() + d.update({ + 'progress_changed' : after != before, + 'progress' : after.ternary_str(), + }) + return json.dumps(d, cls=ComplexEncoder) def closed(self): ''' Is the student still allowed to submit answers? ''' @@ -283,24 +314,22 @@ class Module(XModule): def answer_available(self): ''' Is the user allowed to see an answer? - TODO: simplify. ''' if self.show_answer == '': return False + if self.show_answer == "never": return False - if self.show_answer == 'attempted' and self.attempts == 0: - return False - if self.show_answer == 'attempted' and self.attempts > 0: - return True - if self.show_answer == 'answered' and self.lcp.done: - return True - if self.show_answer == 'answered' and not self.lcp.done: - return False - if self.show_answer == 'closed' and self.closed(): - return True - if self.show_answer == 'closed' and not self.closed(): - return False + + if self.show_answer == 'attempted': + return self.attempts > 0 + + if self.show_answer == 'answered': + return self.lcp.done + + if self.show_answer == 'closed': + return self.closed() + if self.show_answer == 'always': return True raise self.system.exception404 #TODO: Not 404 @@ -310,45 +339,64 @@ class Module(XModule): For the "show answer" button. TODO: show answer events should be logged here, not just in the problem.js + + Returns the answers: {'answers' : answers} ''' if not self.answer_available(): raise self.system.exception404 else: answers = self.lcp.get_question_answers() - return json.dumps(answers, - cls=ComplexEncoder) + return {'answers' : answers} + # Figure out if we should move these to capa_problem? def get_problem(self, get): - ''' Same as get_problem_html -- if we want to reconfirm we - have the right thing e.g. after several AJAX calls.''' - return self.get_problem_html(encapsulate=False) + ''' Return results of get_problem_html, as a simple dict for json-ing. + { 'html': } + + Used if we want to reconfirm we have the right thing e.g. after + several AJAX calls. + ''' + return {'html' : self.get_problem_html(encapsulate=False)} + + @staticmethod + def make_dict_of_responses(get): + '''Make dictionary of student responses (aka "answers") + get is POST dictionary. + ''' + answers = dict() + for key in get: + # e.g. input_resistor_1 ==> resistor_1 + answers['_'.join(key.split('_')[1:])] = get[key] + + return answers def check_problem(self, get): ''' Checks whether answers to a problem are correct, and - returns a map of correct/incorrect answers''' + returns a map of correct/incorrect answers: + + {'success' : bool, + 'contents' : html} + ''' event_info = dict() event_info['state'] = self.lcp.get_state() event_info['filename'] = self.filename - # make a dict of all the student responses ("answers"). - answers=dict() - # input_resistor_1 ==> resistor_1 - for key in get: - answers['_'.join(key.split('_')[1:])]=get[key] + answers = self.make_dict_of_responses(get) - event_info['answers']=answers + event_info['answers'] = answers # Too late. Cannot submit if self.closed(): - event_info['failure']='closed' + event_info['failure'] = 'closed' self.tracker('save_problem_check_fail', event_info) + # TODO: probably not 404? raise self.system.exception404 # Problem submitted. Student should reset before checking # again. if self.lcp.done and self.rerandomize == "always": - event_info['failure']='unreset' + event_info['failure'] = 'unreset' self.tracker('save_problem_check_fail', event_info) raise self.system.exception404 @@ -357,89 +405,107 @@ class Module(XModule): lcp_id = self.lcp.problem_id correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: - self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) + # TODO: why is this line here? + self.lcp = LoncapaProblem(self.filestore.open(self.filename), + id=lcp_id, state=old_state, system=self.system) traceback.print_exc() - return json.dumps({'success':inst.message}) + return {'success': inst.message} except: - self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) + # TODO: why is this line here? + self.lcp = LoncapaProblem(self.filestore.open(self.filename), + id=lcp_id, state=old_state, system=self.system) traceback.print_exc() raise Exception,"error in capa_module" - return json.dumps({'success':'Unknown Error'}) + # TODO: Dead code... is this a bug, or just old? + return {'success':'Unknown Error'} self.attempts = self.attempts + 1 - self.lcp.done=True + self.lcp.done = True - success = 'correct' # success = correct if ALL questions in this problem are correct + # success = correct if ALL questions in this problem are correct + success = 'correct' for answer_id in correct_map: if not correct_map.is_correct(answer_id): success = 'incorrect' - event_info['correct_map']=correct_map.get_dict() # log this in the tracker - event_info['success']=success + event_info['correct_map'] = correct_map.get_dict() # log this in the tracker + event_info['success'] = success self.tracker('save_problem_check', event_info) try: html = self.get_problem_html(encapsulate=False) # render problem into HTML except Exception,err: log.error('failed to generate html') - raise Exception,err + raise Exception, err + + return {'success': success, + 'contents': html, + } - return json.dumps({'success': success, - 'contents': html, - }) def save_problem(self, get): + ''' + Save the passed in answers. + Returns a dict { 'success' : bool, ['error' : error-msg]}, + with the error key only present if success is False. + ''' event_info = dict() event_info['state'] = self.lcp.get_state() event_info['filename'] = self.filename - answers=dict() - for key in get: - answers['_'.join(key.split('_')[1:])]=get[key] + answers = self.make_dict_of_responses(get) event_info['answers'] = answers # Too late. Cannot submit if self.closed(): - event_info['failure']='closed' + event_info['failure'] = 'closed' self.tracker('save_problem_fail', event_info) - return "Problem is closed" + return {'success': False, + 'error': "Problem is closed"} # Problem submitted. Student should reset before saving # again. if self.lcp.done and self.rerandomize == "always": - event_info['failure']='done' + event_info['failure'] = 'done' self.tracker('save_problem_fail', event_info) - return "Problem needs to be reset prior to save." + return {'success' : False, + 'error' : "Problem needs to be reset prior to save."} - self.lcp.student_answers=answers + self.lcp.student_answers = answers + # TODO: should this be save_problem_fail? Looks like success to me... self.tracker('save_problem_fail', event_info) - return json.dumps({'success':True}) + return {'success': True} def reset_problem(self, get): ''' Changes problem state to unfinished -- removes student answers, - and causes problem to rerender itself. ''' + and causes problem to rerender itself. + + Returns problem html as { 'html' : html-string }. + ''' event_info = dict() - event_info['old_state']=self.lcp.get_state() - event_info['filename']=self.filename + event_info['old_state'] = self.lcp.get_state() + event_info['filename'] = self.filename if self.closed(): - event_info['failure']='closed' + event_info['failure'] = 'closed' self.tracker('reset_problem_fail', event_info) return "Problem is closed" if not self.lcp.done: - event_info['failure']='not_done' + event_info['failure'] = 'not_done' self.tracker('reset_problem_fail', event_info) return "Refresh the page and make an attempt before resetting." - self.lcp.do_reset() # call method in LoncapaProblem to reset itself + self.lcp.do_reset() if self.rerandomize == "always": - self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line) - - self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system) + # reset random number generator seed (note the self.lcp.get_state() in next line) + self.lcp.seed=None + + self.lcp = LoncapaProblem(self.filestore.open(self.filename), + self.item_id, self.lcp.get_state(), system=self.system) - event_info['new_state']=self.lcp.get_state() + event_info['new_state'] = self.lcp.get_state() self.tracker('reset_problem', event_info) - return json.dumps(self.get_problem_html(encapsulate=False)) + return {'html' : self.get_problem_html(encapsulate=False)} diff --git a/common/lib/xmodule/progress.py b/common/lib/xmodule/progress.py index 1ce5d821f3..b9e242f2b2 100644 --- a/common/lib/xmodule/progress.py +++ b/common/lib/xmodule/progress.py @@ -13,6 +13,8 @@ class Progress(object): Progress can only represent Progress for modules where that makes sense. Other modules (e.g. html) should return None from get_progress(). + + TODO: add tag for module type? Would allow for smarter merging. ''' def __init__(self, a, b): diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 91ff6d2671..06244393a9 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -1,8 +1,12 @@ import json +import logging from lxml import etree from x_module import XModule, XModuleDescriptor +from xmodule.progress import Progress + +log = logging.getLogger("mitx.common.lib.seq_module") # HACK: This shouldn't be hard-coded to two types # OBSOLETE: This obsoletes 'type' @@ -37,6 +41,16 @@ class Module(XModule): self.render() return self.destroy_js + def get_progress(self): + ''' Return the total progress, adding total done and total available. + (assumes that each submodule uses the same "units" for progress.) + ''' + # TODO: Cache progress or children array? + children = self.get_children() + progresses = [child.get_progress() for child in children] + progress = reduce(Progress.add_counts, progresses) + return progress + def handle_ajax(self, dispatch, get): # TODO: bounds checking ''' get = request.POST instance ''' if dispatch=='goto_position': @@ -53,10 +67,15 @@ class Module(XModule): titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \ for e in self.xmltree] + children = self.get_children() + progresses = [child.get_progress() for child in children] + self.contents = self.rendered_children() - for contents, title in zip(self.contents, titles): + for contents, title, progress in zip(self.contents, titles, progresses): contents['title'] = title + contents['progress_str'] = str(progress) if progress is not None else "" + contents['progress_stat'] = progress.ternary_str() if progress is not None else "" for (content, element_class) in zip(self.contents, child_classes): new_class = 'other' @@ -68,16 +87,17 @@ class Module(XModule): # Split tags -- browsers handle this as end # of script, even if it occurs mid-string. Do this after json.dumps()ing # so that we can be sure of the quotations being used - params={'items':json.dumps(self.contents).replace('', '<"+"/script>'), - 'id':self.item_id, + params={'items': json.dumps(self.contents).replace('', '<"+"/script>'), + 'id': self.item_id, 'position': self.position, - 'titles':titles, - 'tag':self.xmltree.tag} + 'titles': titles, + 'tag': self.xmltree.tag} if self.xmltree.tag in ['sequential', 'videosequence']: self.content = self.system.render_template('seq_module.html', params) if self.xmltree.tag == 'tab': self.content = self.system.render_template('tab_module.html', params) + log.debug("rendered content: %s", content) self.rendered = True def __init__(self, system, xml, item_id, state=None): diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py index 7eb6dda0b8..b3feec8bae 100644 --- a/common/lib/xmodule/vertical_module.py +++ b/common/lib/xmodule/vertical_module.py @@ -1,12 +1,14 @@ import json from x_module import XModule, XModuleDescriptor +from xmodule.progress import Progress from lxml import etree class ModuleDescriptor(XModuleDescriptor): pass class Module(XModule): + ''' Layout module for laying out submodules vertically.''' id_attribute = 'id' def get_state(self): @@ -21,6 +23,13 @@ class Module(XModule): 'items': self.contents }) + def get_progress(self): + # TODO: Cache progress or children array? + children = self.get_children() + progresses = [child.get_progress() for child in children] + progress = reduce(Progress.add_counts, progresses) + return progress + def __init__(self, system, xml, item_id, state=None): XModule.__init__(self, system, xml, item_id, state) xmltree=etree.fromstring(xml) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 0361e08537..725c3636ca 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -59,6 +59,13 @@ class XModule(object): else: raise "We should iterate through children and find a default name" + def get_children(self): + ''' + Return module instances for all the children of this module. + ''' + children = [self.module_from_xml(e) for e in self.__xmltree] + return children + def rendered_children(self): ''' Render all children. @@ -92,6 +99,7 @@ class XModule(object): self.tracker = system.track_function self.filestore = system.filestore self.render_function = system.render_function + self.module_from_xml = system.module_from_xml self.DEBUG = system.DEBUG self.system = system diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 354cf5991f..4a11ec2d51 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -174,6 +174,8 @@ def get_score(user, problem, cache, coursename=None): else: ## HACK 1: We shouldn't specifically reference capa_module ## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system + # TODO: These are no longer correct params for I4xSystem -- figure out what this code + # does, clean it up. from module_render import I4xSystem system = I4xSystem(None, None, None, coursename=coursename) total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score()) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index af5fec6b85..9b5e7e4940 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -34,7 +34,8 @@ class I4xSystem(object): and user, or other environment-specific info. ''' def __init__(self, ajax_url, track_function, render_function, - render_template, request=None, filestore=None): + module_from_xml, render_template, request=None, + filestore=None): ''' Create a closure around the system environment. @@ -43,6 +44,8 @@ class I4xSystem(object): or otherwise tracking the event. TODO: Not used, and has inconsistent args in different files. Update or remove. + module_from_xml - function that takes (module_xml) and returns a corresponding + module instance object. render_function - function that takes (module_xml) and renders it, returning a dictionary with a context for rendering the module to html. Dictionary will contain keys 'content' @@ -62,6 +65,7 @@ class I4xSystem(object): if settings.DEBUG: log.info("[courseware.module_render.I4xSystem] filestore path = %s", filestore) + self.module_from_xml = module_from_xml self.render_function = render_function self.render_template = render_template self.exception404 = Http404 @@ -127,6 +131,18 @@ def grade_histogram(module_id): return [] return grades + +def make_module_from_xml_fn(user, request, student_module_cache, position): + '''Create the make_from_xml() function''' + def module_from_xml(xml): + '''Modules need a way to convert xml to instance objects. + Pass the rest of the context through.''' + (instance, sm, module_type) = get_module( + user, request, xml, student_module_cache, position) + return instance + return module_from_xml + + def get_module(user, request, module_xml, student_module_cache, position=None): ''' Get an instance of the xmodule class corresponding to module_xml, setting the state based on an existing StudentModule, or creating one if none @@ -165,6 +181,9 @@ def get_module(user, request, module_xml, student_module_cache, position=None): # Setup system context for module instance ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/' + module_from_xml = make_module_from_xml_fn( + user, request, student_module_cache, position) + system = I4xSystem(track_function = make_track_function(request), render_function = lambda xml: render_x_module( user, request, xml, student_module_cache, position), @@ -172,6 +191,7 @@ def get_module(user, request, module_xml, student_module_cache, position=None): ajax_url = ajax_url, request = request, filestore = OSFS(data_root), + module_from_xml = module_from_xml, ) # pass position specified in URL to module through I4xSystem system.set('position', position) @@ -295,9 +315,17 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): response = HttpResponse(json.dumps({'success': error_msg})) return response + # TODO: This doesn't have a cache of child student modules. Just + # passing the current one. If ajax calls end up needing children, + # this won't work (but fixing it may cause performance issues...) + # Figure out :) + module_from_xml = make_module_from_xml_fn( + request.user, request, [s], None) + # Create the module system = I4xSystem(track_function = make_track_function(request), - render_function = None, + render_function = None, + module_from_xml = module_from_xml, render_template = render_to_string, ajax_url = ajax_url, request = request, @@ -316,7 +344,11 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): return response # Let the module handle the AJAX - ajax_return = instance.handle_ajax(dispatch, request.POST) + try: + ajax_return = instance.handle_ajax(dispatch, request.POST) + except: + log.exception("error processing ajax call") + raise # Save the state back to the database s.state = instance.get_state() diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index aad41f23d4..1b254d5d3f 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -17,12 +17,20 @@ class @Problem @$('section.action input.save').click @save @$('input.math').keyup(@refreshMath).each(@refreshMath) + update_progress: (response) => + if response.progress_changed + @element.attr progress: response.progress + @element.trigger('progressChanged') + render: (content) -> if content @element.html(content) @bind() else - @element.load @content_url, @bind + $.postWithPrefix "/modx/problem/#{@id}/problem_get", '', (response) => + @element.html(response.html) + @bind() + check: => Logger.log 'problem_check', @answers @@ -30,19 +38,22 @@ class @Problem switch response.success when 'incorrect', 'correct' @render(response.contents) + @update_progress response else alert(response.success) reset: => Logger.log 'problem_reset', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (content) => - @render(content) + $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) => + @render(response.html) + @update_progress response show: => if !@element.hasClass 'showed' Logger.log 'problem_show', problem: @id $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => - $.each response, (key, value) => + answers = response.answers + $.each answers, (key, value) => if $.isArray(value) for choice in value @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' @@ -51,6 +62,7 @@ class @Problem MathJax.Hub.Queue ["Typeset", MathJax.Hub] @$('.show').val 'Hide Answer' @element.addClass 'showed' + @update_progress response else @$('[id^=answer_], [id^=solution_]').text '' @$('[correct_answer]').attr correct_answer: null @@ -62,6 +74,7 @@ class @Problem $.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => if response.success alert 'Saved' + @update_progress response refreshMath: (event, element) => element = event.target unless element diff --git a/lms/static/coffee/src/modules/sequence.coffee b/lms/static/coffee/src/modules/sequence.coffee index 463bf419fc..72a1c82ab6 100644 --- a/lms/static/coffee/src/modules/sequence.coffee +++ b/lms/static/coffee/src/modules/sequence.coffee @@ -2,6 +2,7 @@ class @Sequence constructor: (@id, @elements, @tag, position) -> @element = $("#sequence_#{@id}") @buildNavigation() + @initProgress() @bind() @render position @@ -11,11 +12,52 @@ class @Sequence bind: -> @$('#sequence-list a').click @goto + initProgress: -> + @progressTable = {} # "#problem_#{id}" -> progress + + + hookUpProgressEvent: -> + $('.problems-wrapper').bind 'progressChanged', @updateProgress + + mergeProgress: (p1, p2) -> + if p1 == "done" and p2 == "done" + return "done" + # not done, so if any progress on either, in_progress + w1 = p1 == "done" or p1 == "in_progress" + w2 = p2 == "done" or p2 == "in_progress" + if w1 or w2 + return "in_progress" + + return "none" + + updateProgress: => + new_progress = "none" + _this = this + $('.problems-wrapper').each (index) -> + progress = $(this).attr 'progress' + new_progress = _this.mergeProgress progress, new_progress + + @progressTable[@position] = new_progress + @setProgress(new_progress, @link_for(@position)) + + setProgress: (progress, element) -> + element.removeClass('progress-none') + .removeClass('progress-some') + .removeClass('progress-done') + switch progress + when 'none' then element.addClass('progress-none') + when 'in_progress' then element.addClass('progress-some') + when 'done' then element.addClass('progress-done') + buildNavigation: -> $.each @elements, (index, item) => link = $('').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1 title = $('

        ').html(item.title) + # TODO: add item.progress_str either to the title or somewhere else. + # Make sure it gets updated after ajax calls list_item = $('

      • ').append(link.append(title)) + @setProgress item.progress_stat, link + @$('#sequence-list').append list_item toggleArrows: => @@ -36,13 +78,14 @@ class @Sequence if @position != undefined @mark_visited @position $.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position - + @mark_active new_position @$('#seq_content').html @elements[new_position - 1].content MathJax.Hub.Queue(["Typeset", MathJax.Hub]) @position = new_position @toggleArrows() + @hookUpProgressEvent() @element.trigger 'contentChanged' goto: (event) => @@ -67,7 +110,17 @@ class @Sequence @$("#sequence-list a[data-element=#{position}]") mark_visited: (position) -> - @link_for(position).attr class: "seq_#{@elements[position - 1].type}_visited" + # Don't overwrite class attribute to avoid changing Progress class + type = @elements[position - 1].type + element = @link_for(position) + element.removeClass("seq_#{type}_inactive") + .removeClass("seq_#{type}_active") + .addClass("seq_#{type}_visited") mark_active: (position) -> - @link_for(position).attr class: "seq_#{@elements[position - 1].type}_active" + # Don't overwrite class attribute to avoid changing Progress class + type = @elements[position - 1].type + element = @link_for(position) + element.removeClass("seq_#{type}_inactive") + .removeClass("seq_#{type}_visited") + .addClass("seq_#{type}_active") From ed6a658afeb088df13739d7b72b63022d69eb614 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 20 Jun 2012 08:33:46 -0400 Subject: [PATCH 104/252] Add note about optional revision in location --- common/lib/keystore/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 2605424517..801ba57f80 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -43,6 +43,8 @@ class Location(object): } Location: another Location object + In both the dict and list forms, the revision is optional, and can be ommitted. + None of the components of a location may contain the '/' character Components may be set to None, which may be interpreted by some contexts to mean From e872f3183533b18fdfcfd172f0ce4155ed9caf37 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 20 Jun 2012 10:31:37 -0400 Subject: [PATCH 105/252] minor edits to address Calen's comments. --- common/lib/xmodule/capa_module.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 7c75d1666a..3ace45cff4 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -367,7 +367,8 @@ class Module(XModule): answers = dict() for key in get: # e.g. input_resistor_1 ==> resistor_1 - answers['_'.join(key.split('_')[1:])] = get[key] + _, _, name = key.partition('_') + answers[name] = get[key] return answers @@ -390,7 +391,7 @@ class Module(XModule): if self.closed(): event_info['failure'] = 'closed' self.tracker('save_problem_check_fail', event_info) - # TODO: probably not 404? + # TODO (vshnayder): probably not 404? raise self.system.exception404 # Problem submitted. Student should reset before checking @@ -405,7 +406,7 @@ class Module(XModule): lcp_id = self.lcp.problem_id correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: - # TODO: why is this line here? + # TODO (vshnayder): why is this line here? self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() @@ -436,7 +437,7 @@ class Module(XModule): html = self.get_problem_html(encapsulate=False) # render problem into HTML except Exception,err: log.error('failed to generate html') - raise Exception, err + raise return {'success': success, 'contents': html, From 1853b99861c29fb4611b18288be049e7d2a77cc8 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 20 Jun 2012 11:13:54 -0400 Subject: [PATCH 106/252] Work in progress editing. Committing to allow kfiedler to work in parallel --- cms/djangoapps/contentstore/views.py | 7 +++ cms/envs/common.py | 4 ++ cms/static/coffee/.gitignore | 1 + cms/static/coffee/main.coffee | 67 +++++++++++++++++++++ cms/static/js/main.js | 85 --------------------------- cms/templates/base.html | 4 ++ cms/templates/index.html | 1 + cms/templates/widgets/navigation.html | 4 +- cms/urls.py | 1 + common/lib/xmodule/x_module.py | 1 + 10 files changed, 88 insertions(+), 87 deletions(-) create mode 100644 cms/static/coffee/.gitignore create mode 100644 cms/static/coffee/main.coffee delete mode 100644 cms/static/js/main.js diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index a87520ab13..da85d99dcb 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,6 +1,7 @@ from mitxmako.shortcuts import render_to_response from keystore.django import keystore from django.contrib.auth.decorators import login_required +from django.http import HttpResponse def index(request): @@ -11,3 +12,9 @@ def index(request): course = keystore().get_item(['i4x', org, course, 'Course', name]) weeks = course.get_children() return render_to_response('index.html', {'weeks': weeks}) + + +def edit_item(request): + item_id = request.GET['id'] + item = keystore().get_item(item_id) + return HttpResponse("
        Problem content
        ") diff --git a/cms/envs/common.py b/cms/envs/common.py index 20d49b7ac5..2fdc3fa6a4 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -155,6 +155,10 @@ PIPELINE_CSS = { PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] PIPELINE_JS = { + 'main': { + 'source_filenames': ['coffee/main.coffee'], + 'output_filename': 'js/main.js', + }, } PIPELINE_COMPILERS = [ diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore new file mode 100644 index 0000000000..a6c7c2852d --- /dev/null +++ b/cms/static/coffee/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee new file mode 100644 index 0000000000..e2d26d9a7e --- /dev/null +++ b/cms/static/coffee/main.coffee @@ -0,0 +1,67 @@ +$ -> + $('section.main-content').children().hide() + $('.editable').inlineEdit() + $('.editable-textarea').inlineEdit({control: 'textarea'}) + + heighest = 0 + $('.cal ol > li').each -> + heighest = if $(this).height() > heighest then $(this).height() else heighest + + $('.cal ol > li').css('height',heighest + 'px') + + $('.add-new-section').click -> return false + + $('.new-week .close').click -> + $(this).parents('.new-week').hide() + $('p.add-new-week').show() + return false + + $('.save-update').click -> + $(this).parent().parent().hide() + return false + + edit_item = (id) -> + $.get('/edit_item', {id: id}, (data) -> + $('section.edit-pane').empty().append(data) + $('section.edit-pane').show() + ) + + setHeight = -> + windowHeight = $(this).height() + contentHeight = windowHeight - 29 + + $('section.main-content > section').css('min-height', contentHeight) + $('body.content .cal').css('height', contentHeight) + + $('.edit-week').click -> + $('body').addClass('content') + $('body.content .cal').css('height', contentHeight) + $('section.edit-pane').show() + return false + + $('a.week-edit').click -> + $('body').addClass('content') + $('body.content .cal').css('height', contentHeight) + $('section.edit-pane').show() + return false + + $('a.sequence-edit').click -> + $('body').addClass('content') + $('body.content .cal').css('height', contentHeight) + $('section.edit-pane').show() + return false + + $(document).ready(setHeight) + $(window).bind('resize', setHeight) + + $('a.module-edit').click -> + edit_item($(this).attr('id')) + return false + + $('.video-new a').click -> + $('section.edit-pane').show() + return false + + $('.problem-new a').click -> + $('section.edit-pane').show() + return false diff --git a/cms/static/js/main.js b/cms/static/js/main.js deleted file mode 100644 index 2d72edc4bf..0000000000 --- a/cms/static/js/main.js +++ /dev/null @@ -1,85 +0,0 @@ -$(document).ready(function(){ - $('section.main-content').children().hide(); - - $(function(){ - $('.editable').inlineEdit(); - $('.editable-textarea').inlineEdit({control: 'textarea'}); - }); - - var heighest = 0; - $('.cal ol > li').each(function(){ - heighest = ($(this).height() > heighest) ? $(this).height() : heighest; - - }); - - $('.cal ol > li').css('height',heighest + 'px'); - - $('.add-new-section').click(function() { - return false; - }); - - $('.new-week .close').click( function(){ - $(this).parents('.new-week').hide(); - $('p.add-new-week').show(); - return false; - }); - - $('.save-update').click(function(){ - $(this).parent().parent().hide(); - return false; - }); - - setHeight = function(){ - var windowHeight = $(this).height(); - var contentHeight = windowHeight - 29; - - $('section.main-content > section').css('min-height', contentHeight); - $('body.content .cal').css('height', contentHeight); - - $('.edit-week').click( function() { - $('body').addClass('content'); - $('body.content .cal').css('height', contentHeight); - $('section.week-new').show(); - return false; - }); - - $('.cal ol li header h1 a').click( function() { - $('body').addClass('content'); - $('body.content .cal').css('height', contentHeight); - $('section.week-edit').show(); - return false; - }); - - $('a.sequence-edit').click(function(){ - $('body').addClass('content'); - $('body.content .cal').css('height', contentHeight); - $('section.sequence-edit').show(); - return false; - }); - } - - $(document).ready(setHeight); - $(window).bind('resize', setHeight); - - $('.video-new a').click(function(){ - $('section.video-new').show(); - return false; - }); - - $('a.video-edit').click(function(){ - $('section.video-edit').show(); - return false; - }); - - $('.problem-new a').click(function(){ - $('section.problem-new').show(); - return false; - }); - - $('a.problem-edit').click(function(){ - $('section.problem-edit').show(); - return false; - }); - -}); - diff --git a/cms/templates/base.html b/cms/templates/base.html index 271f73614d..2b56579bbb 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -26,7 +26,11 @@ + % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: + <%static:js group='main'/> + % else: + % endif diff --git a/cms/templates/index.html b/cms/templates/index.html index 11c226ae3d..2e2a8924e2 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -14,6 +14,7 @@ <%include file="widgets/video-new.html"/> <%include file="widgets/problem-edit.html"/> <%include file="widgets/problem-new.html"/> +
        diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 2d5af9ead1..3fbcda675c 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -38,7 +38,7 @@ % for week in weeks:
      • -

        ${week.name}

        +

        ${week.name}

      • a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
        "+""+"
        ",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
        t
        ",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
        ",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

        ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
        ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
        ","
        "],thead:[1,"","
        "],tr:[2,"","
        "],td:[3,"","
        "],col:[2,"","
        "],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
        ","
        "]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
        ").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/cms/templates/base.html b/cms/templates/base.html index 2b56579bbb..3d35f513ab 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -23,7 +23,7 @@ <%block name="content"> - + % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']: @@ -31,6 +31,8 @@ % else: % endif + + <%static:js group='module-js'/> diff --git a/cms/templates/index.html b/cms/templates/index.html index 2e2a8924e2..6bc04dc8a2 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -7,14 +7,9 @@ <%include file="widgets/navigation.html"/>
        - <%include file="widgets/week-edit.html"/> - <%include file="widgets/week-new.html"/> - <%include file="widgets/sequnce-edit.html"/> - <%include file="widgets/video-edit.html"/> - <%include file="widgets/video-new.html"/> - <%include file="widgets/problem-edit.html"/> - <%include file="widgets/problem-new.html"/> -
        +
        +
        +
        diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html new file mode 100644 index 0000000000..db85364510 --- /dev/null +++ b/cms/templates/widgets/html-edit.html @@ -0,0 +1,18 @@ +<%namespace name='static' file='../static_content.html'/> + +
        +
        + Cancel + Save & Update +
        + +
        +
        +

        ${module.name}

        +
        + +
        ${module.definition['data']['text']}
        +
        + Save & Update +
        +
        diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html index 49f67e3e26..991326c987 100644 --- a/cms/templates/widgets/problem-edit.html +++ b/cms/templates/widgets/problem-edit.html @@ -6,7 +6,7 @@
        -

        New Problem

        +

        ${module.name}

        Last modified:

        diff --git a/cms/templates/widgets/sequnce-edit.html b/cms/templates/widgets/sequence-edit.html similarity index 50% rename from cms/templates/widgets/sequnce-edit.html rename to cms/templates/widgets/sequence-edit.html index b69b523bc4..219145aefb 100644 --- a/cms/templates/widgets/sequnce-edit.html +++ b/cms/templates/widgets/sequence-edit.html @@ -1,16 +1,8 @@
        -
        -

        Week 1

        -
          -
        • -

          Goal title: This is the goal body and is where the goal will be further explained

          -
        • -
        -
        -

        Lecture sequence

        -

        Group type: Ordered Sequence

        +

        ${module.name}

        +

        Module Type:${module.type}

        @@ -51,72 +43,12 @@
          1. + % for child in module.get_children():
          2. - Problem title 11 - handle -
          3. -
          4. - Problem Group - handle -
          5. -
          6. - Problem title 14 - handle -
          7. -
          8. - Video 3 - handle -
          9. -
          10. -
            -

            - Problem group - handle -

            -
            -
              -
            1. - Problem title 11 - handle -
            2. -
            3. - Problem title 11 - handle -
            4. -
            5. - Problem title 11 - handle -
            6. -
            -
          11. -
          12. - Problem title 13 - handle -
          13. -
          14. - Problem title 14 - handle -
          15. -
          16. - Video 3 - handle -
          17. -
          18. - Problem title 11 - handle -
          19. -
          20. - Problem Group - handle -
          21. -
          22. - Problem title 14 - handle -
          23. -
          24. - Video 3 + ${child.name} handle
          25. + %endfor
        1. diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 3ace45cff4..e1bd9c6502 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -10,7 +10,8 @@ import StringIO from datetime import timedelta from lxml import etree -from x_module import XModule, XModuleDescriptor +from x_module import XModule +from mako_module import MakoModuleDescriptor from progress import Progress from capa.capa_problem import LoncapaProblem from capa.responsetypes import StudentInputError @@ -63,8 +64,14 @@ class ComplexEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, obj) -class ModuleDescriptor(XModuleDescriptor): - pass +class CapaModuleDescriptor(MakoModuleDescriptor): + """ + Module implementing problems in the LON-CAPA format, + as implemented by capa.capa_problem + """ + + mako_template = 'widgets/problem-edit.html' + class Module(XModule): diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index 8d9d0b17d6..17d60c731c 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -1,14 +1,28 @@ import json import logging -from x_module import XModule, XModuleDescriptor +from x_module import XModule +from mako_module import MakoModuleDescriptor from lxml import etree log = logging.getLogger("mitx.courseware") + #----------------------------------------------------------------------------- -class ModuleDescriptor(XModuleDescriptor): - pass +class HtmlModuleDescriptor(MakoModuleDescriptor): + """ + Module for putting raw html in a course + """ + mako_template = "widgets/html-edit.html" + + # TODO (cpennington): Make this into a proper module + js = {'coffee': [""" + window.construct_html = (id) -> + $('#' + id + " #edit-box").on('input', -> + $('#' + id + ' #edit-preview').empty().append($(this).val()) + ) + """]} + class Module(XModule): id_attribute = 'filename' diff --git a/common/lib/xmodule/mako_module.py b/common/lib/xmodule/mako_module.py new file mode 100644 index 0000000000..7887e13c12 --- /dev/null +++ b/common/lib/xmodule/mako_module.py @@ -0,0 +1,18 @@ +from x_module import XModuleDescriptor +from mitxmako.shortcuts import render_to_string + + +class MakoModuleDescriptor(XModuleDescriptor): + """ + Module descriptor intended as a mixin that uses a mako template + to specify the module html. + + Expects the descriptor to have the `mako_template` attribute set + with the name of the template to render, and it will pass + the descriptor as the `module` parameter to that template + """ + + def get_html(self): + return render_to_string(self.mako_template, { + 'module': self + }) diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 598fc4443e..4d853dbe08 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -3,7 +3,8 @@ import logging from lxml import etree -from x_module import XModule, XModuleDescriptor +from x_module import XModule +from mako_module import MakoModuleDescriptor from xmodule.progress import Progress log = logging.getLogger("mitx.common.lib.seq_module") @@ -12,9 +13,6 @@ log = logging.getLogger("mitx.common.lib.seq_module") # OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem'] -class ModuleDescriptor(XModuleDescriptor): - pass - class Module(XModule): ''' Layout module which lays out content in a temporal sequence ''' @@ -117,5 +115,5 @@ class Module(XModule): self.rendered = False -class SectionDescriptor(XModuleDescriptor): - pass +class SectionDescriptor(MakoModuleDescriptor): + mako_template = 'widgets/sequence-edit.html' diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 7f3370ed37..48af72479c 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -19,6 +19,9 @@ setup( "TutorialIndex = seq_module:SectionDescriptor", "Exam = seq_module:SectionDescriptor", "VideoSegment = video_module:VideoSegmentDescriptor", + "ProblemSet = seq_module:SectionDescriptor", + "Problem = capa_module:CapaModuleDescriptor", + "HTML = html_module:HtmlModuleDescriptor", ] } ) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 044cfefce3..89df6b96b2 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -3,7 +3,6 @@ import pkg_resources import logging from keystore import Location -from progress import Progress log = logging.getLogger('mitx.' + __name__) @@ -30,6 +29,12 @@ class Plugin(object): return classes[0].load() + @classmethod + def load_classes(cls): + return [class_.load() + for class_ + in pkg_resources.iter_entry_points(cls.entry_point)] + class XModule(object): ''' Implements a generic learning module. @@ -154,6 +159,7 @@ class XModuleDescriptor(Plugin): and can generate XModules (which do know about student state). """ entry_point = "xmodule.v1" + js = {} @staticmethod def load_from_json(json_data, system): @@ -178,6 +184,19 @@ class XModuleDescriptor(Plugin): """ return cls(system=system, **json_data) + @classmethod + def get_javascript(cls): + """ + Return a dictionary containing some of the following keys: + coffee: A list of coffeescript fragments that should be compiled and + placed on the page + js: A list of javascript fragments that should be included on the page + + All of these will be loaded onto the page in the CMS + """ + return cls.js + + def __init__(self, system, definition=None, @@ -221,21 +240,27 @@ class XModuleDescriptor(Plugin): else: return [child for child in self._child_instances if child.type in categories] + def get_html(self): + """ + Return the html used to edit this module + """ + raise NotImplementedError("get_html() must be provided by specific modules") + def get_xml(self): ''' For conversions between JSON and legacy XML representations. ''' - if self.xml: + if self.xml: return self.xml - else: + else: raise NotImplementedError("JSON->XML Translation not implemented") def get_json(self): ''' For conversions between JSON and legacy XML representations. ''' - if self.json: + if self.json: raise NotImplementedError - return self.json # TODO: Return context as well -- files, etc. - else: + return self.json # TODO: Return context as well -- files, etc. + else: raise NotImplementedError("XML->JSON Translation not implemented") #def handle_cms_json(self): From 184d0ab038bb1aa5675b954f610c4a07e9c6f694 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Wed, 20 Jun 2012 15:53:29 -0400 Subject: [PATCH 123/252] added styles for lists and paragraphs in problems --- lms/static/sass/courseware/_courseware.scss | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lms/static/sass/courseware/_courseware.scss b/lms/static/sass/courseware/_courseware.scss index c886177437..e9b3c279d7 100644 --- a/lms/static/sass/courseware/_courseware.scss +++ b/lms/static/sass/courseware/_courseware.scss @@ -121,6 +121,18 @@ div.course-wrapper { top: 6px; } } + + ul { + list-style: disc outside none; + } + + ol { + list-style: upper-roman outside none; + } + + p { + margin-bottom: lh(); + } } div { From 82023f6bd33cc3f21993d5cc10d5521bee6dc8c6 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Thu, 21 Jun 2012 15:41:22 -0400 Subject: [PATCH 124/252] Added styles for more basic html elements and reworked design of textinput --- common/lib/capa/templates/textinput.html | 34 +- .../capa/templates/textinput_dynamath.html | 44 +-- lms/static/sass/application.scss | 2 +- lms/static/sass/courseware/_courseware.scss | 148 --------- lms/static/sass/courseware/_problems.scss | 292 ++++++++++++++++++ 5 files changed, 340 insertions(+), 180 deletions(-) create mode 100644 lms/static/sass/courseware/_problems.scss diff --git a/common/lib/capa/templates/textinput.html b/common/lib/capa/templates/textinput.html index 9736199f02..0ba27c6938 100644 --- a/common/lib/capa/templates/textinput.html +++ b/common/lib/capa/templates/textinput.html @@ -1,22 +1,36 @@
          + % if state == 'unsubmitted': +
          + % elif state == 'correct': +
          + % elif state == 'incorrect': +
          + % elif state == 'incomplete': +
          + % endif + - +

          + % if state == 'unsubmitted': + unanswered + % elif state == 'correct': + correct + % elif state == 'incorrect': + incorrect + % elif state == 'incomplete': + incomplete + % endif +

          + +

          - % if state == 'unsubmitted': - - % elif state == 'correct': - - % elif state == 'incorrect': - - % elif state == 'incomplete': - - % endif % if msg: ${msg|n} % endif +
          diff --git a/common/lib/capa/templates/textinput_dynamath.html b/common/lib/capa/templates/textinput_dynamath.html index 41b9c5d172..2fc44c9d3e 100644 --- a/common/lib/capa/templates/textinput_dynamath.html +++ b/common/lib/capa/templates/textinput_dynamath.html @@ -2,34 +2,36 @@ ### version of textline.html which does dynammic math ###
          -
        - - - - - - - - -
        - - - + % if state == 'unsubmitted': +
        + % elif state == 'correct': +
        + % elif state == 'incorrect': +
        + % elif state == 'incomplete': +
        + % endif + + +

        % if state == 'unsubmitted': - + unanswered % elif state == 'correct': - + correct % elif state == 'incorrect': - + incorrect % elif state == 'incomplete': - + incomplete % endif -

        - `{::}` - +

        + +

        + +
        `{::}`
        + + -
        % if msg: ${msg|n} % endif diff --git a/lms/static/sass/application.scss b/lms/static/sass/application.scss index c493202e33..ebae48bec6 100644 --- a/lms/static/sass/application.scss +++ b/lms/static/sass/application.scss @@ -7,7 +7,7 @@ @import "plugins/jquery-ui-1.8.16.custom", "plugins/jquery.qtip.min"; // pages -@import "courseware/courseware", "courseware/sidebar", "courseware/video", "courseware/sequence-nav", "courseware/amplifier"; +@import "courseware/courseware", "courseware/sidebar", "courseware/video", "courseware/sequence-nav", "courseware/amplifier", "courseware/problems"; @import "textbook"; @import "info"; @import "profile"; diff --git a/lms/static/sass/courseware/_courseware.scss b/lms/static/sass/courseware/_courseware.scss index e9b3c279d7..cc822c6d77 100644 --- a/lms/static/sass/courseware/_courseware.scss +++ b/lms/static/sass/courseware/_courseware.scss @@ -38,154 +38,6 @@ div.course-wrapper { } } - .problem-set { - position: relative; - @extend .clearfix; - - h2 { - margin-top: 0; - margin-bottom: 15px; - width: flex-grid(2, 9); - padding-right: flex-gutter(9); - border-right: 1px dashed #ddd; - @include box-sizing(border-box); - display: table-cell; - vertical-align: top; - - &.problem-header { - section.staff { - margin-top: 30px; - font-size: 80%; - } - } - - @media screen and (max-width:1120px) { - display: block; - width: auto; - border-right: 0; - } - - @media print { - display: block; - width: auto; - border-right: 0; - } - } - - section.problem { - display: table-cell; - width: flex-grid(7, 9); - padding-left: flex-gutter(9); - - @media screen and (max-width:1120px) { - display: block; - width: auto; - padding: 0; - } - - @media print { - display: block; - width: auto; - padding: 0; - - canvas, img { - page-break-inside: avoid; - } - } - - span { - &.unanswered, &.ui-icon-bullet { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - position: relative; - top: 4px; - width: 14px; - } - - &.correct, &.ui-icon-check { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - position: relative; - top: 6px; - width: 25px; - } - - &.incorrect, &.ui-icon-close { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - position: relative; - top: 6px; - } - } - - ul { - list-style: disc outside none; - } - - ol { - list-style: upper-roman outside none; - } - - p { - margin-bottom: lh(); - } - } - - div { - > span { - display: block; - margin-bottom: lh(.5); - - &[answer] { - border-top: 1px solid #ededed; - border-bottom: 1px solid #ededed; - background: #f3f3f3; - margin: 0 (-(lh())); - padding: lh(.5) lh(); - } - } - } - - input[type="text"] { - display: inline-block; - width: 50%; - } - - center { - display: block; - margin: lh() 0; - border: 1px solid #ccc; - padding: lh(); - } - - section.action { - margin-top: lh(); - - input[type="button"] { - padding: lh(.4) lh(); - text-shadow: 0 -1px 0 #666; - } - } - } - - section.problems-wrapper, div#seq_content { - @extend .problem-set; - } - - section.problems-wrapper { - display: table; - width: 100%; - - @media screen and (max-width:1120px) { - display: block; - width: auto; - } - } - div#seq_content { h1 { background: none; diff --git a/lms/static/sass/courseware/_problems.scss b/lms/static/sass/courseware/_problems.scss new file mode 100644 index 0000000000..0b056ca549 --- /dev/null +++ b/lms/static/sass/courseware/_problems.scss @@ -0,0 +1,292 @@ +section.problem-set { + position: relative; + @extend .clearfix; + + h2 { + margin-top: 0; + margin-bottom: 15px; + width: flex-grid(2, 9); + padding-right: flex-gutter(9); + border-right: 1px dashed #ddd; + @include box-sizing(border-box); + display: table-cell; + vertical-align: top; + + &.problem-header { + section.staff { + margin-top: 30px; + font-size: 80%; + } + } + + @media screen and (max-width:1120px) { + display: block; + width: auto; + border-right: 0; + } + + @media print { + display: block; + width: auto; + border-right: 0; + } + } + + section.problem { + display: table-cell; + width: flex-grid(7, 9); + padding-left: flex-gutter(9); + + @media screen and (max-width:1120px) { + display: block; + width: auto; + padding: 0; + } + + @media print { + display: block; + width: auto; + padding: 0; + + canvas, img { + page-break-inside: avoid; + } + } + + + div { + p.status { + text-indent: -9999px; + margin: -1px 0 0 10px; + } + + &.unanswered { + p.status { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + width: 14px; + } + } + + &.correct, &.ui-icon-check { + p.status { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + width: 25px; + } + + input { + border-color: green; + } + } + + &.incorrect, &.ui-icon-close { + p.status { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + text-indent: -9999px; + } + + input { + border-color: red; + } + } + + > span { + display: block; + margin-bottom: lh(.5); + } + + p.answer { + @include inline-block(); + margin-bottom: 0; + margin-left: 10px; + + &:before { + content: "Answer: "; + font-weight: bold; + display: inline; + + } + &:empty { + &:before { + display: none; + } + } + } + + div.equation { + clear: both; + padding: 6px; + background: #eee; + + span { + margin-bottom: 0; + } + } + + span { + &.unanswered, &.ui-icon-bullet { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + position: relative; + top: 4px; + width: 14px; + } + + &.correct, &.ui-icon-check { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + position: relative; + top: 6px; + width: 25px; + } + + &.incorrect, &.ui-icon-close { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + position: relative; + top: 6px; + } + } + } + + ul { + list-style: disc outside none; + margin-bottom: lh(); + margin-left: .75em; + margin-left: .75rem; + } + + ol { + list-style: decimal outside none; + margin-bottom: lh(); + margin-left: .75em; + margin-left: .75rem; + } + + dl { + line-height: 1.4em; + } + + dl dt { + font-weight: bold; + } + + dl dd { + margin-bottom: 0; + } + + dd { + margin-left: .5em; + margin-left: .5rem; + } + + li { + line-height: 1.4em; + margin-bottom: lh(.5); + + &:last-child { + margin-bottom: 0; + } + } + + p { + margin-bottom: lh(); + } + + table { + margin-bottom: lh(); + width: 100%; + border: 1px solid #ddd; + border-collapse: collapse; + + th { + border-bottom: 2px solid #ccc; + font-weight: bold; + text-align: left; + } + + td { + border: 1px solid #ddd; + } + + caption, th, td { + padding: .25em .75em .25em 0; + padding: .25rem .75rem .25rem 0; + } + + caption { + background: #f1f1f1; + margin-bottom: .75em; + margin-bottom: .75rem; + padding: .75em 0; + padding: .75rem 0; + } + + tr, td, th { + vertical-align: middle; + } + + } + + hr { + background: #ddd; + border: none; + clear: both; + color: #ddd; + float: none; + height: 1px; + margin: 0 0 .75rem; + width: 100%; + } + + .hidden { + display: none; + visibility: hidden; + } + + input[type="text"] { + display: inline-block; + width: 50%; + } + + center { + display: block; + margin: lh() 0; + border: 1px solid #ccc; + padding: lh(); + } + } + + section.action { + margin-top: lh(.5); + + input[type="button"] { + padding: lh(.4) lh(); + text-shadow: 0 -1px 0 #666; + } + } +} + +section.problems-wrapper, div#seq_content { + @extend .problem-set; +} + +section.problems-wrapper { + display: table; + width: 100%; + + @media screen and (max-width:1120px) { + display: block; + width: auto; + } +} From 657fcd998443750108870456fcb3fcb8c632677b Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Thu, 21 Jun 2012 17:36:21 -0400 Subject: [PATCH 125/252] Added new styles for html editing and removed a bunch of unused templates --- cms/static/sass/_base.scss | 4 + cms/static/sass/_problem.scss | 53 ++++- cms/templates/widgets/captions.html | 242 -------------------- cms/templates/widgets/header.html | 3 - cms/templates/widgets/html-edit.html | 17 +- cms/templates/widgets/raw-videos.html | 3 - cms/templates/widgets/save-captions.html | 4 - cms/templates/widgets/speed-tooltip.html | 7 - cms/templates/widgets/video-box-unused.html | 38 --- cms/templates/widgets/video-box.html | 35 --- 10 files changed, 67 insertions(+), 339 deletions(-) delete mode 100644 cms/templates/widgets/captions.html delete mode 100644 cms/templates/widgets/raw-videos.html delete mode 100644 cms/templates/widgets/save-captions.html delete mode 100644 cms/templates/widgets/speed-tooltip.html delete mode 100644 cms/templates/widgets/video-box-unused.html delete mode 100644 cms/templates/widgets/video-box.html diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 3a2ef86363..d961f461fd 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -111,3 +111,7 @@ input[type="submit"], .button { display: block; float: right; } + +textarea { + overflow: auto; +} diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_problem.scss index 66acacf65c..cb418ef497 100644 --- a/cms/static/sass/_problem.scss +++ b/cms/static/sass/_problem.scss @@ -1,20 +1,67 @@ -section.problem-new, section.problem-edit { +section.problem-new, +section.problem-edit, +section.html-edit { + > header { + border-bottom: 2px solid #333; + @include clearfix(); + padding: 6px 20px; + + h1 { + font-size: 18px; + text-transform: uppercase; + letter-spacing: 1px; + float: left; + } + + p { + float: right; + } + } + > section { + padding: 20px; + textarea { @include box-sizing(border-box); display: block; width: 100%; } + div.preview { background: #eee; @include box-sizing(border-box); - height: 40px; + min-height: 40px; padding: 10px; width: 100%; + margin-top: 10px; + + h1 { + font-size: 24px; + margin-bottom: 1em; + } + + h2 { + font-size: 20px; + margin-bottom: 1em; + } + + h3 { + font-size: 18; + margin-bottom: 1em; + } + + ul { + padding-left: 20px; + margin-bottom: 1em; + } + + p { + margin-bottom: 1em; + } } - a.save { + a.save-update { @extend .button; @include inline-block(); margin-top: 20px; diff --git a/cms/templates/widgets/captions.html b/cms/templates/widgets/captions.html deleted file mode 100644 index 088beb7a33..0000000000 --- a/cms/templates/widgets/captions.html +++ /dev/null @@ -1,242 +0,0 @@ -
          -
        • English (main)
        • -
        • French
        • -
        • English v2
        • -
        • +
        • -
        - - diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 49965e4026..4577ac64d8 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -5,9 +5,6 @@
      • New Section
      • -
      • - New Module -
      • New Unit
      • diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index db85364510..1cc201d40b 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -2,17 +2,26 @@
        - Cancel - Save & Update +

        ${module.name}

        +

        Unit type: HTML

        + + + +
        -

        ${module.name}

        +
        +
        ${module.definition['data']['text']}
        - Save & Update + +
        diff --git a/cms/templates/widgets/raw-videos.html b/cms/templates/widgets/raw-videos.html deleted file mode 100644 index f466fd59bc..0000000000 --- a/cms/templates/widgets/raw-videos.html +++ /dev/null @@ -1,3 +0,0 @@ -
      • -
        Video-file-name
        -
      • diff --git a/cms/templates/widgets/save-captions.html b/cms/templates/widgets/save-captions.html deleted file mode 100644 index 87342f0cd0..0000000000 --- a/cms/templates/widgets/save-captions.html +++ /dev/null @@ -1,4 +0,0 @@ -
        - Cancel - -
        diff --git a/cms/templates/widgets/speed-tooltip.html b/cms/templates/widgets/speed-tooltip.html deleted file mode 100644 index 2a82e237e7..0000000000 --- a/cms/templates/widgets/speed-tooltip.html +++ /dev/null @@ -1,7 +0,0 @@ -
        - -
        diff --git a/cms/templates/widgets/video-box-unused.html b/cms/templates/widgets/video-box-unused.html deleted file mode 100644 index 3d643ff3c9..0000000000 --- a/cms/templates/widgets/video-box-unused.html +++ /dev/null @@ -1,38 +0,0 @@ -
      • - -
        - -
        - video-name 236mb Uploaded 6 hours ago by Anant Agrawal -

        -

          - Speed -
        • - 0.75x - <%include file="speed-tooltip.html"/> -
        • -
        • Normal - <%include file="speed-tooltip.html"/> -
        • -
        • 1.25x - <%include file="speed-tooltip.html"/> -
        • -
        • 1.5x - <%include file="speed-tooltip.html"/> -
        • -
        • +
        • -
        -

        -

        - Download All — - Delete All — - Edit Captions — - Use clip ⬆ -

        -
        -
        - <%include file="captions.html"/> - <%include file="save-captions.html"/> -
        -
      • - diff --git a/cms/templates/widgets/video-box.html b/cms/templates/widgets/video-box.html deleted file mode 100644 index 1f17e33511..0000000000 --- a/cms/templates/widgets/video-box.html +++ /dev/null @@ -1,35 +0,0 @@ -
      • -
        - -
        - video-name 236mb -

        Uploaded 6 hours ago by Anant Agrawal

        -

        -

          - Speed -
        • - 0.75x - <%include file="speed-tooltip.html"/> -
        • -
        • Normal - <%include file="speed-tooltip.html"/> -
        • -
        • 1.25x - <%include file="speed-tooltip.html"/> -
        • -
        • 1.5x - <%include file="speed-tooltip.html"/> -
        • -
        • +
        • -
        -

        -

        - Download all — -Remove ⬇ - -

        -
        -
        - <%include file="captions.html"/> -
        -
      • From 17a4d7fd093aa09b7c43b390b6adbd1f76b673d8 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 22 Jun 2012 08:59:53 -0400 Subject: [PATCH 126/252] Call js module name dynamically, rather than statically calling the HTML module --- cms/djangoapps/contentstore/views.py | 5 ++++- cms/static/coffee/main.coffee | 2 +- cms/templates/unit.html | 3 +++ common/lib/xmodule/html_module.py | 8 ++------ common/lib/xmodule/js/module/html.coffee | 5 +++++ common/lib/xmodule/setup.py | 3 +++ 6 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 cms/templates/unit.html create mode 100644 common/lib/xmodule/js/module/html.coffee diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b8667da156..138b7434a8 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -17,4 +17,7 @@ def index(request): def edit_item(request): item_id = request.GET['id'] item = keystore().get_item(item_id) - return HttpResponse(item.get_html()) + return render_to_response('unit.html', { + 'contents': item.get_html(), + 'type': item.type, + }) diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee index 142e8c7089..b74cd3c7d3 100644 --- a/cms/static/coffee/main.coffee +++ b/cms/static/coffee/main.coffee @@ -9,7 +9,7 @@ edit_item = (id) -> bind_edit_links() $('section.edit-pane').show() $('body').addClass('content') - window['construct_html']('module-html') + new window[$('#unit-wrapper').attr('class')] 'module-html' ) $ -> diff --git a/cms/templates/unit.html b/cms/templates/unit.html new file mode 100644 index 0000000000..0ce973bb9e --- /dev/null +++ b/cms/templates/unit.html @@ -0,0 +1,3 @@ +
        + ${contents} +
        diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index 17d60c731c..977b5ef606 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -4,6 +4,7 @@ import logging from x_module import XModule from mako_module import MakoModuleDescriptor from lxml import etree +from pkg_resources import resource_string log = logging.getLogger("mitx.courseware") @@ -16,12 +17,7 @@ class HtmlModuleDescriptor(MakoModuleDescriptor): mako_template = "widgets/html-edit.html" # TODO (cpennington): Make this into a proper module - js = {'coffee': [""" - window.construct_html = (id) -> - $('#' + id + " #edit-box").on('input', -> - $('#' + id + ' #edit-preview').empty().append($(this).val()) - ) - """]} + js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} class Module(XModule): diff --git a/common/lib/xmodule/js/module/html.coffee b/common/lib/xmodule/js/module/html.coffee new file mode 100644 index 0000000000..47c6084837 --- /dev/null +++ b/common/lib/xmodule/js/module/html.coffee @@ -0,0 +1,5 @@ +class @HTML + constructor: (id) -> + $('#' + id + " #edit-box").on('input', -> + $('#' + id + ' #edit-preview').empty().append($(this).val()) + ) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 48af72479c..7b67029f34 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -5,6 +5,9 @@ setup( version="0.1", packages=find_packages(), install_requires=['distribute'], + package_data={ + '': ['js/*'] + }, # See http://guide.python-distribute.org/creation.html#entry-points # for a description of entry_points From 5bef1e14b2f145ad9a28e2129a1633cabf17e117 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 22 Jun 2012 09:21:09 -0400 Subject: [PATCH 127/252] Split problem metadata out from problem editing contents when rendering --- cms/djangoapps/contentstore/views.py | 3 +- cms/static/sass/_problem.scss | 90 ++++++++++++------------ cms/templates/unit.html | 20 +++++- cms/templates/widgets/html-edit.html | 27 +------ common/lib/xmodule/js/module/html.coffee | 9 ++- 5 files changed, 72 insertions(+), 77 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 138b7434a8..1ab78c7c90 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,7 +1,5 @@ from mitxmako.shortcuts import render_to_response from keystore.django import keystore -from django.contrib.auth.decorators import login_required -from django.http import HttpResponse def index(request): @@ -20,4 +18,5 @@ def edit_item(request): return render_to_response('unit.html', { 'contents': item.get_html(), 'type': item.type, + 'name': item.name, }) diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_problem.scss index cb418ef497..39df062cde 100644 --- a/cms/static/sass/_problem.scss +++ b/cms/static/sass/_problem.scss @@ -1,6 +1,4 @@ -section.problem-new, -section.problem-edit, -section.html-edit { +section#unit-wrapper { > header { border-bottom: 2px solid #333; @include clearfix(); @@ -17,50 +15,8 @@ section.html-edit { float: right; } } - > section { padding: 20px; - - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - - - div.preview { - background: #eee; - @include box-sizing(border-box); - min-height: 40px; - padding: 10px; - width: 100%; - margin-top: 10px; - - h1 { - font-size: 24px; - margin-bottom: 1em; - } - - h2 { - font-size: 20px; - margin-bottom: 1em; - } - - h3 { - font-size: 18; - margin-bottom: 1em; - } - - ul { - padding-left: 20px; - margin-bottom: 1em; - } - - p { - margin-bottom: 1em; - } - } - a.save-update { @extend .button; @include inline-block(); @@ -69,3 +25,47 @@ section.html-edit { } } +section.problem-new, +section.problem-edit, +section.html-edit { + textarea { + @include box-sizing(border-box); + display: block; + width: 100%; + } + + + div.preview { + background: #eee; + @include box-sizing(border-box); + min-height: 40px; + padding: 10px; + width: 100%; + margin-top: 10px; + + h1 { + font-size: 24px; + margin-bottom: 1em; + } + + h2 { + font-size: 20px; + margin-bottom: 1em; + } + + h3 { + font-size: 18; + margin-bottom: 1em; + } + + ul { + padding-left: 20px; + margin-bottom: 1em; + } + + p { + margin-bottom: 1em; + } + } +} + diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 0ce973bb9e..c9a3d82b5a 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -1,3 +1,19 @@ -
        +
        +
        +

        ${name}

        +

        Unit type: ${type}

        + + + + +
        +
        ${contents} -
        + +
        + +
        + diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index 1cc201d40b..cbd8a7f3df 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -1,27 +1,4 @@ -<%namespace name='static' file='../static_content.html'/> -
        -
        -

        ${module.name}

        -

        Unit type: HTML

        - - - - -
        - -
        -
        -
        - -
        - -
        ${module.definition['data']['text']}
        -
        - - -
        + +
        ${module.definition['data']['text']}
        diff --git a/common/lib/xmodule/js/module/html.coffee b/common/lib/xmodule/js/module/html.coffee index 47c6084837..e2cba2320c 100644 --- a/common/lib/xmodule/js/module/html.coffee +++ b/common/lib/xmodule/js/module/html.coffee @@ -1,5 +1,8 @@ class @HTML - constructor: (id) -> - $('#' + id + " #edit-box").on('input', -> - $('#' + id + ' #edit-preview').empty().append($(this).val()) + constructor: (@id) -> + id = @id + $("##{id} .edit-box").on('input', -> + $("##{id} .preview").empty().append($(this).val()) ) + + save: -> $("##{@id} .edit-box").val() From f9bb8a701ae49545d64b7a9fff5a703d08043c2c Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 22 Jun 2012 09:42:16 -0400 Subject: [PATCH 128/252] Added new readme info and started to rework some of the css --- cms/static/sass/README.txt | 2 +- cms/static/sass/_module-header.scss | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/cms/static/sass/README.txt b/cms/static/sass/README.txt index 72225393e4..3b760ff214 100644 --- a/cms/static/sass/README.txt +++ b/cms/static/sass/README.txt @@ -1,3 +1,3 @@ Sass Watch: -sass --watch cms/static/sass:cms/static/css -r ./cms/static/sass/bourbon/lib/bourbon.rb +sass --watch cms/static/sass:cms/static/sass -r ./cms/static/sass/bourbon/lib/bourbon.rb diff --git a/cms/static/sass/_module-header.scss b/cms/static/sass/_module-header.scss index e2af263618..03acbecd9b 100644 --- a/cms/static/sass/_module-header.scss +++ b/cms/static/sass/_module-header.scss @@ -1,18 +1,15 @@ -section.video-new, section.video-edit, section.problem-new, section.problem-edit { - position: absolute; - top: 72px; - right: 0; +section.video-new, +section.video-edit, +section.problem-new, +section.problem-edit, +section.html-edit { background: #fff; - width: flex-grid(6); - @include box-shadow(0 0 6px #666); border: 1px solid #333; border-right: 0; - z-index: 4; > header { - background: #666; + background: #eee; @include clearfix; - color: #fff; padding: 6px; border-bottom: 1px solid #333; -webkit-font-smoothing: antialiased; From 72eef72d8b60ed737b06401bd9bdaee42a940e2f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 22 Jun 2012 10:50:46 -0400 Subject: [PATCH 129/252] Html Module can now be editing, saved, and re-opened --- cms/djangoapps/contentstore/views.py | 11 ++++++ cms/envs/common.py | 14 ++++++- cms/static/.gitignore | 1 - cms/static/coffee/.gitignore | 1 + cms/static/coffee/main.coffee | 8 ++-- cms/static/coffee/unit.coffee | 13 +++++++ cms/static/js/jquery.cookie.js | 47 ++++++++++++++++++++++++ cms/templates/base.html | 1 + cms/urls.py | 1 + common/lib/xmodule/js/module/html.coffee | 9 +++-- 10 files changed, 96 insertions(+), 10 deletions(-) delete mode 100644 cms/static/.gitignore create mode 100644 cms/static/coffee/unit.coffee create mode 100644 cms/static/js/jquery.cookie.js diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 1ab78c7c90..8bd55bf60f 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,7 +1,11 @@ from mitxmako.shortcuts import render_to_response from keystore.django import keystore +from django_future.csrf import ensure_csrf_cookie +from django.http import HttpResponse +import json +@ensure_csrf_cookie def index(request): # TODO (cpennington): These need to be read in from the active user org = 'mit.edu' @@ -20,3 +24,10 @@ def edit_item(request): 'type': item.type, 'name': item.name, }) + + +def save_item(request): + item_id = request.POST['id'] + data = json.loads(request.POST['data']) + keystore().update_item(item_id, data) + return HttpResponse(json.dumps({})) diff --git a/cms/envs/common.py b/cms/envs/common.py index d5e84c836c..fc721ca820 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -22,6 +22,8 @@ Longer TODO: import sys import tempfile import os.path +import os +import errno from path import path ############################ FEATURE CONFIGURATION ############################# @@ -156,7 +158,15 @@ PIPELINE_CSS = { PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] from x_module import XModuleDescriptor -js_file_dir = tempfile.mkdtemp('js', dir=PROJECT_ROOT / "static") +js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" +try: + os.makedirs(js_file_dir) +except OSError as exc: + if exc.errno == errno.EEXIST: + pass + else: + raise + module_js_sources = [] for xmodule in XModuleDescriptor.load_classes(): js = xmodule.get_javascript() @@ -172,7 +182,7 @@ for xmodule in XModuleDescriptor.load_classes(): PIPELINE_JS = { 'main': { - 'source_filenames': ['coffee/main.coffee'], + 'source_filenames': ['coffee/main.coffee', 'coffee/unit.coffee'], 'output_filename': 'js/main.js', }, 'module-js': { diff --git a/cms/static/.gitignore b/cms/static/.gitignore deleted file mode 100644 index 61e4416e07..0000000000 --- a/cms/static/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp*js diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore index a6c7c2852d..bb90193362 100644 --- a/cms/static/coffee/.gitignore +++ b/cms/static/coffee/.gitignore @@ -1 +1,2 @@ *.js +module diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee index b74cd3c7d3..ce7f29cc1e 100644 --- a/cms/static/coffee/main.coffee +++ b/cms/static/coffee/main.coffee @@ -3,16 +3,18 @@ bind_edit_links = -> edit_item($(this).attr('id')) return false -edit_item = (id) -> - $.get('/edit_item', {id: id}, (data) -> +edit_item = (id) => + $.get('/edit_item', {id: id}, (data) => $('#module-html').empty().append(data) bind_edit_links() $('section.edit-pane').show() $('body').addClass('content') - new window[$('#unit-wrapper').attr('class')] 'module-html' + new @Unit('unit-wrapper', id) ) $ -> + $.ajaxSetup + headers : { 'X-CSRFToken': $.cookie 'csrftoken' } $('section.main-content').children().hide() $('.editable').inlineEdit() $('.editable-textarea').inlineEdit({control: 'textarea'}) diff --git a/cms/static/coffee/unit.coffee b/cms/static/coffee/unit.coffee new file mode 100644 index 0000000000..f41bd7f96a --- /dev/null +++ b/cms/static/coffee/unit.coffee @@ -0,0 +1,13 @@ +class @Unit + constructor: (@element_id, @module_id) -> + @module = new window[$("##{@element_id}").attr('class')] 'module-html' + + $("##{@element_id} .save-update").click( (event) => + event.preventDefault() + $.post("save_item", { + id: @module_id + data: JSON.stringify(@module.save()) + }) + + ) + diff --git a/cms/static/js/jquery.cookie.js b/cms/static/js/jquery.cookie.js new file mode 100644 index 0000000000..6d5974a2c5 --- /dev/null +++ b/cms/static/js/jquery.cookie.js @@ -0,0 +1,47 @@ +/*! + * jQuery Cookie Plugin + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2011, Klaus Hartl + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://www.opensource.org/licenses/mit-license.php + * http://www.opensource.org/licenses/GPL-2.0 + */ +(function($) { + $.cookie = function(key, value, options) { + + // key and at least value given, set cookie... + if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) { + options = $.extend({}, options); + + if (value === null || value === undefined) { + options.expires = -1; + } + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setDate(t.getDate() + days); + } + + value = String(value); + + return (document.cookie = [ + encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // key and possibly options given, get cookie... + options = value || {}; + var decode = options.raw ? function(s) { return s; } : decodeURIComponent; + + var pairs = document.cookie.split('; '); + for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) { + if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined + } + return null; + }; +})(jQuery); diff --git a/cms/templates/base.html b/cms/templates/base.html index 3d35f513ab..12df2b7a28 100644 --- a/cms/templates/base.html +++ b/cms/templates/base.html @@ -34,6 +34,7 @@ <%static:js group='module-js'/> + diff --git a/cms/urls.py b/cms/urls.py index dad9528387..d7314aafae 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -7,4 +7,5 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('', url(r'^$', 'contentstore.views.index', name='index'), url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'), + url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), ) diff --git a/common/lib/xmodule/js/module/html.coffee b/common/lib/xmodule/js/module/html.coffee index e2cba2320c..5e072c27a3 100644 --- a/common/lib/xmodule/js/module/html.coffee +++ b/common/lib/xmodule/js/module/html.coffee @@ -1,8 +1,9 @@ class @HTML constructor: (@id) -> - id = @id - $("##{id} .edit-box").on('input', -> - $("##{id} .preview").empty().append($(this).val()) + @edit_box = $("##{@id} .edit-box") + @preview = $("##{@id} .preview") + @edit_box.on('input', => + @preview.empty().append(@edit_box.val()) ) - save: -> $("##{@id} .edit-box").val() + save: -> {text: @edit_box.val()} From 83b3d51fd2b47094242e87bda87bac3d9bba5e41 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 22 Jun 2012 10:58:09 -0400 Subject: [PATCH 130/252] Wire up the cancel button in the unit editor --- cms/static/coffee/main.coffee | 27 ++++++++++++++------------- cms/static/coffee/unit.coffee | 8 +++++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee index ce7f29cc1e..06b8701366 100644 --- a/cms/static/coffee/main.coffee +++ b/cms/static/coffee/main.coffee @@ -1,16 +1,17 @@ -bind_edit_links = -> - $('a.module-edit').click -> - edit_item($(this).attr('id')) - return false +class @CMS + @bind = => + $('a.module-edit').click -> + CMS.edit_item($(this).attr('id')) + return false -edit_item = (id) => - $.get('/edit_item', {id: id}, (data) => - $('#module-html').empty().append(data) - bind_edit_links() - $('section.edit-pane').show() - $('body').addClass('content') - new @Unit('unit-wrapper', id) - ) + @edit_item = (id) => + $.get('/edit_item', {id: id}, (data) => + $('#module-html').empty().append(data) + CMS.bind() + $('section.edit-pane').show() + $('body').addClass('content') + new Unit('unit-wrapper', id) + ) $ -> $.ajaxSetup @@ -73,5 +74,5 @@ $ -> $('section.edit-pane').show() return false - bind_edit_links() + CMS.bind() diff --git a/cms/static/coffee/unit.coffee b/cms/static/coffee/unit.coffee index f41bd7f96a..b81bc0df08 100644 --- a/cms/static/coffee/unit.coffee +++ b/cms/static/coffee/unit.coffee @@ -2,12 +2,14 @@ class @Unit constructor: (@element_id, @module_id) -> @module = new window[$("##{@element_id}").attr('class')] 'module-html' - $("##{@element_id} .save-update").click( (event) => + $("##{@element_id} .save-update").click (event) => event.preventDefault() $.post("save_item", { id: @module_id data: JSON.stringify(@module.save()) }) - - ) + + $("##{@element_id} .cancel").click (event) => + event.preventDefault() + CMS.edit_item(@module_id) From 9e8f20104c17aab7005b3c0199de0612f24b0fce Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 22 Jun 2012 11:17:31 -0400 Subject: [PATCH 131/252] added styles for ol in html units --- lms/static/sass/courseware/_courseware.scss | 19 ++++++++----------- lms/static/sass/courseware/_problems.scss | 1 - lms/static/sass/courseware/_video.scss | 2 ++ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lms/static/sass/courseware/_courseware.scss b/lms/static/sass/courseware/_courseware.scss index cc822c6d77..dfc27ad652 100644 --- a/lms/static/sass/courseware/_courseware.scss +++ b/lms/static/sass/courseware/_courseware.scss @@ -12,7 +12,11 @@ div.course-wrapper { @extend .table-wrapper; ul, ol { - list-style: none; + padding-left: lh(); + + li { + margin-bottom: lh(.5); + } } section.course-content { @@ -32,12 +36,6 @@ div.course-wrapper { } } - ul { - li { - margin-bottom: lh(.5); - } - } - div#seq_content { h1 { background: none; @@ -48,6 +46,9 @@ div.course-wrapper { } ol.vert-mod { + list-style: none; + padding-left: 0; + > li { @extend .clearfix; @extend .problem-set; @@ -85,10 +86,6 @@ div.course-wrapper { height: 150px; } - ul { - list-style: disc outside none; - padding-left: 1em; - } nav.sequence-bottom { ul { diff --git a/lms/static/sass/courseware/_problems.scss b/lms/static/sass/courseware/_problems.scss index 0b056ca549..cbcc2ab4bc 100644 --- a/lms/static/sass/courseware/_problems.scss +++ b/lms/static/sass/courseware/_problems.scss @@ -53,7 +53,6 @@ section.problem-set { } } - div { p.status { text-indent: -9999px; diff --git a/lms/static/sass/courseware/_video.scss b/lms/static/sass/courseware/_video.scss index 87092fdc54..15cc75e501 100644 --- a/lms/static/sass/courseware/_video.scss +++ b/lms/static/sass/courseware/_video.scss @@ -442,6 +442,8 @@ section.course-content { max-height: 460px; overflow: auto; width: flex-grid(3, 9); + padding-left: 0; + list-style: none; li { border: 0; From 5908d844b87df491e04f8828551485f0d2c2c9ee Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Fri, 22 Jun 2012 16:21:51 -0400 Subject: [PATCH 132/252] Added styles to be more consistant across pages --- cms/static/sass/_module-header.scss | 125 ---------- cms/static/sass/_problem.scss | 238 ++++++++++++++---- cms/static/sass/_week.scss | 302 ++++++++++------------- cms/static/sass/base-style.scss | 2 +- cms/templates/unit.html | 19 +- cms/templates/widgets/html-edit.html | 58 +++++ cms/templates/widgets/problem-edit.html | 118 ++++----- cms/templates/widgets/sequence-edit.html | 201 +++++++-------- 8 files changed, 527 insertions(+), 536 deletions(-) delete mode 100644 cms/static/sass/_module-header.scss diff --git a/cms/static/sass/_module-header.scss b/cms/static/sass/_module-header.scss deleted file mode 100644 index 03acbecd9b..0000000000 --- a/cms/static/sass/_module-header.scss +++ /dev/null @@ -1,125 +0,0 @@ -section.video-new, -section.video-edit, -section.problem-new, -section.problem-edit, -section.html-edit { - background: #fff; - border: 1px solid #333; - border-right: 0; - - > header { - background: #eee; - @include clearfix; - padding: 6px; - border-bottom: 1px solid #333; - -webkit-font-smoothing: antialiased; - - h2 { - float: left; - font-size: 14px; - } - - a { - color: #fff; - - &.save-update { - float: right; - } - - &.cancel { - float: left; - } - } - - } - - > section { - padding: 20px; - - > header { - h1 { - font-size: 24px; - margin: 12px 0; - } - - section { - &.status-settings { - ul { - list-style: none; - @include border-radius(2px); - border: 1px solid #999; - @include inline-block(); - - li { - @include inline-block(); - border-right: 1px solid #999; - padding: 6px; - - &:last-child { - border-right: 0; - } - - &.current { - background: #eee; - } - } - } - - a.settings { - @include inline-block(); - margin: 0 20px; - border: 1px solid #999; - padding: 6px; - } - - select { - float: right; - } - } - - &.meta { - background: #eee; - padding: 10px; - margin: 20px 0; - @include clearfix(); - - div { - float: left; - margin-right: 20px; - - h2 { - font-size: 14px; - @include inline-block(); - } - - p { - @include inline-block(); - } - } - } - } - } - - section.notes { - margin-top: 20px; - padding: 6px; - background: #eee; - border: 1px solid #ccc; - - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - - h2 { - font-size: 14px; - margin-bottom: 6px; - } - - input[type="submit"]{ - margin-top: 10px; - } - } - } -} diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_problem.scss index 39df062cde..fa2657c0d1 100644 --- a/cms/static/sass/_problem.scss +++ b/cms/static/sass/_problem.scss @@ -4,68 +4,200 @@ section#unit-wrapper { @include clearfix(); padding: 6px 20px; - h1 { - font-size: 18px; - text-transform: uppercase; - letter-spacing: 1px; + section { float: left; + + h1 { + font-size: 16px; + text-transform: uppercase; + letter-spacing: 1px; + @include inline-block(); + } + + p { + @include inline-block(); + margin-left: 10px; + color: #999; + font-size: 12px; + font-style: italic; + } } - p { + div { float: right; + color: #666; } } + > section { padding: 20px; - a.save-update { - @extend .button; - @include inline-block(); + + section.meta { + section { + &.status-settings { + float: left; + margin-bottom: 10px; + + ul { + list-style: none; + @include border-radius(2px); + border: 1px solid #999; + @include inline-block(); + + li { + @include inline-block(); + border-right: 1px solid #999; + padding: 6px; + + &:last-child { + border-right: 0; + } + + &.current { + background: #eee; + } + } + } + + a.settings { + @include inline-block(); + margin: 0 20px; + border: 1px solid #999; + padding: 6px; + } + + select { + float: right; + } + } + + &.author { + float: right; + + dl { + dt { + font-weight: bold; + } + + dd, dt { + @include inline-block(); + } + } + } + + &.tags { + background: #eee; + padding: 10px; + margin: 0 0 20px; + @include clearfix(); + clear: both; + + div { + float: left; + margin-right: 20px; + + h2 { + font-size: 14px; + @include inline-block(); + } + + p { + @include inline-block(); + } + } + } + } + } + + //general styles for main content + textarea { + @include box-sizing(border-box); + display: block; + width: 100%; + } + + div.preview { + background: #eee; + @include box-sizing(border-box); + min-height: 40px; + padding: 10px; + width: 100%; + margin-top: 10px; + + h1 { + font-size: 24px; + margin-bottom: 1em; + } + + h2 { + font-size: 20px; + margin-bottom: 1em; + } + + h3 { + font-size: 18; + margin-bottom: 1em; + } + + ul { + padding-left: 20px; + margin-bottom: 1em; + } + + p { + margin-bottom: 1em; + } + } + + //notes + section.notes { margin-top: 20px; + padding: 20px 0 0; + border-top: 1px solid #ccc; + + h2 { + font-size: 14px; + margin-bottom: 6px; + } + + form { + margin-bottom: 20px; + + textarea { + @include box-sizing(border-box); + display: block; + width: 100%; + } + + input[type="submit"]{ + margin-top: 10px; + } + } + + ul { + list-style: none; + + li { + margin-bottom: 20px; + + p { + margin-bottom: 10px; + + &.author { + font-style: italic; + color: #999; + } + } + } + } + } + + div.actions { + a.save-update { + @extend .button; + @include inline-block(); + margin-top: 20px; + } } } } - -section.problem-new, -section.problem-edit, -section.html-edit { - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - - - div.preview { - background: #eee; - @include box-sizing(border-box); - min-height: 40px; - padding: 10px; - width: 100%; - margin-top: 10px; - - h1 { - font-size: 24px; - margin-bottom: 1em; - } - - h2 { - font-size: 20px; - margin-bottom: 1em; - } - - h3 { - font-size: 18; - margin-bottom: 1em; - } - - ul { - padding-left: 20px; - margin-bottom: 1em; - } - - p { - margin-bottom: 1em; - } - } -} - diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_week.scss index b638a36f5c..0aa42f99de 100644 --- a/cms/static/sass/_week.scss +++ b/cms/static/sass/_week.scss @@ -6,47 +6,6 @@ section.sequence-edit { border-bottom: 2px solid #333; @include clearfix(); - div { - @include clearfix(); - padding: 6px 20px; - - h1 { - font-size: 18px; - text-transform: uppercase; - letter-spacing: 1px; - float: left; - } - - p { - float: right; - } - - &.week { - background: #eee; - font-size: 12px; - border-bottom: 1px solid #ccc; - - h2 { - font-size: 12px; - @include inline-block(); - margin-right: 20px; - } - - ul { - list-style: none; - @include inline-block(); - - li { - @include inline-block(); - margin-right: 10px; - - p { - float: none; - } - } - } - } - } section.goals { background: #eee; @@ -68,121 +27,116 @@ section.sequence-edit { } } - > section.content { - @include box-sizing(border-box); - padding: 20px; + section.filters { + @include clearfix; + margin-bottom: 10px; + background: #efefef; + border: 1px solid #ddd; - section.filters { - @include clearfix; - margin-bottom: 10px; - background: #efefef; - border: 1px solid #ddd; + ul { + @include clearfix(); + list-style: none; + padding: 6px; - ul { - @include clearfix(); - list-style: none; - padding: 6px; + li { + @include inline-block(); - li { - @include inline-block(); - - &.advanced { - float: right; - } + &.advanced { + float: right; } } } + } - > div { - display: table; - border: 1px solid; - width: 100%; + > div { + display: table; + border: 1px solid; + width: 100%; - section { - header { - background: #eee; - padding: 6px; - border-bottom: 1px solid #ccc; - @include clearfix; + section { + header { + background: #eee; + padding: 6px; + border-bottom: 1px solid #ccc; + @include clearfix; - h2 { - text-transform: uppercase; - letter-spacing: 1px; - font-size: 12px; - float: left; + h2 { + text-transform: uppercase; + letter-spacing: 1px; + font-size: 12px; + float: left; + } + } + + &.modules { + @include box-sizing(border-box); + display: table-cell; + width: flex-grid(6, 9); + border-right: 1px solid #333; + + &.empty { + text-align: center; + vertical-align: middle; + + a { + @extend .button; + @include inline-block(); + margin-top: 10px; } } - &.modules { - @include box-sizing(border-box); - display: table-cell; - width: flex-grid(6, 9); - border-right: 1px solid #333; + ol { + list-style: none; + border-bottom: 1px solid #333; - &.empty { - text-align: center; - vertical-align: middle; - - a { - @extend .button; - @include inline-block(); - margin-top: 10px; - } - } - - ol { - list-style: none; + li { border-bottom: 1px solid #333; - li { - border-bottom: 1px solid #333; + &:last-child{ + border-bottom: 0; + } - &:last-child{ - border-bottom: 0; - } + a { + color: #000; + } - a { - color: #000; - } + ol { + list-style: none; - ol { - list-style: none; - - li { - padding: 6px; - - &:hover { - a.draggable { - opacity: 1; - } - } + li { + padding: 6px; + &:hover { a.draggable { - float: right; - opacity: .5; + opacity: 1; + } + } + + a.draggable { + float: right; + opacity: .5; + } + + &.group { + padding: 0; + + header { + padding: 6px; + background: none; + + h3 { + font-size: 14px; + } } - &.group { - padding: 0; - header { - padding: 6px; - background: none; - - h3 { - font-size: 14px; - } - } - - - ol { + ol { border-left: 4px solid #999; border-bottom: 0; - li { - &:last-child { - border-bottom: 0; - } + li { + &:last-child { + border-bottom: 0; } } } @@ -191,63 +145,63 @@ section.sequence-edit { } } } + } - &.scratch-pad { - @include box-sizing(border-box); - display: table-cell; - width: flex-grid(3, 9) + flex-gutter(9); - vertical-align: top; + &.scratch-pad { + @include box-sizing(border-box); + display: table-cell; + width: flex-grid(3, 9) + flex-gutter(9); + vertical-align: top; - ol { - list-style: none; + ol { + list-style: none; + border-bottom: 1px solid #999; + + li { border-bottom: 1px solid #999; + background: #f9f9f9; - li { - border-bottom: 1px solid #999; - background: #f9f9f9; + &:last-child { + border-bottom: 0; + } - &:last-child { - border-bottom: 0; - } + ul { + list-style: none; - ul { - list-style: none; + li { + padding: 6px; - li { - padding: 6px; - - &:last-child { - border-bottom: 0; - } - - &:hover { - a.draggable { - opacity: 1; - } - } - - &.empty { - padding: 12px; - - a { - @extend .button; - display: block; - text-align: center; - } - } + &:last-child { + border-bottom: 0; + } + &:hover { a.draggable { - float: right; - opacity: .3; - } - - a { - color: #000; + opacity: 1; } } - } + &.empty { + padding: 12px; + + a { + @extend .button; + display: block; + text-align: center; + } + } + + a.draggable { + float: right; + opacity: .3; + } + + a { + color: #000; + } + } } + } } } diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 133e1bda1b..092e40d9c5 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -3,4 +3,4 @@ @import 'base'; @import 'calendar'; -@import 'week', 'video', 'problem', 'module-header'; +@import 'week', 'video', 'problem'; diff --git a/cms/templates/unit.html b/cms/templates/unit.html index c9a3d82b5a..92f54fe5d7 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -1,19 +1,16 @@
        -

        ${name}

        -

        Unit type: ${type}

        - - - - -
        -
        - ${contents} -
        -
        +
        +

        ${name}

        +

        ${type}

        +
        + + +
        + ${contents}
        diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index cbd8a7f3df..e34e8e6480 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -1,4 +1,62 @@
        +
        + +
        + + Settings +
        + +
        +
        +
        Last modified:
        +
        mm/dd/yy
        +
        By
        +
        Anant Agarwal
        +
        +
        + +
        +
        +

        Tags:

        +

        Click to edit

        +
        + +
        +

        Goal

        +

        Click to edit

        +
        +
        +
        +
        ${module.definition['data']['text']}
        + + + +
        +

        Add notes

        +
        + + +
        + +
          +
        • +

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          +

          Anant Agarwal

          +
        • +
        • +

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          +

          Anant Agarwal

          +
        • +
        +
        diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html index 991326c987..aa98c468fe 100644 --- a/cms/templates/widgets/problem-edit.html +++ b/cms/templates/widgets/problem-edit.html @@ -1,73 +1,61 @@
        -
        - Cancel - Save & Update -
        - -
        -
        -

        ${module.name}

        -
        -
        -

        Last modified:

        -

        mm/dd/yy

        -
        - -
        -

        By

        -

        Anant Agarwal

        -
        -
        - -
        - - Settings - - -
        -
        -
        -

        Tags:

        -

        Click to edit

        -
        - -
        -

        Goal

        -

        Click to edit

        -
        -
        -
        - -
        - -
        - Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. -
        +
        +
        + + Settings
        -
        -

        Add notes

        +
        +
        +
        Last modified:
        +
        mm/dd/yy
        +
        By
        +
        Anant Agarwal
        +
        +
        + +
        +
        +

        Tags:

        +

        Click to edit

        +
        + +
        +

        Goal

        +

        Click to edit

        +
        +
        +
        + +
        + +
        + Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. +
        +
        + +
        +

        Add notes

        +
        +
        -
          -
        • -

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          -

          Anant Agarwal

          -
        • -
        • -

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          -

          Anant Agarwal

          -
        • -
        - - Save & Update +
          +
        • +

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          +

          Anant Agarwal

          +
        • +
        • +

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          +

          Anant Agarwal

          +
        • +
        + diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html index 219145aefb..62742edd3e 100644 --- a/cms/templates/widgets/sequence-edit.html +++ b/cms/templates/widgets/sequence-edit.html @@ -1,119 +1,106 @@
        -
        -
        -

        ${module.name}

        -

        Module Type:${module.type}

        -
        -
        +
        +
          +
        • + + +
        • -
          -
          -
            +
          • + + +
          • +
          • + +
          • + +
          • + Advanced filters +
          • + +
          • + +
          • +
          +
          + +
          +
          +
          1. - - +
              + % for child in module.get_children(): +
            1. + ${child.name} + handle +
            2. + %endfor +
          2. -
          3. - - -
          4. -
          5. - -
          6. - -
          7. - Advanced filters -
          8. - -
          9. - -
          10. -
        +
        -
        -
        -
          -
        1. -
            - % for child in module.get_children(): -
          1. - ${child.name} - handle -
          2. - %endfor -
          -
        2. - - - - -
        -
        - -
        -
          -
        1. -
          -

          Section Scratch

          -
          - +
        2. +
        3. +
          +

          Course Scratch

          +
          - -
        4. - - - - -
        -
        -
        -
        + + + +
        +
        From 3081258836defd7e1362db5ba107ec4430518e4e Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Mon, 25 Jun 2012 11:21:15 -0400 Subject: [PATCH 133/252] Refactored the css and fixed some smaller bugs --- cms/static/coffee/main.coffee | 3 + cms/static/sass/_base.scss | 68 +++---------------- cms/static/sass/_calendar.scss | 1 - cms/static/sass/_layout.scss | 53 +++++++++++++++ cms/static/sass/{_week.scss => _section.scss} | 32 +-------- cms/static/sass/{_problem.scss => _unit.scss} | 20 +++--- cms/static/sass/_video.scss | 33 --------- cms/static/sass/base-style.scss | 4 +- cms/templates/unit.html | 3 +- cms/templates/widgets/html-edit.html | 19 +----- cms/templates/widgets/notes.html | 21 ++++++ cms/templates/widgets/problem-edit.html | 25 ++----- cms/templates/widgets/sequence-edit.html | 2 +- 13 files changed, 109 insertions(+), 175 deletions(-) create mode 100644 cms/static/sass/_layout.scss rename cms/static/sass/{_week.scss => _section.scss} (88%) rename cms/static/sass/{_problem.scss => _unit.scss} (93%) delete mode 100644 cms/static/sass/_video.scss create mode 100644 cms/templates/widgets/notes.html diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee index 06b8701366..6535034faf 100644 --- a/cms/static/coffee/main.coffee +++ b/cms/static/coffee/main.coffee @@ -63,6 +63,9 @@ $ -> $('section.edit-pane').show() return false + $('a.module-edit').click -> + $('body.content .cal').css('height', contentHeight) + $(document).ready(setHeight) $(window).bind('resize', setHeight) diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index d961f461fd..6d1111980c 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -2,65 +2,14 @@ $fg-column: 70px; $fg-gutter: 26px; $fg-max-columns: 12; $body-font-family: "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif; +$body-font-size: 14px; +$body-line-height: 20px; +// Base html styles html { height: 100%; } -body { - @include clearfix(); - height: 100%; - font: 14px $body-font-family; - - > section { - display: table; - width: 100%; - } - - > header { - background: #000; - color: #fff; - display: block; - float: none; - padding: 6px 20px; - width: 100%; - @include box-sizing(border-box); - - nav { - @include clearfix; - - h2 { - font-size: 14px; - text-transform: uppercase; - float: left; - } - - ul { - float: left; - - &.user-nav { - float: right; - } - - li { - @include inline-block(); - margin-left: 15px; - } - } - } - } - - &.content { - section.main-content { - border-left: 2px solid #000; - @include box-sizing(border-box); - width: flex-grid(9); - float: left; - @include box-shadow( -2px 0 3px #ddd ); - } - } -} - a { text-decoration: none; color: #888; @@ -77,6 +26,13 @@ input[type="submit"], .button { padding: 6px; } +textarea { + @include box-sizing(border-box); + display: block; + width: 100%; +} + +// Extends .new-module { position: relative; @@ -111,7 +67,3 @@ input[type="submit"], .button { display: block; float: right; } - -textarea { - overflow: auto; -} diff --git a/cms/static/sass/_calendar.scss b/cms/static/sass/_calendar.scss index fa10c65950..71f12f2425 100644 --- a/cms/static/sass/_calendar.scss +++ b/cms/static/sass/_calendar.scss @@ -49,7 +49,6 @@ section.cal { ol { list-style: none; @include clearfix; - @include box-sizing(border-box); border-left: 1px solid #333; border-top: 1px solid #333; width: 100%; diff --git a/cms/static/sass/_layout.scss b/cms/static/sass/_layout.scss new file mode 100644 index 0000000000..98f49940fd --- /dev/null +++ b/cms/static/sass/_layout.scss @@ -0,0 +1,53 @@ +body { + @include clearfix(); + height: 100%; + font: 14px $body-font-family; + + > section { + display: table; + width: 100%; + } + + > header { + background: #000; + color: #fff; + display: block; + float: none; + padding: 6px 20px; + width: 100%; + @include box-sizing(border-box); + + nav { + @include clearfix; + + h2 { + font-size: 14px; + text-transform: uppercase; + float: left; + } + + ul { + float: left; + + &.user-nav { + float: right; + } + + li { + @include inline-block(); + margin-left: 15px; + } + } + } + } + + &.content { + section.main-content { + border-left: 2px solid #000; + @include box-sizing(border-box); + width: flex-grid(9); + float: left; + @include box-shadow( -2px 0 3px #ddd ); + } + } +} diff --git a/cms/static/sass/_week.scss b/cms/static/sass/_section.scss similarity index 88% rename from cms/static/sass/_week.scss rename to cms/static/sass/_section.scss index 0aa42f99de..2048607565 100644 --- a/cms/static/sass/_week.scss +++ b/cms/static/sass/_section.scss @@ -1,32 +1,4 @@ -section.week-edit, -section.week-new, -section.sequence-edit { - - > header { - border-bottom: 2px solid #333; - @include clearfix(); - - - section.goals { - background: #eee; - padding: 6px 20px; - border-top: 1px solid #ccc; - - ul { - list-style: none; - color: #999; - - li { - margin-bottom: 6px; - - &:last-child { - margin-bottom: 0; - } - } - } - } - } - +section#unit-wrapper { section.filters { @include clearfix; margin-bottom: 10px; @@ -48,7 +20,7 @@ section.sequence-edit { } } - > div { + div.content { display: table; border: 1px solid; width: 100%; diff --git a/cms/static/sass/_problem.scss b/cms/static/sass/_unit.scss similarity index 93% rename from cms/static/sass/_problem.scss rename to cms/static/sass/_unit.scss index fa2657c0d1..f1e47d36be 100644 --- a/cms/static/sass/_problem.scss +++ b/cms/static/sass/_unit.scss @@ -26,6 +26,14 @@ section#unit-wrapper { div { float: right; color: #666; + + a { + &.cancel { + margin-right: 20px; + font-style: italic; + font-size: 12px; + } + } } } @@ -110,12 +118,6 @@ section#unit-wrapper { } //general styles for main content - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - div.preview { background: #eee; @include box-sizing(border-box); @@ -163,12 +165,6 @@ section#unit-wrapper { form { margin-bottom: 20px; - textarea { - @include box-sizing(border-box); - display: block; - width: 100%; - } - input[type="submit"]{ margin-top: 10px; } diff --git a/cms/static/sass/_video.scss b/cms/static/sass/_video.scss deleted file mode 100644 index b68176e2db..0000000000 --- a/cms/static/sass/_video.scss +++ /dev/null @@ -1,33 +0,0 @@ -section.video-new, section.video-edit { - > section { - - section.upload { - padding: 6px; - margin-bottom: 10px; - border: 1px solid #ddd; - - a.upload-button { - @extend .button; - @include inline-block(); - } - } - - section.in-use { - h2 { - font-size: 14px; - } - - div { - background: #eee; - text-align: center; - padding: 6px; - } - } - - a.save-update { - @extend .button; - @include inline-block(); - margin-top: 20px; - } - } -} diff --git a/cms/static/sass/base-style.scss b/cms/static/sass/base-style.scss index 092e40d9c5..3a6c6e0cea 100644 --- a/cms/static/sass/base-style.scss +++ b/cms/static/sass/base-style.scss @@ -1,6 +1,6 @@ @import 'bourbon/bourbon'; @import 'reset'; -@import 'base'; +@import 'base', 'layout'; @import 'calendar'; -@import 'week', 'video', 'problem'; +@import 'section', 'unit'; diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 92f54fe5d7..8cc75cd3bf 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -4,9 +4,10 @@

        ${name}

        ${type}

        + diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index e34e8e6480..7b17a2ed14 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -41,22 +41,5 @@ Cancel -
        -

        Add notes

        -
        - - -
        - -
          -
        • -

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          -

          Anant Agarwal

          -
        • -
        • -

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          -

          Anant Agarwal

          -
        • -
        -
        + <%include file="notes.html"/> diff --git a/cms/templates/widgets/notes.html b/cms/templates/widgets/notes.html new file mode 100644 index 0000000000..71c7afab85 --- /dev/null +++ b/cms/templates/widgets/notes.html @@ -0,0 +1,21 @@ +
        +

        Notes

        + +
          +
        • +

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          +

          Anant Agarwal

          +
        • +
        • +

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          +

          Anant Agarwal

          +
        • +
        + +
        +

        Add a note

        + + +
        +
        + diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html index aa98c468fe..a2da078927 100644 --- a/cms/templates/widgets/problem-edit.html +++ b/cms/templates/widgets/problem-edit.html @@ -37,25 +37,12 @@
        Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
        + + -
        -

        Add notes

        -
        - - -
        - -
          -
        • -

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          -

          Anant Agarwal

          -
        • -
        • -

          Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.

          -

          Anant Agarwal

          -
        • -
        -
        + <%include file="notes.html"/> - diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html index 62742edd3e..abeec9209d 100644 --- a/cms/templates/widgets/sequence-edit.html +++ b/cms/templates/widgets/sequence-edit.html @@ -30,7 +30,7 @@ -
        +
        1. From 656fa389441372c3aec44e79071c46e9a5f07cec Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Mon, 25 Jun 2012 11:44:06 -0400 Subject: [PATCH 134/252] Added wip class --- cms/static/coffee/main.coffee | 2 ++ cms/static/sass/_base.scss | 4 ++++ cms/templates/widgets/html-edit.html | 4 ++-- cms/templates/widgets/module-dropdown.html | 2 +- cms/templates/widgets/navigation.html | 2 +- cms/templates/widgets/notes.html | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee index 6535034faf..66f375a7cc 100644 --- a/cms/static/coffee/main.coffee +++ b/cms/static/coffee/main.coffee @@ -37,6 +37,8 @@ $ -> $(this).parent().parent().hide() return false + # $('html').keypress -> + # $('.wip').css('visibility', 'visible') setHeight = -> windowHeight = $(this).height() diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 6d1111980c..01f3af3ac7 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -67,3 +67,7 @@ textarea { display: block; float: right; } + +.wip { + outline: 1px solid #ff0 !important; +} diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index 7b17a2ed14..7eec86215a 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -1,5 +1,5 @@
          -
          +
            @@ -36,7 +36,7 @@
            ${module.definition['data']['text']}
            -
            + diff --git a/cms/templates/widgets/module-dropdown.html b/cms/templates/widgets/module-dropdown.html index 7c6e1e068c..6edb142e40 100644 --- a/cms/templates/widgets/module-dropdown.html +++ b/cms/templates/widgets/module-dropdown.html @@ -1,4 +1,4 @@ -
          • +
          • + Add new module diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 3fbcda675c..cbdc7660ce 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -1,5 +1,5 @@
            -
            +

            Filter content:

            • diff --git a/cms/templates/widgets/notes.html b/cms/templates/widgets/notes.html index 71c7afab85..920e88cbcd 100644 --- a/cms/templates/widgets/notes.html +++ b/cms/templates/widgets/notes.html @@ -1,4 +1,4 @@ -
              +

              Notes

                From 863568ec997593ed6472ab57a1c0258496654c35 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Mon, 25 Jun 2012 11:52:43 -0400 Subject: [PATCH 135/252] Added red wip class and added tag for it --- cms/static/sass/_base.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 01f3af3ac7..afda14800f 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -69,5 +69,15 @@ textarea { } .wip { - outline: 1px solid #ff0 !important; + outline: 1px solid #f00 !important; + position: relative; + + &:after { + content: "WIP"; + font-size: 8px; + padding: 2px; + background: #f00; + color: #fff; + @include position(absolute, 0px 0px 0 0); + } } From 05f4e4fb383b972cabf1c6e90795cecbf13e6f68 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Mon, 25 Jun 2012 12:22:37 -0400 Subject: [PATCH 136/252] Scroll the caption upon video seeking --- lms/static/coffee/spec/modules/video/video_caption_spec.coffee | 3 +++ lms/static/coffee/src/modules/video/video_caption.coffee | 1 + 2 files changed, 4 insertions(+) diff --git a/lms/static/coffee/spec/modules/video/video_caption_spec.coffee b/lms/static/coffee/spec/modules/video/video_caption_spec.coffee index e816da593a..3fa6fa7daa 100644 --- a/lms/static/coffee/spec/modules/video/video_caption_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_caption_spec.coffee @@ -35,6 +35,9 @@ describe 'VideoCaption', -> it 'bind player resize event', -> expect($(@player)).toHandleWith 'resize', @caption.onWindowResize + it 'bind player seek event', -> + expect($(@player)).toHandleWith 'seek', @caption.onUpdatePlayTime + it 'bind player updatePlayTime event', -> expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime diff --git a/lms/static/coffee/src/modules/video/video_caption.coffee b/lms/static/coffee/src/modules/video/video_caption.coffee index 7d796245bb..5dde796b78 100644 --- a/lms/static/coffee/src/modules/video/video_caption.coffee +++ b/lms/static/coffee/src/modules/video/video_caption.coffee @@ -10,6 +10,7 @@ class @VideoCaption $(window).bind('resize', @onWindowResize) $(@player).bind('resize', @onWindowResize) $(@player).bind('updatePlayTime', @onUpdatePlayTime) + $(@player).bind('seek', @onUpdatePlayTime) $(@player).bind('play', @onPlay) @$('.hide-subtitles').click @toggle @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) From d9d6f9b749a6fd10e1e365c394bd1f3cfc1a4f73 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Mon, 25 Jun 2012 16:38:06 -0400 Subject: [PATCH 137/252] Introduce `rake cms:import` task to import data Usage: `rake cms:import DATA_DIR=` --- rakefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rakefile b/rakefile index 2db1607237..b6fbb18ac6 100644 --- a/rakefile +++ b/rakefile @@ -159,3 +159,15 @@ end task :publish => :package do sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}") end + +namespace :cms do + desc "Import course data within the given DATA_DIR variable" + task :import do + if ENV['DATA_DIR'] + sh(django_admin(:cms, :dev, :import, ENV['DATA_DIR'])) + else + raise "Please specify a DATA_DIR variable that point to your data directory.\n" + + "Example: \`rake cms:import DATA_DIR=../data\`" + end + end +end From e9a00ffc5a766f2caf56e1ab71783dbda67b8149 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 11:55:20 -0400 Subject: [PATCH 138/252] Parse XModuleDescriptors on import using from_xml Also: Render all XModuleDescriptors in the cms the same way Default them to editing raw xml, if there is no specific module for them --- .../management/commands/import.py | 182 ++---------------- cms/djangoapps/contentstore/views.py | 3 +- cms/envs/common.py | 3 +- cms/static/coffee/main.coffee | 11 +- cms/templates/unit.html | 2 +- cms/templates/widgets/navigation.html | 2 +- cms/templates/widgets/raw-edit.html | 45 +++++ common/lib/keystore/__init__.py | 4 +- common/lib/keystore/django.py | 6 +- common/lib/keystore/mongo.py | 11 +- common/lib/xmodule/html_module.py | 2 +- common/lib/xmodule/js/module/raw.coffee | 9 + common/lib/xmodule/mako_module.py | 10 +- common/lib/xmodule/raw_module.py | 41 ++++ common/lib/xmodule/seq_module.py | 20 +- common/lib/xmodule/setup.py | 14 +- common/lib/xmodule/x_module.py | 79 ++++++-- 17 files changed, 242 insertions(+), 202 deletions(-) create mode 100644 cms/templates/widgets/raw-edit.html create mode 100644 common/lib/xmodule/js/module/raw.coffee create mode 100644 common/lib/xmodule/raw_module.py diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 12ea2e0399..ca94c2af5a 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -2,175 +2,33 @@ ### One-off script for importing courseware form XML format ### - -#import mitxmako.middleware -#from courseware import content_parser -#from django.contrib.auth.models import User -import os.path -from StringIO import StringIO -from mako.template import Template -from mako.lookup import TemplateLookup -from collections import defaultdict - from django.core.management.base import BaseCommand from keystore.django import keystore +from raw_module import RawDescriptor + +from path import path +from x_module import XModuleDescriptor, DescriptorSystem -from lxml import etree class Command(BaseCommand): help = \ -''' Run FTP server.''' +'''Import the specified data directory into the default keystore''' + def handle(self, *args, **options): - print args - data_dir = args[0] - - parser = etree.XMLParser(remove_comments = True) + org, course, data_dir = args + data_dir = path(data_dir) + with open(data_dir / "course.xml") as course_file: - lookup = TemplateLookup(directories=[data_dir]) - template = lookup.get_template("course.xml") - course_string = template.render(groups=[]) - course = etree.parse(StringIO(course_string), parser=parser) + system = DescriptorSystem(keystore().get_item) - elements = list(course.iter()) + def process_xml(xml): + module = XModuleDescriptor.load_from_xml(xml, system, org, course, RawDescriptor) + keystore().create_item(module.url) + if 'data' in module.definition: + keystore().update_item(module.url, module.definition['data']) + if 'children' in module.definition: + keystore().update_children(module.url, module.definition['children']) + return module.url - tag_to_category = { - # Custom tags - 'videodev': 'Custom', - 'slides': 'Custom', - 'book': 'Custom', - 'image': 'Custom', - 'discuss': 'Custom', - # Simple lists - 'chapter': 'Week', - 'course': 'Course', - 'section': defaultdict(lambda: 'Section', { - 'Lab': 'Lab', - 'Lecture Sequence': 'LectureSequence', - 'Homework': 'Homework', - 'Tutorial Index': 'TutorialIndex', - 'Video': 'VideoSegment', - 'Midterm': 'Exam', - 'Final': 'Exam', - 'Problems': 'ProblemSet', - }), - 'videosequence': 'VideoSequence', - 'problemset': 'ProblemSet', - 'vertical': 'Section', - 'sequential': 'Section', - 'tab': 'Section', - # True types - 'video': 'VideoSegment', - 'html': 'HTML', - 'problem': 'Problem', - } - - name_index = 0 - for e in elements: - name = e.attrib.get('name', None) - for f in elements: - if f != e and f.attrib.get('name', None) == name: - name = None - if not name: - name = "{tag}_{index}".format(tag=e.tag, index=name_index) - name_index = name_index + 1 - if e.tag in tag_to_category: - category = tag_to_category[e.tag] - if isinstance(category, dict): - category = category[e.get('format')] - category = category.replace('/', '-') - name = name.replace('/', '-') - e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format( - category=category, - name=name)) - else: - print "Skipping element with tag", e.tag - - - def handle_skip(e): - print "Skipping ", e - - results = {} - - def handle_custom(e): - data = {'type':'i4x://mit.edu/6002xs12/tag/{tag}'.format(tag=e.tag), - 'attrib':dict(e.attrib)} - results[e.attrib['url']] = {'data':data} - - def handle_list(e): - if e.attrib.get("class", None) == "tutorials": - return - children = [le.attrib['url'] for le in e.getchildren()] - results[e.attrib['url']] = {'children':children} - - def handle_video(e): - url = e.attrib['url'] - clip_url = url.replace('VideoSegment', 'VideoClip') - # Take: 0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8 - # Make: [(0.75, 'izygArpw-Qo'), (1.0, 'p2Q6BrNhdh8'), (1.25, '1EeWXzPdhSA'), (1.5, 'rABDYkeK0x8')] - youtube_str = e.attrib['youtube'] - youtube_list = [(float(x), y) for x,y in map(lambda x:x.split(':'), youtube_str.split(','))] - clip_infos = [{ "status": "ready", - "format": "youtube", - "sane": True, - "location": "youtube", - "speed": speed, - "id": youtube_id, - "size": None} \ - for (speed, youtube_id) \ - in youtube_list] - results[clip_url] = {'data':{'clip_infos':clip_infos}} - results[url] = {'children' : [{'url':clip_url}]} - - def handle_html(e): - if 'src' in e.attrib: - text = open(data_dir+'html/'+e.attrib['src']).read() - else: - textlist=[e.text]+[etree.tostring(elem) for elem in e]+[e.tail] - textlist=[i for i in textlist if type(i)==str] - text = "".join(textlist) - - results[e.attrib['url']] = {'data':{'text':text}} - - def handle_problem(e): - data = open(os.path.join(data_dir, 'problems', e.attrib['filename']+'.xml')).read() - results[e.attrib['url']] = {'data':{'statement':data}} - - element_actions = {# Inside HTML ==> Skip these - 'a': handle_skip, - 'h1': handle_skip, - 'h2': handle_skip, - 'hr': handle_skip, - 'strong': handle_skip, - 'ul': handle_skip, - 'li': handle_skip, - 'p': handle_skip, - # Custom tags - 'videodev': handle_custom, - 'slides': handle_custom, - 'book': handle_custom, - 'image': handle_custom, - 'discuss': handle_custom, - # Simple lists - 'chapter': handle_list, - 'course': handle_list, - 'sequential': handle_list, - 'vertical': handle_list, - 'section': handle_list, - 'videosequence': handle_list, - 'problemset': handle_list, - 'tab': handle_list, - # True types - 'video': handle_video, - 'html': handle_html, - 'problem': handle_problem, - } - - for e in elements: - element_actions[e.tag](e) - - for k in results: - keystore().create_item(k, 'Piotr Mitros') - if 'data' in results[k]: - keystore().update_item(k, results[k]['data']) - if 'children' in results[k]: - keystore().update_children(k, results[k]['children']) + system.process_xml = process_xml + system.process_xml(course_file.read()) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 8bd55bf60f..9cc7eec9b2 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -11,7 +11,7 @@ def index(request): org = 'mit.edu' course = '6002xs12' name = '6.002 Spring 2012' - course = keystore().get_item(['i4x', org, course, 'Course', name]) + course = keystore().get_item(['i4x', org, course, 'course', name]) weeks = course.get_children() return render_to_response('index.html', {'weeks': weeks}) @@ -21,6 +21,7 @@ def edit_item(request): item = keystore().get_item(item_id) return render_to_response('unit.html', { 'contents': item.get_html(), + 'js_module': item.js_module_name(), 'type': item.type, 'name': item.name, }) diff --git a/cms/envs/common.py b/cms/envs/common.py index fc721ca820..9e3145fef1 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -158,6 +158,7 @@ PIPELINE_CSS = { PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] from x_module import XModuleDescriptor +from raw_module import RawDescriptor js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" try: os.makedirs(js_file_dir) @@ -168,7 +169,7 @@ except OSError as exc: raise module_js_sources = [] -for xmodule in XModuleDescriptor.load_classes(): +for xmodule in XModuleDescriptor.load_classes() + [RawDescriptor]: js = xmodule.get_javascript() for filetype in ('coffee', 'js'): for idx, fragment in enumerate(js.get(filetype, [])): diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee index 66f375a7cc..8f7d7d7323 100644 --- a/cms/static/coffee/main.coffee +++ b/cms/static/coffee/main.coffee @@ -1,15 +1,21 @@ class @CMS + @setHeight = => + windowHeight = $(this).height() + @contentHeight = windowHeight - 29 + @bind = => $('a.module-edit').click -> CMS.edit_item($(this).attr('id')) return false + $(window).bind('resize', CMS.setHeight) @edit_item = (id) => $.get('/edit_item', {id: id}, (data) => $('#module-html').empty().append(data) CMS.bind() - $('section.edit-pane').show() + $('body.content .cal').css('height', @contentHeight) $('body').addClass('content') + $('section.edit-pane').show() new Unit('unit-wrapper', id) ) @@ -78,6 +84,7 @@ $ -> $('.problem-new a').click -> $('section.edit-pane').show() return false - + + CMS.setHeight() CMS.bind() diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 8cc75cd3bf..59044ab28d 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -1,4 +1,4 @@ -
                +

                ${name}

                diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index cbdc7660ce..38b1cd9d94 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -38,7 +38,7 @@ % for week in weeks:
              • -

                ${week.name}

                +

                ${week.name}

                  % if week.goals: % for goal in week.goals: diff --git a/cms/templates/widgets/raw-edit.html b/cms/templates/widgets/raw-edit.html new file mode 100644 index 0000000000..1ad0758004 --- /dev/null +++ b/cms/templates/widgets/raw-edit.html @@ -0,0 +1,45 @@ +
                  +
                  + +
                  + + Settings +
                  + +
                  +
                  +
                  Last modified:
                  +
                  mm/dd/yy
                  +
                  By
                  +
                  Anant Agarwal
                  +
                  +
                  + +
                  +
                  +

                  Tags:

                  +

                  Click to edit

                  +
                  + +
                  +

                  Goal

                  +

                  Click to edit

                  +
                  +
                  +
                  + + +
                  ${data | h}
                  + + + + <%include file="notes.html"/> +
                  diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 801ba57f80..c0fb40d33e 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -125,7 +125,7 @@ class ModuleStore(object): """ An abstract interface for a database backend that stores XModuleDescriptor instances """ - def get_item(self, location): + def get_item(self, location, default_class=None): """ Returns an XModuleDescriptor instance for the item at location. If location.revision is None, returns the item with the most @@ -136,6 +136,8 @@ class ModuleStore(object): If no object is found at that location, raises keystore.exceptions.ItemNotFoundError location: Something that can be passed to Location + default_class: An XModuleDescriptor subclass to use if no plugin matching the + location is found """ raise NotImplementedError diff --git a/common/lib/keystore/django.py b/common/lib/keystore/django.py index 2ba3f0756e..98479a7f7c 100644 --- a/common/lib/keystore/django.py +++ b/common/lib/keystore/django.py @@ -8,6 +8,7 @@ from __future__ import absolute_import from django.conf import settings from .mongo import MongoModuleStore +from raw_module import RawDescriptor _KEYSTORES = {} @@ -16,6 +17,9 @@ def keystore(name='default'): global _KEYSTORES if name not in _KEYSTORES: - _KEYSTORES[name] = MongoModuleStore(**settings.KEYSTORE[name]) + # TODO (cpennington): Load the default class from a string + _KEYSTORES[name] = MongoModuleStore( + default_class=RawDescriptor, + **settings.KEYSTORE[name]) return _KEYSTORES[name] diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index 29115a33a7..ece8b35b71 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -8,7 +8,7 @@ class MongoModuleStore(ModuleStore): """ A Mongodb backed ModuleStore """ - def __init__(self, host, db, collection, port=27017): + def __init__(self, host, db, collection, port=27017, default_class=None): self.collection = pymongo.connection.Connection( host=host, port=port @@ -16,6 +16,7 @@ class MongoModuleStore(ModuleStore): # Force mongo to report errors, at the expense of performance self.collection.safe = True + self.default_class = default_class def get_item(self, location): """ @@ -28,6 +29,8 @@ class MongoModuleStore(ModuleStore): If no object is found at that location, raises keystore.exceptions.ItemNotFoundError location: Something that can be passed to Location + default_class: An XModuleDescriptor subclass to use if no plugin matching the + location is found """ query = {} @@ -45,9 +48,10 @@ class MongoModuleStore(ModuleStore): if item is None: raise ItemNotFoundError(location) - return XModuleDescriptor.load_from_json(item, DescriptorSystem(self.get_item)) + return XModuleDescriptor.load_from_json( + item, DescriptorSystem(self.get_item), self.default_class) - def create_item(self, location, editor): + def create_item(self, location): """ Create an empty item at the specified location with the supplied editor @@ -55,7 +59,6 @@ class MongoModuleStore(ModuleStore): """ self.collection.insert({ 'location': Location(location).dict(), - 'editor': editor }) def update_item(self, location, data): diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index 977b5ef606..981109a9af 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -16,8 +16,8 @@ class HtmlModuleDescriptor(MakoModuleDescriptor): """ mako_template = "widgets/html-edit.html" - # TODO (cpennington): Make this into a proper module js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} + js_module = 'HTML' class Module(XModule): diff --git a/common/lib/xmodule/js/module/raw.coffee b/common/lib/xmodule/js/module/raw.coffee new file mode 100644 index 0000000000..1b9e05d6b6 --- /dev/null +++ b/common/lib/xmodule/js/module/raw.coffee @@ -0,0 +1,9 @@ +class @Raw + constructor: (@id) -> + @edit_box = $("##{@id} .edit-box") + @preview = $("##{@id} .preview") + @edit_box.on('input', => + @preview.empty().text(@edit_box.val()) + ) + + save: -> @edit_box.val() diff --git a/common/lib/xmodule/mako_module.py b/common/lib/xmodule/mako_module.py index 7887e13c12..2260dddd92 100644 --- a/common/lib/xmodule/mako_module.py +++ b/common/lib/xmodule/mako_module.py @@ -12,7 +12,11 @@ class MakoModuleDescriptor(XModuleDescriptor): the descriptor as the `module` parameter to that template """ + def get_context(self): + """ + Return the context to render the mako template with + """ + return {'module': self} + def get_html(self): - return render_to_string(self.mako_template, { - 'module': self - }) + return render_to_string(self.mako_template, self.get_context()) diff --git a/common/lib/xmodule/raw_module.py b/common/lib/xmodule/raw_module.py new file mode 100644 index 0000000000..7bb94c9b63 --- /dev/null +++ b/common/lib/xmodule/raw_module.py @@ -0,0 +1,41 @@ +from pkg_resources import resource_string +from mako_module import MakoModuleDescriptor +from lxml import etree + +class RawDescriptor(MakoModuleDescriptor): + """ + Module that provides a raw editing view of it's data and children + """ + mako_template = "widgets/raw-edit.html" + + js = {'coffee': [resource_string(__name__, 'js/module/raw.coffee')]} + js_module = 'Raw' + + def get_context(self): + return { + 'module': self, + 'data': self.definition['data'], + } + + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + """ + Creates an instance of this descriptor from the supplied xml_data. + This may be overridden by subclasses + + xml_data: A string of xml that will be translated into data and children for + this module + system: An XModuleSystem for interacting with external resources + org and course are optional strings that will be used in the generated modules + url identifiers + """ + xml_object = etree.fromstring(xml_data) + return cls( + system, + definition={'data': xml_data}, + location=['i4x', + org, + course, + xml_object.tag, + xml_object.get('name')] + ) diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index e909d2a996..833ae8c599 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -115,5 +115,23 @@ class Module(XModule): self.rendered = False -class SectionDescriptor(MakoModuleDescriptor): +class SequenceDescriptor(MakoModuleDescriptor): mako_template = 'widgets/sequence-edit.html' + + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + xml_object = etree.fromstring(xml_data) + + children = [ + system.process_xml(etree.tostring(child_module)) + for child_module in xml_object + ] + + return cls( + system, {'children': children}, + location=['i4x', + org, + course, + xml_object.tag, + xml_object.get('name')] + ) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 7b67029f34..16ca76772a 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,18 +13,8 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ - "Course = seq_module:SectionDescriptor", - "Week = seq_module:SectionDescriptor", - "Section = seq_module:SectionDescriptor", - "LectureSequence = seq_module:SectionDescriptor", - "Lab = seq_module:SectionDescriptor", - "Homework = seq_module:SectionDescriptor", - "TutorialIndex = seq_module:SectionDescriptor", - "Exam = seq_module:SectionDescriptor", - "VideoSegment = video_module:VideoSegmentDescriptor", - "ProblemSet = seq_module:SectionDescriptor", - "Problem = capa_module:CapaModuleDescriptor", - "HTML = html_module:HtmlModuleDescriptor", + "course = seq_module:SequenceDescriptor", + "html = html_module:HtmlModuleDescriptor", ] } ) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 89df6b96b2..b1294f8c2c 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -15,8 +15,24 @@ class ModuleMissingError(Exception): class Plugin(object): + """ + Base class for a system that uses entry_points to load plugins. + + Implementing classes are expected to have the following attributes: + + entry_point: The name of the entry point to load plugins from + """ @classmethod - def load_class(cls, identifier): + def load_class(cls, identifier, default=None): + """ + Loads a single class intance specified by identifier. If identifier + specifies more than a single class, then logs a warning and returns the first + class identified. + + If default is not None, will return default if no entry_point matching identifier + is found. Otherwise, will raise a ModuleMissingError + """ + identifier = identifier.lower() classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) if len(classes) > 1: log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( @@ -25,6 +41,8 @@ class Plugin(object): classes=", ".join(class_.module_name for class_ in classes))) if len(classes) == 0: + if default is not None: + return default raise ModuleMissingError(identifier) return classes[0].load() @@ -160,9 +178,10 @@ class XModuleDescriptor(Plugin): """ entry_point = "xmodule.v1" js = {} + js_module = None @staticmethod - def load_from_json(json_data, system): + def load_from_json(json_data, system, default_class=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of json_data. @@ -170,7 +189,10 @@ class XModuleDescriptor(Plugin): json_data must contain a 'location' element, and must be suitable to be passed into the subclasses `from_json` method. """ - class_ = XModuleDescriptor.load_class(json_data['location']['category']) + class_ = XModuleDescriptor.load_class( + json_data['location']['category'], + default_class + ) return class_.from_json(json_data, system) @classmethod @@ -184,6 +206,36 @@ class XModuleDescriptor(Plugin): """ return cls(system=system, **json_data) + @staticmethod + def load_from_xml(xml_data, system, org=None, course=None, default_class=None): + """ + This method instantiates the correct subclass of XModuleDescriptor based + on the contents of xml_data. + + xml_data must be a string containing valid xml + org and course are optional strings that will be used in the generated modules + url identifiers + """ + class_ = XModuleDescriptor.load_class( + etree.fromstring(xml_data).tag, + default_class + ) + return class_.from_xml(xml_data, system, org, course) + + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + """ + Creates an instance of this descriptor from the supplied xml_data. + This may be overridden by subclasses + + xml_data: A string of xml that will be translated into data and children for + this module + system: An XModuleSystem for interacting with external resources + org and course are optional strings that will be used in the generated modules + url identifiers + """ + raise NotImplementedError('Modules must implement from_xml to be parsable from xml') + @classmethod def get_javascript(cls): """ @@ -196,6 +248,12 @@ class XModuleDescriptor(Plugin): """ return cls.js + def js_module_name(self): + """ + Return the name of the javascript class to instantiate when + this module descriptor is loaded for editing + """ + return self.js_module def __init__(self, system, @@ -230,15 +288,12 @@ class XModuleDescriptor(Plugin): self._child_instances = None - def get_children(self, categories=None): + def get_children(self): """Returns a list of XModuleDescriptor instances for the children of this module""" if self._child_instances is None: - self._child_instances = [self.system.load_item(child) for child in self.definition['children']] + self._child_instances = [self.system.load_item(child) for child in self.definition.get('children', [])] - if categories is None: - return self._child_instances - else: - return [child for child in self._child_instances if child.type in categories] + return self._child_instances def get_html(self): """ @@ -275,9 +330,11 @@ class XModuleDescriptor(Plugin): class DescriptorSystem(object): - def __init__(self, load_item): + def __init__(self, load_item, process_xml=None): """ - load_item: Takes a Location and returns and XModuleDescriptor + load_item: Takes a Location and returns an XModuleDescriptor + process_xml: Takes an xml string, and returns the url of the XModuleDescriptor created from that xml """ self.load_item = load_item + self.process_xml = process_xml From daf3eed4eec69c4709e73c0b5288274e46b7e294 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 12:14:18 -0400 Subject: [PATCH 139/252] Handle unnamed modules during import --- .../contentstore/management/commands/import.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index ca94c2af5a..0ce0ce03fe 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -5,10 +5,15 @@ from django.core.management.base import BaseCommand from keystore.django import keystore from raw_module import RawDescriptor +from lxml import etree from path import path from x_module import XModuleDescriptor, DescriptorSystem +unnamed_modules = 0 + +etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, + remove_comments=True)) class Command(BaseCommand): help = \ @@ -22,7 +27,18 @@ class Command(BaseCommand): system = DescriptorSystem(keystore().get_item) def process_xml(xml): - module = XModuleDescriptor.load_from_xml(xml, system, org, course, RawDescriptor) + try: + xml_data = etree.fromstring(xml) + except: + print xml + raise + if not xml_data.get('name'): + global unnamed_modules + unnamed_modules += 1 + xml_data.set('name', 'Unnamed module %d' % unnamed_modules) + + + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), system, org, course, RawDescriptor) keystore().create_item(module.url) if 'data' in module.definition: keystore().update_item(module.url, module.definition['data']) From f0cf3234880007c548bad894be8a0df797387da7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 12:14:41 -0400 Subject: [PATCH 140/252] Handle chapters as sequence modules --- common/lib/xmodule/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 16ca76772a..044fcbe281 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,6 +13,7 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ + "chapter = seq_module:SequenceDescriptor", "course = seq_module:SequenceDescriptor", "html = html_module:HtmlModuleDescriptor", ] From aee2cf4ed5aa62f3d5bff279d6df63a022313c72 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 26 Jun 2012 14:09:57 -0400 Subject: [PATCH 141/252] Added new styles for the sequence nav including completeness --- .../images/sequence-nav/status/check.png | Bin 0 -> 211 bytes .../images/sequence-nav/status/dash.png | Bin 0 -> 86 bytes .../sequence-nav/status/not-started.png | Bin 0 -> 124 bytes .../images/sequence-nav/status/wrong.png | Bin 0 -> 178 bytes lms/static/sass/README.md | 4 + lms/static/sass/base/_extends.scss | 4 +- lms/static/sass/courseware/_sequence-nav.scss | 86 +++++++++--------- 7 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 lms/static/images/sequence-nav/status/check.png create mode 100644 lms/static/images/sequence-nav/status/dash.png create mode 100644 lms/static/images/sequence-nav/status/not-started.png create mode 100644 lms/static/images/sequence-nav/status/wrong.png diff --git a/lms/static/images/sequence-nav/status/check.png b/lms/static/images/sequence-nav/status/check.png new file mode 100644 index 0000000000000000000000000000000000000000..d13529338b4c1133dc4aad90ac857d175dd46f49 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^d_c_2!3HG#TlXXZsac*bjv*Ddrk>r%)s!I8_AuGC z(RurYRuhYx3c8lAlf@rj5Zm(P+Ldb$cssY0Fe|CD{o-SjcU*Ls`MLd^>Gv1ikWT(N z_qdCDN(3*j?3@cFMLVhtehSB`{8G3i|9r<|9`-iPbwXDy4w(2nRXE6FelwIKasNZP ztul5gb;;bpziupaQufL*xe&lwQ5wj|e@$FV-hWM^v(wgwubGSsa;^o}J$w`obR~nQ LtDnm{r-UW|?6Od` literal 0 HcmV?d00001 diff --git a/lms/static/images/sequence-nav/status/dash.png b/lms/static/images/sequence-nav/status/dash.png new file mode 100644 index 0000000000000000000000000000000000000000..79e7fa8f7f4b0a5a68981e3d238577f3514dc58e GIT binary patch literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)K!3HF!%s-X@q+~r^978H@)t)qD1o9XSDwkex jWnoo)aKrS-m7>klm&!NZE#i&_N-=o4`njxgN@xNAENvGS literal 0 HcmV?d00001 diff --git a/lms/static/images/sequence-nav/status/not-started.png b/lms/static/images/sequence-nav/status/not-started.png new file mode 100644 index 0000000000000000000000000000000000000000..826e28609b3d220d744c2ba6ad6c196e700b0190 GIT binary patch literal 124 zcmeAS@N?(olHy`uVBq!ia0vp^>>$j+1|*LJg))c5*()+!>tz48{Jx*#96x)FkGwZ24`CwDYx_pD%J2yLFyF c?8if9jg@y(`?=Hq0v*BN>FVdQ&MBb@0F^aEm;e9( literal 0 HcmV?d00001 diff --git a/lms/static/sass/README.md b/lms/static/sass/README.md index dccb3a80c3..e94076e6e0 100644 --- a/lms/static/sass/README.md +++ b/lms/static/sass/README.md @@ -23,3 +23,7 @@ The dev server will automatically compile sass files that have changed. Simply s the server using: $ rake runserver + +To run it along side of development: + + $ sass --watch lms/static/sass:lms/static/sass -r ./lms/static/sass/bourbon/lib/bourbon.rb diff --git a/lms/static/sass/base/_extends.scss b/lms/static/sass/base/_extends.scss index 880f6cd3ca..63c3a9de7a 100644 --- a/lms/static/sass/base/_extends.scss +++ b/lms/static/sass/base/_extends.scss @@ -205,7 +205,7 @@ h1.top-header { border-top: 1px solid #fff; // @include box-shadow(inset 0 1px 0 #fff, inset 1px 0 0 #fff); font-size: 12px; - height:46px; + // height:46px; line-height: 46px; margin: (-$body-line-height) (-$body-line-height) $body-line-height; text-shadow: 0 1px 0 #fff; @@ -224,7 +224,7 @@ h1.top-header { } &.block-link { - background: darken($cream, 5%); + // background: darken($cream, 5%); border-left: 1px solid darken($cream, 20%); @include box-shadow(inset 1px 0 0 lighten($cream, 5%)); display: block; diff --git a/lms/static/sass/courseware/_sequence-nav.scss b/lms/static/sass/courseware/_sequence-nav.scss index 4472724e6d..377464971b 100644 --- a/lms/static/sass/courseware/_sequence-nav.scss +++ b/lms/static/sass/courseware/_sequence-nav.scss @@ -17,7 +17,7 @@ nav.sequence-nav { } li { - border-left: 1px solid darken($cream, 20%); + border-left: 1px solid darken($cream, 10%); display: table-cell; min-width: 20px; @@ -29,18 +29,17 @@ nav.sequence-nav { background-repeat: no-repeat; &:hover { - background-color: lighten($cream, 3%); + background-color: lighten($cream, 8%); } } .visited { - background-color: #DCCDA2; + background-color: darken($cream, 4%); background-repeat: no-repeat; - @include box-shadow(inset 0 0 3px darken(#dccda2, 10%)); + @include box-shadow(0); &:hover { background-color: $cream; - background-position: center center; } } @@ -51,7 +50,6 @@ nav.sequence-nav { &:hover { background-color: #fff; - background-position: center; } } @@ -61,86 +59,87 @@ nav.sequence-nav { cursor: pointer; display: block; height: 17px; - padding: 15px 0 14px; + padding: 15px 0 17px; position: relative; @include transition(all, .4s, $ease-in-out-quad); width: 100%; - - &.progress { - border-bottom-style: solid; - border-bottom-width: 4px; - } - - &.progress-none { - @extend .progress; - border-bottom-color: red; - } - - &.progress-some { - @extend .progress; - border-bottom-color: yellow; - } - - &.progress-done { - @extend .progress; - border-bottom-color: green; - } + background-position: center 8px; //video &.seq_video_inactive { @extend .inactive; - background-image: url('../images/sequence-nav/video-icon-normal.png'); - background-position: center; + background-image: url('../images/sequence-nav/video-icon-visited.png'); } &.seq_video_visited { @extend .visited; - background-image: url('../images/sequence-nav/video-icon-visited.png'); - background-position: center; + background-image: url('../images/sequence-nav/video-icon-normal.png'); } &.seq_video_active { @extend .active; background-image: url('../images/sequence-nav/video-icon-current.png'); - background-position: center; } //other &.seq_other_inactive { @extend .inactive; - background-image: url('../images/sequence-nav/document-icon-normal.png'); - background-position: center; + background-image: url('../images/sequence-nav/document-icon-visited.png'); } &.seq_other_visited { @extend .visited; - background-image: url('../images/sequence-nav/document-icon-visited.png'); - background-position: center; + background-image: url('../images/sequence-nav/document-icon-normal.png'); } &.seq_other_active { @extend .active; background-image: url('../images/sequence-nav/document-icon-current.png'); - background-position: center; } //vertical & problems &.seq_vertical_inactive, &.seq_problem_inactive { @extend .inactive; - background-image: url('../images/sequence-nav/list-icon-normal.png'); - background-position: center; + background-image: url('../images/sequence-nav/list-icon-visited.png'); } &.seq_vertical_visited, &.seq_problem_visited { @extend .visited; - background-image: url('../images/sequence-nav/list-icon-visited.png'); - background-position: center; + background-image: url('../images/sequence-nav/list-icon-normal.png'); } &.seq_vertical_active, &.seq_problem_active { @extend .active; background-image: url('../images/sequence-nav/list-icon-current.png'); - background-position: center; + } + + &:after { + content: " "; + display: block; + width: 11px; + height: 11px; + background: url('../images/sequence-nav/status/dash.png') no-repeat center; + @include position( absolute, 0 0 6px 50% ); + margin-left: -5px; + } + + //progress + &.progress-none { + &:after { + background: url('../images/sequence-nav/status/not-started.png') no-repeat center; + } + } + + &.progress-some { + &:after { + background: url('../images/sequence-nav/status/wrong.png') no-repeat center; + } + } + + &.progress-done { + &:after { + background: url('../images/sequence-nav/status/check.png') no-repeat center; + } } p { @@ -180,6 +179,8 @@ nav.sequence-nav { } &:hover { + background-position: center 8px; + p { display: block; margin-top: 4px; @@ -215,6 +216,7 @@ nav.sequence-nav { display: block; text-indent: -9999px; @include transition(all, .2s, $ease-in-out-quad); + line-height: 49px; &:hover { opacity: .5; From 66ca31947aabf55720ca137fc9ad83fd82959047 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 14:10:15 -0400 Subject: [PATCH 142/252] Remove semantically meaningless sections from course.xml by moving their attributes onto the contained element. If there is more than one contained element, turn the section into a sequence. Also handles includes --- .../management/commands/import.py | 45 +++++++------ common/lib/xmodule/html_module.py | 22 ++++++ common/lib/xmodule/seq_module.py | 2 +- common/lib/xmodule/setup.py | 1 + common/lib/xmodule/translation_module.py | 67 +++++++++++++++++++ common/lib/xmodule/x_module.py | 16 ++++- 6 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 common/lib/xmodule/translation_module.py diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 0ce0ce03fe..d99f2bee8b 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -6,9 +6,10 @@ from django.core.management.base import BaseCommand from keystore.django import keystore from raw_module import RawDescriptor from lxml import etree +from fs.osfs import OSFS from path import path -from x_module import XModuleDescriptor, DescriptorSystem +from x_module import XModuleDescriptor, XMLParsingSystem unnamed_modules = 0 @@ -24,27 +25,29 @@ class Command(BaseCommand): data_dir = path(data_dir) with open(data_dir / "course.xml") as course_file: - system = DescriptorSystem(keystore().get_item) + class ImportSystem(XMLParsingSystem): + def __init__(self): + self.load_item = keystore().get_item + self.fs = OSFS(data_dir) - def process_xml(xml): - try: - xml_data = etree.fromstring(xml) - except: - print xml - raise - if not xml_data.get('name'): - global unnamed_modules - unnamed_modules += 1 - xml_data.set('name', 'Unnamed module %d' % unnamed_modules) + def process_xml(self, xml): + try: + xml_data = etree.fromstring(xml) + except: + print xml + raise + if not xml_data.get('name'): + global unnamed_modules + unnamed_modules += 1 + xml_data.set('name', 'Unnamed module %d' % unnamed_modules) - module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), system, org, course, RawDescriptor) - keystore().create_item(module.url) - if 'data' in module.definition: - keystore().update_item(module.url, module.definition['data']) - if 'children' in module.definition: - keystore().update_children(module.url, module.definition['children']) - return module.url + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) + keystore().create_item(module.url) + if 'data' in module.definition: + keystore().update_item(module.url, module.definition['data']) + if 'children' in module.definition: + keystore().update_children(module.url, module.definition['children']) + return module - system.process_xml = process_xml - system.process_xml(course_file.read()) + ImportSystem().process_xml(course_file.read()) diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index 981109a9af..b35549d971 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -19,6 +19,28 @@ class HtmlModuleDescriptor(MakoModuleDescriptor): js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js_module = 'HTML' + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + """ + Creates an instance of this descriptor from the supplied xml_data. + This may be overridden by subclasses + + xml_data: A string of xml that will be translated into data and children for + this module + system: An XModuleSystem for interacting with external resources + org and course are optional strings that will be used in the generated modules + url identifiers + """ + xml_object = etree.fromstring(xml_data) + return cls( + system, + definition={'data': {'text': xml_data}}, + location=['i4x', + org, + course, + xml_object.tag, + xml_object.get('name')] + ) class Module(XModule): id_attribute = 'filename' diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 833ae8c599..e3a19c3d60 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -123,7 +123,7 @@ class SequenceDescriptor(MakoModuleDescriptor): xml_object = etree.fromstring(xml_data) children = [ - system.process_xml(etree.tostring(child_module)) + system.process_xml(etree.tostring(child_module)).url for child_module in xml_object ] diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 044fcbe281..967a7e0b5b 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,6 +13,7 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ + "section = translation_module:SemanticSectionDescriptor", "chapter = seq_module:SequenceDescriptor", "course = seq_module:SequenceDescriptor", "html = html_module:HtmlModuleDescriptor", diff --git a/common/lib/xmodule/translation_module.py b/common/lib/xmodule/translation_module.py new file mode 100644 index 0000000000..b56fed02cd --- /dev/null +++ b/common/lib/xmodule/translation_module.py @@ -0,0 +1,67 @@ +""" +These modules exist to translate old format XML into newer, semantic forms +""" +from x_module import XModuleDescriptor +from lxml import etree +from functools import wraps +import logging + +log = logging.getLogger(__name__) + + +def process_includes(fn): + """ + Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace + any immediate child items with the contents of the file that they are + supposed to include + """ + @wraps(fn) + def from_xml(cls, xml_data, system, org=None, course=None): + xml_object = etree.fromstring(xml_data) + next_include = xml_object.find('include') + while next_include is not None: + file = next_include.get('file') + if file is not None: + try: + ifp = system.fs.open(file) + except Exception: + log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) + log.exception('Cannot find file %s in %s' % (file, dir)) + raise + try: + # read in and convert to XML + incxml = etree.XML(ifp.read()) + except Exception: + log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) + log.exception('Cannot parse XML in %s' % (file)) + raise + # insert new XML into tree in place of inlcude + parent = next_include.getparent() + parent.insert(parent.index(next_include), incxml) + parent.remove(next_include) + + next_include = xml_object.find('include') + return fn(cls, etree.tostring(xml_object), system, org, course) + return from_xml + + +class SemanticSectionDescriptor(XModuleDescriptor): + @classmethod + @process_includes + def from_xml(cls, xml_data, system, org=None, course=None): + """ + Removes sections single child elements in favor of just embedding the child element + + """ + xml_object = etree.fromstring(xml_data) + + if len(xml_object) == 1: + for (key, val) in xml_object.items(): + if key == 'format': + continue + xml_object[0].set(key, val) + + return system.process_xml(etree.tostring(xml_object[0])) + else: + xml_object.tag = 'sequence' + return system.process_xml(etree.tostring(xml_object)) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index b1294f8c2c..336ccc6d0c 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -213,6 +213,7 @@ class XModuleDescriptor(Plugin): on the contents of xml_data. xml_data must be a string containing valid xml + system is an XMLParsingSystem org and course are optional strings that will be used in the generated modules url identifiers """ @@ -230,7 +231,7 @@ class XModuleDescriptor(Plugin): xml_data: A string of xml that will be translated into data and children for this module - system: An XModuleSystem for interacting with external resources + system is an XMLParsingSystem org and course are optional strings that will be used in the generated modules url identifiers """ @@ -330,11 +331,20 @@ class XModuleDescriptor(Plugin): class DescriptorSystem(object): - def __init__(self, load_item, process_xml=None): + def __init__(self, load_item): """ load_item: Takes a Location and returns an XModuleDescriptor - process_xml: Takes an xml string, and returns the url of the XModuleDescriptor created from that xml """ self.load_item = load_item + + +class XMLParsingSystem(DescriptorSystem): + def __init__(self, load_item, process_xml, fs): + """ + process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml + fs: A Filesystem object that contains all of the xml resources needed to parse + the course + """ self.process_xml = process_xml + self.fs = fs From 9983160995fe651c64171a5be00019e77aca7235 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 14:29:30 -0400 Subject: [PATCH 143/252] Parse more module types as sequence modules --- cms/djangoapps/contentstore/management/commands/import.py | 3 +-- common/lib/xmodule/setup.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index d99f2bee8b..18dd321467 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -39,8 +39,7 @@ class Command(BaseCommand): if not xml_data.get('name'): global unnamed_modules unnamed_modules += 1 - xml_data.set('name', 'Unnamed module %d' % unnamed_modules) - + xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) keystore().create_item(module.url) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 967a7e0b5b..17d7af50db 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,10 +13,14 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ - "section = translation_module:SemanticSectionDescriptor", "chapter = seq_module:SequenceDescriptor", "course = seq_module:SequenceDescriptor", "html = html_module:HtmlModuleDescriptor", + "section = translation_module:SemanticSectionDescriptor", + "sequential = seq_module:SequenceDescriptor", + "vertical = seq_module:SequenceDescriptor", + "problemset = seq_module:SequenceDescriptor", + "videosequence = seq_module:SequenceDescriptor", ] } ) From 10c1270b8ab0e592b323c082627b438e5dcfcbc4 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 26 Jun 2012 14:31:04 -0400 Subject: [PATCH 144/252] Added fix for the sidebar buttons in the courseware --- lms/static/sass/courseware/_sidebar.scss | 97 ++++++++++++------------ 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/lms/static/sass/courseware/_sidebar.scss b/lms/static/sass/courseware/_sidebar.scss index 44e9d02c28..235426294e 100644 --- a/lms/static/sass/courseware/_sidebar.scss +++ b/lms/static/sass/courseware/_sidebar.scss @@ -52,14 +52,14 @@ section.course-index { padding: 1em 1.5em; li { - background: transparent; - border: 1px solid transparent; - @include border-radius(4px); margin-bottom: lh(.5); - position: relative; - padding: 5px 36px 5px 10px; a { + border: 1px solid transparent; + background: transparent; + @include border-radius(4px); + position: relative; + padding: 5px 36px 5px 10px; text-decoration: none; display: block; color: #666; @@ -74,67 +74,70 @@ section.course-index { display: block; } } - } - - &:after { - background: transparent; - border-top: 1px solid rgb(180,180,180); - border-right: 1px solid rgb(180,180,180); - content: ""; - display: block; - height: 12px; - margin-top: -6px; - opacity: 0; - position: absolute; - top: 50%; - right: 30px; - @include transform(rotate(45deg)); - width: 12px; - } - - &:hover { - @include background-image(linear-gradient(-90deg, rgba(245,245,245, 0.4), rgba(230,230,230, 0.4))); - border-color: rgb(200,200,200); &:after { - opacity: 1; - right: 15px; - @include transition(all, 0.2s, linear); + background: transparent; + border-top: 1px solid rgb(180,180,180); + border-right: 1px solid rgb(180,180,180); + content: ""; + display: block; + height: 12px; + margin-top: -6px; + opacity: 0; + position: absolute; + top: 50%; + right: 30px; + @include transform(rotate(45deg)); + width: 12px; } - > a p { - color: #333; + &:hover { + @include background-image(linear-gradient(-90deg, rgba(245,245,245, 0.4), rgba(230,230,230, 0.4))); + border-color: rgb(200,200,200); + + &:after { + opacity: 1; + right: 15px; + @include transition(all, 0.2s, linear); + } + + > a p { + color: #333; + } } - } - &:active { - @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1)); - top: 1px; + &:active { + @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1)); - &:after { - opacity: 1; - right: 15px; + &:after { + opacity: 1; + right: 15px; + } } } &.active { - background: rgb(240,240,240); - @include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(230,230,230))); - border-color: rgb(200,200,200); font-weight: bold; - > a p { - color: #333; + > a { + background: rgb(240,240,240); + @include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(230,230,230))); + border-color: rgb(200,200,200); + + &:after { + opacity: 1; + right: 15px; + } + + p { + color: #333; + } } span.subtitle { font-weight: normal; } - &:after { - opacity: 1; - right: 15px; - } } } } From 6fdf44fe8d621ead310ea9de7b7674fd6adc8779 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 14:35:21 -0400 Subject: [PATCH 145/252] Make import work via mako again, to unblock others while I work on making the LMS work using XModuleDescriptors --- .../management/commands/import.py | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 18dd321467..e6ace4b66c 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -7,6 +7,7 @@ from keystore.django import keystore from raw_module import RawDescriptor from lxml import etree from fs.osfs import OSFS +from mako.lookup import TemplateLookup from path import path from x_module import XModuleDescriptor, XMLParsingSystem @@ -23,30 +24,31 @@ class Command(BaseCommand): def handle(self, *args, **options): org, course, data_dir = args data_dir = path(data_dir) - with open(data_dir / "course.xml") as course_file: + class ImportSystem(XMLParsingSystem): + def __init__(self): + self.load_item = keystore().get_item + self.fs = OSFS(data_dir) - class ImportSystem(XMLParsingSystem): - def __init__(self): - self.load_item = keystore().get_item - self.fs = OSFS(data_dir) + def process_xml(self, xml): + try: + xml_data = etree.fromstring(xml) + except: + print xml + raise + if not xml_data.get('name'): + global unnamed_modules + unnamed_modules += 1 + xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) - def process_xml(self, xml): - try: - xml_data = etree.fromstring(xml) - except: - print xml - raise - if not xml_data.get('name'): - global unnamed_modules - unnamed_modules += 1 - xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) + keystore().create_item(module.url) + if 'data' in module.definition: + keystore().update_item(module.url, module.definition['data']) + if 'children' in module.definition: + keystore().update_children(module.url, module.definition['children']) + return module - module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) - keystore().create_item(module.url) - if 'data' in module.definition: - keystore().update_item(module.url, module.definition['data']) - if 'children' in module.definition: - keystore().update_children(module.url, module.definition['children']) - return module - - ImportSystem().process_xml(course_file.read()) + lookup = TemplateLookup(directories=[data_dir]) + template = lookup.get_template("course.xml") + course_string = template.render(groups=[]) + ImportSystem().process_xml(course_string) From fc81cf75c1cffbed13887f4dc361706c8935bc36 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 26 Jun 2012 14:56:17 -0400 Subject: [PATCH 146/252] fixed padding issue on seq nav --- lms/static/sass/courseware/_sequence-nav.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lms/static/sass/courseware/_sequence-nav.scss b/lms/static/sass/courseware/_sequence-nav.scss index 377464971b..e02c67f720 100644 --- a/lms/static/sass/courseware/_sequence-nav.scss +++ b/lms/static/sass/courseware/_sequence-nav.scss @@ -11,6 +11,7 @@ nav.sequence-nav { height: 100%; padding-right: flex-grid(1, 9); width: 100%; + padding-left: 0; a { @extend .block-link; @@ -279,6 +280,7 @@ section.course-content { @include border-radius(3px); @include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%)); @include inline-block(); + padding-left: 0; li { float: left; From 6c95bfc175f6e46e4f9b9d5fac00bf87a27a65b6 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 26 Jun 2012 15:18:17 -0400 Subject: [PATCH 147/252] Fix hover on textbook buttons --- lms/static/sass/_textbook.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lms/static/sass/_textbook.scss b/lms/static/sass/_textbook.scss index 35902c7a57..ae549d723f 100644 --- a/lms/static/sass/_textbook.scss +++ b/lms/static/sass/_textbook.scss @@ -62,6 +62,8 @@ div.book-wrapper { @extend .clearfix; li { + background-color: darken($cream, 4%); + &.last { display: block; float: left; @@ -77,6 +79,10 @@ div.book-wrapper { display: block; float: right; } + + &:hover { + background: none; + } } } From 7475e415a43c94bac044df03949926ee1b1f716e Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 26 Jun 2012 16:25:50 -0400 Subject: [PATCH 148/252] Fix test failure after progress introduction --- lms/static/coffee/spec/helper.coffee | 4 ++- .../coffee/spec/modules/problem_spec.coffee | 34 ++++++++++--------- lms/static/coffee/src/modules/problem.coffee | 4 +-- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/lms/static/coffee/spec/helper.coffee b/lms/static/coffee/spec/helper.coffee index 1bb92160ce..eed1a07bd2 100644 --- a/lms/static/coffee/spec/helper.coffee +++ b/lms/static/coffee/spec/helper.coffee @@ -20,10 +20,12 @@ jasmine.stubRequests = -> settings.success data: jasmine.stubbedMetadata[match[1]] else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/ settings.success jasmine.stubbedCaption + else if settings.url.match /modx\/problem\/.+\/problem_get$/ + settings.success html: readFixtures('problem_content.html') else if settings.url == '/calculate' || settings.url == '/6002x/modx/sequence/1/goto_position' || settings.url.match(/event$/) || - settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/) + settings.url.match(/modx\/problem\/.+\/problem_(check|reset|show|save)$/) # do nothing else throw "External request attempted for #{settings.url}, which is not defined." diff --git a/lms/static/coffee/spec/modules/problem_spec.coffee b/lms/static/coffee/spec/modules/problem_spec.coffee index 78047db3ba..d59f702063 100644 --- a/lms/static/coffee/spec/modules/problem_spec.coffee +++ b/lms/static/coffee/spec/modules/problem_spec.coffee @@ -13,6 +13,7 @@ describe 'Problem', -> spyOn($.fn, 'load').andCallFake (url, callback) -> $(@).html readFixtures('problem_content.html') callback() + jasmine.stubRequests() describe 'constructor', -> beforeEach -> @@ -21,12 +22,6 @@ describe 'Problem', -> it 'set the element', -> expect(@problem.element).toBe '#problem_1' - it 'set the content url', -> - expect(@problem.content_url).toEqual '/problem/url/problem_get?id=1' - - it 'render the content', -> - expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @problem.bind - describe 'bind', -> beforeEach -> spyOn window, 'update_schematics' @@ -77,12 +72,19 @@ describe 'Problem', -> expect(@problem.bind).toHaveBeenCalled() describe 'with no content given', -> + beforeEach -> + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> + callback html: "Hello World" + @problem.render() + it 'load the content via ajax', -> - expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @bind + expect(@problem.element.html()).toEqual 'Hello World' + + it 're-bind the content', -> + expect(@problem.bind).toHaveBeenCalled() describe 'check', -> beforeEach -> - jasmine.stubRequests() @problem = new Problem 1, '/problem/url/' @problem.answers = 'foo=1&bar=2' @@ -116,7 +118,6 @@ describe 'Problem', -> describe 'reset', -> beforeEach -> - jasmine.stubRequests() @problem = new Problem 1, '/problem/url/' it 'log the problem_reset event', -> @@ -130,13 +131,13 @@ describe 'Problem', -> expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_reset', { id: 1 }, jasmine.any(Function) it 'render the returned content', -> - spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback("Reset!") + spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> + callback html: "Reset!" @problem.reset() expect(@problem.element.html()).toEqual 'Reset!' describe 'show', -> beforeEach -> - jasmine.stubRequests() @problem = new Problem 1, '/problem/url/' @problem.element.prepend '
                  ' @@ -154,18 +155,19 @@ describe 'Problem', -> expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_show', jasmine.any(Function) it 'show the answers', -> - spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': 'One', '1_2': 'Two') + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> + callback answers: '1_1': 'One', '1_2': 'Two' @problem.show() expect($('#answer_1_1')).toHaveHtml 'One' expect($('#answer_1_2')).toHaveHtml 'Two' it 'toggle the show answer button', -> - spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({}) + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) @problem.show() expect($('.show')).toHaveValue 'Hide Answer' it 'add the showed class to element', -> - spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({}) + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) @problem.show() expect(@problem.element).toHaveClass 'showed' @@ -179,7 +181,8 @@ describe 'Problem', -> ''' it 'set the correct_answer attribute on the choice', -> - spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': [2, 3]) + spyOn($, 'postWithPrefix').andCallFake (url, callback) -> + callback answers: '1_1': [2, 3] @problem.show() expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer', 'true' expect($('label[for="input_1_1_2"]')).toHaveAttr 'correct_answer', 'true' @@ -214,7 +217,6 @@ describe 'Problem', -> describe 'save', -> beforeEach -> - jasmine.stubRequests() @problem = new Problem 1, '/problem/url/' @problem.answers = 'foo=1&bar=2' diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index a1759b28af..7edd40a04c 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -1,7 +1,6 @@ class @Problem constructor: (@id, url) -> @element = $("#problem_#{id}") - @content_url = "#{url}problem_get?id=#{@id}" @render() $: (selector) -> @@ -27,10 +26,9 @@ class @Problem @element.html(content) @bind() else - $.postWithPrefix "/modx/problem/#{@id}/problem_get", '', (response) => + $.postWithPrefix "/modx/problem/#{@id}/problem_get", (response) => @element.html(response.html) @bind() - check: => Logger.log 'problem_check', @answers From 4580ef89d4eed6e0efe8de057c294df4e2d8d558 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 26 Jun 2012 16:34:06 -0400 Subject: [PATCH 149/252] Queue up MathML conversion to be after render From https://www.pivotaltracker.com/story/show/31700967 Fixes #137 --- .../coffee/spec/modules/problem_spec.coffee | 27 ++++++++++++------- lms/static/coffee/src/modules/problem.coffee | 15 ++++++----- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/lms/static/coffee/spec/modules/problem_spec.coffee b/lms/static/coffee/spec/modules/problem_spec.coffee index d59f702063..7537cd3493 100644 --- a/lms/static/coffee/spec/modules/problem_spec.coffee +++ b/lms/static/coffee/spec/modules/problem_spec.coffee @@ -52,8 +52,11 @@ describe 'Problem', -> it 'bind the math input', -> expect($('input.math')).toHandleWith 'keyup', @problem.refreshMath - it 'display the math input', -> - expect(@stubbedJax.root.toMathML).toHaveBeenCalled() + it 'replace math content on the page', -> + expect(MathJax.Hub.Queue.mostRecentCall.args).toEqual [ + ['Text', @stubbedJax, ''], + [@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)] + ] describe 'render', -> beforeEach -> @@ -238,23 +241,29 @@ describe 'Problem', -> describe 'refreshMath', -> beforeEach -> @problem = new Problem 1, '/problem/url/' - @stubbedJax.root.toMathML.andReturn '' $('#input_example_1').val 'E=mc^2' + @problem.refreshMath target: $('#input_example_1').get(0) + + it 'should queue the conversion and MathML element update', -> + expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2'], + [@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)] + + describe 'updateMathML', -> + beforeEach -> + @problem = new Problem 1, '/problem/url/' + @stubbedJax.root.toMathML.andReturn '' describe 'when there is no exception', -> beforeEach -> - @problem.refreshMath target: $('#input_example_1').get(0) + @problem.updateMathML @stubbedJax, $('#input_example_1').get(0) - it 'should convert and display the MathML object', -> - expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2'] - - it 'should display debug output in hidden div', -> + it 'convert jax to MathML', -> expect($('#input_example_1_dynamath')).toHaveValue '' describe 'when there is an exception', -> beforeEach -> @stubbedJax.root.toMathML.andThrow {restart: true} - @problem.refreshMath target: $('#input_example_1').get(0) + @problem.updateMathML @stubbedJax, $('#input_example_1').get(0) it 'should queue up the exception', -> expect(MathJax.Callback.After).toHaveBeenCalledWith [@problem.refreshMath, @stubbedJax], true diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index 7edd40a04c..f8f24a6020 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -79,14 +79,15 @@ class @Problem target = "display_#{element.id.replace(/^input_/, '')}" if jax = MathJax.Hub.getAllJax(target)[0] - MathJax.Hub.Queue ['Text', jax, $(element).val()] + MathJax.Hub.Queue ['Text', jax, $(element).val()], + [@updateMathML, jax, element] - try - output = jax.root.toMathML '' - $("##{element.id}_dynamath").val(output) - catch exception - throw exception unless exception.restart - MathJax.Callback.After [@refreshMath, jax], exception.restart + updateMathML: (jax, element) => + try + $("##{element.id}_dynamath").val(jax.root.toMathML '') + catch exception + throw exception unless exception.restart + MathJax.Callback.After [@refreshMath, jax], exception.restart refreshAnswers: => @$('input.schematic').each (index, element) -> From 4db0a701662d0ae5f8a8513031e99a8243e7e227 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 26 Jun 2012 16:35:23 -0400 Subject: [PATCH 150/252] Fix code convention --- lms/static/coffee/src/modules/problem.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index f8f24a6020..f29c9eb72b 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -16,7 +16,7 @@ class @Problem @$('section.action input.save').click @save @$('input.math').keyup(@refreshMath).each(@refreshMath) - update_progress: (response) => + updateProgress: (response) => if response.progress_changed @element.attr progress: response.progress_status @element.trigger('progressChanged') @@ -36,7 +36,7 @@ class @Problem switch response.success when 'incorrect', 'correct' @render(response.contents) - @update_progress response + @updateProgress response else alert(response.success) @@ -44,7 +44,7 @@ class @Problem Logger.log 'problem_reset', @answers $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) => @render(response.html) - @update_progress response + @updateProgress response show: => if !@element.hasClass 'showed' @@ -60,7 +60,7 @@ class @Problem MathJax.Hub.Queue ["Typeset", MathJax.Hub] @$('.show').val 'Hide Answer' @element.addClass 'showed' - @update_progress response + @updateProgress response else @$('[id^=answer_], [id^=solution_]').text '' @$('[correct_answer]').attr correct_answer: null @@ -72,7 +72,7 @@ class @Problem $.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => if response.success alert 'Saved' - @update_progress response + @updateProgress response refreshMath: (event, element) => element = event.target unless element From bf4f946f0d3a9b53b972ac6e065de1e6d10bb5d5 Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Wed, 27 Jun 2012 14:47:16 -0400 Subject: [PATCH 151/252] Put in a fix for the Profile page (a bug was showing up for new users). A basecamp discussion will be started on the issue. --- lms/djangoapps/courseware/grades.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 4a11ec2d51..00bdffb697 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -176,11 +176,13 @@ def get_score(user, problem, cache, coursename=None): ## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system # TODO: These are no longer correct params for I4xSystem -- figure out what this code # does, clean it up. - from module_render import I4xSystem - system = I4xSystem(None, None, None, coursename=coursename) - total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score()) - response.max_grade = total - response.save() + # from module_render import I4xSystem + # system = I4xSystem(None, None, None, coursename=coursename) + # total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score()) + # response.max_grade = total + # response.save() + total = 1 + # For a temporary fix, we just assume a problem is worth 1 point if we haven't seen it before. This is totally incorrect #Now we re-weight the problem, if specified weight = problem.get("weight", None) From 2fde7f4c6d7d18f36cee610b3fa761140564781d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 08:39:12 -0400 Subject: [PATCH 152/252] Minor fixes from pull request 131 --- common/lib/mitxmako/shortcuts.py | 2 +- lms/djangoapps/courseware/content_parser.py | 2 +- lms/djangoapps/courseware/management/commands/check_course.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/mitxmako/shortcuts.py b/common/lib/mitxmako/shortcuts.py index 72a9b81e3c..c601d93260 100644 --- a/common/lib/mitxmako/shortcuts.py +++ b/common/lib/mitxmako/shortcuts.py @@ -14,7 +14,7 @@ import logging -log = logging.getLogger("mitx.common.lib.mitxmako") +log = logging.getLogger("mitx." + __name__) from django.template import Context from django.http import HttpResponse diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py index 62163c86b0..95c3afed8c 100644 --- a/lms/djangoapps/courseware/content_parser.py +++ b/lms/djangoapps/courseware/content_parser.py @@ -223,7 +223,7 @@ def get_module(tree, module, id_tag, module_id, sections_dirname, options): result_set = tree.xpath(xpath_search) if len(result_set) < 1: # Not found in main tree. Let's look in the section files. - section_list = (s[:-4] for s in os.listdir(sections_dirname) if s[-4:]=='.xml') + section_list = (s[:-4] for s in os.listdir(sections_dirname) if s.endswith('.xml')) for section in section_list: try: s = get_section(section, options, sections_dirname) diff --git a/lms/djangoapps/courseware/management/commands/check_course.py b/lms/djangoapps/courseware/management/commands/check_course.py index 8de29bdd8b..8af0c5d4be 100644 --- a/lms/djangoapps/courseware/management/commands/check_course.py +++ b/lms/djangoapps/courseware/management/commands/check_course.py @@ -15,7 +15,7 @@ middleware.MakoMiddleware() def check_names(user, course): ''' - Complain if any problems have alphanumeric names. + Complain if any problems have non alphanumeric names. TODO (vshnayder): there are some in 6.002x that don't. Is that actually a problem? ''' all_ok = True From 919f8cf261de85b14c21917fbb0c853e562dea7b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 11:28:42 -0400 Subject: [PATCH 153/252] Import x_module using the full path --- cms/envs/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index fc721ca820..ddaeaa66b3 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -157,7 +157,7 @@ PIPELINE_CSS = { PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] -from x_module import XModuleDescriptor +from xmodule.x_module import XModuleDescriptor js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" try: os.makedirs(js_file_dir) From 19044abed293a29351e7a1928b4d4f8354fcd880 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 11:29:46 -0400 Subject: [PATCH 154/252] Run tests before running code quality checks (for faster turnaround time) --- rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rakefile b/rakefile index b6fbb18ac6..e76e200777 100644 --- a/rakefile +++ b/rakefile @@ -39,7 +39,7 @@ def django_admin(system, env, command, *args) return "#{django_admin} #{command} --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}" end -task :default => [:pep8, :pylint, :test] +task :default => [:test, :pep8, :pylint] directory REPORT_DIR directory LMS_REPORT_DIR From 51d6b3f09684d5728bf6cfcd4f3b75e1c21f86e2 Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Thu, 28 Jun 2012 12:04:03 -0400 Subject: [PATCH 155/252] Changed course settings to be a json file. Moved it to its own file. --- lms/djangoapps/courseware/course_settings.py | 94 ++++++++++++++++++++ lms/djangoapps/courseware/grades.py | 51 +---------- 2 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 lms/djangoapps/courseware/course_settings.py diff --git a/lms/djangoapps/courseware/course_settings.py b/lms/djangoapps/courseware/course_settings.py new file mode 100644 index 0000000000..075605f3d1 --- /dev/null +++ b/lms/djangoapps/courseware/course_settings.py @@ -0,0 +1,94 @@ +""" +Course settings module. All settings in the global_settings are +first applied, and then any settings in the settings.DATA_DIR/course_settings.json +are applied. A setting must be in ALL_CAPS. + +Settings are used by calling + +from courseware.course_settings import course_settings + +Note that courseware.course_settings.course_settings is not a module -- it's an object. So +importing individual settings is not possible: + +from courseware.course_settings.course_settings import GRADER # This won't work. + +""" +import json +import logging + +from django.conf import settings + +from xmodule import graders + +log = logging.getLogger("mitx.courseware") + +global_settings_json = """ +{ + "GRADER" : [ + { + "type" : "Homework", + "min_count" : 12, + "drop_count" : 2, + "short_label" : "HW", + "weight" : 0.15 + }, + { + "type" : "Lab", + "min_count" : 12, + "drop_count" : 2, + "category" : "Labs", + "weight" : 0.15 + }, + { + "type" : "Midterm", + "name" : "Midterm Exam", + "short_label" : "Midterm", + "weight" : 0.3 + }, + { + "type" : "Final", + "name" : "Final Exam", + "short_label" : "Final", + "weight" : 0.4 + } + ], + "GRADE_CUTOFFS" : { + "A" : 0.87, + "B" : 0.7, + "C" : 0.6 + } +} +""" + + +class Settings(object): + def __init__(self): + + #Load the global settings as a dictionary + global_settings = json.loads(global_settings_json) + + + #Load the course settings as a dictionary + course_settings = {} + try: + with open( settings.DATA_DIR + "/course_settings.json") as course_settings_file: + course_settings_string = course_settings_file.read() + course_settings = json.loads(course_settings_string) + except IOError: + log.warning("Unable to load course settings file from " + str(settings.DATA_DIR) + "/course_settings.json") + + + #First, set the properties from the global settings on ourselves + for setting in global_settings: + setting_value = global_settings[setting] + setattr(self, setting, setting_value) + + #Now, set the properties from the course settings on ourselves, possibly overriding global values + for setting in course_settings: + setting_value = course_settings[setting] + setattr(self, setting, setting_value) + + # Here is where we should parse any configurations, so that we can fail early + self.GRADER = graders.grader_from_conf(self.GRADER) + +course_settings = Settings() diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 00bdffb697..6085239453 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -1,20 +1,3 @@ -""" -Course settings module. The settings are based of django.conf. All settings in -courseware.global_course_settings are first applied, and then any settings -in the settings.DATA_DIR/course_settings.py are applied. A setting must be -in ALL_CAPS. - -Settings are used by calling - -from courseware import course_settings - -Note that courseware.course_settings is not a module -- it's an object. So -importing individual settings is not possible: - -from courseware.course_settings import GRADER # This won't work. - -""" - from lxml import etree import random import imp @@ -25,6 +8,7 @@ import types from django.conf import settings from courseware import global_course_settings +from courseware.course_settings import course_settings from xmodule import graders from xmodule.graders import Score from models import StudentModule @@ -33,39 +17,6 @@ import xmodule _log = logging.getLogger("mitx.courseware") -class Settings(object): - def __init__(self): - # update this dict from global settings (but only for ALL_CAPS settings) - for setting in dir(global_course_settings): - if setting == setting.upper(): - setattr(self, setting, getattr(global_course_settings, setting)) - - - data_dir = settings.DATA_DIR - - fp = None - try: - fp, pathname, description = imp.find_module("course_settings", [data_dir]) - mod = imp.load_module("course_settings", fp, pathname, description) - except Exception as e: - _log.exception("Unable to import course settings file from " + data_dir + ". Error: " + str(e)) - mod = types.ModuleType('course_settings') - finally: - if fp: - fp.close() - - for setting in dir(mod): - if setting == setting.upper(): - setting_value = getattr(mod, setting) - setattr(self, setting, setting_value) - - # Here is where we should parse any configurations, so that we can fail early - self.GRADER = graders.grader_from_conf(self.GRADER) - -course_settings = Settings() - - - def grade_sheet(student,coursename=None): """ From 621680e340d619c0e95bcf85d02d2758aaa9fabe Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Thu, 28 Jun 2012 12:31:29 -0400 Subject: [PATCH 156/252] Removed old global course settings file. --- .../courseware/global_course_settings.py | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 lms/djangoapps/courseware/global_course_settings.py diff --git a/lms/djangoapps/courseware/global_course_settings.py b/lms/djangoapps/courseware/global_course_settings.py deleted file mode 100644 index f4e9696d1d..0000000000 --- a/lms/djangoapps/courseware/global_course_settings.py +++ /dev/null @@ -1,28 +0,0 @@ -GRADER = [ - { - 'type' : "Homework", - 'min_count' : 12, - 'drop_count' : 2, - 'short_label' : "HW", - 'weight' : 0.15, - }, - { - 'type' : "Lab", - 'min_count' : 12, - 'drop_count' : 2, - 'category' : "Labs", - 'weight' : 0.15 - }, - { - 'type' : "Midterm", - 'name' : "Midterm Exam", - 'short_label' : "Midterm", - 'weight' : 0.3, - }, - { - 'type' : "Final", - 'name' : "Final Exam", - 'short_label' : "Final", - 'weight' : 0.4, - } -] From 201f093981a49e60d109b972fd41d8ad2f4c0aac Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Thu, 28 Jun 2012 12:34:45 -0400 Subject: [PATCH 157/252] Removed old import. --- lms/djangoapps/courseware/grades.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 6085239453..b31f4421cf 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -7,7 +7,6 @@ import types from django.conf import settings -from courseware import global_course_settings from courseware.course_settings import course_settings from xmodule import graders from xmodule.graders import Score From cbe5c612b481011ab909da4209a1819e67a4cbff Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Thu, 28 Jun 2012 14:04:50 -0400 Subject: [PATCH 158/252] Used dictionary update for cleaner course settings code. --- lms/djangoapps/courseware/course_settings.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lms/djangoapps/courseware/course_settings.py b/lms/djangoapps/courseware/course_settings.py index 075605f3d1..a58fc9042e 100644 --- a/lms/djangoapps/courseware/course_settings.py +++ b/lms/djangoapps/courseware/course_settings.py @@ -64,11 +64,11 @@ global_settings_json = """ class Settings(object): def __init__(self): - #Load the global settings as a dictionary + # Load the global settings as a dictionary global_settings = json.loads(global_settings_json) - #Load the course settings as a dictionary + # Load the course settings as a dictionary course_settings = {} try: with open( settings.DATA_DIR + "/course_settings.json") as course_settings_file: @@ -78,15 +78,13 @@ class Settings(object): log.warning("Unable to load course settings file from " + str(settings.DATA_DIR) + "/course_settings.json") - #First, set the properties from the global settings on ourselves + # Override any global settings with the course settings + global_settings.update(course_settings) + + # Now, set the properties from the course settings on ourselves for setting in global_settings: setting_value = global_settings[setting] setattr(self, setting, setting_value) - - #Now, set the properties from the course settings on ourselves, possibly overriding global values - for setting in course_settings: - setting_value = course_settings[setting] - setattr(self, setting, setting_value) # Here is where we should parse any configurations, so that we can fail early self.GRADER = graders.grader_from_conf(self.GRADER) From 3c60d1a9afe1cb1d3adfdf21b3ca3ec3135d9754 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 12:34:58 -0400 Subject: [PATCH 159/252] Clean up how errors in import command are handled --- .../contentstore/management/commands/import.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index e6ace4b66c..9f0cd7f21c 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -1,8 +1,8 @@ -### +### ### One-off script for importing courseware form XML format ### -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError from keystore.django import keystore from raw_module import RawDescriptor from lxml import etree @@ -17,13 +17,18 @@ unnamed_modules = 0 etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, remove_comments=True)) + class Command(BaseCommand): help = \ '''Import the specified data directory into the default keystore''' def handle(self, *args, **options): + if len(args) != 3: + raise CommandError("import requires 3 arguments: ") + org, course, data_dir = args data_dir = path(data_dir) + class ImportSystem(XMLParsingSystem): def __init__(self): self.load_item = keystore().get_item @@ -33,8 +38,8 @@ class Command(BaseCommand): try: xml_data = etree.fromstring(xml) except: - print xml - raise + raise CommandError("Unable to parse xml: " + xml) + if not xml_data.get('name'): global unnamed_modules unnamed_modules += 1 From 21ba5019893ca0a5ff079baa35635dd81a3ec1a1 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 26 Jun 2012 14:35:47 -0400 Subject: [PATCH 160/252] Revert "Make import work via mako again, to unblock others while I work on making the LMS work using XModuleDescriptors" This reverts commit 6fdf44fe8d621ead310ea9de7b7674fd6adc8779. --- .../management/commands/import.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 9f0cd7f21c..43c908c1bc 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -7,7 +7,6 @@ from keystore.django import keystore from raw_module import RawDescriptor from lxml import etree from fs.osfs import OSFS -from mako.lookup import TemplateLookup from path import path from x_module import XModuleDescriptor, XMLParsingSystem @@ -28,32 +27,30 @@ class Command(BaseCommand): org, course, data_dir = args data_dir = path(data_dir) + with open(data_dir / "course.xml") as course_file: - class ImportSystem(XMLParsingSystem): - def __init__(self): - self.load_item = keystore().get_item - self.fs = OSFS(data_dir) + class ImportSystem(XMLParsingSystem): + def __init__(self): + self.load_item = keystore().get_item + self.fs = OSFS(data_dir) - def process_xml(self, xml): - try: - xml_data = etree.fromstring(xml) - except: - raise CommandError("Unable to parse xml: " + xml) + def process_xml(self, xml): + try: + xml_data = etree.fromstring(xml) + except: + raise CommandError("Unable to parse xml: " + xml) - if not xml_data.get('name'): - global unnamed_modules - unnamed_modules += 1 - xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) + if not xml_data.get('name'): + global unnamed_modules + unnamed_modules += 1 + xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) - module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) - keystore().create_item(module.url) - if 'data' in module.definition: - keystore().update_item(module.url, module.definition['data']) - if 'children' in module.definition: - keystore().update_children(module.url, module.definition['children']) - return module + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) + keystore().create_item(module.url) + if 'data' in module.definition: + keystore().update_item(module.url, module.definition['data']) + if 'children' in module.definition: + keystore().update_children(module.url, module.definition['children']) + return module - lookup = TemplateLookup(directories=[data_dir]) - template = lookup.get_template("course.xml") - course_string = template.render(groups=[]) - ImportSystem().process_xml(course_string) + ImportSystem().process_xml(course_file.read()) From d02abac820d7572bcaa8a5c737cc3c044dd49734 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Jun 2012 14:15:46 -0400 Subject: [PATCH 161/252] Restrict the set of characters allowed in locations further --- common/lib/keystore/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index c0fb40d33e..77f0213281 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -15,6 +15,8 @@ URL_RE = re.compile(""" (/(?P[^/]+))? """, re.VERBOSE) +INVALID_CHARS = re.compile(r"[^\w-]") + class Location(object): ''' @@ -26,6 +28,14 @@ class Location(object): However, they can also be represented a dictionaries (specifying each component), tuples or list (specified in order), or as strings of the url ''' + + @classmethod + def clean(cls, value): + """ + Return value, made into a form legal for locations + """ + return re.sub('_+', '_', INVALID_CHARS.sub('_', value)) + def __init__(self, location): """ Create a new location that is a clone of the specifed one. @@ -45,7 +55,7 @@ class Location(object): In both the dict and list forms, the revision is optional, and can be ommitted. - None of the components of a location may contain the '/' character + Components must be composed of alphanumeric characters, or the characters _, and - Components may be set to None, which may be interpreted by some contexts to mean wildcard selection @@ -88,7 +98,7 @@ class Location(object): raise InvalidLocationError(location) for val in self.list(): - if val is not None and '/' in val: + if val is not None and INVALID_CHARS.search(val) is not None: raise InvalidLocationError(location) def __str__(self): From 3c054306c61575c01a0dd8d9b6b20abac3ac954e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Jun 2012 14:17:15 -0400 Subject: [PATCH 162/252] Add the ability to specify Keystore engines and default descriptor classes by name from settings --- cms/envs/dev.py | 10 +++++++--- common/lib/keystore/django.py | 13 +++++++------ common/lib/keystore/mongo.py | 11 +++++++---- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 16bed60729..ce775d962a 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -8,9 +8,13 @@ TEMPLATE_DEBUG = DEBUG KEYSTORE = { 'default': { - 'host': 'localhost', - 'db': 'mongo_base', - 'collection': 'key_store', + 'ENGINE': 'keystore.mongo.MongoModuleStore', + 'OPTIONS': { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'mongo_base', + 'collection': 'key_store', + } } } diff --git a/common/lib/keystore/django.py b/common/lib/keystore/django.py index 98479a7f7c..89aa9d07b0 100644 --- a/common/lib/keystore/django.py +++ b/common/lib/keystore/django.py @@ -6,9 +6,9 @@ Passes settings.KEYSTORE as kwargs to MongoModuleStore from __future__ import absolute_import +from importlib import import_module + from django.conf import settings -from .mongo import MongoModuleStore -from raw_module import RawDescriptor _KEYSTORES = {} @@ -17,9 +17,10 @@ def keystore(name='default'): global _KEYSTORES if name not in _KEYSTORES: - # TODO (cpennington): Load the default class from a string - _KEYSTORES[name] = MongoModuleStore( - default_class=RawDescriptor, - **settings.KEYSTORE[name]) + class_path = settings.KEYSTORE[name]['ENGINE'] + module_path, _, class_name = class_path.rpartition('.') + class_ = getattr(import_module(module_path), class_name) + _KEYSTORES[name] = class_( + **settings.KEYSTORE[name]['OPTIONS']) return _KEYSTORES[name] diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index ece8b35b71..4317ce0204 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -1,7 +1,9 @@ import pymongo +from importlib import import_module +from xmodule.x_module import XModuleDescriptor, DescriptorSystem + from . import ModuleStore, Location from .exceptions import ItemNotFoundError, InsufficientSpecificationError -from xmodule.x_module import XModuleDescriptor, DescriptorSystem class MongoModuleStore(ModuleStore): @@ -16,7 +18,10 @@ class MongoModuleStore(ModuleStore): # Force mongo to report errors, at the expense of performance self.collection.safe = True - self.default_class = default_class + + module_path, _, class_name = default_class.rpartition('.') + class_ = getattr(import_module(module_path), class_name) + self.default_class = class_ def get_item(self, location): """ @@ -29,8 +34,6 @@ class MongoModuleStore(ModuleStore): If no object is found at that location, raises keystore.exceptions.ItemNotFoundError location: Something that can be passed to Location - default_class: An XModuleDescriptor subclass to use if no plugin matching the - location is found """ query = {} From 5b8120280e5394ed9c090af213d406ac269fad37 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Jun 2012 14:20:24 -0400 Subject: [PATCH 163/252] Move the resources_fs abstraction into the primary DescriptorSystem, as it is needed for more than just XMLParsing --- .../management/commands/import.py | 35 +++++++++---------- common/lib/keystore/mongo.py | 3 +- common/lib/xmodule/translation_module.py | 2 +- common/lib/xmodule/x_module.py | 11 +++--- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 43c908c1bc..1cfdf24e2d 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -31,26 +31,25 @@ class Command(BaseCommand): class ImportSystem(XMLParsingSystem): def __init__(self): - self.load_item = keystore().get_item - self.fs = OSFS(data_dir) + def process_xml(xml): + try: + xml_data = etree.fromstring(xml) + except: + raise CommandError("Unable to parse xml: " + xml) - def process_xml(self, xml): - try: - xml_data = etree.fromstring(xml) - except: - raise CommandError("Unable to parse xml: " + xml) + if not xml_data.get('name'): + global unnamed_modules + unnamed_modules += 1 + xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) - if not xml_data.get('name'): - global unnamed_modules - unnamed_modules += 1 - xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) + keystore().create_item(module.url) + if 'data' in module.definition: + keystore().update_item(module.url, module.definition['data']) + if 'children' in module.definition: + keystore().update_children(module.url, module.definition['children']) + return module - module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) - keystore().create_item(module.url) - if 'data' in module.definition: - keystore().update_item(module.url, module.definition['data']) - if 'children' in module.definition: - keystore().update_children(module.url, module.definition['children']) - return module + XMLParsingSystem.__init__(self, keystore().get_item, OSFS(data_dir), process_xml) ImportSystem().process_xml(course_file.read()) diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index 4317ce0204..20c4ffde1a 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -51,8 +51,9 @@ class MongoModuleStore(ModuleStore): if item is None: raise ItemNotFoundError(location) + # TODO (cpennington): Pass a proper resources_fs to the system return XModuleDescriptor.load_from_json( - item, DescriptorSystem(self.get_item), self.default_class) + item, DescriptorSystem(self.get_item, None), self.default_class) def create_item(self, location): """ diff --git a/common/lib/xmodule/translation_module.py b/common/lib/xmodule/translation_module.py index b56fed02cd..f5c8bc2fbc 100644 --- a/common/lib/xmodule/translation_module.py +++ b/common/lib/xmodule/translation_module.py @@ -23,7 +23,7 @@ def process_includes(fn): file = next_include.get('file') if file is not None: try: - ifp = system.fs.open(file) + ifp = system.resources_fs.open(file) except Exception: log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) log.exception('Cannot find file %s in %s' % (file, dir)) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 336ccc6d0c..aad4dd94dc 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -331,20 +331,21 @@ class XModuleDescriptor(Plugin): class DescriptorSystem(object): - def __init__(self, load_item): + def __init__(self, load_item, resources_fs): """ load_item: Takes a Location and returns an XModuleDescriptor + resources_fs: A Filesystem object that contains all of the + resources needed for the course """ self.load_item = load_item + self.resources_fs = resources_fs class XMLParsingSystem(DescriptorSystem): - def __init__(self, load_item, process_xml, fs): + def __init__(self, load_item, resources_fs, process_xml): """ process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml - fs: A Filesystem object that contains all of the xml resources needed to parse - the course """ + DescriptorSystem.__init__(self, load_item, resources_fs) self.process_xml = process_xml - self.fs = fs From de07b8b345c21905f29982d9af0890caf7210206 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Jun 2012 14:25:44 -0400 Subject: [PATCH 164/252] Begin using a Keystore for XML parsing. Still broken: sequence icons, custom tags, problems, video js --- common/lib/keystore/xml.py | 87 ++++++ common/lib/xmodule/__init__.py | 62 ---- common/lib/xmodule/abtest_module.py | 95 ++++++ common/lib/xmodule/capa_module.py | 39 +-- common/lib/xmodule/hidden_module.py | 10 + common/lib/xmodule/html_module.py | 73 +---- common/lib/xmodule/raw_module.py | 28 +- common/lib/xmodule/schematic_module.py | 13 - common/lib/xmodule/seq_module.py | 118 +++---- common/lib/xmodule/setup.py | 17 +- common/lib/xmodule/template_module.py | 11 +- common/lib/xmodule/vertical_module.py | 31 +- common/lib/xmodule/video_module.py | 38 +-- common/lib/xmodule/x_module.py | 144 ++++----- common/lib/xmodule/xml_module.py | 41 +++ lms/djangoapps/courseware/content_parser.py | 171 +++-------- lms/djangoapps/courseware/grades.py | 48 +-- lms/djangoapps/courseware/models.py | 110 ++++--- lms/djangoapps/courseware/module_render.py | 288 +++++++++++------- lms/djangoapps/courseware/views.py | 121 ++------ .../multicourse/multicourse_settings.py | 42 ++- lms/envs/common.py | 15 + lms/envs/dev.py | 2 +- lms/lib/dogfood/views.py | 22 +- lms/static/coffee/src/modules/sequence.coffee | 4 +- lms/templates/seq_module.html | 4 +- lms/templates/vert_module.html | 6 +- 27 files changed, 814 insertions(+), 826 deletions(-) create mode 100644 common/lib/keystore/xml.py create mode 100644 common/lib/xmodule/abtest_module.py create mode 100644 common/lib/xmodule/hidden_module.py create mode 100644 common/lib/xmodule/xml_module.py diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py new file mode 100644 index 0000000000..d5baefd787 --- /dev/null +++ b/common/lib/keystore/xml.py @@ -0,0 +1,87 @@ +from fs.osfs import OSFS +from importlib import import_module +from lxml import etree +from path import path +from xmodule.x_module import XModuleDescriptor, XMLParsingSystem + +from . import ModuleStore, Location +from .exceptions import ItemNotFoundError + + +class XMLModuleStore(ModuleStore): + """ + An XML backed ModuleStore + """ + def __init__(self, org, course, data_dir, default_class=None): + self.data_dir = path(data_dir) + self.modules = {} + + module_path, _, class_name = default_class.rpartition('.') + class_ = getattr(import_module(module_path), class_name) + self.default_class = class_ + + with open(data_dir / "course.xml") as course_file: + class ImportSystem(XMLParsingSystem): + def __init__(self, keystore): + self.unnamed_modules = 0 + + def process_xml(xml): + try: + xml_data = etree.fromstring(xml) + except: + print xml + raise + if xml_data.get('name'): + xml_data.set('slug', Location.clean(xml_data.get('name'))) + else: + self.unnamed_modules += 1 + xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) + + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, keystore.default_class) + keystore.modules[module.url] = module + return module + + XMLParsingSystem.__init__(self, keystore.get_item, OSFS(data_dir), process_xml) + + ImportSystem(self).process_xml(course_file.read()) + + def get_item(self, location): + """ + Returns an XModuleDescriptor instance for the item at location. + If location.revision is None, returns the most item with the most + recent revision + + If any segment of the location is None except revision, raises + keystore.exceptions.InsufficientSpecificationError + If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + + location: Something that can be passed to Location + """ + location = Location(location) + try: + return self.modules[location.url()] + except KeyError: + raise ItemNotFoundError(location) + + def create_item(self, location): + raise NotImplementedError("XMLModuleStores are read-only") + + def update_item(self, location, data): + """ + Set the data in the item specified by the location to + data + + location: Something that can be passed to Location + data: A nested dictionary of problem data + """ + raise NotImplementedError("XMLModuleStores are read-only") + + def update_children(self, location, children): + """ + Set the children for the item specified by the location to + data + + location: Something that can be passed to Location + children: A list of child item identifiers + """ + raise NotImplementedError("XMLModuleStores are read-only") diff --git a/common/lib/xmodule/__init__.py b/common/lib/xmodule/__init__.py index 307b544b79..e69de29bb2 100644 --- a/common/lib/xmodule/__init__.py +++ b/common/lib/xmodule/__init__.py @@ -1,62 +0,0 @@ -import capa_module -import html_module -import schematic_module -import seq_module -import template_module -import vertical_module -import video_module - -# Import all files in modules directory, excluding backups (# and . in name) -# and __init__ -# -# Stick them in a list -# modx_module_list = [] - -# for f in os.listdir(os.path.dirname(__file__)): -# if f!='__init__.py' and \ -# f[-3:] == ".py" and \ -# "." not in f[:-3] \ -# and '#' not in f: -# mod_path = 'courseware.modules.'+f[:-3] -# mod = __import__(mod_path, fromlist = "courseware.modules") -# if 'Module' in mod.__dict__: -# modx_module_list.append(mod) - -#print modx_module_list -modx_module_list = [capa_module, html_module, schematic_module, seq_module, template_module, vertical_module, video_module] -#print modx_module_list - -modx_modules = {} - -# Convert list to a dictionary for lookup by tag -def update_modules(): - global modx_modules - modx_modules = dict() - for module in modx_module_list: - for tag in module.Module.get_xml_tags(): - modx_modules[tag] = module.Module - -update_modules() - -def get_module_class(tag): - ''' Given an XML tag (e.g. 'video'), return - the associated module (e.g. video_module.Module). - ''' - if tag not in modx_modules: - update_modules() - return modx_modules[tag] - -def get_module_id(tag): - ''' Given an XML tag (e.g. 'video'), return - the default ID for that module (e.g. 'youtube_id') - ''' - return modx_modules[tag].id_attribute - -def get_valid_tags(): - return modx_modules.keys() - -def get_default_ids(): - tags = get_valid_tags() - ids = map(get_module_id, tags) - return dict(zip(tags, ids)) - diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py new file mode 100644 index 0000000000..dda6a58c99 --- /dev/null +++ b/common/lib/xmodule/abtest_module.py @@ -0,0 +1,95 @@ +import json +import random +from lxml import etree + +from x_module import XModule, XModuleDescriptor + + +class ModuleDescriptor(XModuleDescriptor): + pass + + +def group_from_value(groups, v): + ''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value + in [0,1], return the associated group (in the above case, return + 'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7 +''' + sum = 0 + for (g, p) in groups: + sum = sum + p + if sum > v: + return g + + # Round off errors might cause us to run to the end of the list + # If the do, return the last element + return g + + +class Module(XModule): + """ + Implements an A/B test with an aribtrary number of competing groups + + Format: + + + + + + """ + + def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): + XModule.__init__(self, system, xml, item_id, instance_state, shared_state) + self.xmltree = etree.fromstring(xml) + + target_groups = self.xmltree.findall('group') + if shared_state is None: + target_values = [ + (elem.get('name'), float(elem.get('portion'))) + for elem in target_groups + ] + default_value = 1 - sum(val for (_, val) in target_values) + + self.group = group_from_value( + target_values + [(None, default_value)], + random.uniform(0, 1) + ) + else: + shared_state = json.loads(shared_state) + + # TODO (cpennington): Remove this once we aren't passing in + # groups from django groups + if 'groups' in shared_state: + self.group = None + target_names = [elem.get('name') for elem in target_groups] + for group in shared_state['groups']: + if group in target_names: + self.group = group + break + else: + self.group = shared_state['group'] + + def get_shared_state(self): + return json.dumps({'group': self.group}) + + def _xml_children(self): + group = None + if self.group is None: + group = self.xmltree.find('default') + else: + for candidate_group in self.xmltree.find('group'): + if self.group == candidate_group.get('name'): + group = candidate_group + break + + if group is None: + return [] + return list(group) + + def get_children(self): + return [self.module_from_xml(child) for child in self._xml_children()] + + def rendered_children(self): + return [self.render_function(child) for child in self._xml_children()] + + def get_html(self): + return '\n'.join(child.get_html() for child in self.get_children()) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index b59bc9de56..5047b94832 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -81,14 +81,7 @@ class Module(XModule): reset. ''' - id_attribute = "filename" - - @classmethod - def get_xml_tags(c): - return ["problem"] - - - def get_state(self): + def get_instance_state(self): state = self.lcp.get_state() state['attempts'] = self.attempts return json.dumps(state) @@ -191,8 +184,8 @@ class Module(XModule): return html - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) + def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): + XModule.__init__(self, system, xml, item_id, instance_state, shared_state) self.attempts = 0 self.max_attempts = None @@ -232,19 +225,19 @@ class Module(XModule): self.show_answer = "closed" self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize')) - if self.rerandomize == "" or self.rerandomize=="always" or self.rerandomize=="true": - self.rerandomize="always" - elif self.rerandomize=="false" or self.rerandomize=="per_student": - self.rerandomize="per_student" - elif self.rerandomize=="never": - self.rerandomize="never" + if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true": + self.rerandomize = "always" + elif self.rerandomize == "false" or self.rerandomize == "per_student": + self.rerandomize = "per_student" + elif self.rerandomize == "never": + self.rerandomize = "never" else: - raise Exception("Invalid rerandomize attribute "+self.rerandomize) + raise Exception("Invalid rerandomize attribute " + self.rerandomize) - if state!=None: - state=json.loads(state) - if state!=None and 'attempts' in state: - self.attempts=state['attempts'] + if instance_state != None: + instance_state = json.loads(instance_state) + if instance_state != None and 'attempts' in instance_state: + self.attempts = instance_state['attempts'] # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml" @@ -267,7 +260,7 @@ class Module(XModule): else: raise try: - self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system) + self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system) except Exception,err: msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename) log.exception(msg) @@ -277,7 +270,7 @@ class Module(XModule): # create a dummy problem with error message instead of failing fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename,msg)) fp.name = "StringIO" - self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system) + self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system) else: raise diff --git a/common/lib/xmodule/hidden_module.py b/common/lib/xmodule/hidden_module.py new file mode 100644 index 0000000000..d4f2a0fa33 --- /dev/null +++ b/common/lib/xmodule/hidden_module.py @@ -0,0 +1,10 @@ +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor + + +class HiddenModule(XModule): + pass + + +class HiddenDescriptor(RawDescriptor): + module_class = HiddenModule diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index b35549d971..32963600cd 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -1,75 +1,34 @@ import json import logging -from x_module import XModule -from mako_module import MakoModuleDescriptor +from xmodule.x_module import XModule +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor from lxml import etree from pkg_resources import resource_string log = logging.getLogger("mitx.courseware") -#----------------------------------------------------------------------------- -class HtmlModuleDescriptor(MakoModuleDescriptor): +class HtmlModule(XModule): + def get_html(self): + return self.html + + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + self.html = self.definition['data']['text'] + + +class HtmlDescriptor(MakoModuleDescriptor, XmlDescriptor): """ Module for putting raw html in a course """ mako_template = "widgets/html-edit.html" + module_class = HtmlModule js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js_module = 'HTML' @classmethod - def from_xml(cls, xml_data, system, org=None, course=None): - """ - Creates an instance of this descriptor from the supplied xml_data. - This may be overridden by subclasses - - xml_data: A string of xml that will be translated into data and children for - this module - system: An XModuleSystem for interacting with external resources - org and course are optional strings that will be used in the generated modules - url identifiers - """ - xml_object = etree.fromstring(xml_data) - return cls( - system, - definition={'data': {'text': xml_data}}, - location=['i4x', - org, - course, - xml_object.tag, - xml_object.get('name')] - ) - -class Module(XModule): - id_attribute = 'filename' - - def get_state(self): - return json.dumps({ }) - - @classmethod - def get_xml_tags(c): - return ["html"] - - def get_html(self): - if self.filename==None: - xmltree=etree.fromstring(self.xml) - textlist=[xmltree.text]+[etree.tostring(i) for i in xmltree]+[xmltree.tail] - textlist=[i for i in textlist if type(i)==str] - return "".join(textlist) - try: - filename="html/"+self.filename - return self.filestore.open(filename).read() - except: # For backwards compatibility. TODO: Remove - if self.DEBUG: - log.info('[courseware.modules.html_module] filename=%s' % self.filename) - return self.system.render_template(self.filename, {'id': self.item_id}, namespace='course') - - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) - xmltree=etree.fromstring(xml) - self.filename = None - filename_l=xmltree.xpath("/html/@filename") - if len(filename_l)>0: - self.filename=str(filename_l[0]) + def definition_from_xml(cls, xml_object, system): + return {'data': {'text': etree.tostring(xml_object)}} diff --git a/common/lib/xmodule/raw_module.py b/common/lib/xmodule/raw_module.py index 7bb94c9b63..43a92303ad 100644 --- a/common/lib/xmodule/raw_module.py +++ b/common/lib/xmodule/raw_module.py @@ -1,8 +1,9 @@ from pkg_resources import resource_string -from mako_module import MakoModuleDescriptor from lxml import etree +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor -class RawDescriptor(MakoModuleDescriptor): +class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): """ Module that provides a raw editing view of it's data and children """ @@ -18,24 +19,5 @@ class RawDescriptor(MakoModuleDescriptor): } @classmethod - def from_xml(cls, xml_data, system, org=None, course=None): - """ - Creates an instance of this descriptor from the supplied xml_data. - This may be overridden by subclasses - - xml_data: A string of xml that will be translated into data and children for - this module - system: An XModuleSystem for interacting with external resources - org and course are optional strings that will be used in the generated modules - url identifiers - """ - xml_object = etree.fromstring(xml_data) - return cls( - system, - definition={'data': xml_data}, - location=['i4x', - org, - course, - xml_object.tag, - xml_object.get('name')] - ) + def definition_from_xml(cls, xml_object, system): + return {'data': etree.tostring(xml_object)} diff --git a/common/lib/xmodule/schematic_module.py b/common/lib/xmodule/schematic_module.py index 30175c16a8..f95729d4ab 100644 --- a/common/lib/xmodule/schematic_module.py +++ b/common/lib/xmodule/schematic_module.py @@ -6,18 +6,5 @@ class ModuleDescriptor(XModuleDescriptor): pass class Module(XModule): - id_attribute = 'id' - - def get_state(self): - return json.dumps({ }) - - @classmethod - def get_xml_tags(c): - return ["schematic"] - def get_html(self): return ''.format(item_id=self.item_id) - - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) - diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index e3a19c3d60..b60f0e4656 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -3,8 +3,9 @@ import logging from lxml import etree -from x_module import XModule -from mako_module import MakoModuleDescriptor +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.x_module import XModule from xmodule.progress import Progress log = logging.getLogger("mitx.common.lib.seq_module") @@ -13,32 +14,17 @@ log = logging.getLogger("mitx.common.lib.seq_module") # OBSOLETE: This obsoletes 'type' class_priority = ['video', 'problem'] -class Module(XModule): + +class SequenceModule(XModule): ''' Layout module which lays out content in a temporal sequence ''' - id_attribute = 'id' + def get_instance_state(self): + return json.dumps({'position': self.position}) - def get_state(self): - return json.dumps({ 'position':self.position }) - - @classmethod - def get_xml_tags(c): - obsolete_tags = ["sequential", 'tab'] - modern_tags = ["videosequence"] - return obsolete_tags + modern_tags - def get_html(self): self.render() return self.content - def get_init_js(self): - self.render() - return self.init_js - - def get_destroy_js(self): - self.render() - return self.destroy_js - def get_progress(self): ''' Return the total progress, adding total done and total available. (assumes that each submodule uses the same "units" for progress.) @@ -60,53 +46,51 @@ class Module(XModule): if self.rendered: return ## Returns a set of all types of all sub-children - child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree] + contents = [] + for child in self.get_display_items(): + progress = child.get_progress() + contents.append({ + 'content': child.get_html(), + 'title': "\n".join( + grand_child.name.strip() + for grand_child in child.get_children() + if grand_child.name is not None + ), + 'progress_status': Progress.to_js_status_str(progress), + 'progress_detail': Progress.to_js_detail_str(progress), + 'type': child.get_icon_class(), + }) - titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \ - for e in self.xmltree] - - children = self.get_children() - progresses = [child.get_progress() for child in children] - - self.contents = self.rendered_children() - - for contents, title, progress in zip(self.contents, titles, progresses): - contents['title'] = title - contents['progress_status'] = Progress.to_js_status_str(progress) - contents['progress_detail'] = Progress.to_js_detail_str(progress) - - for (content, element_class) in zip(self.contents, child_classes): - new_class = 'other' - for c in class_priority: - if c in element_class: - new_class = c - content['type'] = new_class + print json.dumps(contents, indent=4) # Split tags -- browsers handle this as end # of script, even if it occurs mid-string. Do this after json.dumps()ing # so that we can be sure of the quotations being used - params={'items': json.dumps(self.contents).replace('', '<"+"/script>'), - 'id': self.item_id, - 'position': self.position, - 'titles': titles, - 'tag': self.xmltree.tag} + params = {'items': json.dumps(contents).replace('', '<"+"/script>'), + 'element_id': "-".join(str(v) for v in self.location.list()), + 'item_id': self.id, + 'position': self.position, + 'tag': self.location.category} - if self.xmltree.tag in ['sequential', 'videosequence']: - self.content = self.system.render_template('seq_module.html', params) - if self.xmltree.tag == 'tab': - self.content = self.system.render_template('tab_module.html', params) - log.debug("rendered content: %s", content) + self.content = self.system.render_template('seq_module.html', params) self.rendered = True - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) - self.xmltree = etree.fromstring(xml) + def get_icon_class(self): + child_classes = set(child.get_icon_class() for child in self.get_children()) + new_class = 'other' + for c in class_priority: + if c in child_classes: + new_class = c + return new_class + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) self.position = 1 - if state is not None: - state = json.loads(state) - if 'position' in state: self.position = int(state['position']) + if instance_state is not None: + state = json.loads(instance_state) + if 'position' in state: + self.position = int(state['position']) # if position is specified in system, then use that instead if system.get('position'): @@ -115,23 +99,13 @@ class Module(XModule): self.rendered = False -class SequenceDescriptor(MakoModuleDescriptor): +class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): mako_template = 'widgets/sequence-edit.html' + module_class = SequenceModule @classmethod - def from_xml(cls, xml_data, system, org=None, course=None): - xml_object = etree.fromstring(xml_data) - - children = [ + def definition_from_xml(cls, xml_object, system): + return {'children': [ system.process_xml(etree.tostring(child_module)).url for child_module in xml_object - ] - - return cls( - system, {'children': children}, - location=['i4x', - org, - course, - xml_object.tag, - xml_object.get('name')] - ) + ]} diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 17d7af50db..3e3e33805f 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,14 +13,15 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ - "chapter = seq_module:SequenceDescriptor", - "course = seq_module:SequenceDescriptor", - "html = html_module:HtmlModuleDescriptor", - "section = translation_module:SemanticSectionDescriptor", - "sequential = seq_module:SequenceDescriptor", - "vertical = seq_module:SequenceDescriptor", - "problemset = seq_module:SequenceDescriptor", - "videosequence = seq_module:SequenceDescriptor", + "chapter = xmodule.seq_module:SequenceDescriptor", + "course = xmodule.seq_module:SequenceDescriptor", + "html = xmodule.html_module:HtmlDescriptor", + "section = xmodule.translation_module:SemanticSectionDescriptor", + "sequential = xmodule.seq_module:SequenceDescriptor", + "vertical = xmodule.vertical_module:VerticalDescriptor", + "problemset = xmodule.seq_module:SequenceDescriptor", + "videosequence = xmodule.seq_module:SequenceDescriptor", + "video = xmodule.video_module:VideoDescriptor", ] } ) diff --git a/common/lib/xmodule/template_module.py b/common/lib/xmodule/template_module.py index 51f9447c06..ae276737e6 100644 --- a/common/lib/xmodule/template_module.py +++ b/common/lib/xmodule/template_module.py @@ -31,18 +31,11 @@ class Module(XModule): Renders to:: More information given in the text """ - def get_state(self): - return json.dumps({}) - - @classmethod - def get_xml_tags(c): - return ['customtag'] - def get_html(self): return self.html - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) + def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): + XModule.__init__(self, system, xml, item_id, instance_state, shared_state) xmltree = etree.fromstring(xml) filename = xmltree.find('impl').text params = dict(xmltree.items()) diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py index b3feec8bae..d3f4cd6ad3 100644 --- a/common/lib/xmodule/vertical_module.py +++ b/common/lib/xmodule/vertical_module.py @@ -1,23 +1,10 @@ -import json - -from x_module import XModule, XModuleDescriptor +from xmodule.x_module import XModule +from xmodule.seq_module import SequenceDescriptor from xmodule.progress import Progress -from lxml import etree -class ModuleDescriptor(XModuleDescriptor): - pass -class Module(XModule): +class VerticalModule(XModule): ''' Layout module for laying out submodules vertically.''' - id_attribute = 'id' - - def get_state(self): - return json.dumps({ }) - - @classmethod - def get_xml_tags(c): - return ["vertical", "problemset"] - def get_html(self): return self.system.render_template('vert_module.html', { 'items': self.contents @@ -30,8 +17,10 @@ class Module(XModule): progress = reduce(Progress.add_counts, progresses) return progress - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) - xmltree=etree.fromstring(xml) - self.contents=[(e.get("name"),self.render_function(e)) \ - for e in xmltree] + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + self.contents = [child.get_html() for child in self.get_display_items()] + + +class VerticalDescriptor(SequenceDescriptor): + module_class = VerticalModule diff --git a/common/lib/xmodule/video_module.py b/common/lib/xmodule/video_module.py index f3d615fd3d..1585944cc9 100644 --- a/common/lib/xmodule/video_module.py +++ b/common/lib/xmodule/video_module.py @@ -3,16 +3,13 @@ import logging from lxml import etree -from x_module import XModule, XModuleDescriptor -from progress import Progress +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor -log = logging.getLogger("mitx.courseware.modules") +log = logging.getLogger(__name__) -class ModuleDescriptor(XModuleDescriptor): - pass -class Module(XModule): - id_attribute = 'youtube' +class VideoModule(XModule): video_time = 0 def handle_ajax(self, dispatch, get): @@ -39,14 +36,9 @@ class Module(XModule): ''' return None - def get_state(self): + def get_instance_state(self): log.debug(u"STATE POSITION {0}".format(self.position)) - return json.dumps({ 'position': self.position }) - - @classmethod - def get_xml_tags(c): - '''Tags in the courseware file guaranteed to correspond to the module''' - return ["video"] + return json.dumps({'position': self.position}) def video_list(self): return self.youtube @@ -54,27 +46,27 @@ class Module(XModule): def get_html(self): return self.system.render_template('video.html', { 'streams': self.video_list(), - 'id': self.item_id, + 'id': self.id, 'position': self.position, 'name': self.name, 'annotations': self.annotations, }) - def __init__(self, system, xml, item_id, state=None): - XModule.__init__(self, system, xml, item_id, state) - xmltree = etree.fromstring(xml) + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + xmltree = etree.fromstring(self.definition['data']) self.youtube = xmltree.get('youtube') self.name = xmltree.get('name') self.position = 0 - if state is not None: - state = json.loads(state) + if instance_state is not None: + state = json.loads(instance_state) if 'position' in state: self.position = int(float(state['position'])) - self.annotations=[(e.get("name"),self.render_function(e)) \ + self.annotations = [(e.get("name"), self.render_function(e)) \ for e in xmltree] -class VideoSegmentDescriptor(XModuleDescriptor): - pass +class VideoDescriptor(RawDescriptor): + module_class = VideoModule diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index aad4dd94dc..3787a76752 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -3,6 +3,7 @@ import pkg_resources import logging from keystore import Location +from functools import partial log = logging.getLogger('mitx.' + __name__) @@ -56,85 +57,87 @@ class Plugin(object): class XModule(object): ''' Implements a generic learning module. - Initialized on access with __init__, first time with state=None, and - then with state + Initialized on access with __init__, first time with instance_state=None, and + shared_state=None. In later instantiations, instance_state will not be None, + but shared_state may be See the HTML module for a simple example ''' - id_attribute='id' # An attribute guaranteed to be unique + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + ''' + Construct a new xmodule - @classmethod - def get_xml_tags(c): - ''' Tags in the courseware file guaranteed to correspond to the module ''' - return [] - - @classmethod - def get_usage_tags(c): - ''' We should convert to a real module system - For now, this tells us whether we use this as an xmodule, a CAPA response type - or a CAPA input type ''' - return ['xmodule'] + system: An I4xSystem allowing access to external resources + location: Something Location-like that identifies this xmodule + definition: A dictionary containing 'data' and 'children'. Both are optional + 'data': is a json object specifying the behavior of this xmodule + 'children': is a list of xmodule urls for child modules that this module depends on + ''' + self.system = system + self.location = Location(location) + self.definition = definition + self.instance_state = instance_state + self.shared_state = shared_state + self.id = self.location.url() + self.name = self.location.name + self.display_name = kwargs.get('display_name', '') + self._loaded_children = None def get_name(self): name = self.__xmltree.get('name') - if name: + if name: return name - else: + else: raise "We should iterate through children and find a default name" def get_children(self): ''' Return module instances for all the children of this module. ''' - children = [self.module_from_xml(e) for e in self.__xmltree] - return children + if self._loaded_children is None: + self._loaded_children = [self.system.get_module(child) for child in self.definition.get('children', [])] + return self._loaded_children - def rendered_children(self): + def get_display_items(self): ''' - Render all children. - This really ought to return a list of xmodules, instead of dictionaries + Returns a list of descendent module instances that will display immediately + inside this module ''' - children = [self.render_function(e) for e in self.__xmltree] - return children + items = [] + for child in self.get_children(): + items.extend(child.displayable_items()) - def __init__(self, system = None, xml = None, item_id = None, - json = None, track_url=None, state=None): - ''' In most cases, you must pass state or xml''' - if not item_id: - raise ValueError("Missing Index") - if not xml and not json: - raise ValueError("xml or json required") - if not system: - raise ValueError("System context required") + return items - self.xml = xml - self.json = json - self.item_id = item_id - self.state = state - self.DEBUG = False - - self.__xmltree = etree.fromstring(xml) # PRIVATE + def displayable_items(self): + ''' + Returns list of displayable modules contained by this module. If this module + is visible, should return [self] + ''' + return [self] - if system: - ## These are temporary; we really should go - ## through self.system. - self.ajax_url = system.ajax_url - self.tracker = system.track_function - self.filestore = system.filestore - self.render_function = system.render_function - self.module_from_xml = system.module_from_xml - self.DEBUG = system.DEBUG - self.system = system + def get_icon_class(self): + ''' + Return a class identifying this module in the context of an icon + ''' + return 'other' ### Functions used in the LMS - def get_state(self): - ''' State of the object, as stored in the database + def get_instance_state(self): + ''' State of the object, as stored in the database ''' - return "" + return '{}' + + def get_shared_state(self): + ''' + Get state that should be shared with other instances + using the same 'shared_state_key' attribute. + ''' + return '{}' def get_score(self): - ''' Score the student received on the problem. + ''' Score the student received on the problem. ''' return None @@ -281,6 +284,7 @@ class XModuleDescriptor(Plugin): self.name = Location(kwargs.get('location')).name self.type = Location(kwargs.get('location')).category self.url = Location(kwargs.get('location')).url() + self.display_name = kwargs.get('display_name') # For now, we represent goals as a list of strings, but this # is one of the things that we are going to be iterating on heavily @@ -302,33 +306,13 @@ class XModuleDescriptor(Plugin): """ raise NotImplementedError("get_html() must be provided by specific modules") - def get_xml(self): - ''' For conversions between JSON and legacy XML representations. - ''' - if self.xml: - return self.xml - else: - raise NotImplementedError("JSON->XML Translation not implemented") - - def get_json(self): - ''' For conversions between JSON and legacy XML representations. - ''' - if self.json: - raise NotImplementedError - return self.json # TODO: Return context as well -- files, etc. - else: - raise NotImplementedError("XML->JSON Translation not implemented") - - #def handle_cms_json(self): - # raise NotImplementedError - - #def render(self, size): - # ''' Size: [thumbnail, small, full] - # Small ==> what we drag around - # Full ==> what we edit - # ''' - # raise NotImplementedError - + def xmodule_constructor(self, system): + """ + Returns a constructor for an XModule. This constructor takes two arguments: + instance_state and shared_state, and returns a fully nstantiated XModule + """ + return partial(self.module_class, system, self.url, self.definition, + display_name=self.display_name) class DescriptorSystem(object): def __init__(self, load_item, resources_fs): diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py new file mode 100644 index 0000000000..34881a4d61 --- /dev/null +++ b/common/lib/xmodule/xml_module.py @@ -0,0 +1,41 @@ +from xmodule.x_module import XModuleDescriptor +from lxml import etree + + +class XmlDescriptor(XModuleDescriptor): + """ + Mixin class for standardized parsing of from xml + """ + + @classmethod + def definition_from_xml(cls, xml_object, system): + """ + Return the definition to be passed to the newly created descriptor + during from_xml + """ + raise NotImplementedError("%s does not implement definition_from_xml" % cls.__class__.__name__) + + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + """ + Creates an instance of this descriptor from the supplied xml_data. + This may be overridden by subclasses + + xml_data: A string of xml that will be translated into data and children for + this module + system: An XModuleSystem for interacting with external resources + org and course are optional strings that will be used in the generated modules + url identifiers + """ + xml_object = etree.fromstring(xml_data) + + return cls( + system, + cls.definition_from_xml(xml_object, system), + location=['i4x', + org, + course, + xml_object.tag, + xml_object.get('slug')], + display_name=xml_object.get('name') + ) diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py index 95c3afed8c..70e5eeeeb6 100644 --- a/lms/djangoapps/courseware/content_parser.py +++ b/lms/djangoapps/courseware/content_parser.py @@ -19,10 +19,12 @@ from django.conf import settings from student.models import UserProfile from student.models import UserTestGroup +from courseware.models import StudentModuleCache from mitxmako.shortcuts import render_to_string from util.cache import cache from multicourse import multicourse_settings import xmodule +from keystore.django import keystore ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. @@ -103,6 +105,7 @@ def course_xml_process(tree): items without. Propagate due dates, grace periods, etc. to child items. ''' + process_includes(tree) replace_custom_tags(tree) id_tag(tree) propogate_downward_tag(tree, "due") @@ -113,45 +116,32 @@ def course_xml_process(tree): return tree -def toc_from_xml(dom, active_chapter, active_section): - ''' - Create a table of contents from the course xml. - - Return format: - [ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ] - - where SECTIONS is a list - [ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...] - - active is set for the section and chapter corresponding to the passed - parameters. Everything else comes from the xml, or defaults to "". - - chapters with name 'hidden' are skipped. - ''' - name = dom.xpath('//course/@name')[0] - - chapters = dom.xpath('//course[@name=$name]/chapter', name=name) - ch = list() - for c in chapters: - if c.get('name') == 'hidden': - continue - sections = list() - for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section', - name=name, chname=c.get('name')): - - format = s.get("subtitle") if s.get("subtitle") else s.get("format") or "" - active = (c.get("name") == active_chapter and - s.get("name") == active_section) - - sections.append({'name': s.get("name") or "", - 'format': format, - 'due': s.get("due") or "", - 'active': active}) - - ch.append({'name': c.get("name"), - 'sections': sections, - 'active': c.get("name") == active_chapter}) - return ch +def process_includes_dir(tree, dir): + """ + Process tree to replace all tags + with the contents of the file specified, relative to dir + """ + includes = tree.findall('.//include') + for inc in includes: + file = inc.get('file') + if file is not None: + try: + ifp = open(os.path.join(dir, file)) + except Exception: + log.exception('Error in problem xml include: %s' % (etree.tostring(inc, pretty_print=True))) + log.exception('Cannot find file %s in %s' % (file, dir)) + raise + try: + # read in and convert to XML + incxml = etree.XML(ifp.read()) + except Exception: + log.exception('Error in problem xml include: %s' % (etree.tostring(inc, pretty_print=True))) + log.exception('Cannot parse XML in %s' % (file)) + raise + # insert new XML into tree in place of inlcude + parent = inc.getparent() + parent.insert(parent.index(inc), incxml) + parent.remove(inc) def replace_custom_tags_dir(tree, dir): @@ -181,78 +171,6 @@ def parse_course_file(filename, options, namespace): return course_xml_process(xml) -def get_section(section, options, dirname): - ''' - Given the name of a section, an options dict containing keys - 'dev_content' and 'groups', and a directory to look in, - returns the xml tree for the section, or None if there's no - such section. - ''' - filename = section + ".xml" - - if filename not in os.listdir(dirname): - log.error(filename + " not in " + str(os.listdir(dirname))) - return None - - tree = parse_course_file(filename, options, namespace='sections') - return tree - - -def get_module(tree, module, id_tag, module_id, sections_dirname, options): - ''' - Given the xml tree of the course, get the xml string for a module - with the specified module type, id_tag, module_id. Looks in - sections_dirname for sections. - - id_tag -- use id_tag if the place the module stores its id is not 'id' - ''' - # Sanitize input - if not module.isalnum(): - raise Exception("Module is not alphanumeric") - - if not module_id.isalnum(): - raise Exception("Module ID is not alphanumeric") - - # Generate search - xpath_search='//{module}[(@{id_tag} = "{id}") or (@id = "{id}")]'.format( - module=module, - id_tag=id_tag, - id=module_id) - - - result_set = tree.xpath(xpath_search) - if len(result_set) < 1: - # Not found in main tree. Let's look in the section files. - section_list = (s[:-4] for s in os.listdir(sections_dirname) if s.endswith('.xml')) - for section in section_list: - try: - s = get_section(section, options, sections_dirname) - except etree.XMLSyntaxError: - ex = sys.exc_info() - raise ContentException("Malformed XML in " + section + - "(" + str(ex[1].msg) + ")") - result_set = s.xpath(xpath_search) - if len(result_set) != 0: - break - - if len(result_set) > 1: - log.error("WARNING: Potentially malformed course file", module, module_id) - - if len(result_set)==0: - log.error('[content_parser.get_module] cannot find %s in course.xml tree', - xpath_search) - log.error('tree = %s' % etree.tostring(tree, pretty_print=True)) - return None - - # log.debug('[courseware.content_parser.module_xml] found %s' % result_set) - - return etree.tostring(result_set[0]) - - - - - - # ==== All Django-specific code below ============================================= def user_groups(user): @@ -278,6 +196,11 @@ def get_options(user): 'groups': user_groups(user)} +def process_includes(tree): + '''Replace tags with the contents from the course directory''' + process_includes_dir(tree, settings.DATA_DIR) + + def replace_custom_tags(tree): '''Replace custom tags defined in our custom_tags dir''' replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags') @@ -337,29 +260,3 @@ def sections_dir(coursename=None): xp = multicourse_settings.get_course_xmlpath(coursename) return settings.DATA_DIR + xp + '/sections/' - - - -def section_file(user, section, coursename=None): - ''' - Given a user and the name of a section, return that section. - This is done specific to each course. - - Returns the xml tree for the section, or None if there's no such section. - ''' - dirname = sections_dir(coursename) - - - return get_section(section, options, dirname) - - -def module_xml(user, module, id_tag, module_id, coursename=None): - ''' Get XML for a module based on module and module_id. Assumes - module occurs once in courseware XML file or hidden section. - ''' - tree = course_file(user, coursename) - sdirname = sections_dir(coursename) - options = get_options(user) - - return get_module(tree, module, id_tag, module_id, sdirname, options) - diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 00bdffb697..3c2b654682 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -81,12 +81,12 @@ def grade_sheet(student,coursename=None): course = dom.xpath('//course/@name')[0] xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course) - responses=StudentModule.objects.filter(student=student) + responses = StudentModule.objects.filter(student=student) response_by_id = {} for response in responses: - response_by_id[response.module_id] = response - - + response_by_id[response.module_state_key] = response + + totaled_scores = {} chapters=[] for c in xmlChapters: @@ -147,27 +147,39 @@ def grade_sheet(student,coursename=None): 'grade_summary' : grade_summary} def get_score(user, problem, cache, coursename=None): + """ + Return the score for a user on a problem + + user: a Student object + problem: the xml for the problem + cache: a dictionary mapping module_state_key tuples to instantiated StudentModules + module_state_key is either the problem_id, or a key used by the problem + to share state across instances + """ ## HACK: assumes max score is fixed per problem - id = problem.get('id') + module_type = problem.tag + module_class = xmodule.get_module_class(module_type) + module_id = problem.get('id') + module_state_key = problem.get(module_class.state_key, module_id) correct = 0.0 - + # If the ID is not in the cache, add the item - if id not in cache: - module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__? - module_id = id, - student = user, - state = None, - grade = 0, - max_grade = None, - done = 'i') - cache[id] = module + if module_state_key not in cache: + module = StudentModule(module_type='problem', # TODO: Move into StudentModule.__init__? + module_state_key=id, + student=user, + state=None, + grade=0, + max_grade=None, + done='i') + cache[module_id] = module # Grab the # correct from cache if id in cache: response = cache[id] - if response.grade!=None: - correct=float(response.grade) - + if response.grade != None: + correct = float(response.grade) + # Grab max grade from cache, or if it doesn't exist, compute and save to DB if id in cache and response.max_grade is not None: total = response.max_grade diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index a97b09ae2b..6ca67a84e7 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -13,7 +13,6 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types """ from django.db import models -from django.db.models.signals import post_save, post_delete #from django.core.cache import cache from django.contrib.auth.models import User @@ -21,72 +20,97 @@ from django.contrib.auth.models import User #CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours + class StudentModule(models.Model): # For a homework problem, contains a JSON # object consisting of state - MODULE_TYPES = (('problem','problem'), - ('video','video'), - ('html','html'), + MODULE_TYPES = (('problem', 'problem'), + ('video', 'video'), + ('html', 'html'), ) ## These three are the key for the object module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True) - module_id = models.CharField(max_length=255, db_index=True) # Filename for homeworks, etc. + + # Key used to share state. By default, this is the module_id, + # but for abtests and the like, this can be set to a shared value + # for many instances of the module. + # Filename for homeworks, etc. + module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id') student = models.ForeignKey(User, db_index=True) + class Meta: - unique_together = (('student', 'module_id'),) + unique_together = (('student', 'module_state_key'),) ## Internal state of the object state = models.TextField(null=True, blank=True) - ## Grade, and are we done? + ## Grade, and are we done? grade = models.FloatField(null=True, blank=True, db_index=True) max_grade = models.FloatField(null=True, blank=True) - DONE_TYPES = (('na','NOT_APPLICABLE'), - ('f','FINISHED'), - ('i','INCOMPLETE'), + DONE_TYPES = (('na', 'NOT_APPLICABLE'), + ('f', 'FINISHED'), + ('i', 'INCOMPLETE'), ) done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True) - # DONE_TYPES = (('done','DONE'), # Finished - # ('incomplete','NOTDONE'), # Not finished - # ('na','NA')) # Not applicable (e.g. vertical) - # done = models.CharField(max_length=16, choices=DONE_TYPES) - created = models.DateTimeField(auto_now_add=True, db_index=True) modified = models.DateTimeField(auto_now=True, db_index=True) def __unicode__(self): - return self.module_type+'/'+self.student.username+"/"+self.module_id+'/'+str(self.state)[:20] - - # @classmethod - # def get_with_caching(cls, student, module_id): - # k = cls.key_for(student, module_id) - # student_module = cache.get(k) - # if student_module is None: - # student_module = StudentModule.objects.filter(student=student, - # module_id=module_id)[0] - # # It's possible it really doesn't exist... - # if student_module is not None: - # cache.set(k, student_module, CACHE_TIMEOUT) - - # return student_module - - @classmethod - def key_for(cls, student, module_id): - return "StudentModule-student_id:{0};module_id:{1}".format(student.id, module_id) + return '/'.join([self.module_type, self.student.username, self.module_state_key, str(self.state)[:20]]) -# def clear_cache_by_student_and_module_id(sender, instance, *args, **kwargs): -# k = sender.key_for(instance.student, instance.module_id) -# cache.delete(k) - -# def update_cache_by_student_and_module_id(sender, instance, *args, **kwargs): -# k = sender.key_for(instance.student, instance.module_id) -# cache.set(k, instance, CACHE_TIMEOUT) +# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors -#post_save.connect(update_cache_by_student_and_module_id, sender=StudentModule, weak=False) -#post_delete.connect(clear_cache_by_student_and_module_id, sender=StudentModule, weak=False) -#cache_model(StudentModule) +class StudentModuleCache(object): + """ + A cache of StudentModules for a specific student + """ + def __init__(self, user, descriptor, depth=None): + ''' + Find any StudentModule objects that are needed by any child modules of the + supplied descriptor. Avoids making multiple queries to the database + ''' + if user.is_authenticated(): + module_ids = self._get_module_state_keys(descriptor, depth) + self.cache = list(StudentModule.objects.filter(student=user, + module_state_key__in=module_ids)) + else: + self.cache = [] + def _get_module_state_keys(self, descriptor, depth): + ''' + Get a list of the state_keys needed for StudentModules + required for this chunk of module xml + ''' + keys = [descriptor.url] + + shared_state_key = getattr(descriptor, 'shared_state_key', None) + if shared_state_key is not None: + keys.append(shared_state_key) + + if depth is None or depth > 0: + new_depth = depth - 1 if depth is not None else depth + + for child in descriptor.get_children(): + keys.extend(self._get_module_state_keys(child, new_depth)) + + return keys + + def lookup(self, module_type, module_state_key): + ''' + Look for a student module with the given type and id in the cache. + + cache -- list of student modules + + returns first found object, or None + ''' + for o in self.cache: + if o.module_type == module_type and o.module_state_key == module_state_key: + return o + return None + + def append(self, student_module): + self.cache.append(student_module) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 3a6fcbfb45..d05bdcefab 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -12,19 +12,21 @@ from fs.osfs import OSFS from django.conf import settings from mitxmako.shortcuts import render_to_string, render_to_response -from models import StudentModule +from models import StudentModule, StudentModuleCache from multicourse import multicourse_settings from util.views import accepts import courseware.content_parser as content_parser import xmodule +from keystore.django import keystore log = logging.getLogger("mitx.courseware") + class I4xSystem(object): ''' - This is an abstraction such that x_modules can function independent - of the courseware (e.g. import into other types of courseware, LMS, + This is an abstraction such that x_modules can function independent + of the courseware (e.g. import into other types of courseware, LMS, or if we want to have a sandbox server for user-contributed content) I4xSystem objects are passed to x_modules to provide access to system @@ -34,7 +36,7 @@ class I4xSystem(object): and user, or other environment-specific info. ''' def __init__(self, ajax_url, track_function, render_function, - module_from_xml, render_template, request=None, + get_module, render_template, request=None, filestore=None): ''' Create a closure around the system environment. @@ -44,7 +46,7 @@ class I4xSystem(object): or otherwise tracking the event. TODO: Not used, and has inconsistent args in different files. Update or remove. - module_from_xml - function that takes (module_xml) and returns a corresponding + get_module - function that takes (location) and returns a corresponding module instance object. render_function - function that takes (module_xml) and renders it, returning a dictionary with a context for rendering the @@ -58,14 +60,14 @@ class I4xSystem(object): ''' self.ajax_url = ajax_url self.track_function = track_function - if not filestore: + if not filestore: self.filestore = OSFS(settings.DATA_DIR) else: self.filestore = filestore if settings.DEBUG: log.info("[courseware.module_render.I4xSystem] filestore path = %s", filestore) - self.module_from_xml = module_from_xml + self.get_module = get_module self.render_function = render_function self.render_template = render_template self.exception404 = Http404 @@ -75,8 +77,8 @@ class I4xSystem(object): def get(self, attr): ''' provide uniform access to attributes (like etree).''' return self.__dict__.get(attr) - - def set(self,attr,val): + + def set(self, attr, val): '''provide uniform access to attributes (like etree)''' self.__dict__[attr] = val @@ -86,21 +88,11 @@ class I4xSystem(object): def __str__(self): return str(self.__dict__) -def smod_cache_lookup(cache, module_type, module_id): - ''' - Look for a student module with the given type and id in the cache. - cache -- list of student modules - returns first found object, or None - ''' - for o in cache: - if o.module_type == module_type and o.module_id == module_id: - return o - return None def make_track_function(request): - ''' + ''' Make a tracking function that logs what happened. For use in I4xSystem. ''' @@ -110,8 +102,9 @@ def make_track_function(request): return track.views.server_track(request, event_type, event, page='x_module') return f + def grade_histogram(module_id): - ''' Print out a histogram of grades on a given problem. + ''' Print out a histogram of grades on a given problem. Part of staff member debug info. ''' from django.db import connection @@ -137,13 +130,87 @@ def make_module_from_xml_fn(user, request, student_module_cache, position): def module_from_xml(xml): '''Modules need a way to convert xml to instance objects. Pass the rest of the context through.''' - (instance, sm, module_type) = get_module( + (instance, _, _, _) = get_module( user, request, xml, student_module_cache, position) return instance return module_from_xml -def get_module(user, request, module_xml, student_module_cache, position=None): +def toc_for_course(user, request, course_location, active_chapter, active_section): + ''' + Create a table of contents from the module store + + Return format: + [ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ] + + where SECTIONS is a list + [ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...] + + active is set for the section and chapter corresponding to the passed + parameters. Everything else comes from the xml, or defaults to "". + + chapters with name 'hidden' are skipped. + ''' + + student_module_cache = StudentModuleCache(user, keystore().get_item(course_location), depth=2) + (course, _, _, _) = get_module(user, request, course_location, student_module_cache) + + chapters = list() + for chapter in course.get_display_items(): + sections = list() + for section in chapter.get_display_items(): + + active = (chapter.display_name == active_chapter and + section.display_name == active_section) + + sections.append({'name': section.display_name, + 'format': getattr(section, 'format', ''), + 'due': getattr(section, 'due', ''), + 'active': active}) + + chapters.append({'name': chapter.display_name, + 'sections': sections, + 'active': chapter.display_name == active_chapter}) + return chapters + + +def get_section(course, chapter, section): + """ + Returns the xmodule descriptor for the name course > chapter > section, + or None if this doesn't specify a valid section + + course: Course url + chapter: Chapter name + section: Section name + """ + try: + course_module = keystore().get_item(course) + except: + log.exception("Unable to load course_module") + return None + + if course_module is None: + return + + chapter_module = None + for _chapter in course_module.get_children(): + if _chapter.display_name == chapter: + chapter_module = _chapter + break + + if chapter_module is None: + return + + section_module = None + for _section in chapter_module.get_children(): + if _section.display_name == section: + section_module = _section + break + + return section_module + + +def get_module(user, request, location, student_module_cache, position=None): ''' Get an instance of the xmodule class corresponding to module_xml, setting the state based on an existing StudentModule, or creating one if none exists. @@ -152,65 +219,73 @@ def get_module(user, request, module_xml, student_module_cache, position=None): - user : current django User - request : current django HTTPrequest - module_xml : lxml etree of xml subtree for the requested module - - student_module_cache : list of StudentModule objects, one of which may - match this module type and id - - position : extra information from URL for user-specified + - student_module_cache : a StudentModuleCache + - position : extra information from URL for user-specified position within module Returns: - - a tuple (xmodule instance, student module, module type). + - a tuple (xmodule instance, instance_module, shared_module, module type). + instance_module is a StudentModule specific to this module for this student + shared_module is a StudentModule specific to all modules with the same 'shared_state_key' attribute, or None if the module doesn't elect to share state ''' - module_type = module_xml.tag - module_class = xmodule.get_module_class(module_type) - module_id = module_xml.get('id') + descriptor = keystore().get_item(location) - # Grab xmodule state from StudentModule cache - smod = smod_cache_lookup(student_module_cache, module_type, module_id) - state = smod.state if smod else None - - # get coursename if present in request - coursename = multicourse_settings.get_coursename_from_request(request) - - if coursename and settings.ENABLE_MULTICOURSE: - # path to XML for the course - xp = multicourse_settings.get_course_xmlpath(coursename) - data_root = settings.DATA_DIR + xp + instance_module = student_module_cache.lookup(descriptor.type, descriptor.url) + shared_state_key = getattr(descriptor, 'shared_state_key', None) + if shared_state_key is not None: + shared_module = student_module_cache.lookup(descriptor.type, shared_state_key) else: - data_root = settings.DATA_DIR + shared_module = None + + instance_state = instance_module.state if instance_module is not None else None + shared_state = shared_module.state if shared_module is not None else None # Setup system context for module instance - ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/' + ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.type + '/' + descriptor.url + '/' - module_from_xml = make_module_from_xml_fn( - user, request, student_module_cache, position) - - system = I4xSystem(track_function = make_track_function(request), - render_function = lambda xml: render_x_module( + def _get_module(location): + (module, _, _, _) = get_module(user, request, location, student_module_cache, position) + return module + + system = I4xSystem(track_function=make_track_function(request), + render_function=lambda xml: render_x_module( user, request, xml, student_module_cache, position), - render_template = render_to_string, - ajax_url = ajax_url, - request = request, - filestore = OSFS(data_root), - module_from_xml = module_from_xml, + render_template=render_to_string, + ajax_url=ajax_url, + request=request, + # TODO (cpennington): Figure out how to share info between systems + filestore=descriptor.system.resources_fs, + get_module=_get_module, ) # pass position specified in URL to module through I4xSystem - system.set('position', position) - instance = module_class(system, - etree.tostring(module_xml), - module_id, - state=state) + system.set('position', position) + + module = descriptor.xmodule_constructor(system)(instance_state, shared_state) # If StudentModule for this instance wasn't already in the database, # and this isn't a guest user, create it. - if not smod and user.is_authenticated(): - smod = StudentModule(student=user, module_type = module_type, - module_id=module_id, state=instance.get_state()) - smod.save() - # Add to cache. The caller and the system context have references - # to it, so the change persists past the return - student_module_cache.append(smod) + if user.is_authenticated(): + if not instance_module: + instance_module = StudentModule( + student=user, + module_type=descriptor.type, + module_state_key=module.id, + state=module.get_instance_state()) + instance_module.save() + # Add to cache. The caller and the system context have references + # to it, so the change persists past the return + student_module_cache.append(instance_module) + if not shared_module and shared_state_key is not None: + shared_module = StudentModule( + student=user, + module_type=descriptor.type, + module_state_key=shared_state_key, + state=module.get_shared_state()) + shared_module.save() + student_module_cache.append(shared_module) + + return (module, instance_module, shared_module, descriptor.type) - return (instance, smod, module_type) def render_x_module(user, request, module_xml, student_module_cache, position=None): ''' Generic module for extensions. This renders to HTML. @@ -232,20 +307,20 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No - dict which is context for HTML rendering of the specified module. Will have key 'content', and will have 'type' key if passed a valid module. ''' - if module_xml is None : + if module_xml is None: return {"content": ""} - (instance, smod, module_type) = get_module( + (instance, _, _, module_type) = get_module( user, request, module_xml, student_module_cache, position) content = instance.get_html() - # special extra information about each problem, only for users who are staff + # special extra information about each problem, only for users who are staff if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff: module_id = module_xml.get('id') histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 - staff_context = {'xml': etree.tostring(module_xml), + staff_context = {'xml': etree.tostring(module_xml), 'module_id': module_id, 'histogram': json.dumps(histogram), 'render_histogram': render_histogram} @@ -254,6 +329,7 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No context = {'content': content, 'type': module_type} return context + def modx_dispatch(request, module=None, dispatch=None, id=None): ''' Generic view for extensions. This is where AJAX calls go. @@ -276,24 +352,10 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): error_msg = ("We're sorry, this module is temporarily unavailable. " "Our staff is working to fix it as soon as possible") - - # Grab the student information for the module from the database - s = StudentModule.objects.filter(student=request.user, - module_id=id) - - if s is None or len(s) == 0: - log.debug("Couldn't find module '%s' for user '%s' and id '%s'", - module, request.user, id) - raise Http404 - s = s[0] - - oldgrade = s.grade - oldstate = s.state - # If there are arguments, get rid of them dispatch, _, _ = dispatch.partition('?') - ajax_url = '{root}/modx/{module}/{id}'.format(root = settings.MITX_ROOT_URL, + ajax_url = '{root}/modx/{module}/{id}'.format(root=settings.MITX_ROOT_URL, module=module, id=id) coursename = multicourse_settings.get_coursename_from_request(request) if coursename and settings.ENABLE_MULTICOURSE: @@ -315,26 +377,40 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): response = HttpResponse(json.dumps({'success': error_msg})) return response - # TODO: This doesn't have a cache of child student modules. Just - # passing the current one. If ajax calls end up needing children, - # this won't work (but fixing it may cause performance issues...) - # Figure out :) + module_xml = etree.fromstring(xml) + student_module_cache = StudentModuleCache(request.user, module_xml) + (instance, instance_state, shared_state, module_type) = get_module( + request.user, request, module_xml, + student_module_cache, None) + + if instance_state is None: + log.debug("Couldn't find module '%s' for user '%s' and id '%s'", + module, request.user, id) + raise Http404 + + oldgrade = instance_state.grade + old_instance_state = instance_state.state + old_shared_state = shared_state.state if shared_state is not None else None + module_from_xml = make_module_from_xml_fn( - request.user, request, [s], None) + request.user, request, student_module_cache, None) # Create the module - system = I4xSystem(track_function = make_track_function(request), - render_function = None, - module_from_xml = module_from_xml, - render_template = render_to_string, - ajax_url = ajax_url, - request = request, - filestore = OSFS(data_root), + system = I4xSystem(track_function=make_track_function(request), + render_function=None, + module_from_xml=module_from_xml, + render_template=render_to_string, + ajax_url=ajax_url, + request=request, + filestore=OSFS(data_root), ) try: module_class = xmodule.get_module_class(module) - instance = module_class(system, xml, id, state=oldstate) + instance = module_class( + system, xml, id, + instance_state=old_instance_state, + shared_state=old_shared_state) except: log.exception("Unable to load module instance during ajax call") if accepts(request, 'text/html'): @@ -351,10 +427,16 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): raise # Save the state back to the database - s.state = instance.get_state() - if instance.get_score(): - s.grade = instance.get_score()['score'] - if s.grade != oldgrade or s.state != oldstate: - s.save() + instance_state.state = instance.get_instance_state() + if instance.get_score(): + instance_state.grade = instance.get_score()['score'] + if instance_state.grade != oldgrade or instance_state.state != old_instance_state: + instance_state.save() + + if shared_state is not None: + shared_state.state = instance.get_shared_state() + if shared_state.state != old_shared_state: + shared_state.save() + # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 5cbbe18d7d..6e8eb1ab9e 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -16,11 +16,10 @@ from django.views.decorators.cache import cache_control from lxml import etree -from module_render import render_x_module, make_track_function, I4xSystem -from models import StudentModule +from module_render import render_x_module, toc_for_course, get_module, get_section +from models import StudentModuleCache from student.models import UserProfile from multicourse import multicourse_settings -import xmodule import courseware.content_parser as content_parser @@ -87,23 +86,20 @@ def render_accordion(request, course, chapter, section): If chapter and section are '' or None, renders a default accordion. Returns (initialization_javascript, content)''' - if not course: - course = "6.002 Spring 2012" - toc = content_parser.toc_from_xml( - content_parser.course_file(request.user, course), chapter, section) + course_location = multicourse_settings.get_course_location(course) + toc = toc_for_course(request.user, request, course_location, chapter, section) active_chapter = 1 for i in range(len(toc)): if toc[i]['active']: active_chapter = i - context=dict([('active_chapter', active_chapter), - ('toc', toc), - ('course_name', course), - ('format_url_params', content_parser.format_url_params), - ('csrf', csrf(request)['csrf_token'])] + - template_imports.items()) + context = dict([('active_chapter', active_chapter), + ('toc', toc), + ('course_name', course), + ('format_url_params', content_parser.format_url_params), + ('csrf', csrf(request)['csrf_token'])] + template_imports.items()) return render_to_string('accordion.html', context) @@ -125,16 +121,10 @@ def render_section(request, section): context = { 'csrf': csrf(request)['csrf_token'], - 'accordion': render_accordion(request, '', '', '') + 'accordion': render_accordion(request, get_course(request), '', '') } - module_ids = dom.xpath("//@id") - - if user.is_authenticated(): - student_module_cache = list(StudentModule.objects.filter(student=user, - module_id__in=module_ids)) - else: - student_module_cache = [] + student_module_cache = StudentModuleCache(request.user, dom) try: module = render_x_module(user, request, dom, student_module_cache) @@ -147,13 +137,13 @@ def render_section(request, section): return render_to_response('courseware.html', context) context.update({ - 'init': module.get('init_js', ''), 'content': module['content'], }) result = render_to_response('courseware.html', context) return result + def get_course(request, course): ''' Figure out what the correct course is. @@ -161,7 +151,7 @@ def get_course(request, course): TODO: Can this go away once multicourse becomes standard? ''' - if course==None: + if course == None: if not settings.ENABLE_MULTICOURSE: course = "6.002 Spring 2012" elif 'coursename' in request.session: @@ -170,35 +160,6 @@ def get_course(request, course): course = settings.COURSE_DEFAULT return course -def get_module_xml(user, course, chapter, section): - ''' Look up the module xml for the given course/chapter/section path. - - Takes the user to look up the course file. - - Returns None if there was a problem, or the lxml etree for the module. - ''' - try: - # this is the course.xml etree - dom = content_parser.course_file(user, course) - except: - log.exception("Unable to parse courseware xml") - return None - - # this is the module's parent's etree - path = "//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]" - dom_module = dom.xpath(path, course=course, chapter=chapter, section=section) - - module_wrapper = dom_module[0] if len(dom_module) > 0 else None - if module_wrapper is None: - module = None - elif module_wrapper.get("src"): - module = content_parser.section_file( - user=user, section=module_wrapper.get("src"), coursename=course) - else: - # Copy the element out of the module's etree - module = etree.XML(etree.tostring(module_wrapper[0])) - return module - @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @@ -228,55 +189,6 @@ def index(request, course=None, chapter=None, section=None, ''' return s.replace('_', ' ') if s is not None else None - def get_submodule_ids(module_xml): - ''' - Get a list with ids of the modules within this module. - ''' - return module_xml.xpath("//@id") - - def preload_student_modules(module_xml): - ''' - Find any StudentModule objects for this user that match - one of the given module_ids. Used as a cache to avoid having - each rendered module hit the db separately. - - Returns the list, or None on error. - ''' - if request.user.is_authenticated(): - module_ids = get_submodule_ids(module_xml) - return list(StudentModule.objects.filter(student=request.user, - module_id__in=module_ids)) - else: - return [] - - def get_module_context(): - ''' - Look up the module object and render it. If all goes well, returns - {'init': module-init-js, 'content': module-rendered-content} - - If there's an error, returns - {'content': module-error message} - ''' - user = request.user - - module_xml = get_module_xml(user, course, chapter, section) - if module_xml is None: - log.exception("couldn't get module_xml: course/chapter/section: '%s/%s/%s'", - course, chapter, section) - return {'content' : render_to_string("module-error.html", {})} - - student_module_cache = preload_student_modules(module_xml) - - try: - module_context = render_x_module(user, request, module_xml, - student_module_cache, position) - except: - log.exception("Unable to load module") - return {'content' : render_to_string("module-error.html", {})} - - return {'init': module_context.get('init_js', ''), - 'content': module_context['content']} - if not settings.COURSEWARE_ENABLED: return redirect('/') @@ -300,11 +212,16 @@ def index(request, course=None, chapter=None, section=None, look_for_module = chapter is not None and section is not None if look_for_module: - context.update(get_module_context()) + course_location = multicourse_settings.get_course_location(course) + section = get_section(course_location, chapter, section) + student_module_cache = StudentModuleCache(request.user, section) + module, _, _, _ = get_module(request.user, request, section.url, student_module_cache) + context['content'] = module.get_html() result = render_to_response('courseware.html', context) return result + def jump_to(request, probname=None): ''' Jump to viewing a specific problem. The problem is specified by a diff --git a/lms/djangoapps/multicourse/multicourse_settings.py b/lms/djangoapps/multicourse/multicourse_settings.py index 05b05c8ec9..4d568d55a1 100644 --- a/lms/djangoapps/multicourse/multicourse_settings.py +++ b/lms/djangoapps/multicourse/multicourse_settings.py @@ -31,11 +31,13 @@ if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be repla elif hasattr(settings,'COURSE_NAME'): # backward compatibility COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER, 'title': settings.COURSE_TITLE, + 'location': settings.COURSE_LOCATION, }, } else: # default to 6.002_Spring_2012 COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x', 'title': 'Circuits and Electronics', + 'location': 'i4x://edx/6002xs12/course/6.002 Spring 2012', }, } @@ -51,31 +53,47 @@ def get_coursename_from_request(request): def get_course_settings(coursename): if not coursename: - if hasattr(settings,'COURSE_DEFAULT'): + if hasattr(settings, 'COURSE_DEFAULT'): coursename = settings.COURSE_DEFAULT else: coursename = '6.002_Spring_2012' - if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename] - coursename = coursename.replace(' ','_') - if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename] + if coursename in COURSE_SETTINGS: + return COURSE_SETTINGS[coursename] + coursename = coursename.replace(' ', '_') + if coursename in COURSE_SETTINGS: + return COURSE_SETTINGS[coursename] return None + def is_valid_course(coursename): return get_course_settings(coursename) != None -def get_course_property(coursename,property): + +def get_course_property(coursename, property): cs = get_course_settings(coursename) - if not cs: return '' # raise exception instead? - if property in cs: return cs[property] - return '' # default + + # raise exception instead? + if not cs: + return '' + + if property in cs: + return cs[property] + + # default + return '' + def get_course_xmlpath(coursename): - return get_course_property(coursename,'xmlpath') + return get_course_property(coursename, 'xmlpath') + def get_course_title(coursename): - return get_course_property(coursename,'title') + return get_course_property(coursename, 'title') + def get_course_number(coursename): - return get_course_property(coursename,'number') - + return get_course_property(coursename, 'number') + +def get_course_location(coursename): + return get_course_property(coursename, 'location') diff --git a/lms/envs/common.py b/lms/envs/common.py index 60834f9d91..8c82b2e8c1 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -132,10 +132,25 @@ COURSE_DEFAULT = '6.002_Spring_2012' COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x', 'title' : 'Circuits and Electronics', 'xmlpath': '6002x/', + 'location': 'i4x://edx/6002xs12/course/6_002_Spring_2012', } } +############################### XModule Store ################################## +KEYSTORE = { + 'default': { + 'ENGINE': 'keystore.xml.XMLModuleStore', + 'OPTIONS': { + 'org': 'edx', + 'course': '6002xs12', + 'data_dir': DATA_DIR, + 'default_class': 'xmodule.hidden_module.HiddenDescriptor', + } + } +} + + ############################### DJANGO BUILT-INS ############################### # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here DEBUG = False diff --git a/lms/envs/dev.py b/lms/envs/dev.py index decd92d136..f175ca1f53 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -11,7 +11,7 @@ from .common import * from .logsettings import get_logger_config DEBUG = True -TEMPLATE_DEBUG = True +TEMPLATE_DEBUG = False LOGGING = get_logger_config(ENV_ROOT / "log", logging_env="dev", diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py index 17096afc70..a91314d228 100644 --- a/lms/lib/dogfood/views.py +++ b/lms/lib/dogfood/views.py @@ -184,29 +184,29 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): filestore = OSFS(settings.DATA_DIR + xp), #role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this ) - instance=xmodule.get_module_class(module)(system, - xml, + instance = xmodule.get_module_class(module)(system, + xml, id, state=None) log.info('ajax_url = ' + instance.ajax_url) # create empty student state for this problem, if not previously existing - s = StudentModule.objects.filter(student=request.user, - module_id=id) + s = StudentModule.objects.filter(student=request.user, + module_state_key=id) if len(s) == 0 or s is None: - smod=StudentModule(student=request.user, - module_type = 'problem', - module_id=id, - state=instance.get_state()) + smod = StudentModule(student=request.user, + module_type='problem', + module_state_key=id, + state=instance.get_instance_state()) smod.save() lcp = instance.lcp pxml = lcp.tree - pxmls = etree.tostring(pxml,pretty_print=True) + pxmls = etree.tostring(pxml, pretty_print=True) return instance, pxmls - instance, pxmls = get_lcp(coursename,id) + instance, pxmls = get_lcp(coursename, id) # if there was a POST, then process it msg = '' @@ -246,8 +246,6 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): # get the rendered problem HTML phtml = instance.get_html() # phtml = instance.get_problem_html() - # init_js = instance.get_init_js() - # destory_js = instance.get_destroy_js() context = {'id':id, 'msg' : msg, diff --git a/lms/static/coffee/src/modules/sequence.coffee b/lms/static/coffee/src/modules/sequence.coffee index 32a90f51a5..a4a80e3407 100644 --- a/lms/static/coffee/src/modules/sequence.coffee +++ b/lms/static/coffee/src/modules/sequence.coffee @@ -1,6 +1,6 @@ class @Sequence - constructor: (@id, @elements, @tag, position) -> - @element = $("#sequence_#{@id}") + constructor: (@id, @element_id, @elements, @tag, position) -> + @element = $("#sequence_#{@element_id}") @buildNavigation() @initProgress() @bind() diff --git a/lms/templates/seq_module.html b/lms/templates/seq_module.html index ab903457dc..00221a4951 100644 --- a/lms/templates/seq_module.html +++ b/lms/templates/seq_module.html @@ -1,4 +1,4 @@ -
                  +
                  " return html - def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): - XModule.__init__(self, system, xml, item_id, instance_state, shared_state) + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) self.attempts = 0 self.max_attempts = None - dom2 = etree.fromstring(xml) + dom2 = etree.fromstring(definition['data']) self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), default="closed") @@ -205,7 +192,7 @@ class Module(XModule): self.display_due_date = None grace_period_string = only_one(dom2.xpath('/problem/@graceperiod')) - if len(grace_period_string) >0 and self.display_due_date: + if len(grace_period_string) > 0 and self.display_due_date: self.grace_period = parse_timedelta(grace_period_string) self.close_date = self.display_due_date + self.grace_period #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date)) @@ -240,9 +227,9 @@ class Module(XModule): self.attempts = instance_state['attempts'] # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) - self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml" - self.name=only_one(dom2.xpath('/problem/@name')) - self.weight=only_one(dom2.xpath('/problem/@weight')) + self.filename = "problems/" + only_one(dom2.xpath('/problem/@filename')) + ".xml" + self.name = only_one(dom2.xpath('/problem/@name')) + self.weight = only_one(dom2.xpath('/problem/@weight')) if self.rerandomize == 'never': seed = 1 elif self.rerandomize == "per_student" and hasattr(system, 'id'): @@ -250,27 +237,27 @@ class Module(XModule): else: seed = None try: - fp = self.filestore.open(self.filename) - except Exception,err: - log.exception('[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename)) - if self.DEBUG: + fp = self.system.filestore.open(self.filename) + except Exception: + log.exception('cannot open file %s' % self.filename) + if self.system.DEBUG: # create a dummy problem instead of failing fp = StringIO.StringIO('Problem file %s is missing' % self.filename) fp.name = "StringIO" else: raise try: - self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system) - except Exception,err: - msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename) + self.lcp = LoncapaProblem(fp, self.id, instance_state, seed=seed, system=self.system) + except Exception: + msg = 'cannot create LoncapaProblem %s' % self.filename log.exception(msg) - if self.DEBUG: - msg = '

                  %s

                  ' % msg.replace('<','<') - msg += '

                  %s

                  ' % traceback.format_exc().replace('<','<') + if self.system.DEBUG: + msg = '

                  %s

                  ' % msg.replace('<', '<') + msg += '

                  %s

                  ' % traceback.format_exc().replace('<', '<') # create a dummy problem with error message instead of failing - fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename,msg)) + fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename, msg)) fp.name = "StringIO" - self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system) + self.lcp = LoncapaProblem(fp, self.id, instance_state, seed=seed, system=self.system) else: raise @@ -299,8 +286,8 @@ class Module(XModule): d = handlers[dispatch](get) after = self.get_progress() d.update({ - 'progress_changed' : after != before, - 'progress_status' : Progress.to_js_status_str(after), + 'progress_changed': after != before, + 'progress_status': Progress.to_js_status_str(after), }) return json.dumps(d, cls=ComplexEncoder) @@ -313,7 +300,6 @@ class Module(XModule): return False - def answer_available(self): ''' Is the user allowed to see an answer? ''' @@ -334,7 +320,8 @@ class Module(XModule): if self.show_answer == 'always': return True - raise self.system.exception404 #TODO: Not 404 + #TODO: Not 404 + raise self.system.exception404 def get_answer(self, get): ''' @@ -348,8 +335,7 @@ class Module(XModule): raise self.system.exception404 else: answers = self.lcp.get_question_answers() - return {'answers' : answers} - + return {'answers': answers} # Figure out if we should move these to capa_problem? def get_problem(self, get): @@ -358,8 +344,8 @@ class Module(XModule): Used if we want to reconfirm we have the right thing e.g. after several AJAX calls. - ''' - return {'html' : self.get_problem_html(encapsulate=False)} + ''' + return {'html': self.get_problem_html(encapsulate=False)} @staticmethod def make_dict_of_responses(get): @@ -409,18 +395,16 @@ class Module(XModule): correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: # TODO (vshnayder): why is this line here? - self.lcp = LoncapaProblem(self.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return {'success': inst.message} except: # TODO: why is this line here? - self.lcp = LoncapaProblem(self.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() - raise Exception,"error in capa_module" - # TODO: Dead code... is this a bug, or just old? - return {'success':'Unknown Error'} + raise Exception("error in capa_module") self.attempts = self.attempts + 1 self.lcp.done = True @@ -431,21 +415,18 @@ class Module(XModule): if not correct_map.is_correct(answer_id): success = 'incorrect' - event_info['correct_map'] = correct_map.get_dict() # log this in the tracker + # log this in the tracker + event_info['correct_map'] = correct_map.get_dict() event_info['success'] = success self.tracker('save_problem_check', event_info) - try: - html = self.get_problem_html(encapsulate=False) # render problem into HTML - except Exception,err: - log.error('failed to generate html') - raise + # render problem into HTML + html = self.get_problem_html(encapsulate=False) return {'success': success, 'contents': html, } - def save_problem(self, get): ''' Save the passed in answers. @@ -471,8 +452,8 @@ class Module(XModule): if self.lcp.done and self.rerandomize == "always": event_info['failure'] = 'done' self.tracker('save_problem_fail', event_info) - return {'success' : False, - 'error' : "Problem needs to be reset prior to save."} + return {'success': False, + 'error': "Problem needs to be reset prior to save."} self.lcp.student_answers = answers @@ -485,7 +466,7 @@ class Module(XModule): and causes problem to rerender itself. Returns problem html as { 'html' : html-string }. - ''' + ''' event_info = dict() event_info['old_state'] = self.lcp.get_state() event_info['filename'] = self.filename @@ -503,12 +484,21 @@ class Module(XModule): self.lcp.do_reset() if self.rerandomize == "always": # reset random number generator seed (note the self.lcp.get_state() in next line) - self.lcp.seed=None - - self.lcp = LoncapaProblem(self.filestore.open(self.filename), - self.item_id, self.lcp.get_state(), system=self.system) + self.lcp.seed = None + + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), + self.id, self.lcp.get_state(), system=self.system) event_info['new_state'] = self.lcp.get_state() self.tracker('reset_problem', event_info) - return {'html' : self.get_problem_html(encapsulate=False)} + return {'html': self.get_problem_html(encapsulate=False)} + + +class CapaDescriptor(RawDescriptor): + """ + Module implementing problems in the LON-CAPA format, + as implemented by capa.capa_problem + """ + + module_class = CapaModule diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 3e3e33805f..a9f4e1f4dc 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -19,9 +19,10 @@ setup( "section = xmodule.translation_module:SemanticSectionDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor", + "problem = xmodule.capa_module:CapaDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor", - "videosequence = xmodule.seq_module:SequenceDescriptor", "video = xmodule.video_module:VideoDescriptor", + "videosequence = xmodule.seq_module:SequenceDescriptor", ] } ) diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py index 6153aff324..6008eb4226 100644 --- a/common/lib/xmodule/vertical_module.py +++ b/common/lib/xmodule/vertical_module.py @@ -10,6 +10,9 @@ class_priority = ['video', 'problem'] class VerticalModule(XModule): ''' Layout module for laying out submodules vertically.''' def get_html(self): + if self.contents is None: + self.contents = [child.get_html() for child in self.get_display_items()] + return self.system.render_template('vert_module.html', { 'items': self.contents }) @@ -31,7 +34,7 @@ class VerticalModule(XModule): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - self.contents = [child.get_html() for child in self.get_display_items()] + self.contents = None class VerticalDescriptor(SequenceDescriptor): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d05bdcefab..d8ebb82adb 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -60,13 +60,7 @@ class I4xSystem(object): ''' self.ajax_url = ajax_url self.track_function = track_function - if not filestore: - self.filestore = OSFS(settings.DATA_DIR) - else: - self.filestore = filestore - if settings.DEBUG: - log.info("[courseware.module_render.I4xSystem] filestore path = %s", - filestore) + self.filestore = filestore self.get_module = get_module self.render_function = render_function self.render_template = render_template @@ -241,7 +235,7 @@ def get_module(user, request, location, student_module_cache, position=None): shared_state = shared_module.state if shared_module is not None else None # Setup system context for module instance - ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.type + '/' + descriptor.url + '/' + ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.url + '/' def _get_module(location): (module, _, _, _) = get_module(user, request, location, student_module_cache, position) @@ -330,94 +324,33 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No return context -def modx_dispatch(request, module=None, dispatch=None, id=None): +def modx_dispatch(request, dispatch=None, id=None): ''' Generic view for extensions. This is where AJAX calls go. Arguments: - request -- the django request. - - module -- the type of the module, as used in the course configuration xml. - e.g. 'problem', 'video', etc - dispatch -- the command string to pass through to the module's handle_ajax call (e.g. 'problem_reset'). If this string contains '?', only pass through the part before the first '?'. - - id -- the module id. Used to look up the student module. - e.g. filenamexformularesponse + - id -- the module id. Used to look up the XModule instance ''' # ''' (fix emacs broken parsing) - if not request.user.is_authenticated(): - return redirect('/') - - # python concats adjacent strings - error_msg = ("We're sorry, this module is temporarily unavailable. " - "Our staff is working to fix it as soon as possible") # If there are arguments, get rid of them dispatch, _, _ = dispatch.partition('?') - ajax_url = '{root}/modx/{module}/{id}'.format(root=settings.MITX_ROOT_URL, - module=module, id=id) - coursename = multicourse_settings.get_coursename_from_request(request) - if coursename and settings.ENABLE_MULTICOURSE: - xp = multicourse_settings.get_course_xmlpath(coursename) - data_root = settings.DATA_DIR + xp - else: - data_root = settings.DATA_DIR + student_module_cache = StudentModuleCache(request.user, keystore().get_item(id)) + instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) - # Grab the XML corresponding to the request from course.xml - try: - xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - except: - log.exception( - "Unable to load module during ajax call. module=%s, dispatch=%s, id=%s", - module, dispatch, id) - if accepts(request, 'text/html'): - return render_to_response("module-error.html", {}) - else: - response = HttpResponse(json.dumps({'success': error_msg})) - return response - - module_xml = etree.fromstring(xml) - student_module_cache = StudentModuleCache(request.user, module_xml) - (instance, instance_state, shared_state, module_type) = get_module( - request.user, request, module_xml, - student_module_cache, None) - - if instance_state is None: - log.debug("Couldn't find module '%s' for user '%s' and id '%s'", - module, request.user, id) + if instance_module is None: + log.debug("Couldn't find module '%s' for user '%s'", + id, request.user) raise Http404 - oldgrade = instance_state.grade - old_instance_state = instance_state.state - old_shared_state = shared_state.state if shared_state is not None else None - - module_from_xml = make_module_from_xml_fn( - request.user, request, student_module_cache, None) - - # Create the module - system = I4xSystem(track_function=make_track_function(request), - render_function=None, - module_from_xml=module_from_xml, - render_template=render_to_string, - ajax_url=ajax_url, - request=request, - filestore=OSFS(data_root), - ) - - try: - module_class = xmodule.get_module_class(module) - instance = module_class( - system, xml, id, - instance_state=old_instance_state, - shared_state=old_shared_state) - except: - log.exception("Unable to load module instance during ajax call") - if accepts(request, 'text/html'): - return render_to_response("module-error.html", {}) - else: - response = HttpResponse(json.dumps({'success': error_msg})) - return response + oldgrade = instance_module.grade + old_instance_state = instance_module.state + old_shared_state = shared_module.state if shared_module is not None else None # Let the module handle the AJAX try: @@ -427,16 +360,16 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): raise # Save the state back to the database - instance_state.state = instance.get_instance_state() + instance_module.state = instance.get_instance_state() if instance.get_score(): - instance_state.grade = instance.get_score()['score'] - if instance_state.grade != oldgrade or instance_state.state != old_instance_state: - instance_state.save() + instance_module.grade = instance.get_score()['score'] + if instance_module.grade != oldgrade or instance_module.state != old_instance_state: + instance_module.save() - if shared_state is not None: - shared_state.state = instance.get_shared_state() - if shared_state.state != old_shared_state: - shared_state.save() + if shared_module is not None: + shared_module.state = instance.get_shared_state() + if shared_module.state != old_shared_state: + shared_module.save() # Return whatever the module wanted to return to the client/caller return HttpResponse(ajax_return) diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py index a91314d228..ba8601cc20 100644 --- a/lms/lib/dogfood/views.py +++ b/lms/lib/dogfood/views.py @@ -174,7 +174,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): module = 'problem' xml = content_parser.module_xml(request.user, module, 'id', id, coursename) - ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/' + ajax_url = settings.MITX_ROOT_URL + '/modx/'+id+'/' # Create the module (instance of capa_module.Module) system = I4xSystem(track_function = make_track_function(request), diff --git a/lms/static/coffee/src/courseware.coffee b/lms/static/coffee/src/courseware.coffee index de232e05e4..4e57d13194 100644 --- a/lms/static/coffee/src/courseware.coffee +++ b/lms/static/coffee/src/courseware.coffee @@ -20,8 +20,8 @@ class @Courseware id = $(this).attr('id').replace(/video_/, '') new Video id, $(this).data('streams') $('.course-content .problems-wrapper').each -> - id = $(this).attr('id').replace(/problem_/, '') - new Problem id, $(this).data('url') + id = $(this).attr('problem-id') + new Problem id, $(this).attr('id'), $(this).data('url') $('.course-content .histogram').each -> id = $(this).attr('id').replace(/histogram_/, '') new Histogram id, $(this).data('histogram') diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index f29c9eb72b..eb2c057bef 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -1,6 +1,6 @@ class @Problem - constructor: (@id, url) -> - @element = $("#problem_#{id}") + constructor: (@id, @element_id, url) -> + @element = $("##{element_id}") @render() $: (selector) -> @@ -26,13 +26,13 @@ class @Problem @element.html(content) @bind() else - $.postWithPrefix "/modx/problem/#{@id}/problem_get", (response) => + $.postWithPrefix "/modx/#{@id}/problem_get", (response) => @element.html(response.html) @bind() check: => Logger.log 'problem_check', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) => + $.postWithPrefix "/modx/#{@id}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' @render(response.contents) @@ -42,14 +42,14 @@ class @Problem reset: => Logger.log 'problem_reset', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) => + $.postWithPrefix "/modx/#{@id}/problem_reset", id: @id, (response) => @render(response.html) @updateProgress response show: => if !@element.hasClass 'showed' Logger.log 'problem_show', problem: @id - $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => + $.postWithPrefix "/modx/#{@id}/problem_show", (response) => answers = response.answers $.each answers, (key, value) => if $.isArray(value) @@ -69,7 +69,7 @@ class @Problem save: => Logger.log 'problem_save', @answers - $.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => + $.postWithPrefix "/modx/#{@id}/problem_save", @answers, (response) => if response.success alert 'Saved' @updateProgress response @@ -94,4 +94,4 @@ class @Problem element.schematic.update_value() @$(".CodeMirror").each (index, element) -> element.CodeMirror.save() if element.CodeMirror.save - @answers = @$("[id^=input_#{@id}_]").serialize() + @answers = @$("[id^=input_#{@element_id}_]").serialize() diff --git a/lms/static/coffee/src/modules/sequence.coffee b/lms/static/coffee/src/modules/sequence.coffee index a4a80e3407..2c979f0853 100644 --- a/lms/static/coffee/src/modules/sequence.coffee +++ b/lms/static/coffee/src/modules/sequence.coffee @@ -88,7 +88,7 @@ class @Sequence if @position != new_position if @position != undefined @mark_visited @position - $.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position + $.postWithPrefix "/modx/#{@id}/goto_position", position: new_position @mark_active new_position @$('#seq_content').html @elements[new_position - 1].content diff --git a/lms/templates/problem_ajax.html b/lms/templates/problem_ajax.html index 78b85df3c1..6330edfac0 100644 --- a/lms/templates/problem_ajax.html +++ b/lms/templates/problem_ajax.html @@ -1 +1 @@ -
                  +
                  diff --git a/lms/urls.py b/lms/urls.py index 313be62c51..e43c949643 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -57,7 +57,7 @@ if settings.COURSEWARE_ENABLED: url(r'^courseware/(?P[^/]*)/$', 'courseware.views.index', name="courseware_course"), url(r'^jumpto/(?P[^/]+)/$', 'courseware.views.jump_to'), url(r'^section/(?P
                  [^/]*)/$', 'courseware.views.render_section'), - url(r'^modx/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), + url(r'^modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), url(r'^profile$', 'courseware.views.profile'), url(r'^profile/(?P[^/]*)/$', 'courseware.views.profile'), url(r'^change_setting$', 'student.views.change_setting'), From c140fe87662844ed6fa01f921d72afede53be4e8 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Wed, 27 Jun 2012 16:29:49 -0400 Subject: [PATCH 168/252] Get problem execution working with problems read from keystore --- common/lib/keystore/xml.py | 3 +++ common/lib/xmodule/capa_module.py | 26 ++++++++++---------- lms/static/coffee/src/modules/problem.coffee | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index d5baefd787..988916ed39 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -7,6 +7,9 @@ from xmodule.x_module import XModuleDescriptor, XMLParsingSystem from . import ModuleStore, Location from .exceptions import ItemNotFoundError +etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, + remove_comments=True)) + class XMLModuleStore(ModuleStore): """ diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 0563017ff2..4e40bffb48 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -247,7 +247,7 @@ class CapaModule(XModule): else: raise try: - self.lcp = LoncapaProblem(fp, self.id, instance_state, seed=seed, system=self.system) + self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) except Exception: msg = 'cannot create LoncapaProblem %s' % self.filename log.exception(msg) @@ -257,7 +257,7 @@ class CapaModule(XModule): # create a dummy problem with error message instead of failing fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename, msg)) fp.name = "StringIO" - self.lcp = LoncapaProblem(fp, self.id, instance_state, seed=seed, system=self.system) + self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) else: raise @@ -378,7 +378,7 @@ class CapaModule(XModule): # Too late. Cannot submit if self.closed(): event_info['failure'] = 'closed' - self.tracker('save_problem_check_fail', event_info) + self.system.track_function('save_problem_check_fail', event_info) # TODO (vshnayder): probably not 404? raise self.system.exception404 @@ -386,7 +386,7 @@ class CapaModule(XModule): # again. if self.lcp.done and self.rerandomize == "always": event_info['failure'] = 'unreset' - self.tracker('save_problem_check_fail', event_info) + self.system.track_function('save_problem_check_fail', event_info) raise self.system.exception404 try: @@ -415,10 +415,10 @@ class CapaModule(XModule): if not correct_map.is_correct(answer_id): success = 'incorrect' - # log this in the tracker + # log this in the track_function event_info['correct_map'] = correct_map.get_dict() event_info['success'] = success - self.tracker('save_problem_check', event_info) + self.system.track_function('save_problem_check', event_info) # render problem into HTML html = self.get_problem_html(encapsulate=False) @@ -443,7 +443,7 @@ class CapaModule(XModule): # Too late. Cannot submit if self.closed(): event_info['failure'] = 'closed' - self.tracker('save_problem_fail', event_info) + self.system.track_function('save_problem_fail', event_info) return {'success': False, 'error': "Problem is closed"} @@ -451,14 +451,14 @@ class CapaModule(XModule): # again. if self.lcp.done and self.rerandomize == "always": event_info['failure'] = 'done' - self.tracker('save_problem_fail', event_info) + self.system.track_function('save_problem_fail', event_info) return {'success': False, 'error': "Problem needs to be reset prior to save."} self.lcp.student_answers = answers # TODO: should this be save_problem_fail? Looks like success to me... - self.tracker('save_problem_fail', event_info) + self.system.track_function('save_problem_fail', event_info) return {'success': True} def reset_problem(self, get): @@ -473,12 +473,12 @@ class CapaModule(XModule): if self.closed(): event_info['failure'] = 'closed' - self.tracker('reset_problem_fail', event_info) + self.system.track_function('reset_problem_fail', event_info) return "Problem is closed" if not self.lcp.done: event_info['failure'] = 'not_done' - self.tracker('reset_problem_fail', event_info) + self.system.track_function('reset_problem_fail', event_info) return "Refresh the page and make an attempt before resetting." self.lcp.do_reset() @@ -487,10 +487,10 @@ class CapaModule(XModule): self.lcp.seed = None self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), - self.id, self.lcp.get_state(), system=self.system) + self.location.html_id(), self.lcp.get_state(), system=self.system) event_info['new_state'] = self.lcp.get_state() - self.tracker('reset_problem', event_info) + self.system.track_function('reset_problem', event_info) return {'html': self.get_problem_html(encapsulate=False)} diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee index eb2c057bef..85186a2903 100644 --- a/lms/static/coffee/src/modules/problem.coffee +++ b/lms/static/coffee/src/modules/problem.coffee @@ -94,4 +94,4 @@ class @Problem element.schematic.update_value() @$(".CodeMirror").each (index, element) -> element.CodeMirror.save() if element.CodeMirror.save - @answers = @$("[id^=input_#{@element_id}_]").serialize() + @answers = @$("[id^=input_#{@element_id.replace(/problem_/, '')}_]").serialize() From bae90ab16d86f89579b3d306beb9cb092a1dc2ab Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 08:56:18 -0400 Subject: [PATCH 169/252] Make custom tags work in the LMS --- common/lib/xmodule/setup.py | 12 +++++++++--- common/lib/xmodule/template_module.py | 21 ++++++++++----------- common/lib/xmodule/translation_module.py | 17 +++++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index a9f4e1f4dc..38509f182a 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,15 +13,21 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ + "book = xmodule.translation_module:TranslateCustomTagDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor", "course = xmodule.seq_module:SequenceDescriptor", + "customtag = xmodule.template_module:CustomTagDescriptor", + "discuss = xmodule.translation_module:TranslateCustomTagDescriptor", "html = xmodule.html_module:HtmlDescriptor", - "section = xmodule.translation_module:SemanticSectionDescriptor", - "sequential = xmodule.seq_module:SequenceDescriptor", - "vertical = xmodule.vertical_module:VerticalDescriptor", + "image = xmodule.translation_module:TranslateCustomTagDescriptor", "problem = xmodule.capa_module:CapaDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor", + "section = xmodule.translation_module:SemanticSectionDescriptor", + "sequential = xmodule.seq_module:SequenceDescriptor", + "slides = xmodule.translation_module:TranslateCustomTagDescriptor", + "vertical = xmodule.vertical_module:VerticalDescriptor", "video = xmodule.video_module:VideoDescriptor", + "videodev = xmodule.translation_module:TranslateCustomTagDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor", ] } diff --git a/common/lib/xmodule/template_module.py b/common/lib/xmodule/template_module.py index ae276737e6..52c05616cf 100644 --- a/common/lib/xmodule/template_module.py +++ b/common/lib/xmodule/template_module.py @@ -1,14 +1,9 @@ -import json - -from x_module import XModule, XModuleDescriptor +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor from lxml import etree -class ModuleDescriptor(XModuleDescriptor): - pass - - -class Module(XModule): +class CustomTagModule(XModule): """ This module supports tags of the form @@ -34,9 +29,13 @@ class Module(XModule): def get_html(self): return self.html - def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): - XModule.__init__(self, system, xml, item_id, instance_state, shared_state) - xmltree = etree.fromstring(xml) + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + xmltree = etree.fromstring(self.definition['data']) filename = xmltree.find('impl').text params = dict(xmltree.items()) self.html = self.system.render_template(filename, params, namespace='custom_tags') + + +class CustomTagDescriptor(RawDescriptor): + module_class = CustomTagModule diff --git a/common/lib/xmodule/translation_module.py b/common/lib/xmodule/translation_module.py index f5c8bc2fbc..6c358d4eaa 100644 --- a/common/lib/xmodule/translation_module.py +++ b/common/lib/xmodule/translation_module.py @@ -65,3 +65,20 @@ class SemanticSectionDescriptor(XModuleDescriptor): else: xml_object.tag = 'sequence' return system.process_xml(etree.tostring(xml_object)) + + +class TranslateCustomTagDescriptor(XModuleDescriptor): + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + """ + Transforms the xml_data from <$custom_tag attr="" attr=""/> to + $custom_tag + """ + + xml_object = etree.fromstring(xml_data) + tag = xml_object.tag + xml_object.tag = 'customtag' + impl = etree.SubElement(xml_object, 'impl') + impl.text = tag + + return system.process_xml(etree.tostring(xml_object)) From 7d16dbbcb468f7eeb634977841b476f33cb881ce Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 08:58:18 -0400 Subject: [PATCH 170/252] Clean up module_render.py --- lms/djangoapps/courseware/module_render.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index d8ebb82adb..de51764304 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -5,19 +5,12 @@ from lxml import etree from django.http import Http404 from django.http import HttpResponse -from django.shortcuts import redirect - -from fs.osfs import OSFS from django.conf import settings -from mitxmako.shortcuts import render_to_string, render_to_response +from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache -from multicourse import multicourse_settings -from util.views import accepts -import courseware.content_parser as content_parser -import xmodule from keystore.django import keystore log = logging.getLogger("mitx.courseware") @@ -83,8 +76,6 @@ class I4xSystem(object): return str(self.__dict__) - - def make_track_function(request): ''' Make a tracking function that logs what happened. From 5cd388d638fab17cbb37bb2d768002d9b4726299 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 10:35:39 -0400 Subject: [PATCH 171/252] Have the CMS use the same XMLModuleStore for import that the LMS uses for reading content --- .../management/commands/import.py | 39 ++++--------------- cms/djangoapps/contentstore/views.py | 2 +- common/lib/keystore/__init__.py | 4 +- common/lib/keystore/xml.py | 6 ++- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 1cfdf24e2d..a7f95ea3c0 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -4,12 +4,8 @@ from django.core.management.base import BaseCommand, CommandError from keystore.django import keystore -from raw_module import RawDescriptor from lxml import etree -from fs.osfs import OSFS - -from path import path -from x_module import XModuleDescriptor, XMLParsingSystem +from keystore.xml import XMLModuleStore unnamed_modules = 0 @@ -26,30 +22,11 @@ class Command(BaseCommand): raise CommandError("import requires 3 arguments: ") org, course, data_dir = args - data_dir = path(data_dir) - with open(data_dir / "course.xml") as course_file: - class ImportSystem(XMLParsingSystem): - def __init__(self): - def process_xml(xml): - try: - xml_data = etree.fromstring(xml) - except: - raise CommandError("Unable to parse xml: " + xml) - - if not xml_data.get('name'): - global unnamed_modules - unnamed_modules += 1 - xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules)) - - module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor) - keystore().create_item(module.url) - if 'data' in module.definition: - keystore().update_item(module.url, module.definition['data']) - if 'children' in module.definition: - keystore().update_children(module.url, module.definition['children']) - return module - - XMLParsingSystem.__init__(self, keystore().get_item, OSFS(data_dir), process_xml) - - ImportSystem().process_xml(course_file.read()) + module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor') + for module in module_store.modules.itervalues(): + keystore().create_item(module.url) + if 'data' in module.definition: + keystore().update_item(module.url, module.definition['data']) + if 'children' in module.definition: + keystore().update_children(module.url, module.definition['children']) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 9cc7eec9b2..b85e9c05bf 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -10,7 +10,7 @@ def index(request): # TODO (cpennington): These need to be read in from the active user org = 'mit.edu' course = '6002xs12' - name = '6.002 Spring 2012' + name = '6.002_Spring_2012' course = keystore().get_item(['i4x', org, course, 'course', name]) weeks = course.get_children() return render_to_response('index.html', {'weeks': weeks}) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index df9b72839e..14716fbc2d 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -15,7 +15,7 @@ URL_RE = re.compile(""" (/(?P[^/]+))? """, re.VERBOSE) -INVALID_CHARS = re.compile(r"[^\w-]") +INVALID_CHARS = re.compile(r"[^\w.-]") class Location(object): @@ -55,7 +55,7 @@ class Location(object): In both the dict and list forms, the revision is optional, and can be ommitted. - Components must be composed of alphanumeric characters, or the characters _, and - + Components must be composed of alphanumeric characters, or the characters '_', '-', and '.' Components may be set to None, which may be interpreted by some contexts to mean wildcard selection diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index 988916ed39..dcddb2718e 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -10,6 +10,8 @@ from .exceptions import ItemNotFoundError etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, remove_comments=True)) +log = logging.getLogger(__name__) + class XMLModuleStore(ModuleStore): """ @@ -23,7 +25,7 @@ class XMLModuleStore(ModuleStore): class_ = getattr(import_module(module_path), class_name) self.default_class = class_ - with open(data_dir / "course.xml") as course_file: + with open(self.data_dir / "course.xml") as course_file: class ImportSystem(XMLParsingSystem): def __init__(self, keystore): self.unnamed_modules = 0 @@ -32,7 +34,7 @@ class XMLModuleStore(ModuleStore): try: xml_data = etree.fromstring(xml) except: - print xml + log.exception("Unable to parse xml:" + xml) raise if xml_data.get('name'): xml_data.set('slug', Location.clean(xml_data.get('name'))) From 23195e9d76311bea140e8f5e77f5ec2037b1fb62 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 13:08:09 -0400 Subject: [PATCH 172/252] Make user profiles work again after the switch to an XMLModuleStore. Staff user histograms are still broken --- common/lib/xmodule/abtest_module.py | 3 - common/lib/xmodule/capa_module.py | 8 +- common/lib/xmodule/video_module.py | 4 - common/lib/xmodule/x_module.py | 1 + lms/djangoapps/courseware/grades.py | 215 +++++++++------------ lms/djangoapps/courseware/models.py | 12 +- lms/djangoapps/courseware/module_render.py | 21 +- lms/djangoapps/courseware/views.py | 21 +- lms/envs/common.py | 2 +- lms/templates/video.html | 8 - 10 files changed, 133 insertions(+), 162 deletions(-) diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index dda6a58c99..e14117eb08 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -88,8 +88,5 @@ class Module(XModule): def get_children(self): return [self.module_from_xml(child) for child in self._xml_children()] - def rendered_children(self): - return [self.render_function(child) for child in self._xml_children()] - def get_html(self): return '\n'.join(child.get_html() for child in self.get_children()) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 4e40bffb48..b6bfc91e80 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -229,7 +229,13 @@ class CapaModule(XModule): # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) self.filename = "problems/" + only_one(dom2.xpath('/problem/@filename')) + ".xml" self.name = only_one(dom2.xpath('/problem/@name')) - self.weight = only_one(dom2.xpath('/problem/@weight')) + + weight_string = only_one(dom2.xpath('/problem/@weight')) + if weight_string: + self.weight = float(weight_string) + else: + self.weight = 1 + if self.rerandomize == 'never': seed = 1 elif self.rerandomize == "per_student" and hasattr(system, 'id'): diff --git a/common/lib/xmodule/video_module.py b/common/lib/xmodule/video_module.py index 86f7c0c64d..4aa469db7f 100644 --- a/common/lib/xmodule/video_module.py +++ b/common/lib/xmodule/video_module.py @@ -50,7 +50,6 @@ class VideoModule(XModule): 'id': self.location.html_id(), 'position': self.position, 'name': self.name, - 'annotations': self.annotations, }) def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): @@ -65,9 +64,6 @@ class VideoModule(XModule): if 'position' in state: self.position = int(float(state['position'])) - self.annotations = [(e.get("name"), self.render_function(e)) \ - for e in xmltree] - class VideoDescriptor(RawDescriptor): module_class = VideoModule diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 838611f81f..71d069fdbe 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -83,6 +83,7 @@ class XModule(object): self.id = self.location.url() self.name = self.location.name self.display_name = kwargs.get('display_name', '') + self.type = self.location.category self._loaded_children = None def get_name(self): diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 3c2b654682..6f6ff71d35 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -3,23 +3,21 @@ Course settings module. The settings are based of django.conf. All settings in courseware.global_course_settings are first applied, and then any settings in the settings.DATA_DIR/course_settings.py are applied. A setting must be in ALL_CAPS. - + Settings are used by calling - + from courseware import course_settings -Note that courseware.course_settings is not a module -- it's an object. So +Note that courseware.course_settings is not a module -- it's an object. So importing individual settings is not possible: from courseware.course_settings import GRADER # This won't work. """ -from lxml import etree import random import imp import logging -import sys import types from django.conf import settings @@ -28,21 +26,19 @@ from courseware import global_course_settings from xmodule import graders from xmodule.graders import Score from models import StudentModule -import courseware.content_parser as content_parser -import xmodule _log = logging.getLogger("mitx.courseware") + class Settings(object): def __init__(self): # update this dict from global settings (but only for ALL_CAPS settings) for setting in dir(global_course_settings): if setting == setting.upper(): setattr(self, setting, getattr(global_course_settings, setting)) - - + data_dir = settings.DATA_DIR - + fp = None try: fp, pathname, description = imp.find_module("course_settings", [data_dir]) @@ -53,154 +49,127 @@ class Settings(object): finally: if fp: fp.close() - + for setting in dir(mod): if setting == setting.upper(): setting_value = getattr(mod, setting) setattr(self, setting, setting_value) - + # Here is where we should parse any configurations, so that we can fail early self.GRADER = graders.grader_from_conf(self.GRADER) course_settings = Settings() - - -def grade_sheet(student,coursename=None): +def grade_sheet(student, course, student_module_cache): """ This pulls a summary of all problems in the course. It returns a dictionary with two datastructures: - + - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters, each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded problems, and is good for displaying a course summary with due dates, etc. - + - grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader. + + Arguments: + student: A User object for the student to grade + course: An XModule containing the course to grade + student_module_cache: A StudentModuleCache initialized with all instance_modules for the student """ - dom=content_parser.course_file(student,coursename) - course = dom.xpath('//course/@name')[0] - xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course) - - responses = StudentModule.objects.filter(student=student) - response_by_id = {} - for response in responses: - response_by_id[response.module_state_key] = response - - totaled_scores = {} - chapters=[] - for c in xmlChapters: + chapters = [] + for c in course.get_children(): sections = [] - chname=c.get('name') - - - for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section', - course=course, chname=chname): - problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem', - course=course, chname=chname, section=s.get('name')) + for s in c.get_children(): + def yield_descendents(module): + yield module + for child in module.get_display_items(): + for module in yield_descendents(child): + yield module - graded = True if s.get('graded') == "true" else False - scores=[] - if len(problems)>0: - for p in problems: - (correct,total) = get_score(student, p, response_by_id, coursename=coursename) - - if settings.GENERATE_PROFILE_SCORES: - if total > 1: - correct = random.randrange( max(total-2, 1) , total + 1 ) - else: - correct = total - - if not total > 0: - #We simply cannot grade a problem that is 12/0, because we might need it as a percentage - graded = False - scores.append( Score(correct,total, graded, p.get("name")) ) + graded = getattr(s, 'graded', False) + scores = [] + for module in yield_descendents(s): + (correct, total) = get_score(student, module, student_module_cache) - section_total, graded_total = graders.aggregate_scores(scores, s.get("name")) - #Add the graded total to totaled_scores - format = s.get('format', "") - subtitle = s.get('subtitle', format) - if format and graded_total[1] > 0: - format_scores = totaled_scores.get(format, []) - format_scores.append( graded_total ) - totaled_scores[ format ] = format_scores + if settings.GENERATE_PROFILE_SCORES: + if total > 1: + correct = random.randrange(max(total - 2, 1), total + 1) + else: + correct = total - section_score={'section':s.get("name"), - 'scores':scores, - 'section_total' : section_total, - 'format' : format, - 'subtitle' : subtitle, - 'due' : s.get("due") or "", - 'graded' : graded, - } - sections.append(section_score) + if not total > 0: + #We simply cannot grade a problem that is 12/0, because we might need it as a percentage + graded = False + + if correct is not None and total is not None: + scores.append(Score(correct, total, graded, module.display_name)) + + section_total, graded_total = graders.aggregate_scores(scores, s.display_name) + #Add the graded total to totaled_scores + format = getattr(s, 'format', "") + subtitle = getattr(s, 'subtitle', format) + if format and graded_total[1] > 0: + format_scores = totaled_scores.get(format, []) + format_scores.append(graded_total) + totaled_scores[format] = format_scores + + sections.append({ + 'section': s.display_name, + 'scores': scores, + 'section_total': section_total, + 'format': format, + 'subtitle': subtitle, + 'due': getattr(s, "due", ""), + 'graded': graded, + }) + + chapters.append({'course': course.display_name, + 'chapter': c.display_name, + 'sections': sections}) - chapters.append({'course':course, - 'chapter' : c.get("name"), - 'sections' : sections,}) - - grader = course_settings.GRADER grade_summary = grader.grade(totaled_scores) - - return {'courseware_summary' : chapters, - 'grade_summary' : grade_summary} -def get_score(user, problem, cache, coursename=None): + return {'courseware_summary': chapters, + 'grade_summary': grade_summary} + + +def get_score(user, problem, cache): """ Return the score for a user on a problem user: a Student object - problem: the xml for the problem - cache: a dictionary mapping module_state_key tuples to instantiated StudentModules - module_state_key is either the problem_id, or a key used by the problem - to share state across instances + problem: an XModule + cache: A StudentModuleCache """ - ## HACK: assumes max score is fixed per problem - module_type = problem.tag - module_class = xmodule.get_module_class(module_type) - module_id = problem.get('id') - module_state_key = problem.get(module_class.state_key, module_id) correct = 0.0 # If the ID is not in the cache, add the item - if module_state_key not in cache: - module = StudentModule(module_type='problem', # TODO: Move into StudentModule.__init__? - module_state_key=id, - student=user, - state=None, - grade=0, - max_grade=None, - done='i') - cache[module_id] = module + instance_module = cache.lookup(problem.type, problem.id) + if instance_module is None: + instance_module = StudentModule(module_type=problem.type, + module_state_key=problem.id, + student=user, + state=None, + grade=0, + max_grade=problem.max_score(), + done='i') + cache.append(instance_module) + instance_module.save() - # Grab the # correct from cache - if id in cache: - response = cache[id] - if response.grade != None: - correct = float(response.grade) + # If this problem is ungraded/ungradable, bail + if instance_module.max_grade is None: + return (None, None) - # Grab max grade from cache, or if it doesn't exist, compute and save to DB - if id in cache and response.max_grade is not None: - total = response.max_grade - else: - ## HACK 1: We shouldn't specifically reference capa_module - ## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system - # TODO: These are no longer correct params for I4xSystem -- figure out what this code - # does, clean it up. - # from module_render import I4xSystem - # system = I4xSystem(None, None, None, coursename=coursename) - # total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score()) - # response.max_grade = total - # response.save() - total = 1 - # For a temporary fix, we just assume a problem is worth 1 point if we haven't seen it before. This is totally incorrect - - #Now we re-weight the problem, if specified - weight = problem.get("weight", None) - if weight: - weight = float(weight) - correct = correct * weight / total - total = weight + correct = instance_module.grade if instance_module.grade is not None else 0 + total = instance_module.max_grade + + if correct is not None and total is not None: + #Now we re-weight the problem, if specified + weight = getattr(problem, 'weight', 1) + if weight != 1: + correct = correct * weight / total + total = weight return (correct, total) diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 6ca67a84e7..262d177248 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -75,8 +75,16 @@ class StudentModuleCache(object): ''' if user.is_authenticated(): module_ids = self._get_module_state_keys(descriptor, depth) - self.cache = list(StudentModule.objects.filter(student=user, - module_state_key__in=module_ids)) + + # This works around a limitation in sqlite3 on the number of parameters + # that can be put into a single query + self.cache = [] + chunk_size = 500 + for id_chunk in [module_ids[i:i+chunk_size] for i in xrange(0, len(module_ids), chunk_size)]: + self.cache.extend(StudentModule.objects.filter( + student=user, + module_state_key__in=id_chunk) + ) else: self.cache = [] diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index de51764304..4c82eba974 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -29,7 +29,7 @@ class I4xSystem(object): and user, or other environment-specific info. ''' def __init__(self, ajax_url, track_function, render_function, - get_module, render_template, request=None, + get_module, render_template, user=None, filestore=None): ''' Create a closure around the system environment. @@ -47,7 +47,7 @@ class I4xSystem(object): and 'type'. render_template - a function that takes (template_file, context), and returns rendered html. - request - the request in progress + user - The user to base the seed off of for this request filestore - A filestore ojbect. Defaults to an instance of OSFS based at settings.DATA_DIR. ''' @@ -59,7 +59,7 @@ class I4xSystem(object): self.render_template = render_template self.exception404 = Http404 self.DEBUG = settings.DEBUG - self.id = request.user.id if request is not None else 0 + self.seed = user.id if user is not None else 0 def get(self, attr): ''' provide uniform access to attributes (like etree).''' @@ -234,13 +234,13 @@ def get_module(user, request, location, student_module_cache, position=None): system = I4xSystem(track_function=make_track_function(request), render_function=lambda xml: render_x_module( - user, request, xml, student_module_cache, position), + user, xml, student_module_cache, position), render_template=render_to_string, ajax_url=ajax_url, - request=request, # TODO (cpennington): Figure out how to share info between systems filestore=descriptor.system.resources_fs, get_module=_get_module, + user=user, ) # pass position specified in URL to module through I4xSystem system.set('position', position) @@ -272,7 +272,7 @@ def get_module(user, request, location, student_module_cache, position=None): return (module, instance_module, shared_module, descriptor.type) -def render_x_module(user, request, module_xml, student_module_cache, position=None): +def render_x_module(user, module, student_module_cache, position=None): ''' Generic module for extensions. This renders to HTML. modules include sequential, vertical, problem, video, html @@ -282,10 +282,9 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No Arguments: - user : current django User - - request : current django HTTPrequest - - module_xml : lxml etree of xml subtree for the current module - - student_module_cache : list of StudentModule objects, one of which may match this module type and id - - position : extra information from URL for user-specified position within module + - module : lxml etree of xml subtree for the current module + - student_module_cache : list of StudentModule objects, one of which may match this module type and id + - position : extra information from URL for user-specified position within module Returns: @@ -296,7 +295,7 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No return {"content": ""} (instance, _, _, module_type) = get_module( - user, request, module_xml, student_module_cache, position) + user, module_xml, student_module_cache, position) content = instance.get_html() diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 6e8eb1ab9e..56237f605a 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -20,17 +20,16 @@ from module_render import render_x_module, toc_for_course, get_module, get_secti from models import StudentModuleCache from student.models import UserProfile from multicourse import multicourse_settings +from keystore.django import keystore -import courseware.content_parser as content_parser - -import courseware.grades as grades +from courseware import grades, content_parser log = logging.getLogger("mitx.courseware") etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments = True)) + remove_comments=True)) -template_imports={'urllib':urllib} +template_imports = {'urllib': urllib} @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request): @@ -49,6 +48,7 @@ def gradebook(request): return render_to_response('gradebook.html', {'students': student_info}) + @login_required @cache_control(no_cache=True, no_store=True, must_revalidate=True) def profile(request, student_id=None): @@ -60,11 +60,14 @@ def profile(request, student_id=None): else: if 'course_admin' not in content_parser.user_groups(request.user): raise Http404 - student = User.objects.get( id = int(student_id)) + student = User.objects.get(id=int(student_id)) - user_info = UserProfile.objects.get(user=student) # request.user.profile_cache # + user_info = UserProfile.objects.get(user=student) coursename = multicourse_settings.get_coursename_from_request(request) + course_location = multicourse_settings.get_course_location(coursename) + student_module_cache = StudentModuleCache(request.user, keystore().get_item(course_location)) + course, _, _, _ = get_module(request.user, request, course_location, student_module_cache) context = {'name': user_info.name, 'username': student.username, @@ -74,7 +77,7 @@ def profile(request, student_id=None): 'format_url_params': content_parser.format_url_params, 'csrf': csrf(request)['csrf_token'] } - context.update(grades.grade_sheet(student, coursename)) + context.update(grades.grade_sheet(student, course, student_module_cache)) return render_to_response('profile.html', context) @@ -127,7 +130,7 @@ def render_section(request, section): student_module_cache = StudentModuleCache(request.user, dom) try: - module = render_x_module(user, request, dom, student_module_cache) + module = render_x_module(user, dom, student_module_cache) except: log.exception("Unable to load module") context.update({ diff --git a/lms/envs/common.py b/lms/envs/common.py index 8c82b2e8c1..d1faf00f62 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -132,7 +132,7 @@ COURSE_DEFAULT = '6.002_Spring_2012' COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x', 'title' : 'Circuits and Electronics', 'xmlpath': '6002x/', - 'location': 'i4x://edx/6002xs12/course/6_002_Spring_2012', + 'location': 'i4x://edx/6002xs12/course/6.002_Spring_2012', } } diff --git a/lms/templates/video.html b/lms/templates/video.html index f49b5b56c2..9f38d386a4 100644 --- a/lms/templates/video.html +++ b/lms/templates/video.html @@ -13,11 +13,3 @@
                  - -
                    -% for t in annotations: -
                  1. - ${t[1]['content']} -
                  2. -% endfor -
                  From 35af8101d763952eeb3983883609e27e78db4638 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 13:40:37 -0400 Subject: [PATCH 173/252] Make grade graph on profile work correctly --- common/lib/xmodule/translation_module.py | 2 -- common/lib/xmodule/x_module.py | 11 ++++++++++- common/lib/xmodule/xml_module.py | 4 +++- lms/djangoapps/courseware/grades.py | 8 +++++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/translation_module.py b/common/lib/xmodule/translation_module.py index 6c358d4eaa..d379ced507 100644 --- a/common/lib/xmodule/translation_module.py +++ b/common/lib/xmodule/translation_module.py @@ -57,8 +57,6 @@ class SemanticSectionDescriptor(XModuleDescriptor): if len(xml_object) == 1: for (key, val) in xml_object.items(): - if key == 'format': - continue xml_object[0].set(key, val) return system.process_xml(etree.tostring(xml_object[0])) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 71d069fdbe..8ee3df38ff 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -85,6 +85,8 @@ class XModule(object): self.display_name = kwargs.get('display_name', '') self.type = self.location.category self._loaded_children = None + self.graded = kwargs.get('graded', False) + self.format = kwargs.get('format') def get_name(self): name = self.__xmltree.get('name') @@ -281,6 +283,9 @@ class XModuleDescriptor(Plugin): Current arguments passed in kwargs: location: A keystore.Location object indicating the name and ownership of this problem goals: A list of strings of learning goals associated with this module + display_name: The name to use for displaying this module to the user + format: The format of this module ('Homework', 'Lab', etc) + graded (bool): Whether this module is should be graded or not """ self.system = system self.definition = definition if definition is not None else {} @@ -288,6 +293,8 @@ class XModuleDescriptor(Plugin): self.type = Location(kwargs.get('location')).category self.url = Location(kwargs.get('location')).url() self.display_name = kwargs.get('display_name') + self.format = kwargs.get('format') + self.graded = kwargs.get('graded', False) # For now, we represent goals as a list of strings, but this # is one of the things that we are going to be iterating on heavily @@ -315,7 +322,9 @@ class XModuleDescriptor(Plugin): instance_state and shared_state, and returns a fully nstantiated XModule """ return partial(self.module_class, system, self.url, self.definition, - display_name=self.display_name) + display_name=self.display_name, + format=self.format, + graded=self.graded) class DescriptorSystem(object): def __init__(self, load_item, resources_fs): diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index 34881a4d61..d62957c3d3 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -37,5 +37,7 @@ class XmlDescriptor(XModuleDescriptor): course, xml_object.tag, xml_object.get('slug')], - display_name=xml_object.get('name') + display_name=xml_object.get('name'), + format=xml_object.get('format'), + graded=xml_object.get('graded') == 'true', ) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index 6f6ff71d35..b5fcae86e5 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -92,6 +92,9 @@ def grade_sheet(student, course, student_module_cache): for module in yield_descendents(s): (correct, total) = get_score(student, module, student_module_cache) + if correct is None and total is None: + continue + if settings.GENERATE_PROFILE_SCORES: if total > 1: correct = random.randrange(max(total - 2, 1), total + 1) @@ -102,14 +105,13 @@ def grade_sheet(student, course, student_module_cache): #We simply cannot grade a problem that is 12/0, because we might need it as a percentage graded = False - if correct is not None and total is not None: - scores.append(Score(correct, total, graded, module.display_name)) + scores.append(Score(correct, total, graded, module.display_name)) section_total, graded_total = graders.aggregate_scores(scores, s.display_name) #Add the graded total to totaled_scores format = getattr(s, 'format', "") subtitle = getattr(s, 'subtitle', format) - if format and graded_total[1] > 0: + if format and graded_total.possible > 0: format_scores = totaled_scores.get(format, []) format_scores.append(graded_total) totaled_scores[format] = format_scores From 506c281bccb5db15e607d2f85ca06b1a58dfa9c5 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 13:55:50 -0400 Subject: [PATCH 174/252] Make gradesheet work again --- lms/djangoapps/courseware/views.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 56237f605a..e1b77c8fbe 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -39,12 +39,21 @@ def gradebook(request): coursename = multicourse_settings.get_coursename_from_request(request) student_objects = User.objects.all()[:100] - student_info = [{'username': s.username, - 'id': s.id, - 'email': s.email, - 'grade_info': grades.grade_sheet(s, coursename), - 'realname': UserProfile.objects.get(user = s).name - } for s in student_objects] + student_info = [] + + coursename = multicourse_settings.get_coursename_from_request(request) + course_location = multicourse_settings.get_course_location(coursename) + + for student in student_objects: + student_module_cache = StudentModuleCache(student, keystore().get_item(course_location)) + course, _, _, _ = get_module(request.user, request, course_location, student_module_cache) + student_info.append({ + 'username': student.username, + 'id': student.id, + 'email': student.email, + 'grade_info': grades.grade_sheet(student, course, student_module_cache), + 'realname': UserProfile.objects.get(user = student).name + }) return render_to_response('gradebook.html', {'students': student_info}) From 27b75ca76ec1c22966563118249a0be9f389e5a7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 13:58:50 -0400 Subject: [PATCH 175/252] Use display_name in sequence title bar --- common/lib/xmodule/seq_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index dfa46dd72b..7ef497b837 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -52,9 +52,9 @@ class SequenceModule(XModule): contents.append({ 'content': child.get_html(), 'title': "\n".join( - grand_child.name.strip() + grand_child.display_name.strip() for grand_child in child.get_children() - if grand_child.name is not None + if grand_child.display_name is not None ), 'progress_status': Progress.to_js_status_str(progress), 'progress_detail': Progress.to_js_detail_str(progress), From c3a432f217917de0261d690c289a4d578a292fe3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 14:01:23 -0400 Subject: [PATCH 176/252] Make problemsets display as verticals rather than sequences --- common/lib/xmodule/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 38509f182a..93eddc5c7c 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -21,7 +21,7 @@ setup( "html = xmodule.html_module:HtmlDescriptor", "image = xmodule.translation_module:TranslateCustomTagDescriptor", "problem = xmodule.capa_module:CapaDescriptor", - "problemset = xmodule.seq_module:SequenceDescriptor", + "problemset = xmodule.vertical_module:VerticalDescriptor", "section = xmodule.translation_module:SemanticSectionDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor", "slides = xmodule.translation_module:TranslateCustomTagDescriptor", From dcd74e6dd072009c8289fe2a7ac9152de6f6def3 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 28 Jun 2012 16:27:46 -0400 Subject: [PATCH 177/252] Make abtests work, using the new abtest xml format --- common/lib/xmodule/abtest_module.py | 90 +++++++++++++++++++---------- common/lib/xmodule/exceptions.py | 2 + common/lib/xmodule/setup.py | 1 + common/lib/xmodule/x_module.py | 1 + 4 files changed, 62 insertions(+), 32 deletions(-) create mode 100644 common/lib/xmodule/exceptions.py diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index e14117eb08..3bd268184a 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -2,11 +2,10 @@ import json import random from lxml import etree -from x_module import XModule, XModuleDescriptor - - -class ModuleDescriptor(XModuleDescriptor): - pass +from xmodule.x_module import XModule +from xmodule.raw_module import RawDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.exceptions import InvalidDefinitionError def group_from_value(groups, v): @@ -25,7 +24,7 @@ def group_from_value(groups, v): return g -class Module(XModule): +class ABTestModule(XModule): """ Implements an A/B test with an aribtrary number of competing groups @@ -37,20 +36,14 @@ class Module(XModule): """ - def __init__(self, system, xml, item_id, instance_state=None, shared_state=None): - XModule.__init__(self, system, xml, item_id, instance_state, shared_state) - self.xmltree = etree.fromstring(xml) + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - target_groups = self.xmltree.findall('group') + target_groups = self.definition['data'].keys() if shared_state is None: - target_values = [ - (elem.get('name'), float(elem.get('portion'))) - for elem in target_groups - ] - default_value = 1 - sum(val for (_, val) in target_values) self.group = group_from_value( - target_values + [(None, default_value)], + self.definition['data']['group_portions'], random.uniform(0, 1) ) else: @@ -69,24 +62,57 @@ class Module(XModule): self.group = shared_state['group'] def get_shared_state(self): + print self.group return json.dumps({'group': self.group}) - def _xml_children(self): - group = None - if self.group is None: - group = self.xmltree.find('default') - else: - for candidate_group in self.xmltree.find('group'): - if self.group == candidate_group.get('name'): - group = candidate_group - break + def displayable_items(self): + return [self.system.get_module(child) + for child + in self.definition['data']['group_content'][self.group]] - if group is None: - return [] - return list(group) - def get_children(self): - return [self.module_from_xml(child) for child in self._xml_children()] +class ABTestDescriptor(RawDescriptor, XmlDescriptor): + module_class = ABTestModule - def get_html(self): - return '\n'.join(child.get_html() for child in self.get_children()) + def __init__(self, system, definition=None, **kwargs): + kwargs['shared_state_key'] = definition['data']['experiment'] + RawDescriptor.__init__(self, system, definition, **kwargs) + + @classmethod + def definition_from_xml(cls, xml_object, system): + experiment = xml_object.get('experiment') + + if experiment is None: + raise InvalidDefinitionError("ABTests must specify an experiment. Not found in:\n{xml}".format(xml=etree.tostring(xml_object, pretty_print=True))) + + definition = { + 'data': { + 'experiment': experiment, + 'group_portions': [], + 'group_content': {None: []}, + }, + 'children': []} + for group in xml_object: + if group.tag == 'default': + name = None + else: + name = group.get('name') + definition['data']['group_portions'].append( + (name, float(group.get('portion', 0))) + ) + + child_content_urls = [ + system.process_xml(etree.tostring(child)).url + for child in group + ] + + definition['data']['group_content'][name] = child_content_urls + definition['children'].extend(child_content_urls) + + default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions']) + if default_portion < 0: + raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") + + definition['data']['group_portions'].append((None, default_portion)) + + return definition diff --git a/common/lib/xmodule/exceptions.py b/common/lib/xmodule/exceptions.py new file mode 100644 index 0000000000..9a9258d600 --- /dev/null +++ b/common/lib/xmodule/exceptions.py @@ -0,0 +1,2 @@ +class InvalidDefinitionError(Exception): + pass diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 93eddc5c7c..e45e6654c2 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -13,6 +13,7 @@ setup( # for a description of entry_points entry_points={ 'xmodule.v1': [ + "abtest = xmodule.abtest_module:ABTestDescriptor", "book = xmodule.translation_module:TranslateCustomTagDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor", "course = xmodule.seq_module:SequenceDescriptor", diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 8ee3df38ff..d8559c9bb7 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -295,6 +295,7 @@ class XModuleDescriptor(Plugin): self.display_name = kwargs.get('display_name') self.format = kwargs.get('format') self.graded = kwargs.get('graded', False) + self.shared_state_key = kwargs.get('shared_state_key') # For now, we represent goals as a list of strings, but this # is one of the things that we are going to be iterating on heavily From c837cf797da1f96ed2a86a84540bec494d47186e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 05:57:38 -0400 Subject: [PATCH 178/252] Remove some unused code from content_parser --- lms/djangoapps/courseware/content_parser.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py index 70e5eeeeb6..624608aaa9 100644 --- a/lms/djangoapps/courseware/content_parser.py +++ b/lms/djangoapps/courseware/content_parser.py @@ -19,12 +19,10 @@ from django.conf import settings from student.models import UserProfile from student.models import UserTestGroup -from courseware.models import StudentModuleCache from mitxmako.shortcuts import render_to_string from util.cache import cache from multicourse import multicourse_settings import xmodule -from keystore.django import keystore ''' This file will eventually form an abstraction layer between the course XML file and the rest of the system. @@ -35,22 +33,11 @@ course XML file and the rest of the system. # util.memcache.fasthash (which does not depend on memcache at all) # -class ContentException(Exception): - pass - log = logging.getLogger("mitx.courseware") def format_url_params(params): return [ urllib.quote(string.replace(' ','_')) for string in params ] -def xpath_remove(tree, path): - ''' Remove all items matching path from lxml tree. Works in - place.''' - items = tree.xpath(path) - for item in items: - item.getparent().remove(item) - return tree - def id_tag(course): ''' Tag all course elements with unique IDs ''' default_ids = xmodule.get_default_ids() From d7ee03874dfe1fc5170dc311549f4c6b85d36f53 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 07:20:12 -0400 Subject: [PATCH 179/252] Make staff histograms work again --- lms/djangoapps/courseware/module_render.py | 32 +++++++++++++++++----- lms/templates/staff_problem_info.html | 5 ++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 4c82eba974..c5d87a52b0 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -1,17 +1,14 @@ import json import logging -from lxml import etree - +from django.conf import settings from django.http import Http404 from django.http import HttpResponse - -from django.conf import settings -from mitxmako.shortcuts import render_to_string - -from models import StudentModule, StudentModuleCache +from lxml import etree from keystore.django import keystore +from mitxmako.shortcuts import render_to_string +from models import StudentModule, StudentModuleCache log = logging.getLogger("mitx.courseware") @@ -247,6 +244,9 @@ def get_module(user, request, location, student_module_cache, position=None): module = descriptor.xmodule_constructor(system)(instance_state, shared_state) + if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff: + module = add_histogram(module) + # If StudentModule for this instance wasn't already in the database, # and this isn't a guest user, create it. if user.is_authenticated(): @@ -272,6 +272,24 @@ def get_module(user, request, location, student_module_cache, position=None): return (module, instance_module, shared_module, descriptor.type) +def add_histogram(module): + original_get_html = module.get_html + def get_html(): + module_id = module.id + print "Rendering Histogram for ", module_id + histogram = grade_histogram(module_id) + print histogram + render_histogram = len(histogram) > 0 + staff_context = {'definition': json.dumps(module.definition, indent=4), + 'element_id': module.location.html_id(), + 'histogram': json.dumps(histogram), + 'render_histogram': render_histogram, + 'module_content': original_get_html()} + return render_to_string("staff_problem_info.html", staff_context) + module.get_html = get_html + return module + + def render_x_module(user, module, student_module_cache, position=None): ''' Generic module for extensions. This renders to HTML. diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html index 24450c797a..b5e07f8af4 100644 --- a/lms/templates/staff_problem_info.html +++ b/lms/templates/staff_problem_info.html @@ -1,6 +1,7 @@ +${module_content}
                  -${xml | h} +${definition | h}
                  %if render_histogram: -
                  +
                  %endif From 3fdae56a27710b36feb6c7f8176bd29a0364042b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 07:29:49 -0400 Subject: [PATCH 180/252] Remove dead code --- lms/djangoapps/courseware/module_render.py | 51 +--------------------- lms/djangoapps/courseware/views.py | 43 +----------------- lms/urls.py | 1 - 3 files changed, 2 insertions(+), 93 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index c5d87a52b0..b331a270d4 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -25,7 +25,7 @@ class I4xSystem(object): Note that these functions can be closures over e.g. a django request and user, or other environment-specific info. ''' - def __init__(self, ajax_url, track_function, render_function, + def __init__(self, ajax_url, track_function, get_module, render_template, user=None, filestore=None): ''' @@ -38,10 +38,6 @@ class I4xSystem(object): files. Update or remove. get_module - function that takes (location) and returns a corresponding module instance object. - render_function - function that takes (module_xml) and renders it, - returning a dictionary with a context for rendering the - module to html. Dictionary will contain keys 'content' - and 'type'. render_template - a function that takes (template_file, context), and returns rendered html. user - The user to base the seed off of for this request @@ -52,7 +48,6 @@ class I4xSystem(object): self.track_function = track_function self.filestore = filestore self.get_module = get_module - self.render_function = render_function self.render_template = render_template self.exception404 = Http404 self.DEBUG = settings.DEBUG @@ -230,8 +225,6 @@ def get_module(user, request, location, student_module_cache, position=None): return module system = I4xSystem(track_function=make_track_function(request), - render_function=lambda xml: render_x_module( - user, xml, student_module_cache, position), render_template=render_to_string, ajax_url=ajax_url, # TODO (cpennington): Figure out how to share info between systems @@ -290,48 +283,6 @@ def add_histogram(module): return module -def render_x_module(user, module, student_module_cache, position=None): - ''' Generic module for extensions. This renders to HTML. - - modules include sequential, vertical, problem, video, html - - Note that modules can recurse. problems, video, html, can be inside sequential or vertical. - - Arguments: - - - user : current django User - - module : lxml etree of xml subtree for the current module - - student_module_cache : list of StudentModule objects, one of which may match this module type and id - - position : extra information from URL for user-specified position within module - - Returns: - - - dict which is context for HTML rendering of the specified module. Will have - key 'content', and will have 'type' key if passed a valid module. - ''' - if module_xml is None: - return {"content": ""} - - (instance, _, _, module_type) = get_module( - user, module_xml, student_module_cache, position) - - content = instance.get_html() - - # special extra information about each problem, only for users who are staff - if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff: - module_id = module_xml.get('id') - histogram = grade_histogram(module_id) - render_histogram = len(histogram) > 0 - staff_context = {'xml': etree.tostring(module_xml), - 'module_id': module_id, - 'histogram': json.dumps(histogram), - 'render_histogram': render_histogram} - content += render_to_string("staff_problem_info.html", staff_context) - - context = {'content': content, 'type': module_type} - return context - - def modx_dispatch(request, dispatch=None, id=None): ''' Generic view for extensions. This is where AJAX calls go. diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index e1b77c8fbe..444e830072 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -16,7 +16,7 @@ from django.views.decorators.cache import cache_control from lxml import etree -from module_render import render_x_module, toc_for_course, get_module, get_section +from module_render import toc_for_course, get_module, get_section from models import StudentModuleCache from student.models import UserProfile from multicourse import multicourse_settings @@ -115,47 +115,6 @@ def render_accordion(request, course, chapter, section): return render_to_string('accordion.html', context) -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -def render_section(request, section): - ''' TODO: Consolidate with index - ''' - user = request.user - if not settings.COURSEWARE_ENABLED: - return redirect('/') - - coursename = multicourse_settings.get_coursename_from_request(request) - - try: - dom = content_parser.section_file(user, section, coursename) - except: - log.exception("Unable to parse courseware xml") - return render_to_response('courseware-error.html', {}) - - context = { - 'csrf': csrf(request)['csrf_token'], - 'accordion': render_accordion(request, get_course(request), '', '') - } - - student_module_cache = StudentModuleCache(request.user, dom) - - try: - module = render_x_module(user, dom, student_module_cache) - except: - log.exception("Unable to load module") - context.update({ - 'init': '', - 'content': render_to_string("module-error.html", {}), - }) - return render_to_response('courseware.html', context) - - context.update({ - 'content': module['content'], - }) - - result = render_to_response('courseware.html', context) - return result - - def get_course(request, course): ''' Figure out what the correct course is. diff --git a/lms/urls.py b/lms/urls.py index e43c949643..d8d4356e5c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -56,7 +56,6 @@ if settings.COURSEWARE_ENABLED: url(r'^courseware/(?P[^/]*)/(?P[^/]*)/$', 'courseware.views.index', name="courseware_chapter"), url(r'^courseware/(?P[^/]*)/$', 'courseware.views.index', name="courseware_course"), url(r'^jumpto/(?P[^/]+)/$', 'courseware.views.jump_to'), - url(r'^section/(?P
                  [^/]*)/$', 'courseware.views.render_section'), url(r'^modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'), url(r'^profile$', 'courseware.views.profile'), url(r'^profile/(?P[^/]*)/$', 'courseware.views.profile'), From 3a26b9802710ecea7dad7dc675fbe99535e877ab Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 08:44:03 -0400 Subject: [PATCH 181/252] Remove errant print line left over from debugging --- lms/djangoapps/courseware/module_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index b331a270d4..c2d20f1b67 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -267,9 +267,9 @@ def get_module(user, request, location, student_module_cache, position=None): def add_histogram(module): original_get_html = module.get_html + def get_html(): module_id = module.id - print "Rendering Histogram for ", module_id histogram = grade_histogram(module_id) print histogram render_histogram = len(histogram) > 0 @@ -279,6 +279,7 @@ def add_histogram(module): 'render_histogram': render_histogram, 'module_content': original_get_html()} return render_to_string("staff_problem_info.html", staff_context) + module.get_html = get_html return module From c7f95695c53e107bd83fd38c9424b7cb8bc7653a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 08:44:16 -0400 Subject: [PATCH 182/252] Fix check_course command --- lms/djangoapps/courseware/content_parser.py | 44 ------------ .../management/commands/check_course.py | 71 ++++++++----------- 2 files changed, 31 insertions(+), 84 deletions(-) diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py index 624608aaa9..d1d9cf3980 100644 --- a/lms/djangoapps/courseware/content_parser.py +++ b/lms/djangoapps/courseware/content_parser.py @@ -193,50 +193,6 @@ def replace_custom_tags(tree): replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags') -def course_file(user, coursename=None): - ''' Given a user, return an xml tree object for the course file. - - Handles getting the right file, and processing it depending on the - groups the user is in. Does caching of the xml strings. - ''' - - if user.is_authenticated(): - # use user.profile_cache.courseware? - filename = UserProfile.objects.get(user=user).courseware - else: - filename = 'guest_course.xml' - - # if a specific course is specified, then use multicourse to get - # the right path to the course XML directory - if coursename and settings.ENABLE_MULTICOURSE: - xp = multicourse_settings.get_course_xmlpath(coursename) - filename = xp + filename # prefix the filename with the path - - groups = user_groups(user) - options = get_options(user) - - # Try the cache... - cache_key = "{0}_processed?dev_content:{1}&groups:{2}".format( - filename, - options['dev_content'], - sorted(groups)) - - if "dev" in settings.DEFAULT_GROUPS: - tree_string = None - else: - tree_string = cache.get(cache_key) - - if tree_string: - tree = etree.XML(tree_string) - else: - tree = parse_course_file(filename, options, namespace='course') - # Cache it - tree_string = etree.tostring(tree) - cache.set(cache_key, tree_string, 60) - - return tree - - def sections_dir(coursename=None): ''' Get directory where sections information is stored. ''' diff --git a/lms/djangoapps/courseware/management/commands/check_course.py b/lms/djangoapps/courseware/management/commands/check_course.py index 8af0c5d4be..afc7e47857 100644 --- a/lms/djangoapps/courseware/management/commands/check_course.py +++ b/lms/djangoapps/courseware/management/commands/check_course.py @@ -6,50 +6,36 @@ from django.core.management.base import BaseCommand from django.conf import settings from django.contrib.auth.models import User -from courseware.content_parser import course_file -import courseware.module_render import xmodule import mitxmako.middleware as middleware middleware.MakoMiddleware() +from keystore.django import keystore +from courseware.models import StudentModuleCache +from courseware.module_render import get_module -def check_names(user, course): - ''' - Complain if any problems have non alphanumeric names. - TODO (vshnayder): there are some in 6.002x that don't. Is that actually a problem? - ''' - all_ok = True - print "Confirming all problems have alphanumeric names" - for problem in course.xpath('//problem'): - filename = problem.get('filename') - if not filename.isalnum(): - print "==============> Invalid (non-alphanumeric) filename", filename - all_ok = False - return all_ok -def check_rendering(user, course): +def check_rendering(module): '''Check that all modules render''' all_ok = True print "Confirming all modules render. Nothing should print during this step. " - for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'): - module_class = xmodule.modx_modules[module.tag] - # TODO: Abstract this out in render_module.py - try: - module_class(etree.tostring(module), - module.get('id'), - ajax_url='', - state=None, - track_function = lambda x,y,z:None, - render_function = lambda x: {'content':'','type':'video'}) + + def _check_module(module): + try: + module.get_html() except Exception as ex: - print "==============> Error in ", etree.tostring(module) + print "==============> Error in ", module.id print "" print ex all_ok = False + for child in module.get_children(): + _check_module(child) + _check_module(module) print "Module render check finished" return all_ok -def check_sections(user, course): + +def check_sections(course): all_ok = True sections_dir = settings.DATA_DIR + "/sections" print "Checking that all sections exist and parse properly" @@ -69,11 +55,13 @@ def check_sections(user, course): all_ok = False print "checked all sections" else: - print "Skipping check of include files -- no section includes dir ("+sections_dir+")" + print "Skipping check of include files -- no section includes dir (" + sections_dir + ")" return all_ok + class Command(BaseCommand): help = "Does basic validity tests on course.xml." + def handle(self, *args, **options): all_ok = True @@ -86,22 +74,25 @@ class Command(BaseCommand): sample_user = User.objects.all()[0] - print "Attempting to load courseware" - course = course_file(sample_user) - to_run = [check_names, - # TODO (vshnayder) : make check_rendering work (use module_render.py), - # turn it on - # check_rendering, - check_sections, - ] + # TODO (cpennington): Get coursename in a legitimate way + course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' + student_module_cache = StudentModuleCache(sample_user, keystore().get_item(course_location)) + (course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache) + + to_run = [ + #TODO (vshnayder) : make check_rendering work (use module_render.py), + # turn it on + check_rendering, + check_sections, + ] for check in to_run: - all_ok = check(sample_user, course) and all_ok + all_ok = check(course) and all_ok # TODO: print "Checking course properly annotated with preprocess.py" - + if all_ok: print 'Courseware passes all checks!' - else: + else: print "Courseware fails some checks" From 2a9eba38862dee453ec65183d4fb53af61e986e4 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 09:12:17 -0400 Subject: [PATCH 183/252] Removing content_parser --- lms/djangoapps/courseware/content_parser.py | 205 -------------------- lms/djangoapps/courseware/views.py | 48 +++-- 2 files changed, 36 insertions(+), 217 deletions(-) delete mode 100644 lms/djangoapps/courseware/content_parser.py diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py deleted file mode 100644 index d1d9cf3980..0000000000 --- a/lms/djangoapps/courseware/content_parser.py +++ /dev/null @@ -1,205 +0,0 @@ -''' -courseware/content_parser.py - -This file interfaces between all courseware modules and the top-level course.xml file for a course. - -Does some caching (to be explained). - -''' - -import logging -import os -import sys -import urllib - -from lxml import etree -from util.memcache import fasthash - -from django.conf import settings - -from student.models import UserProfile -from student.models import UserTestGroup -from mitxmako.shortcuts import render_to_string -from util.cache import cache -from multicourse import multicourse_settings -import xmodule - -''' This file will eventually form an abstraction layer between the -course XML file and the rest of the system. -''' - -# ==== This section has no direct dependencies on django ==================================== -# NOTE: it does still have some indirect dependencies: -# util.memcache.fasthash (which does not depend on memcache at all) -# - -log = logging.getLogger("mitx.courseware") - -def format_url_params(params): - return [ urllib.quote(string.replace(' ','_')) for string in params ] - -def id_tag(course): - ''' Tag all course elements with unique IDs ''' - default_ids = xmodule.get_default_ids() - - # Tag elements with unique IDs - elements = course.xpath("|".join('//' + c for c in default_ids)) - for elem in elements: - if elem.get('id'): - pass - elif elem.get(default_ids[elem.tag]): - new_id = elem.get(default_ids[elem.tag]) - # Convert to alphanumeric - new_id = "".join(a for a in new_id if a.isalnum()) - - # Without this, a conflict may occur between an html or youtube id - new_id = default_ids[elem.tag] + new_id - elem.set('id', new_id) - else: - elem.set('id', "id" + fasthash(etree.tostring(elem))) - -def propogate_downward_tag(element, attribute_name, parent_attribute = None): - ''' This call is to pass down an attribute to all children. If an element - has this attribute, it will be "inherited" by all of its children. If a - child (A) already has that attribute, A will keep the same attribute and - all of A's children will inherit A's attribute. This is a recursive call.''' - - if (parent_attribute is None): - #This is the entry call. Select all elements with this attribute - all_attributed_elements = element.xpath("//*[@" + attribute_name +"]") - for attributed_element in all_attributed_elements: - attribute_value = attributed_element.get(attribute_name) - for child_element in attributed_element: - propogate_downward_tag(child_element, attribute_name, attribute_value) - else: - '''The hack below is because we would get _ContentOnlyELements from the - iterator that can't have attributes set. We can't find API for it. If we - ever have an element which subclasses BaseElement, we will not tag it''' - if not element.get(attribute_name) and type(element) == etree._Element: - element.set(attribute_name, parent_attribute) - - for child_element in element: - propogate_downward_tag(child_element, attribute_name, parent_attribute) - else: - #This element would have already been found by Xpath, so we return - #for now and trust that this element will get its turn to propogate - #to its children later. - return - - -def course_xml_process(tree): - ''' Do basic pre-processing of an XML tree. Assign IDs to all - items without. Propagate due dates, grace periods, etc. to child - items. - ''' - process_includes(tree) - replace_custom_tags(tree) - id_tag(tree) - propogate_downward_tag(tree, "due") - propogate_downward_tag(tree, "graded") - propogate_downward_tag(tree, "graceperiod") - propogate_downward_tag(tree, "showanswer") - propogate_downward_tag(tree, "rerandomize") - return tree - - -def process_includes_dir(tree, dir): - """ - Process tree to replace all tags - with the contents of the file specified, relative to dir - """ - includes = tree.findall('.//include') - for inc in includes: - file = inc.get('file') - if file is not None: - try: - ifp = open(os.path.join(dir, file)) - except Exception: - log.exception('Error in problem xml include: %s' % (etree.tostring(inc, pretty_print=True))) - log.exception('Cannot find file %s in %s' % (file, dir)) - raise - try: - # read in and convert to XML - incxml = etree.XML(ifp.read()) - except Exception: - log.exception('Error in problem xml include: %s' % (etree.tostring(inc, pretty_print=True))) - log.exception('Cannot parse XML in %s' % (file)) - raise - # insert new XML into tree in place of inlcude - parent = inc.getparent() - parent.insert(parent.index(inc), incxml) - parent.remove(inc) - - -def replace_custom_tags_dir(tree, dir): - ''' - Process tree to replace all custom tags defined in dir. - ''' - tags = os.listdir(dir) - for tag in tags: - for element in tree.iter(tag): - element.tag = 'customtag' - impl = etree.SubElement(element, 'impl') - impl.text = tag - -def parse_course_file(filename, options, namespace): - ''' - Parse a course file with the given options, and return the resulting - xml tree object. - - Options should be a dictionary including keys - 'dev_content': bool, - 'groups' : [list, of, user, groups] - - namespace is used to in searching for the file. Could be e.g. 'course', - 'sections'. - ''' - xml = etree.XML(render_to_string(filename, options, namespace=namespace)) - return course_xml_process(xml) - - -# ==== All Django-specific code below ============================================= - -def user_groups(user): - if not user.is_authenticated(): - return [] - - # TODO: Rewrite in Django - key = 'user_group_names_{user.id}'.format(user=user) - cache_expiration = 60 * 60 # one hour - - # Kill caching on dev machines -- we switch groups a lot - group_names = cache.get(key) - - if group_names is None: - group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] - cache.set(key, group_names, cache_expiration) - - return group_names - - -def get_options(user): - return {'dev_content': settings.DEV_CONTENT, - 'groups': user_groups(user)} - - -def process_includes(tree): - '''Replace tags with the contents from the course directory''' - process_includes_dir(tree, settings.DATA_DIR) - - -def replace_custom_tags(tree): - '''Replace custom tags defined in our custom_tags dir''' - replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags') - - -def sections_dir(coursename=None): - ''' Get directory where sections information is stored. - ''' - # if a specific course is specified, then use multicourse to get the - # right path to the course XML directory - xp = '' - if coursename and settings.ENABLE_MULTICOURSE: - xp = multicourse_settings.get_course_xmlpath(coursename) - - return settings.DATA_DIR + xp + '/sections/' diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 444e830072..e1e1c16632 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1,8 +1,6 @@ import logging import urllib -from fs.osfs import OSFS - from django.conf import settings from django.core.context_processors import csrf from django.contrib.auth.models import User @@ -22,7 +20,9 @@ from student.models import UserProfile from multicourse import multicourse_settings from keystore.django import keystore -from courseware import grades, content_parser +from util.cache import cache +from student.models import UserTestGroup +from courseware import grades log = logging.getLogger("mitx.courseware") @@ -31,16 +31,39 @@ etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, template_imports = {'urllib': urllib} + +def user_groups(user): + if not user.is_authenticated(): + return [] + + # TODO: Rewrite in Django + key = 'user_group_names_{user.id}'.format(user=user) + cache_expiration = 60 * 60 # one hour + + # Kill caching on dev machines -- we switch groups a lot + group_names = cache.get(key) + + if group_names is None: + group_names = [u.name for u in UserTestGroup.objects.filter(users=user)] + cache.set(key, group_names, cache_expiration) + + return group_names + + +def format_url_params(params): + return [urllib.quote(string.replace(' ', '_')) for string in params] + + @cache_control(no_cache=True, no_store=True, must_revalidate=True) def gradebook(request): - if 'course_admin' not in content_parser.user_groups(request.user): + if 'course_admin' not in user_groups(request.user): raise Http404 coursename = multicourse_settings.get_coursename_from_request(request) student_objects = User.objects.all()[:100] student_info = [] - + coursename = multicourse_settings.get_coursename_from_request(request) course_location = multicourse_settings.get_course_location(coursename) @@ -52,7 +75,7 @@ def gradebook(request): 'id': student.id, 'email': student.email, 'grade_info': grades.grade_sheet(student, course, student_module_cache), - 'realname': UserProfile.objects.get(user = student).name + 'realname': UserProfile.objects.get(user=student).name }) return render_to_response('gradebook.html', {'students': student_info}) @@ -67,7 +90,7 @@ def profile(request, student_id=None): if student_id is None: student = request.user else: - if 'course_admin' not in content_parser.user_groups(request.user): + if 'course_admin' not in user_groups(request.user): raise Http404 student = User.objects.get(id=int(student_id)) @@ -83,7 +106,7 @@ def profile(request, student_id=None): 'location': user_info.location, 'language': user_info.language, 'email': student.email, - 'format_url_params': content_parser.format_url_params, + 'format_url_params': format_url_params, 'csrf': csrf(request)['csrf_token'] } context.update(grades.grade_sheet(student, course, student_module_cache)) @@ -110,7 +133,7 @@ def render_accordion(request, course, chapter, section): context = dict([('active_chapter', active_chapter), ('toc', toc), ('course_name', course), - ('format_url_params', content_parser.format_url_params), + ('format_url_params', format_url_params), ('csrf', csrf(request)['csrf_token'])] + template_imports.items()) return render_to_string('accordion.html', context) @@ -215,7 +238,8 @@ def jump_to(request, probname=None): # look for problem of given name pxml = xml.xpath('//problem[@filename="%s"]' % probname) - if pxml: pxml = pxml[0] + if pxml: + pxml = pxml[0] # get the parent element parent = pxml.getparent() @@ -224,7 +248,7 @@ def jump_to(request, probname=None): chapter = None section = None branch = parent - for k in range(4): # max depth of recursion + for k in range(4): # max depth of recursion if branch.tag == 'section': section = branch.get('name') if branch.tag == 'chapter': @@ -233,7 +257,7 @@ def jump_to(request, probname=None): position = None if parent.tag == 'sequential': - position = parent.index(pxml) + 1 # position in sequence + position = parent.index(pxml) + 1 # position in sequence return index(request, course=coursename, chapter=chapter, From f25478b3d45bdc03f8d19e92644131d52caab624 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 11:56:38 -0400 Subject: [PATCH 184/252] Consolidate access to metadata, and allow some of it to be inherited between modules --- common/lib/xmodule/capa_module.py | 14 ++--- common/lib/xmodule/seq_module.py | 4 +- common/lib/xmodule/x_module.py | 67 +++++++++++++++------- common/lib/xmodule/xml_module.py | 16 +++++- lms/djangoapps/courseware/grades.py | 18 +++--- lms/djangoapps/courseware/module_render.py | 20 +++---- lms/templates/profile.html | 2 +- lms/templates/staff_problem_info.html | 3 +- 8 files changed, 89 insertions(+), 55 deletions(-) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index b6bfc91e80..9e82fbe8d4 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -125,7 +125,7 @@ class CapaModule(XModule): # User submitted a problem, and hasn't reset. We don't want # more submissions. - if self.lcp.done and self.rerandomize == "always": + if self.lcp.done and self.metadata['rerandomize'] == "always": check_button = False save_button = False @@ -184,15 +184,15 @@ class CapaModule(XModule): # TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed") self.explain_available = only_one(dom2.xpath('/problem/@explain_available')) - display_due_date_string = only_one(dom2.xpath('/problem/@due')) - if len(display_due_date_string) > 0: + display_due_date_string = self.metadata.get('due', None) + if display_due_date_string is not None: self.display_due_date = dateutil.parser.parse(display_due_date_string) #log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date)) else: self.display_due_date = None - grace_period_string = only_one(dom2.xpath('/problem/@graceperiod')) - if len(grace_period_string) > 0 and self.display_due_date: + grace_period_string = self.metadata.get('graceperiod', None) + if grace_period_string is not None and self.display_due_date: self.grace_period = parse_timedelta(grace_period_string) self.close_date = self.display_due_date + self.grace_period #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date)) @@ -206,12 +206,12 @@ class CapaModule(XModule): else: self.max_attempts = None - self.show_answer = only_one(dom2.xpath('/problem/@showanswer')) + self.show_answer = self.metadata.get('showanwser', 'closed') if self.show_answer == "": self.show_answer = "closed" - self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize')) + self.rerandomize = self.metadata.get('rerandomize', 'always') if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true": self.rerandomize = "always" elif self.rerandomize == "false" or self.rerandomize == "per_student": diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 7ef497b837..ffcaed2599 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -52,9 +52,9 @@ class SequenceModule(XModule): contents.append({ 'content': child.get_html(), 'title': "\n".join( - grand_child.display_name.strip() + grand_child.metadata['display_name'].strip() for grand_child in child.get_children() - if grand_child.display_name is not None + if 'metadata' in grand_child.metadata ), 'progress_status': Progress.to_js_status_str(progress), 'progress_detail': Progress.to_js_detail_str(progress), diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index d8559c9bb7..c027d1d774 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -82,11 +82,9 @@ class XModule(object): self.shared_state = shared_state self.id = self.location.url() self.name = self.location.name - self.display_name = kwargs.get('display_name', '') self.type = self.location.category + self.metadata = kwargs.get('metadata', {}) self._loaded_children = None - self.graded = kwargs.get('graded', False) - self.format = kwargs.get('format') def get_name(self): name = self.__xmltree.get('name') @@ -188,6 +186,9 @@ class XModuleDescriptor(Plugin): js = {} js_module = None + # A list of metadata that this module can inherit from its parent module + inheritable_metadata = ('graded', 'due', 'graceperiod', 'showanswer', 'rerandomize') + @staticmethod def load_from_json(json_data, system, default_class=None): """ @@ -215,7 +216,11 @@ class XModuleDescriptor(Plugin): return cls(system=system, **json_data) @staticmethod - def load_from_xml(xml_data, system, org=None, course=None, default_class=None): + def load_from_xml(xml_data, + system, + org=None, + course=None, + default_class=None): """ This method instantiates the correct subclass of XModuleDescriptor based on the contents of xml_data. @@ -282,32 +287,48 @@ class XModuleDescriptor(Plugin): Current arguments passed in kwargs: location: A keystore.Location object indicating the name and ownership of this problem - goals: A list of strings of learning goals associated with this module - display_name: The name to use for displaying this module to the user - format: The format of this module ('Homework', 'Lab', etc) - graded (bool): Whether this module is should be graded or not + shared_state_key: The key to use for sharing StudentModules with other + modules of this type + metadata: A dictionary containing the following optional keys: + goals: A list of strings of learning goals associated with this module + display_name: The name to use for displaying this module to the user + format: The format of this module ('Homework', 'Lab', etc) + graded (bool): Whether this module is should be graded or not + due (string): The due date for this module + graceperiod (string): The amount of grace period to allow when enforcing the due date + showanswer (string): When to show answers for this module + rerandomize (string): When to generate a newly randomized instance of the module data """ self.system = system self.definition = definition if definition is not None else {} self.name = Location(kwargs.get('location')).name self.type = Location(kwargs.get('location')).category self.url = Location(kwargs.get('location')).url() - self.display_name = kwargs.get('display_name') - self.format = kwargs.get('format') - self.graded = kwargs.get('graded', False) + self.metadata = kwargs.get('metadata', {}) self.shared_state_key = kwargs.get('shared_state_key') - # For now, we represent goals as a list of strings, but this - # is one of the things that we are going to be iterating on heavily - # to find the best teaching method - self.goals = kwargs.get('goals', []) - self._child_instances = None + def inherit_metadata(self, metadata): + """ + Updates this module with metadata inherited from a containing module. + Only metadata specified in self.inheritable_metadata will + be inherited + """ + # Set all inheritable metadata from kwargs that are + # in self.inheritable_metadata and aren't already set in metadata + for attr in self.inheritable_metadata: + if attr not in self.metadata and attr in metadata: + self.metadata[attr] = metadata[attr] + def get_children(self): """Returns a list of XModuleDescriptor instances for the children of this module""" if self._child_instances is None: - self._child_instances = [self.system.load_item(child) for child in self.definition.get('children', [])] + self._child_instances = [] + for child_loc in self.definition.get('children', []): + child = self.system.load_item(child_loc) + child.inherit_metadata(self.metadata) + self._child_instances.append(child) return self._child_instances @@ -322,10 +343,14 @@ class XModuleDescriptor(Plugin): Returns a constructor for an XModule. This constructor takes two arguments: instance_state and shared_state, and returns a fully nstantiated XModule """ - return partial(self.module_class, system, self.url, self.definition, - display_name=self.display_name, - format=self.format, - graded=self.graded) + return partial( + self.module_class, + system, + self.url, + self.definition, + metadata=self.metadata + ) + class DescriptorSystem(object): def __init__(self, load_item, resources_fs): diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index d62957c3d3..b167e52e88 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -29,6 +29,18 @@ class XmlDescriptor(XModuleDescriptor): """ xml_object = etree.fromstring(xml_data) + metadata = {} + for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'): + from_xml = xml_object.get(attr) + if from_xml is not None: + metadata[attr] = from_xml + + if xml_object.get('graded') is not None: + metadata['graded'] = xml_object.get('graded') == 'true' + + if xml_object.get('name') is not None: + metadata['display_name'] = xml_object.get('name') + return cls( system, cls.definition_from_xml(xml_object, system), @@ -37,7 +49,5 @@ class XmlDescriptor(XModuleDescriptor): course, xml_object.tag, xml_object.get('slug')], - display_name=xml_object.get('name'), - format=xml_object.get('format'), - graded=xml_object.get('graded') == 'true', + metadata=metadata, ) diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index b5fcae86e5..deab9d47d4 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -87,7 +87,7 @@ def grade_sheet(student, course, student_module_cache): for module in yield_descendents(child): yield module - graded = getattr(s, 'graded', False) + graded = s.metadata.get('graded', False) scores = [] for module in yield_descendents(s): (correct, total) = get_score(student, module, student_module_cache) @@ -105,29 +105,27 @@ def grade_sheet(student, course, student_module_cache): #We simply cannot grade a problem that is 12/0, because we might need it as a percentage graded = False - scores.append(Score(correct, total, graded, module.display_name)) + scores.append(Score(correct, total, graded, module.metadata.get('display_name'))) - section_total, graded_total = graders.aggregate_scores(scores, s.display_name) + section_total, graded_total = graders.aggregate_scores(scores, s.metadata.get('display_name')) #Add the graded total to totaled_scores - format = getattr(s, 'format', "") - subtitle = getattr(s, 'subtitle', format) + format = s.metadata.get('format', "") if format and graded_total.possible > 0: format_scores = totaled_scores.get(format, []) format_scores.append(graded_total) totaled_scores[format] = format_scores sections.append({ - 'section': s.display_name, + 'section': s.metadata.get('display_name'), 'scores': scores, 'section_total': section_total, 'format': format, - 'subtitle': subtitle, - 'due': getattr(s, "due", ""), + 'due': s.metadata.get("due", ""), 'graded': graded, }) - chapters.append({'course': course.display_name, - 'chapter': c.display_name, + chapters.append({'course': course.metadata.get('display_name'), + 'chapter': c.metadata.get('display_name'), 'sections': sections}) grader = course_settings.GRADER diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index c2d20f1b67..ee2ccee018 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -137,17 +137,17 @@ def toc_for_course(user, request, course_location, active_chapter, active_sectio sections = list() for section in chapter.get_display_items(): - active = (chapter.display_name == active_chapter and - section.display_name == active_section) + active = (chapter.metadata.get('display_name') == active_chapter and + section.metadata.get('display_name') == active_section) - sections.append({'name': section.display_name, - 'format': getattr(section, 'format', ''), - 'due': getattr(section, 'due', ''), + sections.append({'name': section.metadata.get('display_name'), + 'format': section.metadata.get('format', ''), + 'due': section.metadata.get('due', ''), 'active': active}) - chapters.append({'name': chapter.display_name, + chapters.append({'name': chapter.metadata.get('display_name'), 'sections': sections, - 'active': chapter.display_name == active_chapter}) + 'active': chapter.metadata.get('display_name') == active_chapter}) return chapters @@ -171,7 +171,7 @@ def get_section(course, chapter, section): chapter_module = None for _chapter in course_module.get_children(): - if _chapter.display_name == chapter: + if _chapter.metadata.get('display_name') == chapter: chapter_module = _chapter break @@ -180,7 +180,7 @@ def get_section(course, chapter, section): section_module = None for _section in chapter_module.get_children(): - if _section.display_name == section: + if _section.metadata.get('display_name') == section: section_module = _section break @@ -271,9 +271,9 @@ def add_histogram(module): def get_html(): module_id = module.id histogram = grade_histogram(module_id) - print histogram render_histogram = len(histogram) > 0 staff_context = {'definition': json.dumps(module.definition, indent=4), + 'metadata': json.dumps(module.metadata, indent=4), 'element_id': module.location.html_id(), 'histogram': json.dumps(histogram), 'render_histogram': render_histogram, diff --git a/lms/templates/profile.html b/lms/templates/profile.html index e732616d5a..1ba0940eff 100644 --- a/lms/templates/profile.html +++ b/lms/templates/profile.html @@ -156,7 +156,7 @@ $(function() {

                  ${ section['section'] } ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}

                  - ${section['subtitle']} + ${section['format']} %if 'due' in section and section['due']!="": due ${section['due']} %endif diff --git a/lms/templates/staff_problem_info.html b/lms/templates/staff_problem_info.html index b5e07f8af4..2cc9de9df7 100644 --- a/lms/templates/staff_problem_info.html +++ b/lms/templates/staff_problem_info.html @@ -1,6 +1,7 @@ ${module_content}
                  -${definition | h} +definition = ${definition | h} +metadata = ${metadata | h}
                  %if render_histogram:
                  From 87d80835c056776e0836e4ccba3aefbbb066500d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 12:02:41 -0400 Subject: [PATCH 185/252] Initialize StudentModule with the max_score --- lms/djangoapps/courseware/module_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index ee2ccee018..6cf4e43cc5 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -248,7 +248,8 @@ def get_module(user, request, location, student_module_cache, position=None): student=user, module_type=descriptor.type, module_state_key=module.id, - state=module.get_instance_state()) + state=module.get_instance_state(), + max_grade=module.max_score()) instance_module.save() # Add to cache. The caller and the system context have references # to it, so the change persists past the return From 99ae0c2041c5a2b4947f870ed8a4e691b9bfe3c2 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 13:16:04 -0400 Subject: [PATCH 186/252] Import logging where needed --- common/lib/keystore/xml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index dcddb2718e..d0f777efa9 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -1,3 +1,4 @@ +import logging from fs.osfs import OSFS from importlib import import_module from lxml import etree From 19a958a3d4b2aa556d57b9343f6114a9ce9828be Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Sun, 1 Jul 2012 13:33:32 -0400 Subject: [PATCH 187/252] Renaming keystore to modulestore for clarity --- common/lib/keystore/xml.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index d0f777efa9..c3327b8ff3 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -28,14 +28,17 @@ class XMLModuleStore(ModuleStore): with open(self.data_dir / "course.xml") as course_file: class ImportSystem(XMLParsingSystem): - def __init__(self, keystore): + def __init__(self, modulestore): + """ + modulestore: the XMLModuleStore to store the loaded modules in + """ self.unnamed_modules = 0 def process_xml(xml): try: xml_data = etree.fromstring(xml) except: - log.exception("Unable to parse xml:" + xml) + log.exception("Unable to parse xml: {xml}".format(xml=xml)) raise if xml_data.get('name'): xml_data.set('slug', Location.clean(xml_data.get('name'))) @@ -43,11 +46,11 @@ class XMLModuleStore(ModuleStore): self.unnamed_modules += 1 xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) - module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, keystore.default_class) - keystore.modules[module.url] = module + module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class) + modulestore.modules[module.url] = module return module - XMLParsingSystem.__init__(self, keystore.get_item, OSFS(data_dir), process_xml) + XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml) ImportSystem(self).process_xml(course_file.read()) From e61946dfea10757afb0242d6a1f2b51354d1e32e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Sun, 1 Jul 2012 13:52:06 -0400 Subject: [PATCH 188/252] Fix titles in sequence module display --- common/lib/xmodule/seq_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index ffcaed2599..af8563c442 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -54,7 +54,7 @@ class SequenceModule(XModule): 'title': "\n".join( grand_child.metadata['display_name'].strip() for grand_child in child.get_children() - if 'metadata' in grand_child.metadata + if 'display_name' in grand_child.metadata ), 'progress_status': Progress.to_js_status_str(progress), 'progress_detail': Progress.to_js_detail_str(progress), From 6686179bb604ae6f8fb6dab3867555e46a27bdbd Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Sun, 1 Jul 2012 14:15:02 -0400 Subject: [PATCH 189/252] Moving __init__ functions in xmodule to top of class for consistancy --- common/lib/xmodule/capa_module.py | 192 +++++++++++++------------- common/lib/xmodule/seq_module.py | 31 +++-- common/lib/xmodule/template_module.py | 4 +- common/lib/xmodule/vertical_module.py | 9 +- common/lib/xmodule/video_module.py | 24 ++-- common/lib/xmodule/x_module.py | 79 +++++------ 6 files changed, 171 insertions(+), 168 deletions(-) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 9e82fbe8d4..1b186be3db 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -72,6 +72,102 @@ class CapaModule(XModule): ''' icon_class = 'problem' + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + + self.attempts = 0 + self.max_attempts = None + + dom2 = etree.fromstring(definition['data']) + + self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), + default="closed") + # TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed") + self.explain_available = only_one(dom2.xpath('/problem/@explain_available')) + + display_due_date_string = self.metadata.get('due', None) + if display_due_date_string is not None: + self.display_due_date = dateutil.parser.parse(display_due_date_string) + #log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date)) + else: + self.display_due_date = None + + grace_period_string = self.metadata.get('graceperiod', None) + if grace_period_string is not None and self.display_due_date: + self.grace_period = parse_timedelta(grace_period_string) + self.close_date = self.display_due_date + self.grace_period + #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date)) + else: + self.grace_period = None + self.close_date = self.display_due_date + + self.max_attempts = only_one(dom2.xpath('/problem/@attempts')) + if len(self.max_attempts) > 0: + self.max_attempts = int(self.max_attempts) + else: + self.max_attempts = None + + self.show_answer = self.metadata.get('showanwser', 'closed') + + if self.show_answer == "": + self.show_answer = "closed" + + self.rerandomize = self.metadata.get('rerandomize', 'always') + if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true": + self.rerandomize = "always" + elif self.rerandomize == "false" or self.rerandomize == "per_student": + self.rerandomize = "per_student" + elif self.rerandomize == "never": + self.rerandomize = "never" + else: + raise Exception("Invalid rerandomize attribute " + self.rerandomize) + + if instance_state != None: + instance_state = json.loads(instance_state) + if instance_state != None and 'attempts' in instance_state: + self.attempts = instance_state['attempts'] + + # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) + self.filename = "problems/" + only_one(dom2.xpath('/problem/@filename')) + ".xml" + self.name = only_one(dom2.xpath('/problem/@name')) + + weight_string = only_one(dom2.xpath('/problem/@weight')) + if weight_string: + self.weight = float(weight_string) + else: + self.weight = 1 + + if self.rerandomize == 'never': + seed = 1 + elif self.rerandomize == "per_student" and hasattr(system, 'id'): + seed = system.id + else: + seed = None + try: + fp = self.system.filestore.open(self.filename) + except Exception: + log.exception('cannot open file %s' % self.filename) + if self.system.DEBUG: + # create a dummy problem instead of failing + fp = StringIO.StringIO('Problem file %s is missing' % self.filename) + fp.name = "StringIO" + else: + raise + try: + self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) + except Exception: + msg = 'cannot create LoncapaProblem %s' % self.filename + log.exception(msg) + if self.system.DEBUG: + msg = '

                  %s

                  ' % msg.replace('<', '<') + msg += '

                  %s

                  ' % traceback.format_exc().replace('<', '<') + # create a dummy problem with error message instead of failing + fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename, msg)) + fp.name = "StringIO" + self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) + else: + raise + def get_instance_state(self): state = self.lcp.get_state() state['attempts'] = self.attempts @@ -171,102 +267,6 @@ class CapaModule(XModule): return html - def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): - XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - - self.attempts = 0 - self.max_attempts = None - - dom2 = etree.fromstring(definition['data']) - - self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), - default="closed") - # TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed") - self.explain_available = only_one(dom2.xpath('/problem/@explain_available')) - - display_due_date_string = self.metadata.get('due', None) - if display_due_date_string is not None: - self.display_due_date = dateutil.parser.parse(display_due_date_string) - #log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date)) - else: - self.display_due_date = None - - grace_period_string = self.metadata.get('graceperiod', None) - if grace_period_string is not None and self.display_due_date: - self.grace_period = parse_timedelta(grace_period_string) - self.close_date = self.display_due_date + self.grace_period - #log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date)) - else: - self.grace_period = None - self.close_date = self.display_due_date - - self.max_attempts = only_one(dom2.xpath('/problem/@attempts')) - if len(self.max_attempts) > 0: - self.max_attempts = int(self.max_attempts) - else: - self.max_attempts = None - - self.show_answer = self.metadata.get('showanwser', 'closed') - - if self.show_answer == "": - self.show_answer = "closed" - - self.rerandomize = self.metadata.get('rerandomize', 'always') - if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true": - self.rerandomize = "always" - elif self.rerandomize == "false" or self.rerandomize == "per_student": - self.rerandomize = "per_student" - elif self.rerandomize == "never": - self.rerandomize = "never" - else: - raise Exception("Invalid rerandomize attribute " + self.rerandomize) - - if instance_state != None: - instance_state = json.loads(instance_state) - if instance_state != None and 'attempts' in instance_state: - self.attempts = instance_state['attempts'] - - # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) - self.filename = "problems/" + only_one(dom2.xpath('/problem/@filename')) + ".xml" - self.name = only_one(dom2.xpath('/problem/@name')) - - weight_string = only_one(dom2.xpath('/problem/@weight')) - if weight_string: - self.weight = float(weight_string) - else: - self.weight = 1 - - if self.rerandomize == 'never': - seed = 1 - elif self.rerandomize == "per_student" and hasattr(system, 'id'): - seed = system.id - else: - seed = None - try: - fp = self.system.filestore.open(self.filename) - except Exception: - log.exception('cannot open file %s' % self.filename) - if self.system.DEBUG: - # create a dummy problem instead of failing - fp = StringIO.StringIO('Problem file %s is missing' % self.filename) - fp.name = "StringIO" - else: - raise - try: - self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) - except Exception: - msg = 'cannot create LoncapaProblem %s' % self.filename - log.exception(msg) - if self.system.DEBUG: - msg = '

                  %s

                  ' % msg.replace('<', '<') - msg += '

                  %s

                  ' % traceback.format_exc().replace('<', '<') - # create a dummy problem with error message instead of failing - fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename, msg)) - fp.name = "StringIO" - self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) - else: - raise - def handle_ajax(self, dispatch, get): ''' This is called by courseware.module_render, to handle an AJAX call. diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index af8563c442..b11d540143 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -18,6 +18,22 @@ class_priority = ['video', 'problem'] class SequenceModule(XModule): ''' Layout module which lays out content in a temporal sequence ''' + + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + self.position = 1 + + if instance_state is not None: + state = json.loads(instance_state) + if 'position' in state: + self.position = int(state['position']) + + # if position is specified in system, then use that instead + if system.get('position'): + self.position = int(system.get('position')) + + self.rendered = False + def get_instance_state(self): return json.dumps({'position': self.position}) @@ -81,21 +97,6 @@ class SequenceModule(XModule): new_class = c return new_class - def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): - XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - self.position = 1 - - if instance_state is not None: - state = json.loads(instance_state) - if 'position' in state: - self.position = int(state['position']) - - # if position is specified in system, then use that instead - if system.get('position'): - self.position = int(system.get('position')) - - self.rendered = False - class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): mako_template = 'widgets/sequence-edit.html' diff --git a/common/lib/xmodule/template_module.py b/common/lib/xmodule/template_module.py index 52c05616cf..1057fc2a25 100644 --- a/common/lib/xmodule/template_module.py +++ b/common/lib/xmodule/template_module.py @@ -26,8 +26,6 @@ class CustomTagModule(XModule): Renders to:: More information given in the text """ - def get_html(self): - return self.html def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) @@ -36,6 +34,8 @@ class CustomTagModule(XModule): params = dict(xmltree.items()) self.html = self.system.render_template(filename, params, namespace='custom_tags') + def get_html(self): + return self.html class CustomTagDescriptor(RawDescriptor): module_class = CustomTagModule diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py index 6008eb4226..c9ecc5ea18 100644 --- a/common/lib/xmodule/vertical_module.py +++ b/common/lib/xmodule/vertical_module.py @@ -9,6 +9,11 @@ class_priority = ['video', 'problem'] class VerticalModule(XModule): ''' Layout module for laying out submodules vertically.''' + + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + self.contents = None + def get_html(self): if self.contents is None: self.contents = [child.get_html() for child in self.get_display_items()] @@ -32,10 +37,6 @@ class VerticalModule(XModule): new_class = c return new_class - def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): - XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - self.contents = None - class VerticalDescriptor(SequenceDescriptor): module_class = VerticalModule diff --git a/common/lib/xmodule/video_module.py b/common/lib/xmodule/video_module.py index 4aa469db7f..ed44a2d422 100644 --- a/common/lib/xmodule/video_module.py +++ b/common/lib/xmodule/video_module.py @@ -13,6 +13,18 @@ class VideoModule(XModule): video_time = 0 icon_class = 'video' + def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): + XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) + xmltree = etree.fromstring(self.definition['data']) + self.youtube = xmltree.get('youtube') + self.name = xmltree.get('name') + self.position = 0 + + if instance_state is not None: + state = json.loads(instance_state) + if 'position' in state: + self.position = int(float(state['position'])) + def handle_ajax(self, dispatch, get): ''' Handle ajax calls to this video. @@ -52,18 +64,6 @@ class VideoModule(XModule): 'name': self.name, }) - def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): - XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - xmltree = etree.fromstring(self.definition['data']) - self.youtube = xmltree.get('youtube') - self.name = xmltree.get('name') - self.position = 0 - - if instance_state is not None: - state = json.loads(instance_state) - if 'position' in state: - self.position = int(float(state['position'])) - class VideoDescriptor(RawDescriptor): module_class = VideoModule diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index c027d1d774..20a52642d1 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -189,6 +189,46 @@ class XModuleDescriptor(Plugin): # A list of metadata that this module can inherit from its parent module inheritable_metadata = ('graded', 'due', 'graceperiod', 'showanswer', 'rerandomize') + def __init__(self, + system, + definition=None, + **kwargs): + """ + Construct a new XModuleDescriptor. The only required arguments are the + system, used for interaction with external resources, and the definition, + which specifies all the data needed to edit and display the problem (but none + of the associated metadata that handles recordkeeping around the problem). + + This allows for maximal flexibility to add to the interface while preserving + backwards compatibility. + + system: An XModuleSystem for interacting with external resources + definition: A dict containing `data` and `children` representing the problem definition + + Current arguments passed in kwargs: + location: A keystore.Location object indicating the name and ownership of this problem + shared_state_key: The key to use for sharing StudentModules with other + modules of this type + metadata: A dictionary containing the following optional keys: + goals: A list of strings of learning goals associated with this module + display_name: The name to use for displaying this module to the user + format: The format of this module ('Homework', 'Lab', etc) + graded (bool): Whether this module is should be graded or not + due (string): The due date for this module + graceperiod (string): The amount of grace period to allow when enforcing the due date + showanswer (string): When to show answers for this module + rerandomize (string): When to generate a newly randomized instance of the module data + """ + self.system = system + self.definition = definition if definition is not None else {} + self.name = Location(kwargs.get('location')).name + self.type = Location(kwargs.get('location')).category + self.url = Location(kwargs.get('location')).url() + self.metadata = kwargs.get('metadata', {}) + self.shared_state_key = kwargs.get('shared_state_key') + + self._child_instances = None + @staticmethod def load_from_json(json_data, system, default_class=None): """ @@ -269,45 +309,6 @@ class XModuleDescriptor(Plugin): """ return self.js_module - def __init__(self, - system, - definition=None, - **kwargs): - """ - Construct a new XModuleDescriptor. The only required arguments are the - system, used for interaction with external resources, and the definition, - which specifies all the data needed to edit and display the problem (but none - of the associated metadata that handles recordkeeping around the problem). - - This allows for maximal flexibility to add to the interface while preserving - backwards compatibility. - - system: An XModuleSystem for interacting with external resources - definition: A dict containing `data` and `children` representing the problem definition - - Current arguments passed in kwargs: - location: A keystore.Location object indicating the name and ownership of this problem - shared_state_key: The key to use for sharing StudentModules with other - modules of this type - metadata: A dictionary containing the following optional keys: - goals: A list of strings of learning goals associated with this module - display_name: The name to use for displaying this module to the user - format: The format of this module ('Homework', 'Lab', etc) - graded (bool): Whether this module is should be graded or not - due (string): The due date for this module - graceperiod (string): The amount of grace period to allow when enforcing the due date - showanswer (string): When to show answers for this module - rerandomize (string): When to generate a newly randomized instance of the module data - """ - self.system = system - self.definition = definition if definition is not None else {} - self.name = Location(kwargs.get('location')).name - self.type = Location(kwargs.get('location')).category - self.url = Location(kwargs.get('location')).url() - self.metadata = kwargs.get('metadata', {}) - self.shared_state_key = kwargs.get('shared_state_key') - - self._child_instances = None def inherit_metadata(self, metadata): """ From cff8ae462317f1c80e369418cf22314b88990a33 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 08:50:56 -0400 Subject: [PATCH 190/252] Add more documentation to XModule --- common/lib/xmodule/x_module.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 20a52642d1..af9e048385 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -56,13 +56,16 @@ class Plugin(object): class XModule(object): - ''' Implements a generic learning module. - Initialized on access with __init__, first time with instance_state=None, and - shared_state=None. In later instantiations, instance_state will not be None, - but shared_state may be + ''' Implements a generic learning module. - See the HTML module for a simple example + Subclasses must at a minimum provide a definition for get_html in order to be displayed to users. + + See the HTML module for a simple example. ''' + + # The default implementation of get_icon_class returns the icon_class attribute of the class + # This attribute can be overridden by subclasses, and the function can also be overridden + # if the icon class depends on the data in the module icon_class = 'other' def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): @@ -72,8 +75,18 @@ class XModule(object): system: An I4xSystem allowing access to external resources location: Something Location-like that identifies this xmodule definition: A dictionary containing 'data' and 'children'. Both are optional - 'data': is a json object specifying the behavior of this xmodule + 'data': is JSON-like (string, dictionary, list, bool, or None, optionally nested) 'children': is a list of xmodule urls for child modules that this module depends on + instance_state: A string of serialized json that contains the state of this module for + current student accessing the system, or None if no state has been saved + shared_state: A string of serialized json that contains the state that is shared between + this module and any modules of the same type with the same shared_state_key. This + state is only shared per-student, not across different students + kwargs: Optional arguments. Subclasses should always accept kwargs and pass them + to the parent class constructor. + Current known uses of kwargs: + metadata: A dictionary containing data that specifies information that is particular + to a problem in the context of a course ''' self.system = system self.location = Location(location) @@ -121,7 +134,7 @@ class XModule(object): def get_icon_class(self): ''' - Return a class identifying this module in the context of an icon + Return a css class identifying this module in the context of an icon ''' return self.icon_class @@ -155,7 +168,7 @@ class XModule(object): def get_html(self): ''' HTML, as shown in the browser. This is the only method that must be implemented ''' - return "Unimplemented" + raise NotImplementedError("get_html must be defined for all XModules that appear on the screen. Not defined in %s" % self.__class__.__name__) def get_progress(self): ''' Return a progress.Progress object that represents how far the student has gone From 32ed18fef60fabb886caafc15005692d1ee337f7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 08:54:13 -0400 Subject: [PATCH 191/252] Allow slightly more latitude in what is passed as the children array --- common/lib/xmodule/x_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index af9e048385..68f381a755 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -76,7 +76,7 @@ class XModule(object): location: Something Location-like that identifies this xmodule definition: A dictionary containing 'data' and 'children'. Both are optional 'data': is JSON-like (string, dictionary, list, bool, or None, optionally nested) - 'children': is a list of xmodule urls for child modules that this module depends on + 'children': is a list of Location-like values for child modules that this module depends on instance_state: A string of serialized json that contains the state of this module for current student accessing the system, or None if no state has been saved shared_state: A string of serialized json that contains the state that is shared between From 4ae711c421a8a013c0890cf1f972504ed9c4f763 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 08:54:23 -0400 Subject: [PATCH 192/252] Clarify from_json arguments --- common/lib/xmodule/x_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 68f381a755..35cde8b6f4 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -263,7 +263,8 @@ class XModuleDescriptor(Plugin): Creates an instance of this descriptor from the supplied json_data. This may be overridden by subclasses - json_data: Json data specifying the data, children, and metadata for the descriptor + json_data: A json object specifying the definition and any optional keyword arguments for + the XModuleDescriptor system: An XModuleSystem for interacting with external resources """ return cls(system=system, **json_data) From 7b78fa5278f2417452b873591273785dcd91ec2a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 08:57:05 -0400 Subject: [PATCH 193/252] Make self.rerandomize a property accessor that reads from metadata --- common/lib/xmodule/capa_module.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 1b186be3db..6a95789417 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -112,16 +112,6 @@ class CapaModule(XModule): if self.show_answer == "": self.show_answer = "closed" - self.rerandomize = self.metadata.get('rerandomize', 'always') - if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true": - self.rerandomize = "always" - elif self.rerandomize == "false" or self.rerandomize == "per_student": - self.rerandomize = "per_student" - elif self.rerandomize == "never": - self.rerandomize = "never" - else: - raise Exception("Invalid rerandomize attribute " + self.rerandomize) - if instance_state != None: instance_state = json.loads(instance_state) if instance_state != None and 'attempts' in instance_state: @@ -168,6 +158,21 @@ class CapaModule(XModule): else: raise + @property + def rerandomize(self): + """ + Property accessor that returns self.metadata['rerandomize'] in a canonical form + """ + rerandomize = self.metadata.get('rerandomize', 'always') + if rerandomize in ("", "always", "true"): + return "always" + elif rerandomize in ("false", "per_student"): + return "per_student" + elif rerandomize == "never": + return "never" + else: + raise Exception("Invalid rerandomize attribute " + rerandomize) + def get_instance_state(self): state = self.lcp.get_state() state['attempts'] = self.attempts @@ -221,7 +226,7 @@ class CapaModule(XModule): # User submitted a problem, and hasn't reset. We don't want # more submissions. - if self.lcp.done and self.metadata['rerandomize'] == "always": + if self.lcp.done and self.rerandomize == "always": check_button = False save_button = False From 7ac8fecb38582cfa0f7a73e8a223dfd08affe543 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 09:01:08 -0400 Subject: [PATCH 194/252] Rename XModule[Descriptor].type to .category to be parallel to Location --- cms/djangoapps/contentstore/views.py | 2 +- cms/templates/unit.html | 2 +- common/lib/xmodule/x_module.py | 4 ++-- lms/djangoapps/courseware/grades.py | 4 ++-- lms/djangoapps/courseware/module_render.py | 10 +++++----- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index b85e9c05bf..f7d5efe22a 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -22,7 +22,7 @@ def edit_item(request): return render_to_response('unit.html', { 'contents': item.get_html(), 'js_module': item.js_module_name(), - 'type': item.type, + 'category': item.category, 'name': item.name, }) diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 59044ab28d..34e21ca049 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -2,7 +2,7 @@

                  ${name}

                  -

                  ${type}

                  +

                  ${category}

                  diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 35cde8b6f4..f791e7f307 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -95,7 +95,7 @@ class XModule(object): self.shared_state = shared_state self.id = self.location.url() self.name = self.location.name - self.type = self.location.category + self.category = self.location.category self.metadata = kwargs.get('metadata', {}) self._loaded_children = None @@ -235,7 +235,7 @@ class XModuleDescriptor(Plugin): self.system = system self.definition = definition if definition is not None else {} self.name = Location(kwargs.get('location')).name - self.type = Location(kwargs.get('location')).category + self.category = Location(kwargs.get('location')).category self.url = Location(kwargs.get('location')).url() self.metadata = kwargs.get('metadata', {}) self.shared_state_key = kwargs.get('shared_state_key') diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index deab9d47d4..b9b89d6cd4 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -146,9 +146,9 @@ def get_score(user, problem, cache): correct = 0.0 # If the ID is not in the cache, add the item - instance_module = cache.lookup(problem.type, problem.id) + instance_module = cache.lookup(problem.category, problem.id) if instance_module is None: - instance_module = StudentModule(module_type=problem.type, + instance_module = StudentModule(module_type=problem.category, module_state_key=problem.id, student=user, state=None, diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 6cf4e43cc5..2d47a55248 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -207,10 +207,10 @@ def get_module(user, request, location, student_module_cache, position=None): ''' descriptor = keystore().get_item(location) - instance_module = student_module_cache.lookup(descriptor.type, descriptor.url) + instance_module = student_module_cache.lookup(descriptor.category, descriptor.url) shared_state_key = getattr(descriptor, 'shared_state_key', None) if shared_state_key is not None: - shared_module = student_module_cache.lookup(descriptor.type, shared_state_key) + shared_module = student_module_cache.lookup(descriptor.category, shared_state_key) else: shared_module = None @@ -246,7 +246,7 @@ def get_module(user, request, location, student_module_cache, position=None): if not instance_module: instance_module = StudentModule( student=user, - module_type=descriptor.type, + module_type=descriptor.category, module_state_key=module.id, state=module.get_instance_state(), max_grade=module.max_score()) @@ -257,13 +257,13 @@ def get_module(user, request, location, student_module_cache, position=None): if not shared_module and shared_state_key is not None: shared_module = StudentModule( student=user, - module_type=descriptor.type, + module_type=descriptor.category, module_state_key=shared_state_key, state=module.get_shared_state()) shared_module.save() student_module_cache.append(shared_module) - return (module, instance_module, shared_module, descriptor.type) + return (module, instance_module, shared_module, descriptor.category) def add_histogram(module): From 736148f21dd133359be1e593fe8230bcc47dd15f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 09:02:41 -0400 Subject: [PATCH 195/252] Adding clarifying comment about the contents of 'data' --- common/lib/xmodule/x_module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index f791e7f307..0b8fbb54d4 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -75,7 +75,10 @@ class XModule(object): system: An I4xSystem allowing access to external resources location: Something Location-like that identifies this xmodule definition: A dictionary containing 'data' and 'children'. Both are optional - 'data': is JSON-like (string, dictionary, list, bool, or None, optionally nested) + 'data': is JSON-like (string, dictionary, list, bool, or None, optionally nested). + This defines all of the data necessary for a problem to display that is intrinsic to the problem. + It should not include any data that would vary between two courses using the same problem + (due dates, grading policy, randomization, etc.) 'children': is a list of Location-like values for child modules that this module depends on instance_state: A string of serialized json that contains the state of this module for current student accessing the system, or None if no state has been saved From 7ed9b4aa89c5a0c09104d63715a2fdbe9d500a15 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 09:08:24 -0400 Subject: [PATCH 196/252] Add hashing and equality methods to Location --- common/lib/keystore/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 14716fbc2d..b204e487e2 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -101,9 +101,6 @@ class Location(object): if val is not None and INVALID_CHARS.search(val) is not None: raise InvalidLocationError(location) - def __str__(self): - return self.url() - def url(self): """ Return a string containing the URL for this location @@ -136,6 +133,19 @@ class Location(object): 'name': self.name, 'revision': self.revision} + def __str__(self): + return self.url() + + def __repr__(self): + return 'Location(%r)' % str(self) + + def __hash__(self): + return self.url() + + def __eq__(self, other): + return (isinstance(other, Location) and + str(self) == str(other)) + class ModuleStore(object): """ From 3cf29af8fe5521fc08abf6d88862b92fac6a5bc7 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 10:17:59 -0400 Subject: [PATCH 197/252] Make location into a named tuple, and use it more as a first class entity, rather than URL for identifying content --- .../management/commands/import.py | 6 +- cms/templates/widgets/navigation.html | 6 +- cms/templates/widgets/sequence-edit.html | 2 +- common/lib/keystore/__init__.py | 92 ++++++++----------- common/lib/keystore/tests/test_location.py | 15 ++- common/lib/keystore/xml.py | 4 +- common/lib/xmodule/abtest_module.py | 2 +- common/lib/xmodule/seq_module.py | 2 +- common/lib/xmodule/x_module.py | 4 +- lms/djangoapps/courseware/models.py | 3 +- lms/djangoapps/courseware/module_render.py | 4 +- lms/djangoapps/courseware/views.py | 2 +- 12 files changed, 70 insertions(+), 72 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index a7f95ea3c0..e24111dbb7 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -25,8 +25,8 @@ class Command(BaseCommand): module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor') for module in module_store.modules.itervalues(): - keystore().create_item(module.url) + keystore().create_item(module.location) if 'data' in module.definition: - keystore().update_item(module.url, module.definition['data']) + keystore().update_item(module.location, module.definition['data']) if 'children' in module.definition: - keystore().update_children(module.url, module.definition['children']) + keystore().update_children(module.location, module.definition['children']) diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 38b1cd9d94..bed0d1b4f8 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -38,7 +38,7 @@ % for week in weeks:
                • -

                  ${week.name}

                  +

                  ${week.name}

                    % if week.goals: % for goal in week.goals: @@ -52,8 +52,8 @@
                      % for module in week.get_children(): -
                    • - ${module.name} +
                    • + ${module.name} handle
                    • % endfor diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html index abeec9209d..319e137638 100644 --- a/cms/templates/widgets/sequence-edit.html +++ b/cms/templates/widgets/sequence-edit.html @@ -37,7 +37,7 @@
                        % for child in module.get_children():
                      1. - ${child.name} + ${child.name} handle
                      2. %endfor diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index b204e487e2..dab758fef1 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -4,6 +4,7 @@ that are stored in a database an accessible using their Location as an identifie """ import re +from collections import namedtuple from .exceptions import InvalidLocationError URL_RE = re.compile(""" @@ -17,8 +18,8 @@ URL_RE = re.compile(""" INVALID_CHARS = re.compile(r"[^\w.-]") - -class Location(object): +_LocationBase = namedtuple('LocationBase', 'tag org course category name revision') +class Location(_LocationBase): ''' Encodes a location. @@ -28,6 +29,7 @@ class Location(object): However, they can also be represented a dictionaries (specifying each component), tuples or list (specified in order), or as strings of the url ''' + __slots__ = () @classmethod def clean(cls, value): @@ -36,7 +38,7 @@ class Location(object): """ return re.sub('_+', '_', INVALID_CHARS.sub('_', value)) - def __init__(self, location): + def __new__(_cls, loc_or_tag, org=None, course=None, category=None, name=None, revision=None): """ Create a new location that is a clone of the specifed one. @@ -60,47 +62,50 @@ class Location(object): Components may be set to None, which may be interpreted by some contexts to mean wildcard selection """ - self.update(location) - def update(self, location): - """ - Update this instance with data from another Location object. + if org is None and course is None and category is None and name is None and revision is None: + location = loc_or_tag + else: + location = (loc_or_tag, org, course, category, name, revision) - location: can take the same forms as specified by `__init__` - """ - self.tag = self.org = self.course = self.category = self.name = self.revision = None + def check_dict(dict_): + check_list(dict_.values()) + + def check_list(list_): + for val in list_: + if val is not None and INVALID_CHARS.search(val) is not None: + raise InvalidLocationError(location) if isinstance(location, basestring): match = URL_RE.match(location) if match is None: raise InvalidLocationError(location) else: - self.update(match.groupdict()) - elif isinstance(location, list): + groups = match.groupdict() + check_dict(groups) + return _LocationBase.__new__(_cls, **groups) + elif isinstance(location, (list, tuple)): if len(location) not in (5, 6): raise InvalidLocationError(location) - (self.tag, self.org, self.course, self.category, self.name) = location[0:5] - self.revision = location[5] if len(location) == 6 else None + if len(location) == 5: + args = tuple(location) + (None, ) + else: + args = tuple(location) + + check_list(args) + return _LocationBase.__new__(_cls, *args) elif isinstance(location, dict): - try: - self.tag = location['tag'] - self.org = location['org'] - self.course = location['course'] - self.category = location['category'] - self.name = location['name'] - except KeyError: - raise InvalidLocationError(location) - self.revision = location.get('revision') + kwargs = dict(location) + kwargs.setdefault('revision', None) + + check_dict(kwargs) + return _LocationBase.__new__(_cls, **kwargs) elif isinstance(location, Location): - self.update(location.list()) + return _LocationBase.__new__(_cls, location) else: raise InvalidLocationError(location) - for val in self.list(): - if val is not None and INVALID_CHARS.search(val) is not None: - raise InvalidLocationError(location) - def url(self): """ Return a string containing the URL for this location @@ -114,38 +119,19 @@ class Location(object): """ Return a string with a version of the location that is safe for use in html id attributes """ - return "-".join(str(v) for v in self.list() if v is not None) - - def list(self): - """ - Return a list representing this location - """ - return [self.tag, self.org, self.course, self.category, self.name, self.revision] + return "-".join(str(v) for v in self if v is not None) def dict(self): - """ - Return a dictionary representing this location - """ - return {'tag': self.tag, - 'org': self.org, - 'course': self.course, - 'category': self.category, - 'name': self.name, - 'revision': self.revision} + return self.__dict__ + + def list(self): + return list(self) def __str__(self): return self.url() def __repr__(self): - return 'Location(%r)' % str(self) - - def __hash__(self): - return self.url() - - def __eq__(self, other): - return (isinstance(other, Location) and - str(self) == str(other)) - + return "Location%r" % tuple(self) class ModuleStore(object): """ diff --git a/common/lib/keystore/tests/test_location.py b/common/lib/keystore/tests/test_location.py index f10f03c0b0..01d36d946b 100644 --- a/common/lib/keystore/tests/test_location.py +++ b/common/lib/keystore/tests/test_location.py @@ -1,4 +1,4 @@ -from nose.tools import assert_equals, assert_raises +from nose.tools import assert_equals, assert_raises, assert_not_equals from keystore import Location from keystore.exceptions import InvalidLocationError @@ -11,7 +11,6 @@ def check_string_roundtrip(url): def test_string_roundtrip(): check_string_roundtrip("tag://org/course/category/name") check_string_roundtrip("tag://org/course/category/name/revision") - check_string_roundtrip("tag://org/course/category/name with spaces/revision") def test_dict(): @@ -50,3 +49,15 @@ def test_invalid_locations(): assert_raises(InvalidLocationError, Location, ["foo", "bar"]) assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"]) assert_raises(InvalidLocationError, Location, None) + assert_raises(InvalidLocationError, Location, "tag://org/course/category/name with spaces/revision") + +def test_equality(): + assert_equals( + Location('tag', 'org', 'course', 'category', 'name'), + Location('tag', 'org', 'course', 'category', 'name') + ) + + assert_not_equals( + Location('tag', 'org', 'course', 'category', 'name1'), + Location('tag', 'org', 'course', 'category', 'name') + ) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index c3327b8ff3..e7adb56ad6 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -47,7 +47,7 @@ class XMLModuleStore(ModuleStore): xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class) - modulestore.modules[module.url] = module + modulestore.modules[module.location] = module return module XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml) @@ -68,7 +68,7 @@ class XMLModuleStore(ModuleStore): """ location = Location(location) try: - return self.modules[location.url()] + return self.modules[location] except KeyError: raise ItemNotFoundError(location) diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index 3bd268184a..beaeb4ad1c 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -102,7 +102,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ) child_content_urls = [ - system.process_xml(etree.tostring(child)).url + system.process_xml(etree.tostring(child)).location.url() for child in group ] diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index b11d540143..6d493b96ad 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -105,6 +105,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): return {'children': [ - system.process_xml(etree.tostring(child_module)).url + system.process_xml(etree.tostring(child_module)).location.url() for child_module in xml_object ]} diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 0b8fbb54d4..191cda6b06 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -239,7 +239,7 @@ class XModuleDescriptor(Plugin): self.definition = definition if definition is not None else {} self.name = Location(kwargs.get('location')).name self.category = Location(kwargs.get('location')).category - self.url = Location(kwargs.get('location')).url() + self.location = Location(kwargs.get('location')) self.metadata = kwargs.get('metadata', {}) self.shared_state_key = kwargs.get('shared_state_key') @@ -364,7 +364,7 @@ class XModuleDescriptor(Plugin): return partial( self.module_class, system, - self.url, + self.location, self.definition, metadata=self.metadata ) diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py index 262d177248..f0bd8dc17e 100644 --- a/lms/djangoapps/courseware/models.py +++ b/lms/djangoapps/courseware/models.py @@ -85,6 +85,7 @@ class StudentModuleCache(object): student=user, module_state_key__in=id_chunk) ) + else: self.cache = [] @@ -93,7 +94,7 @@ class StudentModuleCache(object): Get a list of the state_keys needed for StudentModules required for this chunk of module xml ''' - keys = [descriptor.url] + keys = [descriptor.location.url()] shared_state_key = getattr(descriptor, 'shared_state_key', None) if shared_state_key is not None: diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 2d47a55248..5119cc2910 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -207,7 +207,7 @@ def get_module(user, request, location, student_module_cache, position=None): ''' descriptor = keystore().get_item(location) - instance_module = student_module_cache.lookup(descriptor.category, descriptor.url) + instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url()) shared_state_key = getattr(descriptor, 'shared_state_key', None) if shared_state_key is not None: shared_module = student_module_cache.lookup(descriptor.category, shared_state_key) @@ -218,7 +218,7 @@ def get_module(user, request, location, student_module_cache, position=None): shared_state = shared_module.state if shared_module is not None else None # Setup system context for module instance - ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.url + '/' + ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/' def _get_module(location): (module, _, _, _) = get_module(user, request, location, student_module_cache, position) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index e1e1c16632..48e9bcc795 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -209,7 +209,7 @@ def index(request, course=None, chapter=None, section=None, course_location = multicourse_settings.get_course_location(course) section = get_section(course_location, chapter, section) student_module_cache = StudentModuleCache(request.user, section) - module, _, _, _ = get_module(request.user, request, section.url, student_module_cache) + module, _, _, _ = get_module(request.user, request, section.location, student_module_cache) context['content'] = module.get_html() result = render_to_response('courseware.html', context) From e0e42ae8aca464ea6802fda5c84bc4feabfe3fe0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 10:18:24 -0400 Subject: [PATCH 198/252] Fix up cms after changing html and sequence descriptors to use metadata --- cms/templates/widgets/html-edit.html | 4 ++-- cms/templates/widgets/navigation.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index 7eec86215a..666aa1de81 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -33,8 +33,8 @@
              • - -
                ${module.definition['data']['text']}
                + +
                ${module.definition['data']}
                Save & Update diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index bed0d1b4f8..ea158d305a 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -40,8 +40,8 @@

                ${week.name}

                  - % if week.goals: - % for goal in week.goals: + % if 'goals' in week.metadata: + % for goal in week.metadata['goals']:
                • ${goal}
                • % endfor % else: From 312dda760edeff2d68474f6cad895623f07cf6a0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 10:25:25 -0400 Subject: [PATCH 199/252] Fixing repr for Locations --- common/lib/keystore/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index dab758fef1..0671e7e568 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -131,7 +131,8 @@ class Location(_LocationBase): return self.url() def __repr__(self): - return "Location%r" % tuple(self) + return "Location%s" % repr(tuple(self)) + class ModuleStore(object): """ From a7d0e2e12236cb6f0ca2a86423c6f94bff872858 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 13:29:04 -0400 Subject: [PATCH 200/252] Use a string key for default groups in abtests (and remove code that was expecting groups from django) --- common/lib/xmodule/abtest_module.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index beaeb4ad1c..648082bf94 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -7,6 +7,8 @@ from xmodule.raw_module import RawDescriptor from xmodule.xml_module import XmlDescriptor from xmodule.exceptions import InvalidDefinitionError +DEFAULT = "_DEFAULT_GROUP" + def group_from_value(groups, v): ''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value @@ -39,7 +41,6 @@ class ABTestModule(XModule): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - target_groups = self.definition['data'].keys() if shared_state is None: self.group = group_from_value( @@ -48,18 +49,7 @@ class ABTestModule(XModule): ) else: shared_state = json.loads(shared_state) - - # TODO (cpennington): Remove this once we aren't passing in - # groups from django groups - if 'groups' in shared_state: - self.group = None - target_names = [elem.get('name') for elem in target_groups] - for group in shared_state['groups']: - if group in target_names: - self.group = group - break - else: - self.group = shared_state['group'] + self.group = shared_state['group'] def get_shared_state(self): print self.group @@ -89,12 +79,12 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): 'data': { 'experiment': experiment, 'group_portions': [], - 'group_content': {None: []}, + 'group_content': {DEFAULT: []}, }, 'children': []} for group in xml_object: if group.tag == 'default': - name = None + name = DEFAULT else: name = group.get('name') definition['data']['group_portions'].append( @@ -113,6 +103,6 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): if default_portion < 0: raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") - definition['data']['group_portions'].append((None, default_portion)) + definition['data']['group_portions'].append((DEFAULT, default_portion)) return definition From 520fac1aa2d27b7f04e442b40b6e654f9e9f6a6a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 13:40:06 -0400 Subject: [PATCH 201/252] Enforce an index over the location key in mongo --- common/lib/keystore/mongo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index 20c4ffde1a..4c50b634ea 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -15,6 +15,7 @@ class MongoModuleStore(ModuleStore): host=host, port=port )[db][collection] + self.collection.ensure_index('location') # Force mongo to report errors, at the expense of performance self.collection.safe = True From f859457037577833fd784eed430d25bd8cccd745 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 13:58:07 -0400 Subject: [PATCH 202/252] Cache loaded plugins in memory --- common/lib/xmodule/x_module.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 191cda6b06..f628abe5f1 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -23,6 +23,9 @@ class Plugin(object): entry_point: The name of the entry point to load plugins from """ + + _plugin_cache = None + @classmethod def load_class(cls, identifier, default=None): """ @@ -33,20 +36,25 @@ class Plugin(object): If default is not None, will return default if no entry_point matching identifier is found. Otherwise, will raise a ModuleMissingError """ - identifier = identifier.lower() - classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) - if len(classes) > 1: - log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( - entry_point=cls.entry_point, - id=identifier, - classes=", ".join(class_.module_name for class_ in classes))) + if cls._plugin_cache is None: + cls._plugin_cache = {} - if len(classes) == 0: - if default is not None: - return default - raise ModuleMissingError(identifier) + if identifier not in cls._plugin_cache: + identifier = identifier.lower() + classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) + if len(classes) > 1: + log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( + entry_point=cls.entry_point, + id=identifier, + classes=", ".join(class_.module_name for class_ in classes))) - return classes[0].load() + if len(classes) == 0: + if default is not None: + return default + raise ModuleMissingError(identifier) + + cls._plugin_cache[identifier] = classes[0].load() + return cls._plugin_cache[identifier] @classmethod def load_classes(cls): From 51a790173f370d1240034e5ae033540e63178ea0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:03:17 -0400 Subject: [PATCH 203/252] Only set the default etree parser options in the module that is starting the xml parsing --- cms/djangoapps/contentstore/management/commands/import.py | 4 ---- lms/djangoapps/courseware/views.py | 5 ----- 2 files changed, 9 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index e24111dbb7..12806debb7 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -4,14 +4,10 @@ from django.core.management.base import BaseCommand, CommandError from keystore.django import keystore -from lxml import etree from keystore.xml import XMLModuleStore unnamed_modules = 0 -etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True)) - class Command(BaseCommand): help = \ diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 48e9bcc795..52a86fdee4 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -12,8 +12,6 @@ from mitxmako.shortcuts import render_to_response, render_to_string from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control -from lxml import etree - from module_render import toc_for_course, get_module, get_section from models import StudentModuleCache from student.models import UserProfile @@ -26,9 +24,6 @@ from courseware import grades log = logging.getLogger("mitx.courseware") -etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True)) - template_imports = {'urllib': urllib} From 7b89b1eb54ae66cbdb97254db364832a43ceafa0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:04:45 -0400 Subject: [PATCH 204/252] Add ability to update modulestore metadata for a module separately from data or children --- .../contentstore/management/commands/import.py | 1 + common/lib/keystore/__init__.py | 12 +++++++++++- common/lib/keystore/mongo.py | 18 +++++++++++++++++- common/lib/keystore/xml.py | 10 ++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 12806debb7..bb9697d6a1 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -26,3 +26,4 @@ class Command(BaseCommand): keystore().update_item(module.location, module.definition['data']) if 'children' in module.definition: keystore().update_children(module.location, module.definition['children']) + keystore().update_metadata(module.url, module.metadata) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 0671e7e568..43ffa464ff 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -171,9 +171,19 @@ class ModuleStore(object): def update_children(self, location, children): """ Set the children for the item specified by the location to - data + children location: Something that can be passed to Location children: A list of child item identifiers """ raise NotImplementedError + + def update_metadata(self, location, metadata): + """ + Set the metadata for the item specified by the location to + metadata + + location: Something that can be passed to Location + metadata: A nested dictionary of module metadata + """ + raise NotImplementedError diff --git a/common/lib/keystore/mongo.py b/common/lib/keystore/mongo.py index 4c50b634ea..d92782600c 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/keystore/mongo.py @@ -85,7 +85,7 @@ class MongoModuleStore(ModuleStore): def update_children(self, location, children): """ Set the children for the item specified by the location to - data + children location: Something that can be passed to Location children: A list of child item identifiers @@ -97,3 +97,19 @@ class MongoModuleStore(ModuleStore): {'location': Location(location).dict()}, {'$set': {'definition.children': children}} ) + + def update_metadata(self, location, metadata): + """ + Set the children for the item specified by the location to + metadata + + location: Something that can be passed to Location + metadata: A nested dictionary of module metadata + """ + + # See http://www.mongodb.org/display/DOCS/Updating for + # atomic update syntax + self.collection.update( + {'location': Location(location).dict()}, + {'$set': {'metadata': metadata}} + ) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index e7adb56ad6..d475077733 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -94,3 +94,13 @@ class XMLModuleStore(ModuleStore): children: A list of child item identifiers """ raise NotImplementedError("XMLModuleStores are read-only") + + def update_metadata(self, location, metadata): + """ + Set the metadata for the item specified by the location to + metadata + + location: Something that can be passed to Location + metadata: A nested dictionary of module metadata + """ + raise NotImplementedError("XMLModuleStores are read-only") From 8a64029b079c4ac98b62a1be47bff2e1f57c1b89 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:05:03 -0400 Subject: [PATCH 205/252] Remove blank text nodes during xml parsing --- common/lib/keystore/xml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index d475077733..0672e4a7ff 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -9,7 +9,7 @@ from . import ModuleStore, Location from .exceptions import ItemNotFoundError etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, - remove_comments=True)) + remove_comments=True, remove_blank_text=True)) log = logging.getLogger(__name__) From b7062ca5ca4f3f80e626479de2dffaa878a62dde Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:05:20 -0400 Subject: [PATCH 206/252] Only set the xml slug if it isn't already set --- common/lib/keystore/xml.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index 0672e4a7ff..b078474a00 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -40,11 +40,12 @@ class XMLModuleStore(ModuleStore): except: log.exception("Unable to parse xml: {xml}".format(xml=xml)) raise - if xml_data.get('name'): - xml_data.set('slug', Location.clean(xml_data.get('name'))) - else: - self.unnamed_modules += 1 - xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) + if xml_data.get('slug') is None: + if xml_data.get('name'): + xml_data.set('slug', Location.clean(xml_data.get('name'))) + else: + self.unnamed_modules += 1 + xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class) modulestore.modules[module.location] = module From 5b8c3dc1e4663a308eb0166a6a1ace893382fe40 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:06:23 -0400 Subject: [PATCH 207/252] Make html a RawDescriptor with a slightly different UI --- cms/templates/widgets/html-edit.html | 4 ++-- common/lib/xmodule/html_module.py | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html index 666aa1de81..f0f63ea905 100644 --- a/cms/templates/widgets/html-edit.html +++ b/cms/templates/widgets/html-edit.html @@ -33,8 +33,8 @@
                - -
                ${module.definition['data']}
                + +
                ${data}
                Save & Update diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index 32963600cd..307b1309e4 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -2,8 +2,7 @@ import json import logging from xmodule.x_module import XModule -from xmodule.mako_module import MakoModuleDescriptor -from xmodule.xml_module import XmlDescriptor +from xmodule.raw_module import RawDescriptor from lxml import etree from pkg_resources import resource_string @@ -19,7 +18,7 @@ class HtmlModule(XModule): self.html = self.definition['data']['text'] -class HtmlDescriptor(MakoModuleDescriptor, XmlDescriptor): +class HtmlDescriptor(RawDescriptor): """ Module for putting raw html in a course """ @@ -28,7 +27,3 @@ class HtmlDescriptor(MakoModuleDescriptor, XmlDescriptor): js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js_module = 'HTML' - - @classmethod - def definition_from_xml(cls, xml_object, system): - return {'data': {'text': etree.tostring(xml_object)}} From ada152758dbc5a164b11dc77f1f14e7d92fc8b8e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:07:06 -0400 Subject: [PATCH 208/252] Make abtest store group portions as a dictionary --- common/lib/xmodule/abtest_module.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index 648082bf94..0571cda4da 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -44,7 +44,7 @@ class ABTestModule(XModule): if shared_state is None: self.group = group_from_value( - self.definition['data']['group_portions'], + self.definition['data']['group_portions'].items(), random.uniform(0, 1) ) else: @@ -52,7 +52,6 @@ class ABTestModule(XModule): self.group = shared_state['group'] def get_shared_state(self): - print self.group return json.dumps({'group': self.group}) def displayable_items(self): @@ -78,7 +77,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): definition = { 'data': { 'experiment': experiment, - 'group_portions': [], + 'group_portions': {}, 'group_content': {DEFAULT: []}, }, 'children': []} @@ -87,9 +86,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): name = DEFAULT else: name = group.get('name') - definition['data']['group_portions'].append( - (name, float(group.get('portion', 0))) - ) + definition['data']['group_portions'][name] = float(group.get('portion', 0)) child_content_urls = [ system.process_xml(etree.tostring(child)).location.url() @@ -99,10 +96,10 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): definition['data']['group_content'][name] = child_content_urls definition['children'].extend(child_content_urls) - default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions']) + default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions'].items()) if default_portion < 0: raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") - definition['data']['group_portions'].append((DEFAULT, default_portion)) + definition['data']['group_portions'][DEFAULT] = default_portion return definition From c6d5eea841aab2a65de73b3ceb635245b803d518 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:07:22 -0400 Subject: [PATCH 209/252] Fix typo --- common/lib/xmodule/x_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index f628abe5f1..91d1bce1f4 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -29,7 +29,7 @@ class Plugin(object): @classmethod def load_class(cls, identifier, default=None): """ - Loads a single class intance specified by identifier. If identifier + Loads a single class instance specified by identifier. If identifier specifies more than a single class, then logs a warning and returns the first class identified. From a94e4d2f1b43e69c466e26bfa0e73ff1e8e30bed Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:07:51 -0400 Subject: [PATCH 210/252] Rearrange x_module definition into sections --- common/lib/xmodule/x_module.py | 90 ++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 91d1bce1f4..a2019cd5bf 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -213,6 +213,7 @@ class XModuleDescriptor(Plugin): # A list of metadata that this module can inherit from its parent module inheritable_metadata = ('graded', 'due', 'graceperiod', 'showanswer', 'rerandomize') + # ============================= STRUCTURAL MANIPULATION =========================== def __init__(self, system, definition=None, @@ -253,6 +254,43 @@ class XModuleDescriptor(Plugin): self._child_instances = None + def inherit_metadata(self, metadata): + """ + Updates this module with metadata inherited from a containing module. + Only metadata specified in self.inheritable_metadata will + be inherited + """ + # Set all inheritable metadata from kwargs that are + # in self.inheritable_metadata and aren't already set in metadata + for attr in self.inheritable_metadata: + if attr not in self.metadata and attr in metadata: + self.metadata[attr] = metadata[attr] + + def get_children(self): + """Returns a list of XModuleDescriptor instances for the children of this module""" + if self._child_instances is None: + self._child_instances = [] + for child_loc in self.definition.get('children', []): + child = self.system.load_item(child_loc) + child.inherit_metadata(self.metadata) + self._child_instances.append(child) + + return self._child_instances + + def xmodule_constructor(self, system): + """ + Returns a constructor for an XModule. This constructor takes two arguments: + instance_state and shared_state, and returns a fully nstantiated XModule + """ + return partial( + self.module_class, + system, + self.location, + self.definition, + metadata=self.metadata + ) + + # ================================= JSON PARSING =================================== @staticmethod def load_from_json(json_data, system, default_class=None): """ @@ -280,6 +318,7 @@ class XModuleDescriptor(Plugin): """ return cls(system=system, **json_data) + # ================================= XML PARSING ==================================== @staticmethod def load_from_xml(xml_data, system, @@ -315,6 +354,20 @@ class XModuleDescriptor(Plugin): """ raise NotImplementedError('Modules must implement from_xml to be parsable from xml') + def export_to_xml(self, resource_fs): + """ + Returns an xml string representing this module, and all modules underneath it. + May also write required resources out to resource_fs + + Assumes that modules have single parantage (that no module appears twice in the same course), + and that it is thus safe to nest modules as xml children as appropriate. + + The returned XML should be able to be parsed back into an identical XModuleDescriptor + using the from_xml method with the same system, org, and course + """ + raise NotImplementedError('Modules must implement export_to_xml to enable xml export') + + # ================================== HTML INTERFACE DEFINITIONS ====================== @classmethod def get_javascript(cls): """ @@ -334,49 +387,12 @@ class XModuleDescriptor(Plugin): """ return self.js_module - - def inherit_metadata(self, metadata): - """ - Updates this module with metadata inherited from a containing module. - Only metadata specified in self.inheritable_metadata will - be inherited - """ - # Set all inheritable metadata from kwargs that are - # in self.inheritable_metadata and aren't already set in metadata - for attr in self.inheritable_metadata: - if attr not in self.metadata and attr in metadata: - self.metadata[attr] = metadata[attr] - - def get_children(self): - """Returns a list of XModuleDescriptor instances for the children of this module""" - if self._child_instances is None: - self._child_instances = [] - for child_loc in self.definition.get('children', []): - child = self.system.load_item(child_loc) - child.inherit_metadata(self.metadata) - self._child_instances.append(child) - - return self._child_instances - def get_html(self): """ Return the html used to edit this module """ raise NotImplementedError("get_html() must be provided by specific modules") - def xmodule_constructor(self, system): - """ - Returns a constructor for an XModule. This constructor takes two arguments: - instance_state and shared_state, and returns a fully nstantiated XModule - """ - return partial( - self.module_class, - system, - self.location, - self.definition, - metadata=self.metadata - ) - class DescriptorSystem(object): def __init__(self, load_item, resources_fs): From f375258b38bd99eaf8914e958e551f8c83ca581a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:08:15 -0400 Subject: [PATCH 211/252] Add xml export infrastructure for all existing modules --- common/lib/xmodule/abtest_module.py | 18 ++++++++++++++++ common/lib/xmodule/raw_module.py | 3 +++ common/lib/xmodule/seq_module.py | 6 ++++++ common/lib/xmodule/xml_module.py | 33 +++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index 0571cda4da..f6057171e5 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -103,3 +103,21 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): definition['data']['group_portions'][DEFAULT] = default_portion return definition + + def definition_to_xml(self, resource_fs): + xml_object = etree.Element('abtest') + xml_object.set('experiment', self.definition['data']['experiment']) + for name, group in self.definition['data']['group_content'].items(): + if name == DEFAULT: + group_elem = etree.SubElement(xml_object, 'default') + else: + group_elem = etree.SubElement(xml_object, 'group', attrib={ + 'portion': str(self.definition['data']['group_portions'][name]), + 'name': name, + }) + + for child_loc in group: + child = self.system.load_item(child_loc) + group_elem.append(etree.fromstring(child.export_to_xml(resource_fs))) + + return xml_object diff --git a/common/lib/xmodule/raw_module.py b/common/lib/xmodule/raw_module.py index 43a92303ad..59f28ff4f0 100644 --- a/common/lib/xmodule/raw_module.py +++ b/common/lib/xmodule/raw_module.py @@ -21,3 +21,6 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): @classmethod def definition_from_xml(cls, xml_object, system): return {'data': etree.tostring(xml_object)} + + def definition_to_xml(self, resource_fs): + return etree.fromstring(self.definition['data']) diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index 6d493b96ad..9f00c3be87 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -108,3 +108,9 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): system.process_xml(etree.tostring(child_module)).location.url() for child_module in xml_object ]} + + def definition_to_xml(self, resource_fs): + xml_object = etree.Element('sequential') + for child in self.get_children(): + xml_object.append(etree.fromstring(child.export_to_xml(resource_fs))) + return xml_object diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index b167e52e88..d6338aeb39 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -51,3 +51,36 @@ class XmlDescriptor(XModuleDescriptor): xml_object.get('slug')], metadata=metadata, ) + + def export_to_xml(self, resource_fs): + """ + Returns an xml string representing this module, and all modules underneath it. + May also write required resources out to resource_fs + + Assumes that modules have single parantage (that no module appears twice in the same course), + and that it is thus safe to nest modules as xml children as appropriate. + + The returned XML should be able to be parsed back into an identical XModuleDescriptor + using the from_xml method with the same system, org, and course + """ + xml_object = self.definition_to_xml(resource_fs) + xml_object.set('slug', self.name) + xml_object.tag = self.type + + for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'): + if attr in self.metadata: + xml_object.set(attr, self.metadata[attr]) + + if 'graded' in self.metadata: + xml_object.set('graded', str(self.metadata['graded']).lower()) + + if 'display_name' in self.metadata: + xml_object.set('name', self.metadata['display_name']) + + return etree.tostring(xml_object, pretty_print=True) + + def definition_to_xml(self, resource_fs): + """ + Return a new etree Element object created from this modules definition. + """ + raise NotImplementedError("%s does not implement definition_to_xml" % self.__class__.__name__) From 1e8acbefac409c125ddc7b26ef5979727430a3b0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:08:35 -0400 Subject: [PATCH 212/252] Add a temporary url for testing xml export triggering --- cms/djangoapps/contentstore/views.py | 14 ++++++++++++++ cms/urls.py | 1 + 2 files changed, 15 insertions(+) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index f7d5efe22a..d9036515a9 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -4,6 +4,7 @@ from django_future.csrf import ensure_csrf_cookie from django.http import HttpResponse import json +from fs.osfs import OSFS @ensure_csrf_cookie def index(request): @@ -32,3 +33,16 @@ def save_item(request): data = json.loads(request.POST['data']) keystore().update_item(item_id, data) return HttpResponse(json.dumps({})) + + +def temp_force_export(request): + org = 'mit.edu' + course = '6002xs12' + name = '6.002_Spring_2012' + course = keystore().get_item(['i4x', org, course, 'course', name]) + fs = OSFS('../data-export-test') + xml = course.export_to_xml(fs) + with fs.open('course.xml', 'w') as course_xml: + course_xml.write(xml) + + return HttpResponse('Done') diff --git a/cms/urls.py b/cms/urls.py index d7314aafae..9d827c3fe3 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -8,4 +8,5 @@ urlpatterns = patterns('', url(r'^$', 'contentstore.views.index', name='index'), url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), + url(r'^temp_force_export$', 'contentstore.views.temp_force_export') ) From 2f95146b9be2cf31f5881b2feec1ac342bfbbcb9 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:08:54 -0400 Subject: [PATCH 213/252] Just use the class name when complaining about definition_from_xml not being implemented --- common/lib/xmodule/xml_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index d6338aeb39..fa275c67d3 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -13,7 +13,7 @@ class XmlDescriptor(XModuleDescriptor): Return the definition to be passed to the newly created descriptor during from_xml """ - raise NotImplementedError("%s does not implement definition_from_xml" % cls.__class__.__name__) + raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__) @classmethod def from_xml(cls, xml_data, system, org=None, course=None): From b9dd30cd58b1657f45c65315385c897fa88761ac Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 16:26:04 -0400 Subject: [PATCH 214/252] Don't dump inherited metadata when exporting xml --- common/lib/xmodule/x_module.py | 2 ++ common/lib/xmodule/xml_module.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index a2019cd5bf..8367120748 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -253,6 +253,7 @@ class XModuleDescriptor(Plugin): self.shared_state_key = kwargs.get('shared_state_key') self._child_instances = None + self._inherited_metadata = set() def inherit_metadata(self, metadata): """ @@ -264,6 +265,7 @@ class XModuleDescriptor(Plugin): # in self.inheritable_metadata and aren't already set in metadata for attr in self.inheritable_metadata: if attr not in self.metadata and attr in metadata: + self._inherited_metadata.add(attr) self.metadata[attr] = metadata[attr] def get_children(self): diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index fa275c67d3..6639a77d3e 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -68,10 +68,10 @@ class XmlDescriptor(XModuleDescriptor): xml_object.tag = self.type for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'): - if attr in self.metadata: + if attr in self.metadata and attr not in self._inherited_metadata: xml_object.set(attr, self.metadata[attr]) - if 'graded' in self.metadata: + if 'graded' in self.metadata and 'graded' not in self._inherited_metadata: xml_object.set('graded', str(self.metadata['graded']).lower()) if 'display_name' in self.metadata: From d95be5aa24e85937253025b4ee47d326e2d1d778 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 19:32:48 -0400 Subject: [PATCH 215/252] Fix html rendering after making it a RawDescriptor --- common/lib/xmodule/html_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index 307b1309e4..cf45ec3a18 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -15,7 +15,7 @@ class HtmlModule(XModule): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) - self.html = self.definition['data']['text'] + self.html = self.definition['data'] class HtmlDescriptor(RawDescriptor): From 1d4e1d55d5b172eb00e817a9bc9627528d178a56 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 22:08:17 -0400 Subject: [PATCH 216/252] Enforce location uniqueness in xml keystore --- common/lib/keystore/xml.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index b078474a00..baa54c4248 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -33,6 +33,7 @@ class XMLModuleStore(ModuleStore): modulestore: the XMLModuleStore to store the loaded modules in """ self.unnamed_modules = 0 + self.used_slugs = set() def process_xml(xml): try: @@ -42,10 +43,17 @@ class XMLModuleStore(ModuleStore): raise if xml_data.get('slug') is None: if xml_data.get('name'): - xml_data.set('slug', Location.clean(xml_data.get('name'))) + slug = Location.clean(xml_data.get('name')) else: self.unnamed_modules += 1 - xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) + slug = '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules) + + if slug in self.used_slugs: + self.unnamed_modules += 1 + slug = '{slug}_{count}'.format(slug=slug, count=self.unnamed_modules) + + self.used_slugs.add(slug) + xml_data.set('slug', slug) module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class) modulestore.modules[module.location] = module From 79987666df6955d7b445c7201b8b77a380194b2b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 22:26:11 -0400 Subject: [PATCH 217/252] Lazily load module definition and metadata as needed, rather than immediately --- .../management/commands/import.py | 4 +- common/lib/keystore/xml.py | 13 +++- common/lib/xmodule/xml_module.py | 74 ++++++++++++++++--- lms/djangoapps/courseware/module_render.py | 7 +- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index bb9697d6a1..75f59a0ef6 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -19,11 +19,11 @@ class Command(BaseCommand): org, course, data_dir = args - module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor') + module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor', eager=True) for module in module_store.modules.itervalues(): keystore().create_item(module.location) if 'data' in module.definition: keystore().update_item(module.location, module.definition['data']) if 'children' in module.definition: keystore().update_children(module.location, module.definition['children']) - keystore().update_metadata(module.url, module.metadata) + keystore().update_metadata(module.location, dict(module.metadata)) diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py index baa54c4248..52eaf0ce0f 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/keystore/xml.py @@ -18,7 +18,15 @@ class XMLModuleStore(ModuleStore): """ An XML backed ModuleStore """ - def __init__(self, org, course, data_dir, default_class=None): + def __init__(self, org, course, data_dir, default_class=None, eager=False): + """ + Initialize an XMLModuleStore from data_dir + + org, course: Strings to be used in module keys + data_dir: path to data directory containing course.xml + default_class: dot-separated string defining the default descriptor class to use if non is specified in entry_points + eager: If true, load the modules children immediately to force the entire course tree to be parsed + """ self.data_dir = path(data_dir) self.modules = {} @@ -57,6 +65,9 @@ class XMLModuleStore(ModuleStore): module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class) modulestore.modules[module.location] = module + + if eager: + module.get_children() return module XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml) diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index 6639a77d3e..a224e4391d 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -1,7 +1,56 @@ +from collections import MutableMapping from xmodule.x_module import XModuleDescriptor from lxml import etree +class LazyLoadingDict(MutableMapping): + """ + A dictionary object that lazily loads it's contents from a provided + function on reads (of members that haven't already been set) + """ + + def __init__(self, loader): + self._contents = {} + self._loaded = False + self._loader = loader + self._deleted = set() + + def __getitem__(self, name): + if not (self._loaded or name in self._contents or name in self._deleted): + self.load() + + return self._contents[name] + + def __setitem__(self, name, value): + self._contents[name] = value + self._deleted.discard(name) + + def __delitem__(self, name): + del self._contents[name] + self._deleted.add(name) + + def __contains__(self, name): + self.load() + return name in self._contents + + def __len__(self): + self.load() + return len(self._contents) + + def __iter__(self): + self.load() + return iter(self._contents) + + def load(self): + if self._loaded: + return + + loaded_contents = self._loader() + loaded_contents.update(self._contents) + self._contents = loaded_contents + self._loaded = True + + class XmlDescriptor(XModuleDescriptor): """ Mixin class for standardized parsing of from xml @@ -29,27 +78,30 @@ class XmlDescriptor(XModuleDescriptor): """ xml_object = etree.fromstring(xml_data) - metadata = {} - for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'): - from_xml = xml_object.get(attr) - if from_xml is not None: - metadata[attr] = from_xml + def metadata_loader(): + metadata = {} + for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'): + from_xml = xml_object.get(attr) + if from_xml is not None: + metadata[attr] = from_xml - if xml_object.get('graded') is not None: - metadata['graded'] = xml_object.get('graded') == 'true' + if xml_object.get('graded') is not None: + metadata['graded'] = xml_object.get('graded') == 'true' - if xml_object.get('name') is not None: - metadata['display_name'] = xml_object.get('name') + if xml_object.get('name') is not None: + metadata['display_name'] = xml_object.get('name') + + return metadata return cls( system, - cls.definition_from_xml(xml_object, system), + LazyLoadingDict(lambda: cls.definition_from_xml(xml_object, system)), location=['i4x', org, course, xml_object.tag, xml_object.get('slug')], - metadata=metadata, + metadata=LazyLoadingDict(metadata_loader), ) def export_to_xml(self, resource_fs): diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 5119cc2910..4e5ee62e63 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -273,8 +273,11 @@ def add_histogram(module): module_id = module.id histogram = grade_histogram(module_id) render_histogram = len(histogram) > 0 - staff_context = {'definition': json.dumps(module.definition, indent=4), - 'metadata': json.dumps(module.metadata, indent=4), + + # Cast module.definition and module.metadata to dicts so that json can dump them + # even though they are lazily loaded + staff_context = {'definition': json.dumps(dict(module.definition), indent=4), + 'metadata': json.dumps(dict(module.metadata), indent=4), 'element_id': module.location.html_id(), 'histogram': json.dumps(histogram), 'render_histogram': render_histogram, From 9c715b60a60801c60dc270697b4b009491a4f006 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 22:42:33 -0400 Subject: [PATCH 218/252] Fix broken element ids for modules with .s in their names --- common/lib/keystore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/keystore/__init__.py b/common/lib/keystore/__init__.py index 43ffa464ff..13ff322e83 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/keystore/__init__.py @@ -119,7 +119,7 @@ class Location(_LocationBase): """ Return a string with a version of the location that is safe for use in html id attributes """ - return "-".join(str(v) for v in self if v is not None) + return "-".join(str(v) for v in self.list() if v is not None).replace('.', '_') def dict(self): return self.__dict__ From bacce3e65625b733cdcca3960e4241d481240730 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Fri, 29 Jun 2012 23:29:36 -0400 Subject: [PATCH 219/252] Load module contents from the file specified by the filename attribute --- common/lib/xmodule/html_module.py | 5 +++++ common/lib/xmodule/xml_module.py | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index cf45ec3a18..aac97b5fd3 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -24,6 +24,11 @@ class HtmlDescriptor(RawDescriptor): """ mako_template = "widgets/html-edit.html" module_class = HtmlModule + filename_extension = "html" js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js_module = 'HTML' + + @classmethod + def definition_from_file(cls, file, system): + return {'data': file.read()} diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index a224e4391d..79b90c2003 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -56,14 +56,29 @@ class XmlDescriptor(XModuleDescriptor): Mixin class for standardized parsing of from xml """ + # Extension to append to filename paths + filename_extension = 'xml' + @classmethod def definition_from_xml(cls, xml_object, system): """ Return the definition to be passed to the newly created descriptor during from_xml + + xml_object: An etree Element """ raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__) + @classmethod + def definition_from_file(cls, file, system): + """ + Return the definition to be passed to the newly created descriptor + during from_xml + + file: File pointer + """ + return cls.definition_from_xml(etree.parse(file), system) + @classmethod def from_xml(cls, xml_data, system, org=None, course=None): """ @@ -93,9 +108,17 @@ class XmlDescriptor(XModuleDescriptor): return metadata + def definition_loader(): + filename = xml_object.get('filename') + if filename is None: + return cls.definition_from_xml(xml_object, system) + else: + filepath = '{type}/{name}.{ext}'.format(type=xml_object.tag, name=filename, ext=cls.filename_extension) + return cls.definition_from_file(system.resources_fs.open(filepath), system) + return cls( system, - LazyLoadingDict(lambda: cls.definition_from_xml(xml_object, system)), + LazyLoadingDict(definition_loader), location=['i4x', org, course, From 552c199795bd060eb9907c9c5833b7aebe8c95fe Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Sat, 30 Jun 2012 00:20:54 -0400 Subject: [PATCH 220/252] Export large xml as separate files. Note: inherited metadata is creeping into child nodes --- common/lib/xmodule/html_module.py | 6 ------ common/lib/xmodule/xml_module.py | 33 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py index aac97b5fd3..08fe4bbecc 100644 --- a/common/lib/xmodule/html_module.py +++ b/common/lib/xmodule/html_module.py @@ -1,9 +1,7 @@ -import json import logging from xmodule.x_module import XModule from xmodule.raw_module import RawDescriptor -from lxml import etree from pkg_resources import resource_string log = logging.getLogger("mitx.courseware") @@ -28,7 +26,3 @@ class HtmlDescriptor(RawDescriptor): js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js_module = 'HTML' - - @classmethod - def definition_from_file(cls, file, system): - return {'data': file.read()} diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index 79b90c2003..6daf2bca36 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -69,16 +69,6 @@ class XmlDescriptor(XModuleDescriptor): """ raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__) - @classmethod - def definition_from_file(cls, file, system): - """ - Return the definition to be passed to the newly created descriptor - during from_xml - - file: File pointer - """ - return cls.definition_from_xml(etree.parse(file), system) - @classmethod def from_xml(cls, xml_data, system, org=None, course=None): """ @@ -113,8 +103,9 @@ class XmlDescriptor(XModuleDescriptor): if filename is None: return cls.definition_from_xml(xml_object, system) else: - filepath = '{type}/{name}.{ext}'.format(type=xml_object.tag, name=filename, ext=cls.filename_extension) - return cls.definition_from_file(system.resources_fs.open(filepath), system) + filepath = cls._format_filepath(xml_object.tag, filename) + with system.resources_fs.open(filepath) as file: + return cls.definition_from_xml(etree.parse(file).getroot(), system) return cls( system, @@ -127,6 +118,10 @@ class XmlDescriptor(XModuleDescriptor): metadata=LazyLoadingDict(metadata_loader), ) + @classmethod + def _format_filepath(cls, type, name): + return '{type}/{name}.{ext}'.format(type=type, name=name, ext=cls.filename_extension) + def export_to_xml(self, resource_fs): """ Returns an xml string representing this module, and all modules underneath it. @@ -139,6 +134,20 @@ class XmlDescriptor(XModuleDescriptor): using the from_xml method with the same system, org, and course """ xml_object = self.definition_to_xml(resource_fs) + + # Put content in a separate file if it's large (has more than 5 descendent tags) + if len(list(xml_object.iter())) > 5: + + filepath = self.__class__._format_filepath(self.type, self.name) + resource_fs.makedir(self.type, allow_recreate=True) + with resource_fs.open(filepath, 'w') as file: + file.write(etree.tostring(xml_object, pretty_print=True)) + + for child in xml_object: + xml_object.remove(child) + + xml_object.set('filename', self.name) + xml_object.set('slug', self.name) xml_object.tag = self.type From 8cf848b191545f4c509a76333b0c43bb103d57cc Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Sat, 30 Jun 2012 09:11:40 -0400 Subject: [PATCH 221/252] Handle the filename for capa_module in the xml_module code, rather than specially in capa_module --- common/lib/capa/capa_problem.py | 22 +++++++++------------- common/lib/xmodule/capa_module.py | 17 +++-------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py index a06ac1d7b6..2e3620021b 100644 --- a/common/lib/capa/capa_problem.py +++ b/common/lib/capa/capa_problem.py @@ -68,14 +68,13 @@ class LoncapaProblem(object): Main class for capa Problems. ''' - def __init__(self, fileobject, id, state=None, seed=None, system=None): + def __init__(self, problem_text, id, state=None, seed=None, system=None): ''' - Initializes capa Problem. The problem itself is defined by the XML file - pointed to by fileobject. + Initializes capa Problem. Arguments: - - filesobject : an OSFS instance: see fs.osfs + - problem_text : xml defining the problem - id : string used as the identifier for this problem; often a filename (no spaces) - state : student state (represented as a dict) - seed : random number generator seed (int) @@ -103,14 +102,11 @@ class LoncapaProblem(object): if not self.seed: self.seed = struct.unpack('i', os.urandom(4))[0] - self.fileobject = fileobject # save problem file object, so we can use for debugging information later - if getattr(system, 'DEBUG', False): # get the problem XML string from the problem file - log.info("[courseware.capa.capa_problem.lcp.init] fileobject = %s" % fileobject) - file_text = fileobject.read() - file_text = re.sub("startouttext\s*/", "text", file_text) # Convert startouttext and endouttext to proper - file_text = re.sub("endouttext\s*/", "/text", file_text) + problem_text = re.sub("startouttext\s*/", "text", problem_text) # Convert startouttext and endouttext to proper + problem_text = re.sub("endouttext\s*/", "/text", problem_text) + self.problem_text = problem_text - self.tree = etree.XML(file_text) # parse problem XML file into an element tree + self.tree = etree.XML(problem_text) # parse problem XML file into an element tree self._process_includes() # handle any tags # construct script processor context (eg for customresponse problems) @@ -130,7 +126,7 @@ class LoncapaProblem(object): self.done = False def __unicode__(self): - return u"LoncapaProblem ({0})".format(self.fileobject) + return u"LoncapaProblem ({0})".format(self.problem_text) def get_state(self): ''' Stored per-user session data neeeded to: @@ -272,7 +268,7 @@ class LoncapaProblem(object): parent = inc.getparent() # insert new XML into tree in place of inlcude parent.insert(parent.index(inc),incxml) parent.remove(inc) - log.debug('Included %s into %s' % (file,self.fileobject)) + log.debug('Included %s into %s' % (file, self.id)) def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private ''' diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py index 6a95789417..57c5fa88ce 100644 --- a/common/lib/xmodule/capa_module.py +++ b/common/lib/xmodule/capa_module.py @@ -117,8 +117,6 @@ class CapaModule(XModule): if instance_state != None and 'attempts' in instance_state: self.attempts = instance_state['attempts'] - # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) - self.filename = "problems/" + only_one(dom2.xpath('/problem/@filename')) + ".xml" self.name = only_one(dom2.xpath('/problem/@name')) weight_string = only_one(dom2.xpath('/problem/@weight')) @@ -133,20 +131,11 @@ class CapaModule(XModule): seed = system.id else: seed = None + try: - fp = self.system.filestore.open(self.filename) + self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), instance_state, seed=seed, system=self.system) except Exception: - log.exception('cannot open file %s' % self.filename) - if self.system.DEBUG: - # create a dummy problem instead of failing - fp = StringIO.StringIO('Problem file %s is missing' % self.filename) - fp.name = "StringIO" - else: - raise - try: - self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) - except Exception: - msg = 'cannot create LoncapaProblem %s' % self.filename + msg = 'cannot create LoncapaProblem %s' % self.url log.exception(msg) if self.system.DEBUG: msg = '

                %s

                ' % msg.replace('<', '<') From 6612beab463874aa03f10fdc27a9b14373438378 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 11:07:17 -0400 Subject: [PATCH 222/252] Acknowledge the fact that right now keystore is really just a module store. If we need a keystore that returns other objects, we can reexctract the base class into it's own module again --- .../management/commands/import.py | 14 +++++----- cms/djangoapps/contentstore/views.py | 10 +++---- cms/envs/dev.py | 4 +-- cms/envs/test.py | 2 +- common/lib/keystore/django.py | 26 ------------------- .../modulestore}/__init__.py | 4 +-- common/lib/xmodule/modulestore/django.py | 26 +++++++++++++++++++ .../modulestore}/exceptions.py | 0 .../modulestore}/mongo.py | 4 +-- .../modulestore}/tests/test_location.py | 4 +-- .../{keystore => xmodule/modulestore}/xml.py | 4 +-- .../xmodule/{tests.py => tests/__init__.py} | 0 common/lib/xmodule/x_module.py | 4 +-- .../management/commands/check_course.py | 4 +-- lms/djangoapps/courseware/module_render.py | 10 +++---- lms/djangoapps/courseware/views.py | 6 ++--- lms/envs/common.py | 4 +-- 17 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 common/lib/keystore/django.py rename common/lib/{keystore => xmodule/modulestore}/__init__.py (97%) create mode 100644 common/lib/xmodule/modulestore/django.py rename common/lib/{keystore => xmodule/modulestore}/exceptions.py (100%) rename common/lib/{keystore => xmodule/modulestore}/mongo.py (95%) rename common/lib/{keystore => xmodule/modulestore}/tests/test_location.py (95%) rename common/lib/{keystore => xmodule/modulestore}/xml.py (96%) rename common/lib/xmodule/{tests.py => tests/__init__.py} (100%) diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 75f59a0ef6..f97ac10d41 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -3,15 +3,15 @@ ### from django.core.management.base import BaseCommand, CommandError -from keystore.django import keystore -from keystore.xml import XMLModuleStore +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.xml import XMLModuleStore unnamed_modules = 0 class Command(BaseCommand): help = \ -'''Import the specified data directory into the default keystore''' +'''Import the specified data directory into the default ModuleStore''' def handle(self, *args, **options): if len(args) != 3: @@ -21,9 +21,9 @@ class Command(BaseCommand): module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor', eager=True) for module in module_store.modules.itervalues(): - keystore().create_item(module.location) + modulestore().create_item(module.location) if 'data' in module.definition: - keystore().update_item(module.location, module.definition['data']) + modulestore().update_item(module.location, module.definition['data']) if 'children' in module.definition: - keystore().update_children(module.location, module.definition['children']) - keystore().update_metadata(module.location, dict(module.metadata)) + modulestore().update_children(module.location, module.definition['children']) + modulestore().update_metadata(module.location, dict(module.metadata)) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index d9036515a9..76a904a403 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,5 +1,5 @@ from mitxmako.shortcuts import render_to_response -from keystore.django import keystore +from xmodule.modulestore.django import modulestore from django_future.csrf import ensure_csrf_cookie from django.http import HttpResponse import json @@ -12,14 +12,14 @@ def index(request): org = 'mit.edu' course = '6002xs12' name = '6.002_Spring_2012' - course = keystore().get_item(['i4x', org, course, 'course', name]) + course = modulestore().get_item(['i4x', org, course, 'course', name]) weeks = course.get_children() return render_to_response('index.html', {'weeks': weeks}) def edit_item(request): item_id = request.GET['id'] - item = keystore().get_item(item_id) + item = modulestore().get_item(item_id) return render_to_response('unit.html', { 'contents': item.get_html(), 'js_module': item.js_module_name(), @@ -31,7 +31,7 @@ def edit_item(request): def save_item(request): item_id = request.POST['id'] data = json.loads(request.POST['data']) - keystore().update_item(item_id, data) + modulestore().update_item(item_id, data) return HttpResponse(json.dumps({})) @@ -39,7 +39,7 @@ def temp_force_export(request): org = 'mit.edu' course = '6002xs12' name = '6.002_Spring_2012' - course = keystore().get_item(['i4x', org, course, 'course', name]) + course = modulestore().get_item(['i4x', org, course, 'course', name]) fs = OSFS('../data-export-test') xml = course.export_to_xml(fs) with fs.open('course.xml', 'w') as course_xml: diff --git a/cms/envs/dev.py b/cms/envs/dev.py index ce775d962a..5bc5cfebc5 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -6,9 +6,9 @@ from .common import * DEBUG = True TEMPLATE_DEBUG = DEBUG -KEYSTORE = { +MODULESTORE = { 'default': { - 'ENGINE': 'keystore.mongo.MongoModuleStore', + 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'OPTIONS': { 'default_class': 'xmodule.raw_module.RawDescriptor', 'host': 'localhost', diff --git a/cms/envs/test.py b/cms/envs/test.py index 1a20d9e6f8..032de92953 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -17,7 +17,7 @@ for app in os.listdir(PROJECT_ROOT / 'djangoapps'): NOSE_ARGS += ['--cover-package', app] TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -KEYSTORE = { +MODULESTORE = { 'host': 'localhost', 'db': 'mongo_base', 'collection': 'key_store', diff --git a/common/lib/keystore/django.py b/common/lib/keystore/django.py deleted file mode 100644 index 89aa9d07b0..0000000000 --- a/common/lib/keystore/django.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Module that provides a connection to the keystore specified in the django settings. - -Passes settings.KEYSTORE as kwargs to MongoModuleStore -""" - -from __future__ import absolute_import - -from importlib import import_module - -from django.conf import settings - -_KEYSTORES = {} - - -def keystore(name='default'): - global _KEYSTORES - - if name not in _KEYSTORES: - class_path = settings.KEYSTORE[name]['ENGINE'] - module_path, _, class_name = class_path.rpartition('.') - class_ = getattr(import_module(module_path), class_name) - _KEYSTORES[name] = class_( - **settings.KEYSTORE[name]['OPTIONS']) - - return _KEYSTORES[name] diff --git a/common/lib/keystore/__init__.py b/common/lib/xmodule/modulestore/__init__.py similarity index 97% rename from common/lib/keystore/__init__.py rename to common/lib/xmodule/modulestore/__init__.py index 13ff322e83..00b3b13bb0 100644 --- a/common/lib/keystore/__init__.py +++ b/common/lib/xmodule/modulestore/__init__.py @@ -145,8 +145,8 @@ class ModuleStore(object): recent revision If any segment of the location is None except revision, raises - keystore.exceptions.InsufficientSpecificationError - If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + xmodule.modulestore.exceptions.InsufficientSpecificationError + If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError location: Something that can be passed to Location default_class: An XModuleDescriptor subclass to use if no plugin matching the diff --git a/common/lib/xmodule/modulestore/django.py b/common/lib/xmodule/modulestore/django.py new file mode 100644 index 0000000000..546aaf30c8 --- /dev/null +++ b/common/lib/xmodule/modulestore/django.py @@ -0,0 +1,26 @@ +""" +Module that provides a connection to the ModuleStore specified in the django settings. + +Passes settings.MODULESTORE as kwargs to MongoModuleStore +""" + +from __future__ import absolute_import + +from importlib import import_module + +from django.conf import settings + +_MODULESTORES = {} + + +def modulestore(name='default'): + global _MODULESTORES + + if name not in _MODULESTORES: + class_path = settings.MODULESTORE[name]['ENGINE'] + module_path, _, class_name = class_path.rpartition('.') + class_ = getattr(import_module(module_path), class_name) + _MODULESTORES[name] = class_( + **settings.MODULESTORE[name]['OPTIONS']) + + return _MODULESTORES[name] diff --git a/common/lib/keystore/exceptions.py b/common/lib/xmodule/modulestore/exceptions.py similarity index 100% rename from common/lib/keystore/exceptions.py rename to common/lib/xmodule/modulestore/exceptions.py diff --git a/common/lib/keystore/mongo.py b/common/lib/xmodule/modulestore/mongo.py similarity index 95% rename from common/lib/keystore/mongo.py rename to common/lib/xmodule/modulestore/mongo.py index d92782600c..f305f8aa97 100644 --- a/common/lib/keystore/mongo.py +++ b/common/lib/xmodule/modulestore/mongo.py @@ -31,8 +31,8 @@ class MongoModuleStore(ModuleStore): recent revision If any segment of the location is None except revision, raises - keystore.exceptions.InsufficientSpecificationError - If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + xmodule.modulestore.exceptions.InsufficientSpecificationError + If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError location: Something that can be passed to Location """ diff --git a/common/lib/keystore/tests/test_location.py b/common/lib/xmodule/modulestore/tests/test_location.py similarity index 95% rename from common/lib/keystore/tests/test_location.py rename to common/lib/xmodule/modulestore/tests/test_location.py index 01d36d946b..d598d8ae6d 100644 --- a/common/lib/keystore/tests/test_location.py +++ b/common/lib/xmodule/modulestore/tests/test_location.py @@ -1,6 +1,6 @@ from nose.tools import assert_equals, assert_raises, assert_not_equals -from keystore import Location -from keystore.exceptions import InvalidLocationError +from xmodule.modulestore import Location +from xmodule.modulestore.exceptions import InvalidLocationError def check_string_roundtrip(url): diff --git a/common/lib/keystore/xml.py b/common/lib/xmodule/modulestore/xml.py similarity index 96% rename from common/lib/keystore/xml.py rename to common/lib/xmodule/modulestore/xml.py index 52eaf0ce0f..d68a6448cc 100644 --- a/common/lib/keystore/xml.py +++ b/common/lib/xmodule/modulestore/xml.py @@ -81,8 +81,8 @@ class XMLModuleStore(ModuleStore): recent revision If any segment of the location is None except revision, raises - keystore.exceptions.InsufficientSpecificationError - If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + xmodule.modulestore.exceptions.InsufficientSpecificationError + If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError location: Something that can be passed to Location """ diff --git a/common/lib/xmodule/tests.py b/common/lib/xmodule/tests/__init__.py similarity index 100% rename from common/lib/xmodule/tests.py rename to common/lib/xmodule/tests/__init__.py diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 8367120748..0af68a2a1a 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -2,7 +2,7 @@ from lxml import etree import pkg_resources import logging -from keystore import Location +from xmodule.modulestore import Location from functools import partial log = logging.getLogger('mitx.' + __name__) @@ -231,7 +231,7 @@ class XModuleDescriptor(Plugin): definition: A dict containing `data` and `children` representing the problem definition Current arguments passed in kwargs: - location: A keystore.Location object indicating the name and ownership of this problem + location: A xmodule.modulestore.Location object indicating the name and ownership of this problem shared_state_key: The key to use for sharing StudentModules with other modules of this type metadata: A dictionary containing the following optional keys: diff --git a/lms/djangoapps/courseware/management/commands/check_course.py b/lms/djangoapps/courseware/management/commands/check_course.py index afc7e47857..6ccd6d5fe7 100644 --- a/lms/djangoapps/courseware/management/commands/check_course.py +++ b/lms/djangoapps/courseware/management/commands/check_course.py @@ -10,7 +10,7 @@ import xmodule import mitxmako.middleware as middleware middleware.MakoMiddleware() -from keystore.django import keystore +from xmodule.modulestore.django import modulestore from courseware.models import StudentModuleCache from courseware.module_render import get_module @@ -78,7 +78,7 @@ class Command(BaseCommand): # TODO (cpennington): Get coursename in a legitimate way course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012' - student_module_cache = StudentModuleCache(sample_user, keystore().get_item(course_location)) + student_module_cache = StudentModuleCache(sample_user, modulestore().get_item(course_location)) (course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache) to_run = [ diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 4e5ee62e63..679084f28c 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -6,7 +6,7 @@ from django.http import Http404 from django.http import HttpResponse from lxml import etree -from keystore.django import keystore +from xmodule.modulestore.django import modulestore from mitxmako.shortcuts import render_to_string from models import StudentModule, StudentModuleCache @@ -129,7 +129,7 @@ def toc_for_course(user, request, course_location, active_chapter, active_sectio chapters with name 'hidden' are skipped. ''' - student_module_cache = StudentModuleCache(user, keystore().get_item(course_location), depth=2) + student_module_cache = StudentModuleCache(user, modulestore().get_item(course_location), depth=2) (course, _, _, _) = get_module(user, request, course_location, student_module_cache) chapters = list() @@ -161,7 +161,7 @@ def get_section(course, chapter, section): section: Section name """ try: - course_module = keystore().get_item(course) + course_module = modulestore().get_item(course) except: log.exception("Unable to load course_module") return None @@ -205,7 +205,7 @@ def get_module(user, request, location, student_module_cache, position=None): instance_module is a StudentModule specific to this module for this student shared_module is a StudentModule specific to all modules with the same 'shared_state_key' attribute, or None if the module doesn't elect to share state ''' - descriptor = keystore().get_item(location) + descriptor = modulestore().get_item(location) instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url()) shared_state_key = getattr(descriptor, 'shared_state_key', None) @@ -304,7 +304,7 @@ def modx_dispatch(request, dispatch=None, id=None): # If there are arguments, get rid of them dispatch, _, _ = dispatch.partition('?') - student_module_cache = StudentModuleCache(request.user, keystore().get_item(id)) + student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id)) instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache) if instance_module is None: diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 52a86fdee4..8b723ca980 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -16,7 +16,7 @@ from module_render import toc_for_course, get_module, get_section from models import StudentModuleCache from student.models import UserProfile from multicourse import multicourse_settings -from keystore.django import keystore +from xmodule.modulestore.django import modulestore from util.cache import cache from student.models import UserTestGroup @@ -63,7 +63,7 @@ def gradebook(request): course_location = multicourse_settings.get_course_location(coursename) for student in student_objects: - student_module_cache = StudentModuleCache(student, keystore().get_item(course_location)) + student_module_cache = StudentModuleCache(student, modulestore().get_item(course_location)) course, _, _, _ = get_module(request.user, request, course_location, student_module_cache) student_info.append({ 'username': student.username, @@ -93,7 +93,7 @@ def profile(request, student_id=None): coursename = multicourse_settings.get_coursename_from_request(request) course_location = multicourse_settings.get_course_location(coursename) - student_module_cache = StudentModuleCache(request.user, keystore().get_item(course_location)) + student_module_cache = StudentModuleCache(request.user, modulestore().get_item(course_location)) course, _, _, _ = get_module(request.user, request, course_location, student_module_cache) context = {'name': user_info.name, diff --git a/lms/envs/common.py b/lms/envs/common.py index d1faf00f62..4c3cdc2dda 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -138,9 +138,9 @@ COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x', ############################### XModule Store ################################## -KEYSTORE = { +MODULESTORE = { 'default': { - 'ENGINE': 'keystore.xml.XMLModuleStore', + 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'OPTIONS': { 'org': 'edx', 'course': '6002xs12', From 85f294b3e32d9f1e20b7c7775266230b461df767 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 11:21:40 -0400 Subject: [PATCH 223/252] Allow for no default_class in XMLModuleStore --- common/lib/xmodule/modulestore/xml.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/modulestore/xml.py b/common/lib/xmodule/modulestore/xml.py index d68a6448cc..eb23de2e46 100644 --- a/common/lib/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/modulestore/xml.py @@ -30,9 +30,12 @@ class XMLModuleStore(ModuleStore): self.data_dir = path(data_dir) self.modules = {} - module_path, _, class_name = default_class.rpartition('.') - class_ = getattr(import_module(module_path), class_name) - self.default_class = class_ + if default_class is None: + self.default_class = None + else: + module_path, _, class_name = default_class.rpartition('.') + class_ = getattr(import_module(module_path), class_name) + self.default_class = class_ with open(self.data_dir / "course.xml") as course_file: class ImportSystem(XMLParsingSystem): From d7dbced8e64265625bdd47db8780157a2eefe624 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 11:25:21 -0400 Subject: [PATCH 224/252] Store the top level course in the XMLModuleStore (since there is only one course per module store --- common/lib/xmodule/modulestore/xml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/modulestore/xml.py b/common/lib/xmodule/modulestore/xml.py index eb23de2e46..d4e90d59a4 100644 --- a/common/lib/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/modulestore/xml.py @@ -75,7 +75,7 @@ class XMLModuleStore(ModuleStore): XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml) - ImportSystem(self).process_xml(course_file.read()) + self.course = ImportSystem(self).process_xml(course_file.read()) def get_item(self, location): """ From 3a348e5713ddc90037010c5006bb028c2e16fb33 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 11:25:42 -0400 Subject: [PATCH 225/252] Adjust xml export code for the type -> category conversion --- common/lib/xmodule/xml_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index 6daf2bca36..6733fc450c 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -138,8 +138,8 @@ class XmlDescriptor(XModuleDescriptor): # Put content in a separate file if it's large (has more than 5 descendent tags) if len(list(xml_object.iter())) > 5: - filepath = self.__class__._format_filepath(self.type, self.name) - resource_fs.makedir(self.type, allow_recreate=True) + filepath = self.__class__._format_filepath(self.category, self.name) + resource_fs.makedir(self.category, allow_recreate=True) with resource_fs.open(filepath, 'w') as file: file.write(etree.tostring(xml_object, pretty_print=True)) @@ -149,7 +149,7 @@ class XmlDescriptor(XModuleDescriptor): xml_object.set('filename', self.name) xml_object.set('slug', self.name) - xml_object.tag = self.type + xml_object.tag = self.category for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'): if attr in self.metadata and attr not in self._inherited_metadata: From f035d5602dff8efbabcfa391978a256a3327334f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 12:24:07 -0400 Subject: [PATCH 226/252] Keep abtest children in a consistent order (makes testing easier) --- common/lib/xmodule/abtest_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py index f6057171e5..c3e32732f3 100644 --- a/common/lib/xmodule/abtest_module.py +++ b/common/lib/xmodule/abtest_module.py @@ -101,6 +101,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1") definition['data']['group_portions'][DEFAULT] = default_portion + definition['children'].sort() return definition From c57833dab7ba13166fcd8ced1456297c42c4cd72 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 12:24:19 -0400 Subject: [PATCH 227/252] Define equality for XModuleDescriptors --- common/lib/xmodule/raw_module.py | 1 + common/lib/xmodule/template_module.py | 1 + common/lib/xmodule/x_module.py | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/common/lib/xmodule/raw_module.py b/common/lib/xmodule/raw_module.py index 59f28ff4f0..9fe9a9198b 100644 --- a/common/lib/xmodule/raw_module.py +++ b/common/lib/xmodule/raw_module.py @@ -3,6 +3,7 @@ from lxml import etree from xmodule.mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor + class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): """ Module that provides a raw editing view of it's data and children diff --git a/common/lib/xmodule/template_module.py b/common/lib/xmodule/template_module.py index 1057fc2a25..064d48f431 100644 --- a/common/lib/xmodule/template_module.py +++ b/common/lib/xmodule/template_module.py @@ -37,5 +37,6 @@ class CustomTagModule(XModule): def get_html(self): return self.html + class CustomTagDescriptor(RawDescriptor): module_class = CustomTagModule diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index 0af68a2a1a..9f56d95fe5 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -213,6 +213,10 @@ class XModuleDescriptor(Plugin): # A list of metadata that this module can inherit from its parent module inheritable_metadata = ('graded', 'due', 'graceperiod', 'showanswer', 'rerandomize') + # A list of descriptor attributes that must be equal for the discriptors to be + # equal + equality_attributes = ('definition', 'metadata', 'location', 'shared_state_key', '_inherited_metadata') + # ============================= STRUCTURAL MANIPULATION =========================== def __init__(self, system, @@ -395,6 +399,27 @@ class XModuleDescriptor(Plugin): """ raise NotImplementedError("get_html() must be provided by specific modules") + # =============================== BUILTIN METHODS =========================== + def __eq__(self, other): + eq = (self.__class__ == other.__class__ and + all(getattr(self, attr, None) == getattr(other, attr, None) + for attr in self.equality_attributes)) + + if not eq: + for attr in self.equality_attributes: + print getattr(self, attr, None), getattr(other, attr, None), getattr(self, attr, None) == getattr(other, attr, None) + + return eq + + def __repr__(self): + return "{class_}({system!r}, {definition!r}, location={location!r}, metadata={metadata!r})".format( + class_=self.__class__.__name__, + system=self.system, + definition=self.definition, + location=self.location, + metadata=self.metadata + ) + class DescriptorSystem(object): def __init__(self, load_item, resources_fs): From be40d8bb6991671a42581e567a5a0a79e914721e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 12:25:09 -0400 Subject: [PATCH 228/252] Make sure that xml_module definition xml doesn't have any metadata sprinkled in with it --- common/lib/xmodule/xml_module.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py index 6733fc450c..f860e70afb 100644 --- a/common/lib/xmodule/xml_module.py +++ b/common/lib/xmodule/xml_module.py @@ -1,6 +1,7 @@ from collections import MutableMapping from xmodule.x_module import XModuleDescriptor from lxml import etree +import copy class LazyLoadingDict(MutableMapping): @@ -41,6 +42,10 @@ class LazyLoadingDict(MutableMapping): self.load() return iter(self._contents) + def __repr__(self): + self.load() + return repr(self._contents) + def load(self): if self._loaded: return @@ -59,6 +64,11 @@ class XmlDescriptor(XModuleDescriptor): # Extension to append to filename paths filename_extension = 'xml' + # The attributes will be removed from the definition xml passed + # to definition_from_xml, and from the xml returned by definition_to_xml + metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', + 'due', 'graded', 'name', 'slug') + @classmethod def definition_from_xml(cls, xml_object, system): """ @@ -69,6 +79,15 @@ class XmlDescriptor(XModuleDescriptor): """ raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__) + @classmethod + def clean_metadata_from_xml(cls, xml_object): + """ + Remove any attribute named in self.metadata_attributes from the supplied xml_object + """ + for attr in cls.metadata_attributes: + if xml_object.get(attr) is not None: + del xml_object.attrib[attr] + @classmethod def from_xml(cls, xml_data, system, org=None, course=None): """ @@ -101,11 +120,14 @@ class XmlDescriptor(XModuleDescriptor): def definition_loader(): filename = xml_object.get('filename') if filename is None: - return cls.definition_from_xml(xml_object, system) + definition_xml = copy.deepcopy(xml_object) else: filepath = cls._format_filepath(xml_object.tag, filename) with system.resources_fs.open(filepath) as file: - return cls.definition_from_xml(etree.parse(file).getroot(), system) + definition_xml = etree.parse(file).getroot() + + cls.clean_metadata_from_xml(definition_xml) + return cls.definition_from_xml(definition_xml, system) return cls( system, @@ -134,6 +156,7 @@ class XmlDescriptor(XModuleDescriptor): using the from_xml method with the same system, org, and course """ xml_object = self.definition_to_xml(resource_fs) + self.__class__.clean_metadata_from_xml(xml_object) # Put content in a separate file if it's large (has more than 5 descendent tags) if len(list(xml_object.iter())) > 5: From 8f59521660311adb961a0794e8d22945b854a4ff Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 12:25:48 -0400 Subject: [PATCH 229/252] Add a function to check round-trip export/import cycles --- common/lib/xmodule/tests/test_export.py | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 common/lib/xmodule/tests/test_export.py diff --git a/common/lib/xmodule/tests/test_export.py b/common/lib/xmodule/tests/test_export.py new file mode 100644 index 0000000000..97da2c4fe5 --- /dev/null +++ b/common/lib/xmodule/tests/test_export.py @@ -0,0 +1,28 @@ +from xmodule.modulestore.xml import XMLModuleStore +from nose.tools import assert_equals +from tempfile import mkdtemp +from fs.osfs import OSFS + + +def check_export_roundtrip(data_dir): + print "Starting import" + initial_import = XMLModuleStore('org', 'course', data_dir, eager=True) + initial_course = initial_import.course + + print "Starting export" + export_dir = mkdtemp() + fs = OSFS(export_dir) + xml = initial_course.export_to_xml(fs) + with fs.open('course.xml', 'w') as course_xml: + course_xml.write(xml) + + print "Starting second import" + second_import = XMLModuleStore('org', 'course', export_dir, eager=True) + + print "Checking key equality" + assert_equals(initial_import.modules.keys(), second_import.modules.keys()) + + print "Checking module equality" + for location in initial_import.modules.keys(): + print "Checking", location + assert_equals(initial_import.modules[location], second_import.modules[location]) From cc8ecb1891fd047802c90b9a01d088ad6284860f Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 12:50:03 -0400 Subject: [PATCH 230/252] Make xmodule tests pass again --- common/lib/xmodule/tests/__init__.py | 27 +++++++++---------- .../test_files/formularesponse_with_hint.xml | 0 .../{ => tests}/test_files/imageresponse.xml | 0 .../{ => tests}/test_files/multi_bare.xml | 0 .../{ => tests}/test_files/multichoice.xml | 0 .../{ => tests}/test_files/optionresponse.xml | 0 .../test_files/stringresponse_with_hint.xml | 0 .../test_files/symbolicresponse.xml | 0 .../{ => tests}/test_files/truefalse.xml | 0 9 files changed, 12 insertions(+), 15 deletions(-) rename common/lib/xmodule/{ => tests}/test_files/formularesponse_with_hint.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/imageresponse.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/multi_bare.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/multichoice.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/optionresponse.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/stringresponse_with_hint.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/symbolicresponse.xml (100%) rename common/lib/xmodule/{ => tests}/test_files/truefalse.xml (100%) diff --git a/common/lib/xmodule/tests/__init__.py b/common/lib/xmodule/tests/__init__.py index 90187abc2a..4fb270df13 100644 --- a/common/lib/xmodule/tests/__init__.py +++ b/common/lib/xmodule/tests/__init__.py @@ -43,12 +43,10 @@ class ModelsTest(unittest.TestCase): def setUp(self): pass - def test_get_module_class(self): - vc = xmodule.get_module_class('video') - vc_str = "" + def test_load_class(self): + vc = xmodule.x_module.XModuleDescriptor.load_class('video') + vc_str = "" self.assertEqual(str(vc), vc_str) - video_id = xmodule.get_default_ids()['video'] - self.assertEqual(video_id, 'youtube') def test_calc(self): variables={'R1':2.0, 'R3':4.0} @@ -98,7 +96,7 @@ class ModelsTest(unittest.TestCase): class MultiChoiceTest(unittest.TestCase): def test_MC_grade(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'choice_foil3'} self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') false_answers = {'1_2_1':'choice_foil2'} @@ -106,7 +104,7 @@ class MultiChoiceTest(unittest.TestCase): def test_MC_bare_grades(self): multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'choice_2'} self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') false_answers = {'1_2_1':'choice_1'} @@ -114,7 +112,7 @@ class MultiChoiceTest(unittest.TestCase): def test_TF_grade(self): truefalse_file = os.path.dirname(__file__)+"/test_files/truefalse.xml" - test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(truefalse_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']} self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') false_answers = {'1_2_1':['choice_foil1']} @@ -129,7 +127,7 @@ class MultiChoiceTest(unittest.TestCase): class ImageResponseTest(unittest.TestCase): def test_ir_grade(self): imageresponse_file = os.path.dirname(__file__)+"/test_files/imageresponse.xml" - test_lcp = lcp.LoncapaProblem(open(imageresponse_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'(490,11)-(556,98)', '1_2_2':'(242,202)-(296,276)'} test_answers = {'1_2_1':'[500,20]', @@ -142,7 +140,7 @@ class SymbolicResponseTest(unittest.TestCase): def test_sr_grade(self): raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test symbolicresponse_file = os.path.dirname(__file__)+"/test_files/symbolicresponse.xml" - test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]', '1_2_1_dynamath': ''' @@ -235,7 +233,7 @@ class OptionResponseTest(unittest.TestCase): ''' def test_or_grade(self): optionresponse_file = os.path.dirname(__file__)+"/test_files/optionresponse.xml" - test_lcp = lcp.LoncapaProblem(open(optionresponse_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(optionresponse_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'True', '1_2_2':'False'} test_answers = {'1_2_1':'True', @@ -251,7 +249,7 @@ class FormulaResponseWithHintTest(unittest.TestCase): ''' def test_or_grade(self): problem_file = os.path.dirname(__file__)+"/test_files/formularesponse_with_hint.xml" - test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'2.5*x-5.0'} test_answers = {'1_2_1':'0.4*x-5.0'} self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') @@ -265,7 +263,7 @@ class StringResponseWithHintTest(unittest.TestCase): ''' def test_or_grade(self): problem_file = os.path.dirname(__file__)+"/test_files/stringresponse_with_hint.xml" - test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs) + test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) correct_answers = {'1_2_1':'Michigan'} test_answers = {'1_2_1':'Minnesota'} self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') @@ -618,7 +616,6 @@ class ModuleProgressTest(unittest.TestCase): ''' def test_xmodule_default(self): '''Make sure default get_progress exists, returns None''' - xm = x_module.XModule(i4xs, "", "dummy") + xm = x_module.XModule(i4xs, 'a://b/c/d/e', {}) p = xm.get_progress() self.assertEqual(p, None) - diff --git a/common/lib/xmodule/test_files/formularesponse_with_hint.xml b/common/lib/xmodule/tests/test_files/formularesponse_with_hint.xml similarity index 100% rename from common/lib/xmodule/test_files/formularesponse_with_hint.xml rename to common/lib/xmodule/tests/test_files/formularesponse_with_hint.xml diff --git a/common/lib/xmodule/test_files/imageresponse.xml b/common/lib/xmodule/tests/test_files/imageresponse.xml similarity index 100% rename from common/lib/xmodule/test_files/imageresponse.xml rename to common/lib/xmodule/tests/test_files/imageresponse.xml diff --git a/common/lib/xmodule/test_files/multi_bare.xml b/common/lib/xmodule/tests/test_files/multi_bare.xml similarity index 100% rename from common/lib/xmodule/test_files/multi_bare.xml rename to common/lib/xmodule/tests/test_files/multi_bare.xml diff --git a/common/lib/xmodule/test_files/multichoice.xml b/common/lib/xmodule/tests/test_files/multichoice.xml similarity index 100% rename from common/lib/xmodule/test_files/multichoice.xml rename to common/lib/xmodule/tests/test_files/multichoice.xml diff --git a/common/lib/xmodule/test_files/optionresponse.xml b/common/lib/xmodule/tests/test_files/optionresponse.xml similarity index 100% rename from common/lib/xmodule/test_files/optionresponse.xml rename to common/lib/xmodule/tests/test_files/optionresponse.xml diff --git a/common/lib/xmodule/test_files/stringresponse_with_hint.xml b/common/lib/xmodule/tests/test_files/stringresponse_with_hint.xml similarity index 100% rename from common/lib/xmodule/test_files/stringresponse_with_hint.xml rename to common/lib/xmodule/tests/test_files/stringresponse_with_hint.xml diff --git a/common/lib/xmodule/test_files/symbolicresponse.xml b/common/lib/xmodule/tests/test_files/symbolicresponse.xml similarity index 100% rename from common/lib/xmodule/test_files/symbolicresponse.xml rename to common/lib/xmodule/tests/test_files/symbolicresponse.xml diff --git a/common/lib/xmodule/test_files/truefalse.xml b/common/lib/xmodule/tests/test_files/truefalse.xml similarity index 100% rename from common/lib/xmodule/test_files/truefalse.xml rename to common/lib/xmodule/tests/test_files/truefalse.xml From a0f550396cd94ce9cc014ef09a14cd4b3003597d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 13:19:27 -0400 Subject: [PATCH 231/252] Make xmodule fit the typical python installation setup --- common/lib/xmodule/setup.py | 2 +- common/lib/xmodule/{ => xmodule}/__init__.py | 0 common/lib/xmodule/{ => xmodule}/abtest_module.py | 0 common/lib/xmodule/{ => xmodule}/capa_module.py | 0 common/lib/xmodule/{ => xmodule}/exceptions.py | 0 common/lib/xmodule/{ => xmodule}/graders.py | 0 common/lib/xmodule/{ => xmodule}/hidden_module.py | 0 common/lib/xmodule/{ => xmodule}/html_module.py | 0 common/lib/xmodule/{ => xmodule}/js/module/html.coffee | 0 common/lib/xmodule/{ => xmodule}/js/module/raw.coffee | 0 common/lib/xmodule/{ => xmodule}/mako_module.py | 0 common/lib/xmodule/{ => xmodule}/modulestore/__init__.py | 0 common/lib/xmodule/{ => xmodule}/modulestore/django.py | 0 common/lib/xmodule/{ => xmodule}/modulestore/exceptions.py | 0 common/lib/xmodule/{ => xmodule}/modulestore/mongo.py | 0 .../xmodule/{ => xmodule}/modulestore/tests/test_location.py | 0 common/lib/xmodule/{ => xmodule}/modulestore/xml.py | 0 common/lib/xmodule/{ => xmodule}/progress.py | 0 common/lib/xmodule/{ => xmodule}/raw_module.py | 0 common/lib/xmodule/{ => xmodule}/schematic_module.py | 0 common/lib/xmodule/{ => xmodule}/seq_module.py | 0 common/lib/xmodule/{ => xmodule}/template_module.py | 0 common/lib/xmodule/{ => xmodule}/translation_module.py | 0 common/lib/xmodule/{ => xmodule}/vertical_module.py | 0 common/lib/xmodule/{ => xmodule}/video_module.py | 0 common/lib/xmodule/{ => xmodule}/x_module.py | 0 common/lib/xmodule/{ => xmodule}/xml_module.py | 0 27 files changed, 1 insertion(+), 1 deletion(-) rename common/lib/xmodule/{ => xmodule}/__init__.py (100%) rename common/lib/xmodule/{ => xmodule}/abtest_module.py (100%) rename common/lib/xmodule/{ => xmodule}/capa_module.py (100%) rename common/lib/xmodule/{ => xmodule}/exceptions.py (100%) rename common/lib/xmodule/{ => xmodule}/graders.py (100%) rename common/lib/xmodule/{ => xmodule}/hidden_module.py (100%) rename common/lib/xmodule/{ => xmodule}/html_module.py (100%) rename common/lib/xmodule/{ => xmodule}/js/module/html.coffee (100%) rename common/lib/xmodule/{ => xmodule}/js/module/raw.coffee (100%) rename common/lib/xmodule/{ => xmodule}/mako_module.py (100%) rename common/lib/xmodule/{ => xmodule}/modulestore/__init__.py (100%) rename common/lib/xmodule/{ => xmodule}/modulestore/django.py (100%) rename common/lib/xmodule/{ => xmodule}/modulestore/exceptions.py (100%) rename common/lib/xmodule/{ => xmodule}/modulestore/mongo.py (100%) rename common/lib/xmodule/{ => xmodule}/modulestore/tests/test_location.py (100%) rename common/lib/xmodule/{ => xmodule}/modulestore/xml.py (100%) rename common/lib/xmodule/{ => xmodule}/progress.py (100%) rename common/lib/xmodule/{ => xmodule}/raw_module.py (100%) rename common/lib/xmodule/{ => xmodule}/schematic_module.py (100%) rename common/lib/xmodule/{ => xmodule}/seq_module.py (100%) rename common/lib/xmodule/{ => xmodule}/template_module.py (100%) rename common/lib/xmodule/{ => xmodule}/translation_module.py (100%) rename common/lib/xmodule/{ => xmodule}/vertical_module.py (100%) rename common/lib/xmodule/{ => xmodule}/video_module.py (100%) rename common/lib/xmodule/{ => xmodule}/x_module.py (100%) rename common/lib/xmodule/{ => xmodule}/xml_module.py (100%) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index e45e6654c2..3a380b12e7 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup( name="XModule", version="0.1", - packages=find_packages(), + packages=find_packages(exclude=["tests"]), install_requires=['distribute'], package_data={ '': ['js/*'] diff --git a/common/lib/xmodule/__init__.py b/common/lib/xmodule/xmodule/__init__.py similarity index 100% rename from common/lib/xmodule/__init__.py rename to common/lib/xmodule/xmodule/__init__.py diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py similarity index 100% rename from common/lib/xmodule/abtest_module.py rename to common/lib/xmodule/xmodule/abtest_module.py diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py similarity index 100% rename from common/lib/xmodule/capa_module.py rename to common/lib/xmodule/xmodule/capa_module.py diff --git a/common/lib/xmodule/exceptions.py b/common/lib/xmodule/xmodule/exceptions.py similarity index 100% rename from common/lib/xmodule/exceptions.py rename to common/lib/xmodule/xmodule/exceptions.py diff --git a/common/lib/xmodule/graders.py b/common/lib/xmodule/xmodule/graders.py similarity index 100% rename from common/lib/xmodule/graders.py rename to common/lib/xmodule/xmodule/graders.py diff --git a/common/lib/xmodule/hidden_module.py b/common/lib/xmodule/xmodule/hidden_module.py similarity index 100% rename from common/lib/xmodule/hidden_module.py rename to common/lib/xmodule/xmodule/hidden_module.py diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py similarity index 100% rename from common/lib/xmodule/html_module.py rename to common/lib/xmodule/xmodule/html_module.py diff --git a/common/lib/xmodule/js/module/html.coffee b/common/lib/xmodule/xmodule/js/module/html.coffee similarity index 100% rename from common/lib/xmodule/js/module/html.coffee rename to common/lib/xmodule/xmodule/js/module/html.coffee diff --git a/common/lib/xmodule/js/module/raw.coffee b/common/lib/xmodule/xmodule/js/module/raw.coffee similarity index 100% rename from common/lib/xmodule/js/module/raw.coffee rename to common/lib/xmodule/xmodule/js/module/raw.coffee diff --git a/common/lib/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py similarity index 100% rename from common/lib/xmodule/mako_module.py rename to common/lib/xmodule/xmodule/mako_module.py diff --git a/common/lib/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py similarity index 100% rename from common/lib/xmodule/modulestore/__init__.py rename to common/lib/xmodule/xmodule/modulestore/__init__.py diff --git a/common/lib/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py similarity index 100% rename from common/lib/xmodule/modulestore/django.py rename to common/lib/xmodule/xmodule/modulestore/django.py diff --git a/common/lib/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py similarity index 100% rename from common/lib/xmodule/modulestore/exceptions.py rename to common/lib/xmodule/xmodule/modulestore/exceptions.py diff --git a/common/lib/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py similarity index 100% rename from common/lib/xmodule/modulestore/mongo.py rename to common/lib/xmodule/xmodule/modulestore/mongo.py diff --git a/common/lib/xmodule/modulestore/tests/test_location.py b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py similarity index 100% rename from common/lib/xmodule/modulestore/tests/test_location.py rename to common/lib/xmodule/xmodule/modulestore/tests/test_location.py diff --git a/common/lib/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py similarity index 100% rename from common/lib/xmodule/modulestore/xml.py rename to common/lib/xmodule/xmodule/modulestore/xml.py diff --git a/common/lib/xmodule/progress.py b/common/lib/xmodule/xmodule/progress.py similarity index 100% rename from common/lib/xmodule/progress.py rename to common/lib/xmodule/xmodule/progress.py diff --git a/common/lib/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py similarity index 100% rename from common/lib/xmodule/raw_module.py rename to common/lib/xmodule/xmodule/raw_module.py diff --git a/common/lib/xmodule/schematic_module.py b/common/lib/xmodule/xmodule/schematic_module.py similarity index 100% rename from common/lib/xmodule/schematic_module.py rename to common/lib/xmodule/xmodule/schematic_module.py diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py similarity index 100% rename from common/lib/xmodule/seq_module.py rename to common/lib/xmodule/xmodule/seq_module.py diff --git a/common/lib/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py similarity index 100% rename from common/lib/xmodule/template_module.py rename to common/lib/xmodule/xmodule/template_module.py diff --git a/common/lib/xmodule/translation_module.py b/common/lib/xmodule/xmodule/translation_module.py similarity index 100% rename from common/lib/xmodule/translation_module.py rename to common/lib/xmodule/xmodule/translation_module.py diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/xmodule/vertical_module.py similarity index 100% rename from common/lib/xmodule/vertical_module.py rename to common/lib/xmodule/xmodule/vertical_module.py diff --git a/common/lib/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py similarity index 100% rename from common/lib/xmodule/video_module.py rename to common/lib/xmodule/xmodule/video_module.py diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py similarity index 100% rename from common/lib/xmodule/x_module.py rename to common/lib/xmodule/xmodule/x_module.py diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py similarity index 100% rename from common/lib/xmodule/xml_module.py rename to common/lib/xmodule/xmodule/xml_module.py From e56f8763ac35261a366518d69487679675c9d66e Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 13:26:14 -0400 Subject: [PATCH 232/252] Point to the js files in package data --- common/lib/xmodule/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 3a380b12e7..77b0838ff2 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -6,7 +6,7 @@ setup( packages=find_packages(exclude=["tests"]), install_requires=['distribute'], package_data={ - '': ['js/*'] + 'xmodule': ['js/module/*'] }, # See http://guide.python-distribute.org/creation.html#entry-points From d7178e4a41d00573459bf44555c1cff66798273c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 13:26:31 -0400 Subject: [PATCH 233/252] Add a set of rake tasks for checking settings importability --- rakefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rakefile b/rakefile index e76e200777..f5e32d8110 100644 --- a/rakefile +++ b/rakefile @@ -84,6 +84,14 @@ default_options = { args.with_defaults(:env => 'dev', :options => default_options[system]) sh(django_admin(system, args.env, 'runserver', args.options)) end + + Dir["#{system}/envs/*.py"].each do |env_file| + env = File.basename(env_file).gsub(/\.py/, '') + desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors" + task "#{system}:check_settings:#{env}" do + sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}") + end + end end Dir["common/lib/*"].each do |lib| From 3b4fb616488a1eab7b3d135d26bf4b0d1ffcdc0d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 14:10:29 -0400 Subject: [PATCH 234/252] Push dependency on mitxmako up out of mako_module --- common/lib/xmodule/xmodule/mako_module.py | 16 +++++++++++++--- common/lib/xmodule/xmodule/modulestore/mongo.py | 6 ++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py index 2260dddd92..9a90afb896 100644 --- a/common/lib/xmodule/xmodule/mako_module.py +++ b/common/lib/xmodule/xmodule/mako_module.py @@ -1,5 +1,10 @@ -from x_module import XModuleDescriptor -from mitxmako.shortcuts import render_to_string +from x_module import XModuleDescriptor, DescriptorSystem + + +class MakoDescriptorSystem(DescriptorSystem): + def __init__(self, render_template, *args, **kwargs): + self.render_template = render_template + super(MakoDescriptorSystem, self).__init__(*args, **kwargs) class MakoModuleDescriptor(XModuleDescriptor): @@ -12,6 +17,11 @@ class MakoModuleDescriptor(XModuleDescriptor): the descriptor as the `module` parameter to that template """ + def __init__(self, system, definition=None, **kwargs): + if getattr(system, 'render_template', None) is None: + raise TypeError('{system} must have a render_template function in order to use a MakoDescriptor'.format(system=system)) + super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs) + def get_context(self): """ Return the context to render the mako template with @@ -19,4 +29,4 @@ class MakoModuleDescriptor(XModuleDescriptor): return {'module': self} def get_html(self): - return render_to_string(self.mako_template, self.get_context()) + return self.system.render_template(self.mako_template, self.get_context()) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index f305f8aa97..cc731c929c 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -1,6 +1,8 @@ import pymongo from importlib import import_module -from xmodule.x_module import XModuleDescriptor, DescriptorSystem +from xmodule.x_module import XModuleDescriptor +from xmodule.mako_module import MakoDescriptorSystem +from mitxmako.shortcuts import render_to_string from . import ModuleStore, Location from .exceptions import ItemNotFoundError, InsufficientSpecificationError @@ -54,7 +56,7 @@ class MongoModuleStore(ModuleStore): # TODO (cpennington): Pass a proper resources_fs to the system return XModuleDescriptor.load_from_json( - item, DescriptorSystem(self.get_item, None), self.default_class) + item, MakoDescriptorSystem(load_item=self.get_item, resources_fs=None, render_template=render_to_string), self.default_class) def create_item(self, location): """ From e172be3a26dd06b8fdd657d05ccee3d2f0c75ae9 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 19:55:42 -0400 Subject: [PATCH 235/252] Make XML import pass in an empty render_template function --- common/lib/xmodule/xmodule/modulestore/xml.py | 12 ++++++++++-- common/lib/xmodule/xmodule/x_module.py | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index d4e90d59a4..a5db17054b 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -4,6 +4,7 @@ from importlib import import_module from lxml import etree from path import path from xmodule.x_module import XModuleDescriptor, XMLParsingSystem +from xmodule.mako_module import MakoDescriptorSystem from . import ModuleStore, Location from .exceptions import ItemNotFoundError @@ -38,7 +39,7 @@ class XMLModuleStore(ModuleStore): self.default_class = class_ with open(self.data_dir / "course.xml") as course_file: - class ImportSystem(XMLParsingSystem): + class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): def __init__(self, modulestore): """ modulestore: the XMLModuleStore to store the loaded modules in @@ -73,7 +74,14 @@ class XMLModuleStore(ModuleStore): module.get_children() return module - XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml) + system_kwargs = dict( + render_template=lambda: '', + load_item=modulestore.get_item, + resources_fs=OSFS(data_dir), + process_xml=process_xml + ) + MakoDescriptorSystem.__init__(self, **system_kwargs) + XMLParsingSystem.__init__(self, **system_kwargs) self.course = ImportSystem(self).process_xml(course_file.read()) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 9f56d95fe5..8bfbb5f91a 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -422,7 +422,7 @@ class XModuleDescriptor(Plugin): class DescriptorSystem(object): - def __init__(self, load_item, resources_fs): + def __init__(self, load_item, resources_fs, **kwargs): """ load_item: Takes a Location and returns an XModuleDescriptor resources_fs: A Filesystem object that contains all of the @@ -434,7 +434,7 @@ class DescriptorSystem(object): class XMLParsingSystem(DescriptorSystem): - def __init__(self, load_item, resources_fs, process_xml): + def __init__(self, load_item, resources_fs, process_xml, **kwargs): """ process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml """ From 3355f804d100d6df9eb54b2d2ff8bac5f1b5c303 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 20:01:01 -0400 Subject: [PATCH 236/252] Add logging of filename when module file parsing fails --- cms/envs/dev.py | 4 ++++ common/lib/xmodule/xmodule/xml_module.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 5bc5cfebc5..b4bcbfa9ce 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -3,6 +3,10 @@ This config file runs the simplest dev environment""" from .common import * +import logging +import sys +logging.basicConfig(stream=sys.stdout, ) + DEBUG = True TEMPLATE_DEBUG = DEBUG diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py index f860e70afb..aebb024a59 100644 --- a/common/lib/xmodule/xmodule/xml_module.py +++ b/common/lib/xmodule/xmodule/xml_module.py @@ -2,7 +2,9 @@ from collections import MutableMapping from xmodule.x_module import XModuleDescriptor from lxml import etree import copy +import logging +log = logging.getLogger(__name__) class LazyLoadingDict(MutableMapping): """ @@ -124,7 +126,11 @@ class XmlDescriptor(XModuleDescriptor): else: filepath = cls._format_filepath(xml_object.tag, filename) with system.resources_fs.open(filepath) as file: - definition_xml = etree.parse(file).getroot() + try: + definition_xml = etree.parse(file).getroot() + except: + log.exception("Failed to parse xml in file %s" % filepath) + raise cls.clean_metadata_from_xml(definition_xml) return cls.definition_from_xml(definition_xml, system) From 64a4a62cf53dbb2d871999f142e76ce62dd1957a Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Mon, 2 Jul 2012 20:01:20 -0400 Subject: [PATCH 237/252] Don't pass fileobjects to LoncapaProblem --- common/lib/xmodule/xmodule/capa_module.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 57c5fa88ce..b7f8e68b5e 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -141,9 +141,8 @@ class CapaModule(XModule): msg = '

                %s

                ' % msg.replace('<', '<') msg += '

                %s

                ' % traceback.format_exc().replace('<', '<') # create a dummy problem with error message instead of failing - fp = StringIO.StringIO('Problem file %s has an error:%s' % (self.filename, msg)) - fp.name = "StringIO" - self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system) + problem_text = 'Problem file %s has an error:%s' % (self.filename, msg) + self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system) else: raise @@ -395,13 +394,13 @@ class CapaModule(XModule): correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: # TODO (vshnayder): why is this line here? - self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename).read(), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return {'success': inst.message} except: # TODO: why is this line here? - self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename).read(), id=lcp_id, state=old_state, system=self.system) traceback.print_exc() raise Exception("error in capa_module") @@ -486,7 +485,7 @@ class CapaModule(XModule): # reset random number generator seed (note the self.lcp.get_state() in next line) self.lcp.seed = None - self.lcp = LoncapaProblem(self.system.filestore.open(self.filename), + self.lcp = LoncapaProblem(self.system.filestore.open(self.filename).read(), self.location.html_id(), self.lcp.get_state(), system=self.system) event_info['new_state'] = self.lcp.get_state() From 68c155b2687a3e372a2a821867be5141b9d78f39 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 3 Jul 2012 09:57:51 -0400 Subject: [PATCH 238/252] Don't use __dict__ for namedtuples, because it doesn't work in python 2.7.1 --- common/lib/xmodule/xmodule/modulestore/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 00b3b13bb0..faee5ce303 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -122,7 +122,7 @@ class Location(_LocationBase): return "-".join(str(v) for v in self.list() if v is not None).replace('.', '_') def dict(self): - return self.__dict__ + return self._asdict() def list(self): return list(self) From 42cef5b7885abe68a7da48c9b573924dbab22536 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 3 Jul 2012 14:19:51 -0400 Subject: [PATCH 239/252] Get rid of references to self.filename in capa_module, as it no longer exists --- common/lib/xmodule/xmodule/capa_module.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index b7f8e68b5e..06029b5e3f 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -141,7 +141,7 @@ class CapaModule(XModule): msg = '

                %s

                ' % msg.replace('<', '<') msg += '

                %s

                ' % traceback.format_exc().replace('<', '<') # create a dummy problem with error message instead of failing - problem_text = 'Problem file %s has an error:%s' % (self.filename, msg) + problem_text = 'Problem %s has an error:%s' % (self.location.url(), msg) self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system) else: raise @@ -368,7 +368,7 @@ class CapaModule(XModule): ''' event_info = dict() event_info['state'] = self.lcp.get_state() - event_info['filename'] = self.filename + event_info['problem_id'] = self.location.url() answers = self.make_dict_of_responses(get) @@ -394,13 +394,13 @@ class CapaModule(XModule): correct_map = self.lcp.grade_answers(answers) except StudentInputError as inst: # TODO (vshnayder): why is this line here? - self.lcp = LoncapaProblem(self.system.filestore.open(self.filename).read(), + self.lcp = LoncapaProblem(self.definition['data'], id=lcp_id, state=old_state, system=self.system) traceback.print_exc() return {'success': inst.message} except: # TODO: why is this line here? - self.lcp = LoncapaProblem(self.system.filestore.open(self.filename).read(), + self.lcp = LoncapaProblem(self.definition['data'], id=lcp_id, state=old_state, system=self.system) traceback.print_exc() raise Exception("error in capa_module") @@ -434,7 +434,7 @@ class CapaModule(XModule): ''' event_info = dict() event_info['state'] = self.lcp.get_state() - event_info['filename'] = self.filename + event_info['problem_id'] = self.location.url() answers = self.make_dict_of_responses(get) event_info['answers'] = answers @@ -468,7 +468,7 @@ class CapaModule(XModule): ''' event_info = dict() event_info['old_state'] = self.lcp.get_state() - event_info['filename'] = self.filename + event_info['problem_id'] = self.location.url() if self.closed(): event_info['failure'] = 'closed' @@ -485,7 +485,7 @@ class CapaModule(XModule): # reset random number generator seed (note the self.lcp.get_state() in next line) self.lcp.seed = None - self.lcp = LoncapaProblem(self.system.filestore.open(self.filename).read(), + self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), self.lcp.get_state(), system=self.system) event_info['new_state'] = self.lcp.get_state() From 207777fbe12269d3e1e8605df1daf27c8a05c85d Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Tue, 3 Jul 2012 14:20:11 -0400 Subject: [PATCH 240/252] Fix up problem display to use the new metadata that's available --- common/lib/xmodule/xmodule/capa_module.py | 2 +- lms/templates/problem.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 06029b5e3f..3cafcbde3d 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -194,7 +194,7 @@ class CapaModule(XModule): as necessary based on the problem config and state.''' html = self.lcp.get_html() - content = {'name': self.name, + content = {'name': self.metadata['display_name'], 'html': html, 'weight': self.weight, } diff --git a/lms/templates/problem.html b/lms/templates/problem.html index 3f79be3b3a..36fcb1bc66 100644 --- a/lms/templates/problem.html +++ b/lms/templates/problem.html @@ -1,7 +1,7 @@ <%namespace name='static' file='static_content.html'/>

                ${ problem['name'] } - % if problem['weight']: + % if problem['weight'] != 1: : ${ problem['weight'] } points % endif From 4e183dd6e14b62c634a1e31e9b823d3e0f45138d Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Wed, 20 Jun 2012 15:14:46 -0400 Subject: [PATCH 241/252] Clean up events binding in Video Player * Video Player now acting as a parent that always knowing the state of their children. * Events on the sub-controls are now triggered on itself, and binded by the Video Player instead of triggering everything on the Video Player. * A new helper class, SubView, has been introduced to cleanup repeat logic on scoped jQuery selector, render() and bind() --- lms/static/coffee/spec/helper.coffee | 2 +- .../modules/video/video_caption_spec.coffee | 112 ++++--- .../modules/video/video_control_spec.coffee | 73 ++--- .../modules/video/video_player_spec.coffee | 277 +++++++++--------- .../video/video_progress_slider_spec.coffee | 148 ++++------ .../video/video_speed_control_spec.coffee | 27 +- .../video/video_volume_control_spec.coffee | 42 ++- .../coffee/spec/modules/video_spec.coffee | 4 +- lms/static/coffee/src/_subview.coffee | 14 + lms/static/coffee/src/modules/video.coffee | 2 +- .../src/modules/video/video_caption.coffee | 41 ++- .../src/modules/video/video_control.coffee | 27 +- .../src/modules/video/video_player.coffee | 91 +++--- .../video/video_progress_slider.coffee | 27 +- .../modules/video/video_speed_control.coffee | 21 +- .../modules/video/video_volume_control.coffee | 26 +- lms/static/sass/courseware/_video.scss | 2 + 17 files changed, 442 insertions(+), 494 deletions(-) create mode 100644 lms/static/coffee/src/_subview.coffee diff --git a/lms/static/coffee/spec/helper.coffee b/lms/static/coffee/spec/helper.coffee index eed1a07bd2..c0d3d77950 100644 --- a/lms/static/coffee/spec/helper.coffee +++ b/lms/static/coffee/spec/helper.coffee @@ -52,7 +52,7 @@ jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> context.video = new Video 'example', '.75:abc123,1.0:def456' jasmine.stubYoutubePlayer() if createPlayer - return new VideoPlayer context.video + return new VideoPlayer(video: context.video) spyOn(window, 'onunload') diff --git a/lms/static/coffee/spec/modules/video/video_caption_spec.coffee b/lms/static/coffee/spec/modules/video/video_caption_spec.coffee index 3fa6fa7daa..8e6f447405 100644 --- a/lms/static/coffee/spec/modules/video/video_caption_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_caption_spec.coffee @@ -1,6 +1,7 @@ describe 'VideoCaption', -> beforeEach -> - @player = jasmine.stubVideoPlayer @ + jasmine.stubVideoPlayer @ + $('.subtitles').remove() afterEach -> YT.Player = undefined @@ -12,10 +13,7 @@ describe 'VideoCaption', -> describe 'always', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' - - it 'set the player', -> - expect(@caption.player).toEqual @player + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' it 'set the youtube id', -> expect(@caption.youtubeId).toEqual 'def456' @@ -30,26 +28,14 @@ describe 'VideoCaption', -> expect($.getWithPrefix).toHaveBeenCalledWith @caption.captionURL(), jasmine.any(Function) it 'bind window resize event', -> - expect($(window)).toHandleWith 'resize', @caption.onWindowResize - - it 'bind player resize event', -> - expect($(@player)).toHandleWith 'resize', @caption.onWindowResize - - it 'bind player seek event', -> - expect($(@player)).toHandleWith 'seek', @caption.onUpdatePlayTime - - it 'bind player updatePlayTime event', -> - expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime - - it 'bind player play event', -> - expect($(@player)).toHandleWith 'play', @caption.onPlay + expect($(window)).toHandleWith 'resize', @caption.resize it 'bind the hide caption button', -> expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle it 'bind the mouse movement', -> - expect($('.subtitles')).toHandleWith 'mouseenter', @caption.onMouseEnter - expect($('.subtitles')).toHandleWith 'mouseleave', @caption.onMouseLeave + expect($('.subtitles')).toHandleWith 'mouseover', @caption.onMouseEnter + expect($('.subtitles')).toHandleWith 'mouseout', @caption.onMouseLeave expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement @@ -57,7 +43,7 @@ describe 'VideoCaption', -> describe 'when on a non touch-based device', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn false - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' it 'render the caption', -> expect($('.subtitles').html()).toMatch new RegExp(''' @@ -81,7 +67,7 @@ describe 'VideoCaption', -> describe 'when on a touch-based device', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn true - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' it 'show explaination message', -> expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video." @@ -93,7 +79,7 @@ describe 'VideoCaption', -> beforeEach -> spyOn(window, 'setTimeout').andReturn 100 spyOn window, 'clearTimeout' - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' describe 'when cursor is outside of the caption box', -> beforeEach -> @@ -140,7 +126,7 @@ describe 'VideoCaption', -> describe 'when the player is playing', -> beforeEach -> - spyOn(@player, 'isPlaying').andReturn true + @caption.playing = true $('.subtitles li[data-index]:first').addClass 'current' $('.subtitles').trigger jQuery.Event 'mouseout' @@ -149,7 +135,7 @@ describe 'VideoCaption', -> describe 'when the player is not playing', -> beforeEach -> - spyOn(@player, 'isPlaying').andReturn false + @caption.playing = false $('.subtitles').trigger jQuery.Event 'mouseout' it 'does not scroll the caption', -> @@ -157,7 +143,7 @@ describe 'VideoCaption', -> describe 'search', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' it 'return a correct caption index', -> expect(@caption.search(0)).toEqual 0 @@ -167,20 +153,20 @@ describe 'VideoCaption', -> expect(@caption.search(30000)).toEqual 3 expect(@caption.search(30001)).toEqual 3 - describe 'onPlay', -> + describe 'play', -> describe 'when the caption was not rendered', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn true - @caption = new VideoCaption @player, 'def456' - @caption.onPlay() + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @caption.play() it 'render the caption', -> - expect($('.subtitles').html()).toMatch new RegExp(''' -
              • Caption at 0
              • -
              • Caption at 10000
              • -
              • Caption at 20000
              • -
              • Caption at 30000
              • - '''.replace(/\n/g, '')) + expect($('.subtitles').html()).toMatch new RegExp( + '''
              • Caption at 0
              • ''' + + '''
              • Caption at 10000
              • ''' + + '''
              • Caption at 20000
              • ''' + + '''
              • Caption at 30000
              • ''' + ) it 'add a padding element to caption', -> expect($('.subtitles li:first')).toBe '.spacing' @@ -193,22 +179,34 @@ describe 'VideoCaption', -> it 'set rendered to true', -> expect(@caption.rendered).toBeTruthy() - describe 'onUpdatePlayTime', -> + it 'set playing to true', -> + expect(@caption.playing).toBeTruthy() + + describe 'pause', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' + @caption.playing = true + @caption.pause() + + it 'set playing to false', -> + expect(@caption.playing).toBeFalsy() + + describe 'updatePlayTime', -> + beforeEach -> + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' describe 'when the video speed is 1.0x', -> beforeEach -> - @video.setSpeed '1.0' - @caption.onUpdatePlayTime {}, 25.000 + @caption.currentSpeed = '1.0' + @caption.updatePlayTime 25.000 it 'search the caption based on time', -> expect(@caption.currentIndex).toEqual 2 describe 'when the video speed is not 1.0x', -> beforeEach -> - @video.setSpeed '0.75' - @caption.onUpdatePlayTime {}, 25.000 + @caption.currentSpeed = '0.75' + @caption.updatePlayTime 25.000 it 'search the caption based on 1.0x speed', -> expect(@caption.currentIndex).toEqual 1 @@ -217,7 +215,7 @@ describe 'VideoCaption', -> beforeEach -> @caption.currentIndex = 1 $('.subtitles li[data-index=1]').addClass 'current' - @caption.onUpdatePlayTime {}, 25.000 + @caption.updatePlayTime 25.000 it 'deactivate the previous caption', -> expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current' @@ -235,16 +233,16 @@ describe 'VideoCaption', -> beforeEach -> @caption.currentIndex = 1 $('.subtitles li[data-index=1]').addClass 'current' - @caption.onUpdatePlayTime {}, 15.000 + @caption.updatePlayTime 15.000 it 'does not change current subtitle', -> expect($('.subtitles li[data-index=1]')).toHaveClass 'current' - describe 'onWindowResize', -> + describe 'resize', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' $('.subtitles li[data-index=1]').addClass 'current' - @caption.onWindowResize() + @caption.resize() it 'set the height of caption container', -> expect(parseInt($('.subtitles').css('maxHeight'))).toEqual $('.video-wrapper').height() @@ -260,7 +258,7 @@ describe 'VideoCaption', -> describe 'scrollCaption', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' describe 'when frozen', -> beforeEach -> @@ -288,18 +286,18 @@ describe 'VideoCaption', -> @caption.scrollCaption() it 'scroll to current caption', -> - expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @player.element), + expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @caption.element), offset: - ($('.video-wrapper').height() / 2 - $('.subtitles .current:first').height() / 2) describe 'seekPlayer', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' @time = null - $(@player).bind 'seek', (event, time) => @time = time + $(@caption).bind 'seek', (event, time) => @time = time describe 'when the video speed is 1.0x', -> beforeEach -> - @video.setSpeed '1.0' + @caption.currentSpeed = '1.0' $('.subtitles li[data-start="30000"]').click() it 'trigger seek event with the correct time', -> @@ -307,7 +305,7 @@ describe 'VideoCaption', -> describe 'when the video speed is not 1.0x', -> beforeEach -> - @video.setSpeed '0.75' + @caption.currentSpeed = '0.75' $('.subtitles li[data-start="30000"]').click() it 'trigger seek event with the correct time', -> @@ -315,25 +313,25 @@ describe 'VideoCaption', -> describe 'toggle', -> beforeEach -> - @caption = new VideoCaption @player, 'def456' + @caption = new VideoCaption element: $('.video'), youtubeId: 'def456', currentSpeed: '1.0' $('.subtitles li[data-index=1]').addClass 'current' describe 'when the caption is visible', -> beforeEach -> - @player.element.removeClass 'closed' + @caption.element.removeClass 'closed' @caption.toggle jQuery.Event('click') it 'hide the caption', -> - expect(@player.element).toHaveClass 'closed' + expect(@caption.element).toHaveClass 'closed' describe 'when the caption is hidden', -> beforeEach -> - @player.element.addClass 'closed' + @caption.element.addClass 'closed' @caption.toggle jQuery.Event('click') it 'show the caption', -> - expect(@player.element).not.toHaveClass 'closed' + expect(@caption.element).not.toHaveClass 'closed' it 'scroll the caption', -> expect($.fn.scrollTo).toHaveBeenCalled() diff --git a/lms/static/coffee/spec/modules/video/video_control_spec.coffee b/lms/static/coffee/spec/modules/video/video_control_spec.coffee index 0c8615b8f7..79adb4ad71 100644 --- a/lms/static/coffee/spec/modules/video/video_control_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_control_spec.coffee @@ -1,11 +1,11 @@ describe 'VideoControl', -> beforeEach -> - @player = jasmine.stubVideoPlayer @ + jasmine.stubVideoPlayer @ $('.video-controls').html '' describe 'constructor', -> it 'render the video controls', -> - new VideoControl @player + new VideoControl(element: $('.video-controls')) expect($('.video-controls').html()).toContain '''
                @@ -21,14 +21,8 @@ describe 'VideoControl', ->
                ''' - it 'bind player events', -> - control = new VideoControl @player - expect($(@player)).toHandleWith 'play', control.onPlay - expect($(@player)).toHandleWith 'pause', control.onPause - expect($(@player)).toHandleWith 'ended', control.onPause - it 'bind the playback button', -> - control = new VideoControl @player + control = new VideoControl(element: $('.video-controls')) expect($('.video_control')).toHandleWith 'click', control.togglePlayback describe 'when on a touch based device', -> @@ -36,7 +30,7 @@ describe 'VideoControl', -> spyOn(window, 'onTouchBasedDevice').andReturn true it 'does not add the play class to video control', -> - new VideoControl @player + new VideoControl(element: $('.video-controls')) expect($('.video_control')).not.toHaveClass 'play' expect($('.video_control')).not.toHaveHtml 'Play' @@ -46,24 +40,24 @@ describe 'VideoControl', -> spyOn(window, 'onTouchBasedDevice').andReturn false it 'add the play class to video control', -> - new VideoControl @player + new VideoControl(element: $('.video-controls')) expect($('.video_control')).toHaveClass 'play' expect($('.video_control')).toHaveHtml 'Play' - describe 'onPlay', -> + describe 'play', -> beforeEach -> - @control = new VideoControl @player - @control.onPlay() + @control = new VideoControl(element: $('.video-controls')) + @control.play() it 'switch playback button to play state', -> expect($('.video_control')).not.toHaveClass 'play' expect($('.video_control')).toHaveClass 'pause' expect($('.video_control')).toHaveHtml 'Pause' - describe 'onPause', -> + describe 'pause', -> beforeEach -> - @control = new VideoControl @player - @control.onPause() + @control = new VideoControl(element: $('.video-controls')) + @control.pause() it 'switch playback button to pause state', -> expect($('.video_control')).not.toHaveClass 'pause' @@ -72,7 +66,7 @@ describe 'VideoControl', -> describe 'togglePlayback', -> beforeEach -> - @control = new VideoControl @player + @control = new VideoControl(element: $('.video-controls')) describe 'when the control does not have play or pause class', -> beforeEach -> @@ -80,41 +74,36 @@ describe 'VideoControl', -> describe 'when the video is playing', -> beforeEach -> - spyOn(@player, 'isPlaying').andReturn true - spyOnEvent @player, 'pause' + $('.video_control').addClass('play') + spyOnEvent @control, 'pause' @control.togglePlayback jQuery.Event('click') it 'does not trigger the pause event', -> - expect('pause').not.toHaveBeenTriggeredOn @player + expect('pause').not.toHaveBeenTriggeredOn @control describe 'when the video is paused', -> beforeEach -> - spyOn(@player, 'isPlaying').andReturn false - spyOnEvent @player, 'play' + $('.video_control').addClass('pause') + spyOnEvent @control, 'play' @control.togglePlayback jQuery.Event('click') it 'does not trigger the play event', -> - expect('play').not.toHaveBeenTriggeredOn @player + expect('play').not.toHaveBeenTriggeredOn @control - for className in ['play', 'pause'] - describe "when the control has #{className} class", -> + describe 'when the video is playing', -> beforeEach -> - $('.video_control').addClass className + spyOnEvent @control, 'pause' + $('.video_control').addClass 'pause' + @control.togglePlayback jQuery.Event('click') - describe 'when the video is playing', -> - beforeEach -> - spyOn(@player, 'isPlaying').andReturn true - spyOnEvent @player, 'pause' - @control.togglePlayback jQuery.Event('click') + it 'trigger the pause event', -> + expect('pause').toHaveBeenTriggeredOn @control - it 'trigger the pause event', -> - expect('pause').toHaveBeenTriggeredOn @player + describe 'when the video is paused', -> + beforeEach -> + spyOnEvent @control, 'play' + $('.video_control').addClass 'play' + @control.togglePlayback jQuery.Event('click') - describe 'when the video is paused', -> - beforeEach -> - spyOn(@player, 'isPlaying').andReturn false - spyOnEvent @player, 'play' - @control.togglePlayback jQuery.Event('click') - - it 'trigger the play event', -> - expect('play').toHaveBeenTriggeredOn @player + it 'trigger the play event', -> + expect('play').toHaveBeenTriggeredOn @control diff --git a/lms/static/coffee/spec/modules/video/video_player_spec.coffee b/lms/static/coffee/spec/modules/video/video_player_spec.coffee index ebcb2cc009..7c86b2c9ca 100644 --- a/lms/static/coffee/spec/modules/video/video_player_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_player_spec.coffee @@ -15,7 +15,7 @@ describe 'VideoPlayer', -> describe 'always', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video it 'instanticate current time to zero', -> expect(@player.currentTime).toEqual 0 @@ -24,16 +24,16 @@ describe 'VideoPlayer', -> expect(@player.element).toBe '#video_example' it 'create video control', -> - expect(window.VideoControl).toHaveBeenCalledWith @player + expect(window.VideoControl).toHaveBeenCalledWith element: $('.video-controls', @player.element) it 'create video caption', -> - expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456' + expect(window.VideoCaption).toHaveBeenCalledWith element: @player.element, youtubeId: 'def456', currentSpeed: '1.0' it 'create video speed control', -> - expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0'] + expect(window.VideoSpeedControl).toHaveBeenCalledWith element: $('.secondary-controls', @player.element), speeds: ['0.75', '1.0'], currentSpeed: '1.0' it 'create video progress slider', -> - expect(window.VideoProgressSlider).toHaveBeenCalledWith @player + expect(window.VideoProgressSlider).toHaveBeenCalledWith element: $('.slider', @player.element) it 'create Youtube player', -> expect(YT.Player).toHaveBeenCalledWith 'example' @@ -48,49 +48,48 @@ describe 'VideoPlayer', -> onReady: @player.onReady onStateChange: @player.onStateChange - it 'bind to seek event', -> - expect($(@player)).toHandleWith 'seek', @player.onSeek + it 'bind to video control play event', -> + expect($(@player.control)).toHandleWith 'play', @player.play - it 'bind to updatePlayTime event', -> - expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime + it 'bind to video control pause event', -> + expect($(@player.control)).toHandleWith 'pause', @player.pause - it 'bidn to speedChange event', -> - expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange + it 'bind to video caption seek event', -> + expect($(@player.caption)).toHandleWith 'seek', @player.onSeek - it 'bind to play event', -> - expect($(@player)).toHandleWith 'play', @player.onPlay + it 'bind to video speed control speedChange event', -> + expect($(@player.speedControl)).toHandleWith 'speedChange', @player.onSpeedChange - it 'bind to paused event', -> - expect($(@player)).toHandleWith 'pause', @player.onPause + it 'bind to video progress slider seek event', -> + expect($(@player.progressSlider)).toHandleWith 'seek', @player.onSeek - it 'bind to ended event', -> - expect($(@player)).toHandleWith 'ended', @player.onPause + it 'bind to video volume control volumeChange event', -> + expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange it 'bind to key press', -> expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen it 'bind to fullscreen switching button', -> - console.debug $('.add-fullscreen') expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen describe 'when not on a touch based device', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn false $('.add-fullscreen, .hide-subtitles').removeData 'qtip' - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video it 'add the tooltip to fullscreen and subtitle button', -> expect($('.add-fullscreen')).toHaveData 'qtip' expect($('.hide-subtitles')).toHaveData 'qtip' it 'create video volume control', -> - expect(window.VideoVolumeControl).toHaveBeenCalledWith @player + expect(window.VideoVolumeControl).toHaveBeenCalledWith element: $('.secondary-controls', @player.element) describe 'when on a touch based device', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn true $('.add-fullscreen, .hide-subtitles').removeData 'qtip' - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video it 'does not add the tooltip to fullscreen and subtitle button', -> expect($('.add-fullscreen')).not.toHaveData 'qtip' @@ -107,12 +106,6 @@ describe 'VideoPlayer', -> spyOnEvent @player, 'updatePlayTime' @player.onReady() - it 'reset the progress to zero', -> - expect('updatePlayTime').toHaveBeenTriggeredOn @player - - it 'trigger ready event on the player', -> - expect('ready').toHaveBeenTriggeredOn @player - describe 'when not on a touch based device', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn false @@ -133,93 +126,108 @@ describe 'VideoPlayer', -> describe 'onStateChange', -> beforeEach -> - @player = new VideoPlayer @video - - describe 'when the video is playing', -> - beforeEach -> - spyOnEvent @player, 'play' - @player.onStateChange data: YT.PlayerState.PLAYING - - it 'trigger play event', -> - expect('play').toHaveBeenTriggeredOn @player - - describe 'when the video is paused', -> - beforeEach -> - spyOnEvent @player, 'pause' - @player.onStateChange data: YT.PlayerState.PAUSED - - it 'trigger pause event', -> - expect('pause').toHaveBeenTriggeredOn @player + @player = new VideoPlayer video: @video describe 'when the video is unstarted', -> beforeEach -> - spyOnEvent @player, 'pause' + spyOn @player.control, 'pause' + @player.caption.pause = jasmine.createSpy('VideoCaption.pause') @player.onStateChange data: YT.PlayerState.UNSTARTED - it 'trigger pause event', -> - expect('pause').toHaveBeenTriggeredOn @player + it 'pause the video control', -> + expect(@player.control.pause).toHaveBeenCalled() + + it 'pause the video caption', -> + expect(@player.caption.pause).toHaveBeenCalled() + + describe 'when the video is playing', -> + beforeEach -> + @anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['pauseVideo'] + window.player = @anotherPlayer + spyOn Logger, 'log' + spyOn(window, 'setInterval').andReturn 100 + spyOn @player.control, 'play' + @player.caption.play = jasmine.createSpy('VideoCaption.play') + @player.progressSlider.play = jasmine.createSpy('VideoProgressSlider.play') + @player.player.getVideoEmbedCode.andReturn 'embedCode' + @player.onStateChange data: YT.PlayerState.PLAYING + + it 'log the play_video event', -> + expect(Logger.log).toHaveBeenCalledWith 'play_video', id: @player.currentTime, code: 'embedCode' + + it 'pause other video player', -> + expect(@anotherPlayer.pauseVideo).toHaveBeenCalled() + + it 'set current video player as active player', -> + expect(window.player).toEqual @player.player + + it 'set update interval', -> + expect(window.setInterval).toHaveBeenCalledWith @player.update, 200 + expect(@player.player.interval).toEqual 100 + + it 'play the video control', -> + expect(@player.control.play).toHaveBeenCalled() + + it 'play the video caption', -> + expect(@player.caption.play).toHaveBeenCalled() + + it 'play the video progress slider', -> + expect(@player.progressSlider.play).toHaveBeenCalled() + + describe 'when the video is paused', -> + beforeEach -> + @player = new VideoPlayer video: @video + window.player = @player.player + spyOn Logger, 'log' + spyOn window, 'clearInterval' + spyOn @player.control, 'pause' + @player.caption.pause = jasmine.createSpy('VideoCaption.pause') + @player.player.interval = 100 + @player.player.getVideoEmbedCode.andReturn 'embedCode' + @player.onStateChange data: YT.PlayerState.PAUSED + + it 'log the pause_video event', -> + expect(Logger.log).toHaveBeenCalledWith 'pause_video', id: @player.currentTime, code: 'embedCode' + + it 'set current video player as inactive', -> + expect(window.player).toBeNull() + + it 'clear update interval', -> + expect(window.clearInterval).toHaveBeenCalledWith 100 + expect(@player.player.interval).toBeNull() + + it 'pause the video control', -> + expect(@player.control.pause).toHaveBeenCalled() + + it 'pause the video caption', -> + expect(@player.caption.pause).toHaveBeenCalled() describe 'when the video is ended', -> beforeEach -> - spyOnEvent @player, 'ended' + spyOn @player.control, 'pause' + @player.caption.pause = jasmine.createSpy('VideoCaption.pause') @player.onStateChange data: YT.PlayerState.ENDED - it 'trigger ended event', -> - expect('ended').toHaveBeenTriggeredOn @player + it 'pause the video control', -> + expect(@player.control.pause).toHaveBeenCalled() - describe 'onPlay', -> - beforeEach -> - @player = new VideoPlayer @video - @anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['pauseVideo'] - window.player = @anotherPlayer - spyOn Logger, 'log' - spyOn(window, 'setInterval').andReturn 100 - @player.player.getVideoEmbedCode.andReturn 'embedCode' - @player.onPlay() - - it 'log the play_video event', -> - expect(Logger.log).toHaveBeenCalledWith 'play_video', id: @player.currentTime, code: 'embedCode' - - it 'pause other video player', -> - expect(@anotherPlayer.pauseVideo).toHaveBeenCalled() - - it 'set current video player as active player', -> - expect(window.player).toEqual @player.player - - it 'set update interval', -> - expect(window.setInterval).toHaveBeenCalledWith @player.update, 200 - expect(@player.player.interval).toEqual 100 - - describe 'onPause', -> - beforeEach -> - @player = new VideoPlayer @video - window.player = @player.player - spyOn Logger, 'log' - spyOn window, 'clearInterval' - @player.player.interval = 100 - @player.player.getVideoEmbedCode.andReturn 'embedCode' - @player.onPause() - - it 'log the pause_video event', -> - expect(Logger.log).toHaveBeenCalledWith 'pause_video', id: @player.currentTime, code: 'embedCode' - - it 'set current video player as inactive', -> - expect(window.player).toBeNull() - - it 'clear update interval', -> - expect(window.clearInterval).toHaveBeenCalledWith 100 - expect(@player.player.interval).toBeNull() + it 'pause the video caption', -> + expect(@player.caption.pause).toHaveBeenCalled() describe 'onSeek', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video spyOn window, 'clearInterval' @player.player.interval = 100 + spyOn @player, 'updatePlayTime' @player.onSeek {}, 60 it 'seek the player', -> expect(@player.player.seekTo).toHaveBeenCalledWith 60, true + it 'call updatePlayTime on player', -> + expect(@player.updatePlayTime).toHaveBeenCalledWith 60 + describe 'when the player is playing', -> beforeEach -> @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING @@ -231,19 +239,16 @@ describe 'VideoPlayer', -> describe 'when the player is not playing', -> beforeEach -> @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED - spyOnEvent @player, 'updatePlayTime' @player.onSeek {}, 60 it 'set the current time', -> expect(@player.currentTime).toEqual 60 - it 'trigger updatePlayTime event', -> - expect('updatePlayTime').toHaveBeenTriggeredOn @player - describe 'onSpeedChange', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video @player.currentTime = 60 + spyOn @player, 'updatePlayTime' spyOn(@video, 'setSpeed').andCallThrough() describe 'always', -> @@ -256,34 +261,43 @@ describe 'VideoPlayer', -> it 'set video speed to the new speed', -> expect(@video.setSpeed).toHaveBeenCalledWith '0.75' + it 'tell video caption that the speed has changed', -> + expect(@player.caption.currentSpeed).toEqual '0.75' + describe 'when the video is playing', -> beforeEach -> @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING - spyOnEvent @player, 'updatePlayTime' @player.onSpeedChange {}, '0.75' it 'load the video', -> expect(@player.player.loadVideoById).toHaveBeenCalledWith 'abc123', '80.000' it 'trigger updatePlayTime event', -> - expect('updatePlayTime').toHaveBeenTriggeredOn @player + expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000' describe 'when the video is not playing', -> beforeEach -> @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED - spyOnEvent @player, 'updatePlayTime' @player.onSpeedChange {}, '0.75' it 'cue the video', -> expect(@player.player.cueVideoById).toHaveBeenCalledWith 'abc123', '80.000' it 'trigger updatePlayTime event', -> - expect('updatePlayTime').toHaveBeenTriggeredOn @player + expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000' + + describe 'onVolumeChange', -> + beforeEach -> + @player = new VideoPlayer video: @video + @player.onVolumeChange undefined, 60 + + it 'set the volume on player', -> + expect(@player.player.setVolume).toHaveBeenCalledWith 60 describe 'update', -> beforeEach -> - @player = new VideoPlayer @video - spyOnEvent @player, 'updatePlayTime' + @player = new VideoPlayer video: @video + spyOn @player, 'updatePlayTime' describe 'when the current time is unavailable from the player', -> beforeEach -> @@ -291,7 +305,7 @@ describe 'VideoPlayer', -> @player.update() it 'does not trigger updatePlayTime event', -> - expect('updatePlayTime').not.toHaveBeenTriggeredOn @player + expect(@player.updatePlayTime).not.toHaveBeenCalled() describe 'when the current time is available from the player', -> beforeEach -> @@ -299,25 +313,33 @@ describe 'VideoPlayer', -> @player.update() it 'trigger updatePlayTime event', -> - expect('updatePlayTime').toHaveBeenTriggeredOn @player + expect(@player.updatePlayTime).toHaveBeenCalledWith(60) - describe 'onUpdatePlaytime', -> + describe 'updatePlayTime', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video spyOn(@video, 'getDuration').andReturn 1800 - @player.onUpdatePlayTime {}, 60 + @player.caption.updatePlayTime = jasmine.createSpy('VideoCaption.updatePlayTime') + @player.progressSlider.updatePlayTime = jasmine.createSpy('VideoProgressSlider.updatePlayTime') + @player.updatePlayTime 60 it 'update the video playback time', -> expect($('.vidtime')).toHaveHtml '1:00 / 30:00' + it 'update the playback time on caption', -> + expect(@player.caption.updatePlayTime).toHaveBeenCalledWith 60 + + it 'update the playback time on progress slider', -> + expect(@player.progressSlider.updatePlayTime).toHaveBeenCalledWith 60, 1800 + describe 'toggleFullScreen', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video + @player.caption.resize = jasmine.createSpy('VideoCaption.resize') describe 'when the video player is not full screen', -> beforeEach -> @player.element.removeClass 'fullscreen' - spyOnEvent @player, 'resize' @player.toggleFullScreen(jQuery.Event("click")) it 'replace the full screen button tooltip', -> @@ -329,13 +351,12 @@ describe 'VideoPlayer', -> it 'add the fullscreen class', -> expect(@player.element).toHaveClass 'fullscreen' - it 'trigger resize event', -> - expect('resize').toHaveBeenTriggeredOn @player + it 'tell VideoCaption to resize', -> + expect(@player.caption.resize).toHaveBeenCalled() describe 'when the video player already full screen', -> beforeEach -> @player.element.addClass 'fullscreen' - spyOnEvent @player, 'resize' @player.toggleFullScreen(jQuery.Event("click")) it 'replace the full screen button tooltip', -> @@ -347,12 +368,12 @@ describe 'VideoPlayer', -> it 'remove the fullscreen class', -> expect(@player.element).not.toHaveClass 'fullscreen' - it 'trigger resize event', -> - expect('resize').toHaveBeenTriggeredOn @player + it 'tell VideoCaption to resize', -> + expect(@player.caption.resize).toHaveBeenCalled() describe 'play', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video describe 'when the player is not ready', -> beforeEach -> @@ -372,7 +393,7 @@ describe 'VideoPlayer', -> describe 'isPlaying', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video describe 'when the video is playing', -> beforeEach -> @@ -390,7 +411,7 @@ describe 'VideoPlayer', -> describe 'pause', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video @player.pause() it 'delegate to the Youtube player', -> @@ -398,7 +419,7 @@ describe 'VideoPlayer', -> describe 'duration', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video spyOn @video, 'getDuration' @player.duration() @@ -407,22 +428,8 @@ describe 'VideoPlayer', -> describe 'currentSpeed', -> beforeEach -> - @player = new VideoPlayer @video + @player = new VideoPlayer video: @video @video.speed = '3.0' it 'delegate to the video', -> expect(@player.currentSpeed()).toEqual '3.0' - - describe 'volume', -> - beforeEach -> - @player = new VideoPlayer @video - @player.player.getVolume.andReturn 42 - - describe 'without value', -> - it 'return current volume', -> - expect(@player.volume()).toEqual 42 - - describe 'with value', -> - it 'set player volume', -> - @player.volume(60) - expect(@player.player.setVolume).toHaveBeenCalledWith(60) diff --git a/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee b/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee index ae78048c55..d4dd41e41d 100644 --- a/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_progress_slider_spec.coffee @@ -1,79 +1,13 @@ describe 'VideoProgressSlider', -> beforeEach -> - @player = jasmine.stubVideoPlayer @ + jasmine.stubVideoPlayer @ describe 'constructor', -> describe 'on a non-touch based device', -> beforeEach -> spyOn($.fn, 'slider').andCallThrough() spyOn(window, 'onTouchBasedDevice').andReturn false - @slider = new VideoProgressSlider @player - - it 'build the slider', -> - expect(@slider.slider).toBe '.slider' - expect($.fn.slider).toHaveBeenCalledWith - range: 'min' - change: @slider.onChange - slide: @slider.onSlide - stop: @slider.onStop - - it 'build the seek handle', -> - expect(@slider.handle).toBe '.slider .ui-slider-handle' - expect($.fn.qtip).toHaveBeenCalledWith - content: "0:00" - position: - my: 'bottom center' - at: 'top center' - container: @slider.handle - hide: - delay: 700 - style: - classes: 'ui-tooltip-slider' - widget: true - - it 'bind player events', -> - expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime - expect($(@player)).toHandleWith 'ready', @slider.onReady - expect($(@player)).toHandleWith 'play', @slider.onPlay - - describe 'on a touch-based device', -> - beforeEach -> - spyOn($.fn, 'slider').andCallThrough() - spyOn(window, 'onTouchBasedDevice').andReturn true - @slider = new VideoProgressSlider @player - - it 'does not build the slider', -> - expect(@slider.slider).toBeUndefined - expect($.fn.slider).not.toHaveBeenCalled() - - it 'bind player events', -> - expect($(@player)).toHandleWith 'updatePlayTime', @slider.onUpdatePlayTime - - describe 'onReady', -> - beforeEach -> - spyOn(@player, 'duration').andReturn 120 - @slider = new VideoProgressSlider @player - @slider.onReady() - - it 'set the max value to the length of video', -> - expect(@slider.slider.slider('option', 'max')).toEqual 120 - - describe 'onPlay', -> - beforeEach -> - @slider = new VideoProgressSlider @player - spyOn($.fn, 'slider').andCallThrough() - - describe 'when the slider was already built', -> - beforeEach -> - @slider.onPlay() - - it 'does not build the slider', -> - expect($.fn.slider).not.toHaveBeenCalled - - describe 'when the slider was not already built', -> - beforeEach -> - @slider.slider = null - @slider.onPlay() + @slider = new VideoProgressSlider element: $('.slider') it 'build the slider', -> expect(@slider.slider).toBe '.slider' @@ -97,16 +31,64 @@ describe 'VideoProgressSlider', -> classes: 'ui-tooltip-slider' widget: true - describe 'onUpdatePlayTime', -> + describe 'on a touch-based device', -> + beforeEach -> + spyOn($.fn, 'slider').andCallThrough() + spyOn(window, 'onTouchBasedDevice').andReturn true + @slider = new VideoProgressSlider element: $('.slider') + + it 'does not build the slider', -> + expect(@slider.slider).toBeUndefined + expect($.fn.slider).not.toHaveBeenCalled() + + describe 'play', -> beforeEach -> - @slider = new VideoProgressSlider @player - spyOn(@player, 'duration').andReturn 120 + @slider = new VideoProgressSlider element: $('.slider') + spyOn($.fn, 'slider').andCallThrough() + + describe 'when the slider was already built', -> + beforeEach -> + @slider.play() + + it 'does not build the slider', -> + expect($.fn.slider).not.toHaveBeenCalled + + describe 'when the slider was not already built', -> + beforeEach -> + @slider.slider = null + @slider.play() + + it 'build the slider', -> + expect(@slider.slider).toBe '.slider' + expect($.fn.slider).toHaveBeenCalledWith + range: 'min' + change: @slider.onChange + slide: @slider.onSlide + stop: @slider.onStop + + it 'build the seek handle', -> + expect(@slider.handle).toBe '.ui-slider-handle' + expect($.fn.qtip).toHaveBeenCalledWith + content: "0:00" + position: + my: 'bottom center' + at: 'top center' + container: @slider.handle + hide: + delay: 700 + style: + classes: 'ui-tooltip-slider' + widget: true + + describe 'updatePlayTime', -> + beforeEach -> + @slider = new VideoProgressSlider element: $('.slider') spyOn($.fn, 'slider').andCallThrough() describe 'when frozen', -> beforeEach -> @slider.frozen = true - @slider.onUpdatePlayTime {}, 20 + @slider.updatePlayTime 20, 120 it 'does not update the slider', -> expect($.fn.slider).not.toHaveBeenCalled() @@ -114,7 +96,7 @@ describe 'VideoProgressSlider', -> describe 'when not frozen', -> beforeEach -> @slider.frozen = false - @slider.onUpdatePlayTime {}, 20 + @slider.updatePlayTime 20, 120 it 'update the max value of the slider', -> expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120 @@ -124,10 +106,10 @@ describe 'VideoProgressSlider', -> describe 'onSlide', -> beforeEach -> - @slider = new VideoProgressSlider @player + @slider = new VideoProgressSlider element: $('.slider') @time = null - $(@player).bind 'seek', (event, time) => @time = time - spyOnEvent @player, 'seek' + $(@slider).bind 'seek', (event, time) => @time = time + spyOnEvent @slider, 'seek' @slider.onSlide {}, value: 20 it 'freeze the slider', -> @@ -137,12 +119,12 @@ describe 'VideoProgressSlider', -> expect($.fn.qtip).toHaveBeenCalled() it 'trigger seek event', -> - expect('seek').toHaveBeenTriggeredOn @player + expect('seek').toHaveBeenTriggeredOn @slider expect(@time).toEqual 20 describe 'onChange', -> beforeEach -> - @slider = new VideoProgressSlider @player + @slider = new VideoProgressSlider element: $('.slider') @slider.onChange {}, value: 20 it 'update the tooltip', -> @@ -150,10 +132,10 @@ describe 'VideoProgressSlider', -> describe 'onStop', -> beforeEach -> - @slider = new VideoProgressSlider @player + @slider = new VideoProgressSlider element: $('.slider') @time = null - $(@player).bind 'seek', (event, time) => @time = time - spyOnEvent @player, 'seek' + $(@slider).bind 'seek', (event, time) => @time = time + spyOnEvent @slider, 'seek' spyOn(window, 'setTimeout') @slider.onStop {}, value: 20 @@ -161,7 +143,7 @@ describe 'VideoProgressSlider', -> expect(@slider.frozen).toBeTruthy() it 'trigger seek event', -> - expect('seek').toHaveBeenTriggeredOn @player + expect('seek').toHaveBeenTriggeredOn @slider expect(@time).toEqual 20 it 'set timeout to unfreeze the slider', -> @@ -171,7 +153,7 @@ describe 'VideoProgressSlider', -> describe 'updateTooltip', -> beforeEach -> - @slider = new VideoProgressSlider @player + @slider = new VideoProgressSlider element: $('.slider') @slider.updateTooltip 90 it 'set the tooltip value', -> diff --git a/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee b/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee index 737460d1ce..deced6f948 100644 --- a/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_speed_control_spec.coffee @@ -1,12 +1,12 @@ describe 'VideoSpeedControl', -> beforeEach -> - @player = jasmine.stubVideoPlayer @ + jasmine.stubVideoPlayer @ $('.speeds').remove() describe 'constructor', -> describe 'always', -> beforeEach -> - @speedControl = new VideoSpeedControl @player, @video.speeds + @speedControl = new VideoSpeedControl element: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' it 'add the video speed control to player', -> expect($('.secondary-controls').html()).toContain ''' @@ -19,9 +19,6 @@ describe 'VideoSpeedControl', ->

                ''' - it 'bind to player speedChange event', -> - expect($(@player)).toHandleWith 'speedChange', @speedControl.onSpeedChange - it 'bind to change video speed link', -> expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed @@ -29,7 +26,7 @@ describe 'VideoSpeedControl', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn true $('.speeds').removeClass 'open' - @speedControl = new VideoSpeedControl @player, @video.speeds + @speedControl = new VideoSpeedControl element: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' it 'open the speed toggle on click', -> $('.speeds').click() @@ -41,7 +38,7 @@ describe 'VideoSpeedControl', -> beforeEach -> spyOn(window, 'onTouchBasedDevice').andReturn false $('.speeds').removeClass 'open' - @speedControl = new VideoSpeedControl @player, @video.speeds + @speedControl = new VideoSpeedControl element: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' it 'open the speed toggle on hover', -> $('.speeds').mouseenter() @@ -59,31 +56,31 @@ describe 'VideoSpeedControl', -> describe 'changeVideoSpeed', -> beforeEach -> - @speedControl = new VideoSpeedControl @player, @video.speeds + @speedControl = new VideoSpeedControl element: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' @video.setSpeed '1.0' describe 'when new speed is the same', -> beforeEach -> - spyOnEvent @player, 'speedChange' + spyOnEvent @speedControl, 'speedChange' $('li[data-speed="1.0"] a').click() it 'does not trigger speedChange event', -> - expect('speedChange').not.toHaveBeenTriggeredOn @player + expect('speedChange').not.toHaveBeenTriggeredOn @speedControl describe 'when new speed is not the same', -> beforeEach -> @newSpeed = null - $(@player).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed - spyOnEvent @player, 'speedChange' + $(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed + spyOnEvent @speedControl, 'speedChange' $('li[data-speed="0.75"] a').click() - it 'trigger player speedChange event', -> - expect('speedChange').toHaveBeenTriggeredOn @player + it 'trigger speedChange event', -> + expect('speedChange').toHaveBeenTriggeredOn @speedControl expect(@newSpeed).toEqual 0.75 describe 'onSpeedChange', -> beforeEach -> - @speedControl = new VideoSpeedControl @player, @video.speeds + @speedControl = new VideoSpeedControl element: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' $('li[data-speed="1.0"] a').addClass 'active' @speedControl.setSpeed '0.75' diff --git a/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee b/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee index cbdef03ef0..7ef9c02780 100644 --- a/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee +++ b/lms/static/coffee/spec/modules/video/video_volume_control_spec.coffee @@ -1,15 +1,15 @@ describe 'VideoVolumeControl', -> beforeEach -> - @player = jasmine.stubVideoPlayer @ + jasmine.stubVideoPlayer @ $('.volume').remove() describe 'constructor', -> beforeEach -> spyOn($.fn, 'slider') - @volumeControl = new VideoVolumeControl @player + @volumeControl = new VideoVolumeControl element: $('.secondary-controls') - it 'initialize previousVolume to 100', -> - expect(@volumeControl.previousVolume).toEqual 100 + it 'initialize currentVolume to 100', -> + expect(@volumeControl.currentVolume).toEqual 100 it 'render the volume control', -> expect($('.secondary-controls').html()).toContain """ @@ -32,7 +32,6 @@ describe 'VideoVolumeControl', -> slide: @volumeControl.onChange it 'bind the volume control', -> - expect($(@player)).toHandleWith 'ready', @volumeControl.onReady expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute expect($('.volume')).not.toHaveClass 'open' @@ -41,27 +40,19 @@ describe 'VideoVolumeControl', -> $('.volume').mouseleave() expect($('.volume')).not.toHaveClass 'open' - describe 'onReady', -> - beforeEach -> - @volumeControl = new VideoVolumeControl @player - spyOn $.fn, 'slider' - spyOn(@player, 'volume').andReturn 60 - @volumeControl.onReady() - - it 'set the max value of the slider', -> - expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 60 - describe 'onChange', -> beforeEach -> - spyOn @player, 'volume' - @volumeControl = new VideoVolumeControl @player + spyOnEvent @volumeControl, 'volumeChange' + @newVolume = undefined + @volumeControl = new VideoVolumeControl element: $('.secondary-controls') + $(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume describe 'when the new volume is more than 0', -> beforeEach -> @volumeControl.onChange undefined, value: 60 it 'set the player volume', -> - expect(@player.volume).toHaveBeenCalledWith 60 + expect(@newVolume).toEqual 60 it 'remote muted class', -> expect($('.volume')).not.toHaveClass 'muted' @@ -71,32 +62,33 @@ describe 'VideoVolumeControl', -> @volumeControl.onChange undefined, value: 0 it 'set the player volume', -> - expect(@player.volume).toHaveBeenCalledWith 0 + expect(@newVolume).toEqual 0 it 'add muted class', -> expect($('.volume')).toHaveClass 'muted' describe 'toggleMute', -> beforeEach -> - spyOn @player, 'volume' - @volumeControl = new VideoVolumeControl @player + @newVolume = undefined + @volumeControl = new VideoVolumeControl element: $('.secondary-controls') + $(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume describe 'when the current volume is more than 0', -> beforeEach -> - @player.volume.andReturn 60 + @volumeControl.currentVolume = 60 @volumeControl.toggleMute() it 'save the previous volume', -> expect(@volumeControl.previousVolume).toEqual 60 it 'set the player volume', -> - expect(@player.volume).toHaveBeenCalledWith 0 + expect(@newVolume).toEqual 0 describe 'when the current volume is 0', -> beforeEach -> - @player.volume.andReturn 0 + @volumeControl.currentVolume = 0 @volumeControl.previousVolume = 60 @volumeControl.toggleMute() it 'set the player volume to previous volume', -> - expect(@player.volume).toHaveBeenCalledWith 60 + expect(@newVolume).toEqual 60 diff --git a/lms/static/coffee/spec/modules/video_spec.coffee b/lms/static/coffee/spec/modules/video_spec.coffee index 134b38caff..12fac66a5b 100644 --- a/lms/static/coffee/spec/modules/video_spec.coffee +++ b/lms/static/coffee/spec/modules/video_spec.coffee @@ -57,7 +57,7 @@ describe 'Video', -> window.YT = @originalYT it 'create the Video Player', -> - expect(window.VideoPlayer).toHaveBeenCalledWith @video + expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video) expect(@video.player).toEqual @stubVideoPlayer describe 'when the Youtube API is not ready', -> @@ -84,7 +84,7 @@ describe 'Video', -> window.YT = @originalYT it 'create the Video Player for all video elements', -> - expect(window.VideoPlayer).toHaveBeenCalledWith @video + expect(window.VideoPlayer).toHaveBeenCalledWith(video: @video) expect(@video.player).toEqual @stubVideoPlayer describe 'youtubeId', -> diff --git a/lms/static/coffee/src/_subview.coffee b/lms/static/coffee/src/_subview.coffee new file mode 100644 index 0000000000..bddf91a895 --- /dev/null +++ b/lms/static/coffee/src/_subview.coffee @@ -0,0 +1,14 @@ +class @Subview + constructor: (options) -> + $.each options, (key, value) => + @[key] = value + @initialize() + @render() + @bind() + + $: (selector) -> + $(selector, @element) + + initialize: -> + render: -> + bind: -> diff --git a/lms/static/coffee/src/modules/video.coffee b/lms/static/coffee/src/modules/video.coffee index 888acba777..048bf679b8 100644 --- a/lms/static/coffee/src/modules/video.coffee +++ b/lms/static/coffee/src/modules/video.coffee @@ -36,7 +36,7 @@ class @Video @speed = '1.0' embed: -> - @player = new VideoPlayer(this) + @player = new VideoPlayer video: this fetchMetadata: (url) -> @metadata = {} diff --git a/lms/static/coffee/src/modules/video/video_caption.coffee b/lms/static/coffee/src/modules/video/video_caption.coffee index 5dde796b78..87a6f8f890 100644 --- a/lms/static/coffee/src/modules/video/video_caption.coffee +++ b/lms/static/coffee/src/modules/video/video_caption.coffee @@ -1,17 +1,6 @@ -class @VideoCaption - constructor: (@player, @youtubeId) -> - @render() - @bind() - - $: (selector) -> - @player.$(selector) - +class @VideoCaption extends Subview bind: -> - $(window).bind('resize', @onWindowResize) - $(@player).bind('resize', @onWindowResize) - $(@player).bind('updatePlayTime', @onUpdatePlayTime) - $(@player).bind('seek', @onUpdatePlayTime) - $(@player).bind('play', @onPlay) + $(window).bind('resize', @resize) @$('.hide-subtitles').click @toggle @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) .mousemove(@onMovement).bind('mousewheel', @onMovement) @@ -70,12 +59,16 @@ class @VideoCaption return min - onPlay: => + play: -> @renderCaption() unless @rendered + @playing = true - onUpdatePlayTime: (event, time) => + pause: -> + @playing = false + + updatePlayTime: (time) -> # This 250ms offset is required to match the video speed - time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250) + time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250) newIndex = @search time if newIndex != undefined && @currentIndex != newIndex @@ -86,7 +79,7 @@ class @VideoCaption @currentIndex = newIndex @scrollCaption() - onWindowResize: => + resize: => @$('.subtitles').css maxHeight: @captionHeight() @$('.subtitles .spacing:first').height(@topSpacingHeight()) @$('.subtitles .spacing:last').height(@bottomSpacingHeight()) @@ -102,7 +95,7 @@ class @VideoCaption onMouseLeave: => clearTimeout @frozen if @frozen @frozen = null - @scrollCaption() if @player.isPlaying() + @scrollCaption() if @playing scrollCaption: -> if !@frozen && @$('.subtitles .current:first').length @@ -111,8 +104,8 @@ class @VideoCaption seekPlayer: (event) => event.preventDefault() - time = Math.round(Time.convert($(event.target).data('start'), '1.0', @player.currentSpeed()) / 1000) - $(@player).trigger('seek', time) + time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000) + $(@).trigger('seek', time) calculateOffset: (element) -> @captionHeight() / 2 - element.height() / 2 @@ -125,16 +118,16 @@ class @VideoCaption toggle: (event) => event.preventDefault() - if @player.element.hasClass('closed') + if @element.hasClass('closed') @$('.hide-subtitles').attr('title', 'Turn off captions') - @player.element.removeClass('closed') + @element.removeClass('closed') @scrollCaption() else @$('.hide-subtitles').attr('title', 'Turn on captions') - @player.element.addClass('closed') + @element.addClass('closed') captionHeight: -> - if @player.element.hasClass('fullscreen') + if @element.hasClass('fullscreen') $(window).height() - @$('.video-controls').height() else @$('.video-wrapper').height() diff --git a/lms/static/coffee/src/modules/video/video_control.coffee b/lms/static/coffee/src/modules/video/video_control.coffee index 0c303fc4ad..dba18b5883 100644 --- a/lms/static/coffee/src/modules/video/video_control.coffee +++ b/lms/static/coffee/src/modules/video/video_control.coffee @@ -1,19 +1,9 @@ -class @VideoControl - constructor: (@player) -> - @render() - @bind() - - $: (selector) -> - @player.$(selector) - +class @VideoControl extends Subview bind: -> - $(@player).bind('play', @onPlay) - .bind('pause', @onPause) - .bind('ended', @onPause) @$('.video_control').click @togglePlayback render: -> - @$('.video-controls').append """ + @element.append """
                  @@ -31,16 +21,15 @@ class @VideoControl unless onTouchBasedDevice() @$('.video_control').addClass('play').html('Play') - onPlay: => + play: -> @$('.video_control').removeClass('play').addClass('pause').html('Pause') - onPause: => + pause: -> @$('.video_control').removeClass('pause').addClass('play').html('Play') togglePlayback: (event) => event.preventDefault() - if $('.video_control').hasClass('play') || $('.video_control').hasClass('pause') - if @player.isPlaying() - $(@player).trigger('pause') - else - $(@player).trigger('play') + if @$('.video_control').hasClass('play') + $(@).trigger('play') + else if @$('.video_control').hasClass('pause') + $(@).trigger('pause') diff --git a/lms/static/coffee/src/modules/video/video_player.coffee b/lms/static/coffee/src/modules/video/video_player.coffee index 20df378267..2934eff191 100644 --- a/lms/static/coffee/src/modules/video/video_player.coffee +++ b/lms/static/coffee/src/modules/video/video_player.coffee @@ -1,23 +1,19 @@ -class @VideoPlayer - constructor: (@video) -> +class @VideoPlayer extends Subview + initialize: -> # Define a missing constant of Youtube API YT.PlayerState.UNSTARTED = -1 @currentTime = 0 @element = $("#video_#{@video.id}") - @render() - @bind() - - $: (selector) -> - $(selector, @element) bind: -> - $(@).bind('seek', @onSeek) - .bind('updatePlayTime', @onUpdatePlayTime) - .bind('speedChange', @onSpeedChange) - .bind('play', @onPlay) - .bind('pause', @onPause) - .bind('ended', @onPause) + $(@control).bind('play', @play) + .bind('pause', @pause) + $(@caption).bind('seek', @onSeek) + $(@speedControl).bind('speedChange', @onSpeedChange) + $(@progressSlider).bind('seek', @onSeek) + if @volumeControl + $(@volumeControl).bind('volumeChange', @onVolumeChange) $(document).keyup @bindExitFullScreen @$('.add-fullscreen').click @toggleFullScreen @@ -28,11 +24,12 @@ class @VideoPlayer @toggleFullScreen(event) render: -> - new VideoControl @ - new VideoCaption @, @video.youtubeId('1.0') - new VideoVolumeControl @ unless onTouchBasedDevice() - new VideoSpeedControl @, @video.speeds - new VideoProgressSlider @ + @control = new VideoControl element: @$('.video-controls') + @caption = new VideoCaption element: @element, youtubeId: @video.youtubeId('1.0'), currentSpeed: @currentSpeed() + unless onTouchBasedDevice() + @volumeControl = new VideoVolumeControl element: @$('.secondary-controls') + @speedControl = new VideoSpeedControl element: @$('.secondary-controls'), speeds: @video.speeds, currentSpeed: @currentSpeed() + @progressSlider = new VideoProgressSlider element: @$('.slider') @player = new YT.Player @video.id, playerVars: controls: 0 @@ -52,19 +49,23 @@ class @VideoPlayer at: 'top center' onReady: => - $(@).trigger('ready') - $(@).trigger('updatePlayTime', 0) unless onTouchBasedDevice() $('.course-content .video:first').data('video').player.play() onStateChange: (event) => switch event.data + when YT.PlayerState.UNSTARTED + @onUnstarted() when YT.PlayerState.PLAYING - $(@).trigger('play') - when YT.PlayerState.PAUSED, YT.PlayerState.UNSTARTED - $(@).trigger('pause') + @onPlay() + when YT.PlayerState.PAUSED + @onPause() when YT.PlayerState.ENDED - $(@).trigger('ended') + @onEnded() + + onUnstarted: => + @control.pause() + @caption.pause() onPlay: => Logger.log 'play_video', id: @currentTime, code: @player.getVideoEmbedCode() @@ -72,39 +73,55 @@ class @VideoPlayer window.player = @player unless @player.interval @player.interval = setInterval(@update, 200) + @caption.play() + @control.play() + @progressSlider.play() onPause: => Logger.log 'pause_video', id: @currentTime, code: @player.getVideoEmbedCode() window.player = null if window.player == @player clearInterval(@player.interval) @player.interval = null + @caption.pause() + @control.pause() - onSeek: (event, time) -> + onEnded: => + @control.pause() + @caption.pause() + + onSeek: (event, time) => @player.seekTo(time, true) if @isPlaying() clearInterval(@player.interval) @player.interval = setInterval(@update, 200) else @currentTime = time - $(@).trigger('updatePlayTime', time) + @updatePlayTime time onSpeedChange: (event, newSpeed) => @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) - @video.setSpeed(parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0') + newSpeed = parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0' + @video.setSpeed(newSpeed) + @caption.currentSpeed = newSpeed if @isPlaying() @player.loadVideoById(@video.youtubeId(), @currentTime) else @player.cueVideoById(@video.youtubeId(), @currentTime) - $(@).trigger('updatePlayTime', @currentTime) + @updatePlayTime @currentTime + + onVolumeChange: (event, volume) => + @player.setVolume volume update: => if @currentTime = @player.getCurrentTime() - $(@).trigger('updatePlayTime', @currentTime) + @updatePlayTime @currentTime - onUpdatePlayTime: (event, time) => + updatePlayTime: (time) -> progress = Time.format(time) + ' / ' + Time.format(@duration()) @$(".vidtime").html(progress) + @caption.updatePlayTime(time) + @progressSlider.updatePlayTime(time, @duration()) toggleFullScreen: (event) => event.preventDefault() @@ -116,26 +133,20 @@ class @VideoPlayer @element.append('Exit').addClass('fullscreen') @$('.add-fullscreen').attr('title', 'Exit fill browser') @$('.exit').click @toggleFullScreen - $(@).trigger('resize') + @caption.resize() # Delegates - play: -> + play: => @player.playVideo() if @player.playVideo isPlaying: -> @player.getPlayerState() == YT.PlayerState.PLAYING - pause: -> - @player.pauseVideo() + pause: => + @player.pauseVideo() if @player.pauseVideo duration: -> @video.getDuration() currentSpeed: -> @video.speed - - volume: (value) -> - if value? - @player.setVolume value - else - @player.getVolume() diff --git a/lms/static/coffee/src/modules/video/video_progress_slider.coffee b/lms/static/coffee/src/modules/video/video_progress_slider.coffee index af0bbb53a6..af096f9bb9 100644 --- a/lms/static/coffee/src/modules/video/video_progress_slider.coffee +++ b/lms/static/coffee/src/modules/video/video_progress_slider.coffee @@ -1,15 +1,9 @@ -class @VideoProgressSlider - constructor: (@player) -> +class @VideoProgressSlider extends Subview + initialize: -> @buildSlider() unless onTouchBasedDevice() - $(@player).bind('updatePlayTime', @onUpdatePlayTime) - $(@player).bind('ready', @onReady) - $(@player).bind('play', @onPlay) - - $: (selector) -> - @player.$(selector) buildSlider: -> - @slider = @$('.slider').slider + @slider = @element.slider range: 'min' change: @onChange slide: @onSlide @@ -17,7 +11,7 @@ class @VideoProgressSlider @buildHandle() buildHandle: -> - @handle = @$('.slider .ui-slider-handle') + @handle = @$('.ui-slider-handle') @handle.qtip content: "#{Time.format(@slider.slider('value'))}" position: @@ -30,28 +24,25 @@ class @VideoProgressSlider classes: 'ui-tooltip-slider' widget: true - onReady: => - @slider.slider('option', 'max', @player.duration()) if @slider - - onPlay: => + play: => @buildSlider() unless @slider - onUpdatePlayTime: (event, currentTime) => + updatePlayTime: (currentTime, duration) -> if @slider && !@frozen - @slider.slider('option', 'max', @player.duration()) + @slider.slider('option', 'max', duration) @slider.slider('value', currentTime) onSlide: (event, ui) => @frozen = true @updateTooltip(ui.value) - $(@player).trigger('seek', ui.value) + $(@).trigger('seek', ui.value) onChange: (event, ui) => @updateTooltip(ui.value) onStop: (event, ui) => @frozen = true - $(@player).trigger('seek', ui.value) + $(@).trigger('seek', ui.value) setTimeout (=> @frozen = false), 200 updateTooltip: (value)-> diff --git a/lms/static/coffee/src/modules/video/video_speed_control.coffee b/lms/static/coffee/src/modules/video/video_speed_control.coffee index 6f7631a0ad..0aa238d5e4 100644 --- a/lms/static/coffee/src/modules/video/video_speed_control.coffee +++ b/lms/static/coffee/src/modules/video/video_speed_control.coffee @@ -1,13 +1,5 @@ -class @VideoSpeedControl - constructor: (@player, @speeds) -> - @render() - @bind() - - $: (selector) -> - @player.$(selector) - +class @VideoSpeedControl extends Subview bind: -> - $(@player).bind('speedChange', @onSpeedChange) @$('.video_speeds a').click @changeVideoSpeed if onTouchBasedDevice() @$('.speeds').click (event) -> @@ -23,7 +15,7 @@ class @VideoSpeedControl $(this).removeClass('open') render: -> - @$('.secondary-controls').prepend """ + @element.prepend """

                  Speed

                  @@ -36,15 +28,14 @@ class @VideoSpeedControl $.each @speeds, (index, speed) => link = $('
                  ').attr(href: "#").html("#{speed}x") @$('.video_speeds').prepend($('
                • ').attr('data-speed', speed).html(link)) - @setSpeed(@player.currentSpeed()) + @setSpeed(@currentSpeed) changeVideoSpeed: (event) => event.preventDefault() unless $(event.target).parent().hasClass('active') - $(@player).trigger 'speedChange', $(event.target).parent().data('speed') - - onSpeedChange: (event, speed) => - @setSpeed(parseFloat(speed).toFixed(2).replace /\.00$/, '.0') + @currentSpeed = $(event.target).parent().data('speed') + $(@).trigger 'speedChange', $(event.target).parent().data('speed') + @setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0') setSpeed: (speed) -> @$('.video_speeds li').removeClass('active') diff --git a/lms/static/coffee/src/modules/video/video_volume_control.coffee b/lms/static/coffee/src/modules/video/video_volume_control.coffee index 1c0d3440d5..90061c333e 100644 --- a/lms/static/coffee/src/modules/video/video_volume_control.coffee +++ b/lms/static/coffee/src/modules/video/video_volume_control.coffee @@ -1,14 +1,8 @@ -class @VideoVolumeControl - constructor: (@player) -> - @previousVolume = 100 - @render() - @bind() - - $: (selector) -> - @player.$(selector) +class @VideoVolumeControl extends Subview + initialize: -> + @currentVolume = 100 bind: -> - $(@player).bind('ready', @onReady) @$('.volume').mouseenter -> $(this).addClass('open') @$('.volume').mouseleave -> @@ -16,7 +10,7 @@ class @VideoVolumeControl @$('.volume>a').click(@toggleMute) render: -> - @$('.secondary-controls').prepend """ + @element.prepend """
                  @@ -33,16 +27,14 @@ class @VideoVolumeControl change: @onChange slide: @onChange - onReady: => - @slider.slider 'option', 'max', @player.volume() - onChange: (event, ui) => - @player.volume ui.value - @$('.secondary-controls .volume').toggleClass 'muted', ui.value == 0 + @currentVolume = ui.value + $(@).trigger 'volumeChange', @currentVolume + @$('.volume').toggleClass 'muted', @currentVolume == 0 toggleMute: => - if @player.volume() > 0 - @previousVolume = @player.volume() + if @currentVolume > 0 + @previousVolume = @currentVolume @slider.slider 'option', 'value', 0 else @slider.slider 'option', 'value', @previousVolume diff --git a/lms/static/sass/courseware/_video.scss b/lms/static/sass/courseware/_video.scss index 15cc75e501..a28302d575 100644 --- a/lms/static/sass/courseware/_video.scss +++ b/lms/static/sass/courseware/_video.scss @@ -476,6 +476,7 @@ section.course-content { ol.subtitles { width: 0px; + display: none; } } @@ -498,6 +499,7 @@ section.course-content { ol.subtitles { right: -(flex-grid(4)); width: auto; + display: block; } } From e7e0fe1f3db18ccfb69d3c003806d09e6a03dfa8 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Mon, 2 Jul 2012 12:25:40 -0400 Subject: [PATCH 242/252] Fix element naming convention --- .../coffee/spec/modules/problem_spec.coffee | 28 +++++++-------- .../coffee/spec/modules/sequence_spec.coffee | 8 ++--- .../coffee/spec/modules/tab_spec.coffee | 6 ++-- .../modules/video/video_caption_spec.coffee | 34 +++++++++---------- .../modules/video/video_control_spec.coffee | 14 ++++---- .../modules/video/video_player_spec.coffee | 24 ++++++------- .../video/video_progress_slider_spec.coffee | 16 ++++----- .../video/video_speed_control_spec.coffee | 10 +++--- .../video/video_volume_control_spec.coffee | 6 ++-- .../coffee/spec/modules/video_spec.coffee | 2 +- lms/static/coffee/src/_subview.coffee | 2 +- lms/static/coffee/src/main.coffee | 4 +-- lms/static/coffee/src/modules/problem.coffee | 18 +++++----- lms/static/coffee/src/modules/sequence.coffee | 14 ++++---- lms/static/coffee/src/modules/tab.coffee | 10 +++--- lms/static/coffee/src/modules/video.coffee | 2 +- .../src/modules/video/video_caption.coffee | 8 ++--- .../src/modules/video/video_control.coffee | 2 +- .../src/modules/video/video_player.coffee | 20 +++++------ .../video/video_progress_slider.coffee | 2 +- .../modules/video/video_speed_control.coffee | 2 +- .../modules/video/video_volume_control.coffee | 2 +- 22 files changed, 117 insertions(+), 117 deletions(-) diff --git a/lms/static/coffee/spec/modules/problem_spec.coffee b/lms/static/coffee/spec/modules/problem_spec.coffee index 7537cd3493..48d63784d6 100644 --- a/lms/static/coffee/spec/modules/problem_spec.coffee +++ b/lms/static/coffee/spec/modules/problem_spec.coffee @@ -20,7 +20,7 @@ describe 'Problem', -> @problem = new Problem 1, '/problem/url/' it 'set the element', -> - expect(@problem.element).toBe '#problem_1' + expect(@problem.el).toBe '#problem_1' describe 'bind', -> beforeEach -> @@ -69,7 +69,7 @@ describe 'Problem', -> @problem.render 'Hello World' it 'render the content', -> - expect(@problem.element.html()).toEqual 'Hello World' + expect(@problem.el.html()).toEqual 'Hello World' it 're-bind the content', -> expect(@problem.bind).toHaveBeenCalled() @@ -81,7 +81,7 @@ describe 'Problem', -> @problem.render() it 'load the content via ajax', -> - expect(@problem.element.html()).toEqual 'Hello World' + expect(@problem.el.html()).toEqual 'Hello World' it 're-bind the content', -> expect(@problem.bind).toHaveBeenCalled() @@ -104,13 +104,13 @@ describe 'Problem', -> it 'call render with returned content', -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'correct', contents: 'Correct!') @problem.check() - expect(@problem.element.html()).toEqual 'Correct!' + expect(@problem.el.html()).toEqual 'Correct!' describe 'when the response is incorrect', -> it 'call render with returned content', -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback(success: 'incorrect', contents: 'Correct!') @problem.check() - expect(@problem.element.html()).toEqual 'Correct!' + expect(@problem.el.html()).toEqual 'Correct!' describe 'when the response is undetermined', -> it 'alert the response', -> @@ -137,16 +137,16 @@ describe 'Problem', -> spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback html: "Reset!" @problem.reset() - expect(@problem.element.html()).toEqual 'Reset!' + expect(@problem.el.html()).toEqual 'Reset!' describe 'show', -> beforeEach -> @problem = new Problem 1, '/problem/url/' - @problem.element.prepend '
                  ' + @problem.el.prepend '
                  ' describe 'when the answer has not yet shown', -> beforeEach -> - @problem.element.removeClass 'showed' + @problem.el.removeClass 'showed' it 'log the problem_show event', -> @problem.show() @@ -172,11 +172,11 @@ describe 'Problem', -> it 'add the showed class to element', -> spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) @problem.show() - expect(@problem.element).toHaveClass 'showed' + expect(@problem.el).toHaveClass 'showed' describe 'multiple choice question', -> beforeEach -> - @problem.element.prepend ''' + @problem.el.prepend ''' @@ -194,8 +194,8 @@ describe 'Problem', -> describe 'when the answers are alreay shown', -> beforeEach -> - @problem.element.addClass 'showed' - @problem.element.prepend ''' + @problem.el.addClass 'showed' + @problem.el.prepend '''
              From 841fb8da0bfcd6a249a2818a1a9e5347f53d8196 Mon Sep 17 00:00:00 2001 From: Kyle Fiedler Date: Tue, 3 Jul 2012 16:26:56 -0400 Subject: [PATCH 251/252] Added the wip calss back in --- cms/static/sass/_base.scss | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cms/static/sass/_base.scss b/cms/static/sass/_base.scss index 126d3ff1a0..b5156c0ec0 100644 --- a/cms/static/sass/_base.scss +++ b/cms/static/sass/_base.scss @@ -103,16 +103,17 @@ textarea { padding: 4px 10px; } } -// .wip { -// outline: 1px solid #f00 !important; -// position: relative; -// &:after { -// content: "WIP"; -// font-size: 8px; -// padding: 2px; -// background: #f00; -// color: #fff; -// @include position(absolute, 0px 0px 0 0); -// } -// } +.wip { + outline: 1px solid #f00 !important; + position: relative; + + &:after { + content: "WIP"; + font-size: 8px; + padding: 2px; + background: #f00; + color: #fff; + @include position(absolute, 0px 0px 0 0); + } +} From 87e612f0f2d5c842e50012dd90e49a817c99fe7c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 5 Jul 2012 08:48:08 -0400 Subject: [PATCH 252/252] Don't swallow messages going to the root logger --- lms/envs/logsettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/logsettings.py b/lms/envs/logsettings.py index abfdf04b8e..916cd77748 100644 --- a/lms/envs/logsettings.py +++ b/lms/envs/logsettings.py @@ -77,7 +77,7 @@ def get_logger_config(log_dir, 'level' : 'DEBUG', 'propagate' : False, }, - 'root' : { + '' : { 'handlers' : handlers, 'level' : 'DEBUG', 'propagate' : False